diff --git a/.gitignore b/.gitignore index 5dac550..445d070 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,57 @@ -*/target -target -tmp +# built application files +*.apk +*.ap_ + +# files for the dex VM +*.dex + +# Java class files +*.class +classes/ + +# generated files +bin/ +gen/ + +# Local configuration file (sdk path, etc) +local.properties + +# Eclipse project files +.classpath +.project +.settings/ +*/.classpath +*/.project +*/.settings/ + +# Proguard folder generated by Eclipse +proguard/ + +# Intellij project files +*.iml +*.ipr +*.iws +.idea/ + +# Apple +.DS_Store + +# Vim/Emacs *~ + +# Maven +target/ + +# Gradle +.gradle +build + +# Custom lib -bin */test-output temp-testng-customsuite.xml **pom.xml.releaseBackup release.properties project.properties -*.iws -*.iml -gen */seed.txt -notes -logs gen-external-apklibs -.idea -out -.DS_Store - diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..eea64ea --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +language: java +jdk: oraclejdk7 + +before_install: + # required libs for android build tools + - sudo apt-get update + - sudo apt-get install -qq --force-yes libgd2-xpm ia32-libs ia32-libs-multiarch + # for gradle output style + - export TERM=dumb + # newer version of gradle + - wget http://services.gradle.org/distributions/gradle-1.10-bin.zip + - unzip -qq gradle-1.10-bin.zip + - export GRADLE_HOME=$PWD/gradle-1.10 + - export PATH=$GRADLE_HOME/bin:$PATH + - chmod +x gradlew + # just to test gradle version, against our provided one + - gradle -v + # newest Android SDK 22.3 + - wget http://dl.google.com/android/android-sdk_r22.3-linux.tgz + - tar -zxf android-sdk_r22.3-linux.tgz + - export ANDROID_HOME=`pwd`/android-sdk-linux + - export PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools + # Don't really need this (NDK stuff) for Android Bootstrap, but figured I'd leave it here for anyone who forks it. :) + # newest Android NDK + #- if [ `uname -m` = x86_64]; then wget http://dl.google.com/android/ndk/android-ndk-r9c-linux-x86_64.tar.bz2 -O ndk.tgz; else wget http://dl.google.com/android/ndk/android-ndk-r9c-linux-x86.tar.bz2 -O ndk.tgz; fi + #- tar -xf ndk.tgz + #- export ANDROID_NDK_HOME=`pwd`/android-ndk-r9c + #- export PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${ANDROID_NDK_HOME} + # manually set sdk.dir variable, according to local paths + - echo "sdk.dir=$ANDROID_HOME" > local.properties + - echo yes | android update sdk -a -t tools,platform-tools,extra-android-support,extra-android-m2repository,android-19,build-tools-19.1.0,extra-google-google_play_services,extra-google-m2repository --force --no-ui diff --git a/README.md b/README.md index 393dac5..200012d 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,72 @@ # Android Bootstrap App +[![Build Status](https://travis-ci.org/AndroidBootstrap/android-bootstrap.svg?branch=master)](https://travis-ci.org/AndroidBootstrap/android-bootstrap) + This repository contains the source code for the [Android Bootstrap](http://www.androidbootstrap.com/) -Android app available soon from [Google Play](https://play.google.com/store/apps/details?id=com.donnfelker.android.bootstrap). +Android app available from [Google Play](https://play.google.com/store/apps/details?id=com.donnfelker.android.bootstrap). +Please see the [issues](https://github.com/androidbootstrap/android-bootstrap/issues) section +to report any bugs or feature requests and to see the list of known issues. +Have a questions about Android Bootstrap? Ask away on the [android-bootstrap discussion forum](https://groups.google.com/forum/#!forum/android-bootstrap). -Please see the [issues](https://github.com/donnfelker/android-bootstrap/issues) section -to report any bugs or feature requests and to see the list of known issues. + + + - - + + -## License +## HOW TO +Learn how to develop with IntelliJ and Gradle. -* [Apache Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) +## Authentication +Log into this demo app with the following credentials: + +user: demo@androidbootstrap.com + +password: android + + +## Generating your Bootstrap App +Why generate? Simple ... renaming files, folders, copy and pasting is SUPER error prone and well... it sucks overall. +This can easily take a few days with debugging if you run into issues and perform a lot of typo's. +Using the generator on [AndroidBootstrap.com](http://www.androidbootstrap.com) you can generate your application +with your application name as well as the package (and folder structure) that you want to work with. + +As an example, you know that you want your app name and package to the following: + + - *App Name*: Notify + - *Package Name*: com.notify.app.mobile + +After generating the app on [AndroidBootstrap.com](http://www.androidbootstrap.com) the folder structure of the source +code for the app will change: + - From: __com/donnfelker/android/bootstrap__ + - To: __com/notify/app/mobile__ -Copyright 2012 Donn Felker +At that point all the source files that were located in ____com/donnfelker/android/bootstrap__ will be moved to the +new folder __com/notify/app/mobile__. +All import statments that reference the old resources (__R.com.donnfelker.android.bootstrap.R__) will now be renamed +to the correct package. The artifact id's in the *pom.xml* (and various other places) will be replaced. The App Name +will be replaced in the strings/etc. -Copyright 2012 GitHub Inc. +The end result is that you will be given a zip file with the correct structure. Open the zip and then execute the +*./gradlew* command and your app should be ready for development. + +Enjoy! + +The application + +## License + +* [Apache Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) + + +Copyright 2014 Donn Felker +Copyright 2014 GitHub Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -37,19 +83,19 @@ limitations under the License. ## Building -The build requires [Maven](http://maven.apache.org/download.html) -v3.0.3+ and the [Android SDK](http://developer.android.com/sdk/index.html) +The build requires [Gradle](http://www.gradle.org/downloads) +v1.10+ and the [Android SDK](http://developer.android.com/sdk/index.html) to be installed in your development environment. In addition you'll need to set the `ANDROID_HOME` environment variable to the location of your SDK: - export ANDROID_HOME=/home/donnfelker/tools/android-sdk + export ANDROID_HOME=/path/to/your/android-sdk After satisfying those requirements, the build is pretty simple: -* Run `mvn clean package` from the `app` directory to build the APK only -* Run `mvn clean install` from the root directory to build the app and also run +* Run `gradlew` or `gradle assembleDebug` or `gradle assembleRelease` from the `app` directory to build the APK only +* Run one of the commands above from the root directory to build the app and also run the integration tests, this requires a connected Android device or running - emulator + emulator. You might find that your device doesn't let you install your build if you already have the version from the Android Market installed. This is standard @@ -57,37 +103,51 @@ Android security as it it won't let you directly replace an app that's been signed with a different key. Manually uninstall Android Bootstrap from your device and you will then be able to install your own built version. +## Building in Eclipse + +Why are you using Eclipse still? :) +Please use Android Studio, we do not support Eclipse. + + ## Acknowledgements Android Bootstrap is a result of a template project I've developed over the years as well as a combination of a lot of great work that the [GitHub Gaug.es](http://www.github.com/github/gauges-android) -app and [GitHub Android](http://www.github.com/github/android) app showcased. Some fo the +app and [GitHub Android](http://www.github.com/github/android) app showcased. Some of the code in this project is based on the GitHub Gaug.es and GitHub Android app. Android Bootstrap is built on the awesome [Parse.com API](http://www.parse.com/) and uses many great open-source libraries from the Android dev community: -* [ActionBarSherlock](https://github.com/JakeWharton/ActionBarSherlock) for a +* [AppCompat](http://www.youtube.com/watch?v=6TGgYqfJnyc) for a consistent, great looking header across all Android platforms, [ViewPagerIndicator](https://github.com/JakeWharton/Android-ViewPagerIndicator) - for swiping between content, traffic, & referrer pages, and - [NineOldAndroids](https://github.com/JakeWharton/NineOldAndroids) for the - AirTraffic view animations - all from [Jake Wharton](http://jakewharton.com/). -* [RoboGuice](http://code.google.com/p/roboguice/) for dependency-injection. + for swiping between fragments and + [NineOldAndroids](https://github.com/JakeWharton/NineOldAndroids) for + view animations - all from [Jake Wharton](http://jakewharton.com/). +* [NavigationDrawer](http://developer.android.com/design/patterns/navigation-drawer.html) for the menu drawer navigation. +* [Dagger](https://github.com/square/dagger) for dependency-injection. +* [ButterKnife](https://github.com/JakeWharton/butterknife) for view injection +* [Otto](https://github.com/square/otto) as the event bus * [Robotium](http://code.google.com/p/robotium/) for driving our app during integration tests. * [android-maven-plugin](https://github.com/jayway/maven-android-plugin) for automating our build and producing release-ready APKs. -* [http-request](https://github.com/kevinsawicki/http-request) for interacting with +* [Retrofit](http://square.github.io/retrofit/) for interacting with remote HTTP resources (API's in this case). * [google-gson](http://code.google.com/p/google-gson/) for consuming JSON and hydrating POJO's for use in the app. +## Contributors +Thank you to all the [contributors](http://www.github.com/androidbootstrap/android-bootstrap/contributors) on +this project. Your help is much appreciated. + + ## Contributing Please fork this repository and contribute back using -[pull requests](https://github.com/donnfelker/android-bootstrap/pulls). +[pull requests](https://github.com/androidbootstrap/android-bootstrap/pulls). Any contributions, large or small, major features, bug fixes, additional language translations, unit/integration tests are welcomed and appreciated diff --git a/app/.classpath b/app/.classpath deleted file mode 100644 index ae533e1..0000000 --- a/app/.classpath +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/app/.project b/app/.project deleted file mode 100644 index b075024..0000000 --- a/app/.project +++ /dev/null @@ -1,33 +0,0 @@ - - - com.donnfelker.android.bootstrap - - - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - - diff --git a/app/.settings/org.eclipse.jdt.core.prefs b/app/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 5c91302..0000000 --- a/app/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,291 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 -org.eclipse.jdt.core.formatter.align_type_members_on_columns=false -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_assignment=0 -org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 -org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 -org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 -org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 -org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 -org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 -org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 -org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=0 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 -org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 -org.eclipse.jdt.core.formatter.blank_lines_before_method=1 -org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 -org.eclipse.jdt.core.formatter.blank_lines_before_package=0 -org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 -org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 -org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false -org.eclipse.jdt.core.formatter.comment.format_block_comments=true -org.eclipse.jdt.core.formatter.comment.format_header=false -org.eclipse.jdt.core.formatter.comment.format_html=true -org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true -org.eclipse.jdt.core.formatter.comment.format_line_comments=true -org.eclipse.jdt.core.formatter.comment.format_source_code=true -org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true -org.eclipse.jdt.core.formatter.comment.indent_root_tags=true -org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert -org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert -org.eclipse.jdt.core.formatter.comment.line_length=80 -org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true -org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true -org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false -org.eclipse.jdt.core.formatter.compact_else_if=true -org.eclipse.jdt.core.formatter.continuation_indentation=2 -org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 -org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off -org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on -org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false -org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true -org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_empty_lines=false -org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true -org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false -org.eclipse.jdt.core.formatter.indentation.size=4 -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert -org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert -org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.join_lines_in_comments=true -org.eclipse.jdt.core.formatter.join_wrapped_lines=true -org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false -org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false -org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false -org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false -org.eclipse.jdt.core.formatter.lineSplit=80 -org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false -org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false -org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 -org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 -org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true -org.eclipse.jdt.core.formatter.tabulation.char=space -org.eclipse.jdt.core.formatter.tabulation.size=4 -org.eclipse.jdt.core.formatter.use_on_off_tags=false -org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false -org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true -org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true -org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true diff --git a/app/.settings/org.eclipse.jdt.ui.prefs b/app/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index a6bfc93..0000000 --- a/app/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,56 +0,0 @@ -#Fri Jan 27 15:21:39 PST 2012 -eclipse.preferences.version=1 -editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_settings_version=12 -sp_cleanup.add_default_serial_version_id=true -sp_cleanup.add_generated_serial_version_id=false -sp_cleanup.add_missing_annotations=false -sp_cleanup.add_missing_deprecated_annotations=true -sp_cleanup.add_missing_methods=false -sp_cleanup.add_missing_nls_tags=false -sp_cleanup.add_missing_override_annotations=true -sp_cleanup.add_missing_override_annotations_interface_methods=false -sp_cleanup.add_serial_version_id=false -sp_cleanup.always_use_blocks=true -sp_cleanup.always_use_parentheses_in_expressions=false -sp_cleanup.always_use_this_for_non_static_field_access=false -sp_cleanup.always_use_this_for_non_static_method_access=false -sp_cleanup.convert_to_enhanced_for_loop=false -sp_cleanup.correct_indentation=false -sp_cleanup.format_source_code=false -sp_cleanup.format_source_code_changes_only=false -sp_cleanup.make_local_variable_final=false -sp_cleanup.make_parameters_final=false -sp_cleanup.make_private_fields_final=true -sp_cleanup.make_type_abstract_if_missing_method=false -sp_cleanup.make_variable_declarations_final=false -sp_cleanup.never_use_blocks=false -sp_cleanup.never_use_parentheses_in_expressions=true -sp_cleanup.on_save_use_additional_actions=true -sp_cleanup.organize_imports=false -sp_cleanup.qualify_static_field_accesses_with_declaring_class=false -sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_with_declaring_class=false -sp_cleanup.qualify_static_method_accesses_with_declaring_class=false -sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_trailing_whitespaces=true -sp_cleanup.remove_trailing_whitespaces_all=true -sp_cleanup.remove_trailing_whitespaces_ignore_empty=false -sp_cleanup.remove_unnecessary_casts=false -sp_cleanup.remove_unnecessary_nls_tags=false -sp_cleanup.remove_unused_imports=false -sp_cleanup.remove_unused_local_variables=false -sp_cleanup.remove_unused_private_fields=true -sp_cleanup.remove_unused_private_members=false -sp_cleanup.remove_unused_private_methods=true -sp_cleanup.remove_unused_private_types=true -sp_cleanup.sort_members=false -sp_cleanup.sort_members_all=false -sp_cleanup.use_blocks=false -sp_cleanup.use_blocks_only_for_return_and_throw=false -sp_cleanup.use_parentheses_in_expressions=false -sp_cleanup.use_this_for_non_static_field_access=false -sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true -sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..4fba846 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,85 @@ +buildscript { + repositories { + maven { url 'http://repo1.maven.org/maven2' } + mavenLocal() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:0.11.+' + } +} +apply plugin: 'android' + +repositories { + maven { url 'https://github.com/donnfelker/mvn-repo/raw/master/' } + mavenLocal() + mavenCentral() +} + +dependencies { + compile 'com.android.support:appcompat-v7:19.1.+' + compile 'com.google.code.gson:gson:2.2.4' + compile 'com.squareup.dagger:dagger:1.0.1' + compile 'com.squareup.dagger:dagger-compiler:1.0.1' + compile 'com.jakewharton:butterknife:1.3.2' + compile 'com.actionbarsherlock:viewpagerindicator:2.4.1' + compile 'com.nineoldandroids:library:2.4.0' + compile 'com.github.kevinsawicki:android-pusher:0.6' + compile 'com.github.kevinsawicki:wishlist:0.9' + compile 'com.squareup:otto:1.3.4' + compile 'com.squareup.picasso:picasso:1.1.1' + compile 'com.squareup.retrofit:retrofit:1.5.1' + + + + androidTestCompile 'junit:junit:4.11' + androidTestCompile 'org.hamcrest:hamcrest-library:1.3' + androidTestCompile 'org.mockito:mockito-core:1.9.5' +} + +android { + compileSdkVersion 19 + buildToolsVersion '19.1.0' + + defaultConfig { + minSdkVersion 8 + targetSdkVersion 19 + versionCode 102 + versionName '1.0' + } + + packagingOptions { + // Exclude file to avoid + // Error: Duplicate files during packaging of APK + exclude 'META-INF/services/javax.annotation.processing.Processor' + } + + // signingConfigs { + // release { + // storeFile file(System.getenv('ANDROID_KEYSTORE_PATH')) + // storePassword System.getenv('ANDROID_STORE_PASS') + // keyAlias System.getenv('ANDROID_KEY_ALIAS') + // keyPassword System.getenv('ANDROID_KEY_PASS') + // } + // } + + lintOptions { + abortOnError false + } + + buildTypes { + debug { + applicationIdSuffix '.debug' + runProguard false + // zipAlign false // this is default for debug + } + release { + // runProguard true + // proguardFile '..\proguard.cfg' + // signingConfig signingConfigs.release + // zipAlign true // this is default for release + // testPackageName 'com.donnfelker.android.bootstrap.tests' + // testInstrumentationRunner 'android.test.InstrumentationTestRunner' // this is the default + } + } +} diff --git a/app/default.properties b/app/default.properties index 9bec576..9c7fd82 100644 --- a/app/default.properties +++ b/app/default.properties @@ -1,3 +1,3 @@ # Project target. -target=android-16 +target=android-19 diff --git a/app/pom.xml b/app/pom.xml deleted file mode 100644 index b01296e..0000000 --- a/app/pom.xml +++ /dev/null @@ -1,192 +0,0 @@ - - - - - 4.0.0 - - android-bootstrap - apk - Android Bootstrap app - https://github.com/donnfelker/android-bootstrap - - - 1.4 - com.donnfelker.android.bootstrap - android-bootstrap-parent - - - - 4.1.0 - - - - com.google.android - android - provided - ${android.version} - - - com.google.code.gson - gson - 2.1 - - - com.github.rtyley - roboguice-sherlock - 1.4 - - - org.roboguice - roboguice - 2.0 - - - com.actionbarsherlock - library - ${abs.version} - apklib - - - com.actionbarsherlock - library - ${abs.version} - jar - provided - - - com.github.kevinsawicki - http-request - 2.1 - - - com.viewpagerindicator - library - 2.3.1 - apklib - - - com.nineoldandroids - library - 2.2.0 - - - com.github.kevinsawicki - android-pusher - 0.6 - - - com.github.kevinsawicki - wishlist - 0.3 - apklib - - - junit - junit - 4.10 - test - - - org.hamcrest - hamcrest-library - 1.3.RC2 - test - - - org.mockito - mockito-core - 1.9.0 - test - - - - - - com.jayway.maven.plugins.android.generation2 - android-maven-plugin - - - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - - - - - - - release - - - - performRelease - true - - - - - - org.apache.maven.plugins - maven-jarsigner-plugin - - - signing - - sign - verify - - package - true - - true - - - ${bootstrap.sign.keystore} - ${boostrap.sign.alias} - ${bootstrap.sign.storepass} - ${bootstrap.sign.keypass} - true - - - - - - - com.jayway.maven.plugins.android.generation2 - android-maven-plugin - true - - - false - - - true - ${project.build.directory}/${project.artifactId}-${project.version}-signed-aligned.apk - - - false - - - - - alignApk - package - - zipalign - - - - - - - - - diff --git a/app/res/layout/carousel_view.xml b/app/res/layout/carousel_view.xml deleted file mode 100644 index 54d0bac..0000000 --- a/app/res/layout/carousel_view.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/AndroidManifest.xml b/app/src/main/AndroidManifest.xml similarity index 65% rename from app/AndroidManifest.xml rename to app/src/main/AndroidManifest.xml index 30e28d0..fcb776b 100644 --- a/app/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,12 +2,12 @@ + android:versionCode="102" + android:versionName="1.1" > + android:targetSdkVersion="19" /> @@ -19,12 +19,12 @@ android:name=".BootstrapApplication" android:icon="@drawable/icon" android:label="@string/app_name" - android:theme="@style/Theme.Bootstrap.Dark" - android:debuggable="true"> + android:theme="@style/Theme.Bootstrap.Dark"> + android:name=".ui.MainActivity" + android:configChanges="orientation|keyboardHidden|screenSize" + android:label="@string/app_name"> @@ -34,6 +34,19 @@ + + + + + + + - \ No newline at end of file + diff --git a/app/src/main/assets/fonts/Roboto-Regular.ttf b/app/src/main/assets/fonts/Roboto-Regular.ttf new file mode 100755 index 0000000..7d9a6c4 Binary files /dev/null and b/app/src/main/assets/fonts/Roboto-Regular.ttf differ diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/AndroidModule.java b/app/src/main/java/com/donnfelker/android/bootstrap/AndroidModule.java new file mode 100644 index 0000000..3e7f5c3 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/AndroidModule.java @@ -0,0 +1,80 @@ +package com.donnfelker.android.bootstrap; + +import android.accounts.AccountManager; +import android.app.NotificationManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.preference.PreferenceManager; +import android.telephony.TelephonyManager; +import android.view.inputmethod.InputMethodManager; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +/** + * Module for all Android related provisions + */ +@Module(complete = false, library = true) +public class AndroidModule { + + @Provides + @Singleton + Context provideAppContext() { + return BootstrapApplication.getInstance().getApplicationContext(); + } + + @Provides + SharedPreferences provideDefaultSharedPreferences(final Context context) { + return PreferenceManager.getDefaultSharedPreferences(context); + } + + @Provides + PackageInfo providePackageInfo(Context context) { + try { + return context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + } + + @Provides + TelephonyManager provideTelephonyManager(Context context) { + return getSystemService(context, Context.TELEPHONY_SERVICE); + } + + @SuppressWarnings("unchecked") + public T getSystemService(Context context, String serviceConstant) { + return (T) context.getSystemService(serviceConstant); + } + + @Provides + InputMethodManager provideInputMethodManager(final Context context) { + return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + } + + @Provides + ApplicationInfo provideApplicationInfo(final Context context) { + return context.getApplicationInfo(); + } + + @Provides + AccountManager provideAccountManager(final Context context) { + return AccountManager.get(context); + } + + @Provides + ClassLoader provideClassLoader(final Context context) { + return context.getClassLoader(); + } + + @Provides + NotificationManager provideNotificationManager(final Context context) { + return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + } + +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/BootstrapApplication.java b/app/src/main/java/com/donnfelker/android/bootstrap/BootstrapApplication.java index e8c9fd0..7e3d6b0 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/BootstrapApplication.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/BootstrapApplication.java @@ -2,26 +2,21 @@ package com.donnfelker.android.bootstrap; -import static android.os.Build.VERSION.SDK_INT; -import static android.os.Build.VERSION_CODES.FROYO; import android.app.Application; import android.app.Instrumentation; import android.content.Context; -import com.github.kevinsawicki.http.HttpRequest; - /** * Android Bootstrap application */ public class BootstrapApplication extends Application { + private static BootstrapApplication instance; + /** * Create main application */ public BootstrapApplication() { - // Disable http.keepAlive on Froyo and below - if (SDK_INT <= FROYO) - HttpRequest.keepAlive(false); } /** @@ -34,6 +29,22 @@ public BootstrapApplication(final Context context) { attachBaseContext(context); } + @Override + public void onCreate() { + super.onCreate(); + + instance = this; + + // Perform injection + Injector.init(getRootModule(), this); + + } + + private Object getRootModule() { + return new RootModule(); + } + + /** * Create main application * @@ -43,4 +54,8 @@ public BootstrapApplication(final Instrumentation instrumentation) { this(); attachBaseContext(instrumentation.getTargetContext()); } + + public static BootstrapApplication getInstance() { + return instance; + } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/BootstrapModule.java b/app/src/main/java/com/donnfelker/android/bootstrap/BootstrapModule.java new file mode 100644 index 0000000..a69e733 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/BootstrapModule.java @@ -0,0 +1,123 @@ +package com.donnfelker.android.bootstrap; + +import android.accounts.AccountManager; +import android.content.Context; + +import com.donnfelker.android.bootstrap.authenticator.ApiKeyProvider; +import com.donnfelker.android.bootstrap.authenticator.BootstrapAuthenticatorActivity; +import com.donnfelker.android.bootstrap.authenticator.LogoutService; +import com.donnfelker.android.bootstrap.core.BootstrapService; +import com.donnfelker.android.bootstrap.core.Constants; +import com.donnfelker.android.bootstrap.core.PostFromAnyThreadBus; +import com.donnfelker.android.bootstrap.core.RestAdapterRequestInterceptor; +import com.donnfelker.android.bootstrap.core.RestErrorHandler; +import com.donnfelker.android.bootstrap.core.TimerService; +import com.donnfelker.android.bootstrap.core.UserAgentProvider; +import com.donnfelker.android.bootstrap.ui.BootstrapTimerActivity; +import com.donnfelker.android.bootstrap.ui.CheckInsListFragment; +import com.donnfelker.android.bootstrap.ui.MainActivity; +import com.donnfelker.android.bootstrap.ui.NavigationDrawerFragment; +import com.donnfelker.android.bootstrap.ui.NewsActivity; +import com.donnfelker.android.bootstrap.ui.NewsListFragment; +import com.donnfelker.android.bootstrap.ui.UserActivity; +import com.donnfelker.android.bootstrap.ui.UserListFragment; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.squareup.otto.Bus; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; +import retrofit.RestAdapter; +import retrofit.converter.GsonConverter; + +/** + * Dagger module for setting up provides statements. + * Register all of your entry points below. + */ +@Module( + complete = false, + + injects = { + BootstrapApplication.class, + BootstrapAuthenticatorActivity.class, + MainActivity.class, + BootstrapTimerActivity.class, + CheckInsListFragment.class, + NavigationDrawerFragment.class, + NewsActivity.class, + NewsListFragment.class, + UserActivity.class, + UserListFragment.class, + TimerService.class + } +) +public class BootstrapModule { + + @Singleton + @Provides + Bus provideOttoBus() { + return new PostFromAnyThreadBus(); + } + + @Provides + @Singleton + LogoutService provideLogoutService(final Context context, final AccountManager accountManager) { + return new LogoutService(context, accountManager); + } + + @Provides + BootstrapService provideBootstrapService(RestAdapter restAdapter) { + return new BootstrapService(restAdapter); + } + + @Provides + BootstrapServiceProvider provideBootstrapServiceProvider(RestAdapter restAdapter, ApiKeyProvider apiKeyProvider) { + return new BootstrapServiceProvider(restAdapter, apiKeyProvider); + } + + @Provides + ApiKeyProvider provideApiKeyProvider(AccountManager accountManager) { + return new ApiKeyProvider(accountManager); + } + + @Provides + Gson provideGson() { + /** + * GSON instance to use for all request with date format set up for proper parsing. + *

+ * You can also configure GSON with different naming policies for your API. + * Maybe your API is Rails API and all json values are lower case with an underscore, + * like this "first_name" instead of "firstName". + * You can configure GSON as such below. + *

+ * + * public static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd") + * .setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES).create(); + */ + return new GsonBuilder().setDateFormat("yyyy-MM-dd").create(); + } + + @Provides + RestErrorHandler provideRestErrorHandler(Bus bus) { + return new RestErrorHandler(bus); + } + + @Provides + RestAdapterRequestInterceptor provideRestAdapterRequestInterceptor(UserAgentProvider userAgentProvider) { + return new RestAdapterRequestInterceptor(userAgentProvider); + } + + @Provides + RestAdapter provideRestAdapter(RestErrorHandler restErrorHandler, RestAdapterRequestInterceptor restRequestInterceptor, Gson gson) { + return new RestAdapter.Builder() + .setEndpoint(Constants.Http.URL_BASE) + .setErrorHandler(restErrorHandler) + .setRequestInterceptor(restRequestInterceptor) + .setLogLevel(RestAdapter.LogLevel.FULL) + .setConverter(new GsonConverter(gson)) + .build(); + } + +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/BootstrapServiceProvider.java b/app/src/main/java/com/donnfelker/android/bootstrap/BootstrapServiceProvider.java index 6be04fd..b2700af 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/BootstrapServiceProvider.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/BootstrapServiceProvider.java @@ -2,32 +2,46 @@ package com.donnfelker.android.bootstrap; import android.accounts.AccountsException; +import android.app.Activity; import com.donnfelker.android.bootstrap.authenticator.ApiKeyProvider; import com.donnfelker.android.bootstrap.core.BootstrapService; import com.donnfelker.android.bootstrap.core.UserAgentProvider; -import com.google.inject.Inject; import java.io.IOException; +import javax.inject.Inject; + +import retrofit.RestAdapter; + /** * Provider for a {@link com.donnfelker.android.bootstrap.core.BootstrapService} instance */ public class BootstrapServiceProvider { - @Inject private ApiKeyProvider keyProvider; - @Inject private UserAgentProvider userAgentProvider; + private RestAdapter restAdapter; + private ApiKeyProvider keyProvider; + + public BootstrapServiceProvider(RestAdapter restAdapter, ApiKeyProvider keyProvider) { + this.restAdapter = restAdapter; + this.keyProvider = keyProvider; + } /** * Get service for configured key provider - *

+ *

* This method gets an auth key and so it blocks and shouldn't be called on the main thread. * * @return bootstrap service * @throws IOException * @throws AccountsException */ - public BootstrapService getService() throws IOException, AccountsException { - return new BootstrapService(keyProvider.getAuthKey(), userAgentProvider); + public BootstrapService getService(final Activity activity) + throws IOException, AccountsException { + // The call to keyProvider.getAuthKey(...) is what initiates the login screen. Call that now. + keyProvider.getAuthKey(activity); + + // TODO: See how that affects the bootstrap service. + return new BootstrapService(restAdapter); } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/Injector.java b/app/src/main/java/com/donnfelker/android/bootstrap/Injector.java new file mode 100644 index 0000000..0051d48 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/Injector.java @@ -0,0 +1,34 @@ +package com.donnfelker.android.bootstrap; + +import dagger.ObjectGraph; + +public final class Injector { + + private static ObjectGraph objectGraph = null; + + public static void init(final Object rootModule) { + + if (objectGraph == null) { + objectGraph = ObjectGraph.create(rootModule); + } else { + objectGraph = objectGraph.plus(rootModule); + } + + // Inject statics + objectGraph.injectStatics(); + + } + + public static void init(final Object rootModule, final Object target) { + init(rootModule); + inject(target); + } + + public static void inject(final Object target) { + objectGraph.inject(target); + } + + public static T resolve(Class type) { + return objectGraph.get(type); + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/RootModule.java b/app/src/main/java/com/donnfelker/android/bootstrap/RootModule.java new file mode 100644 index 0000000..e8e1e5d --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/RootModule.java @@ -0,0 +1,15 @@ +package com.donnfelker.android.bootstrap; + +import dagger.Module; + +/** + * Add all the other modules to this one. + */ +@Module( + includes = { + AndroidModule.class, + BootstrapModule.class + } +) +public class RootModule { +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/AccountAuthenticatorService.java b/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/AccountAuthenticatorService.java index e9eb16c..1dcd71d 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/AccountAuthenticatorService.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/AccountAuthenticatorService.java @@ -1,26 +1,31 @@ package com.donnfelker.android.bootstrap.authenticator; -import static android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT; import android.app.Service; import android.content.Intent; import android.os.IBinder; +import static android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT; + /** - * Authenticator service that returns a subclass of AbstractAccountAuthenticator in onBind() + * Authenticator service that returns a subclass of AbstractAccountAuthenticator in onBind(). */ public class AccountAuthenticatorService extends Service { - private static BootstrapAccountAuthenticator AUTHENTICATOR = null; + private static BootstrapAccountAuthenticator authenticator = null; @Override - public IBinder onBind(Intent intent) { - return intent.getAction().equals(ACTION_AUTHENTICATOR_INTENT) ? getAuthenticator().getIBinder() : null; + public IBinder onBind(final Intent intent) { + if (intent != null && ACTION_AUTHENTICATOR_INTENT.equals(intent.getAction())) { + return getAuthenticator().getIBinder(); + } + return null; } private BootstrapAccountAuthenticator getAuthenticator() { - if (AUTHENTICATOR == null) - AUTHENTICATOR = new BootstrapAccountAuthenticator(this); - return AUTHENTICATOR; + if (authenticator == null) { + authenticator = new BootstrapAccountAuthenticator(this); + } + return authenticator; } } \ No newline at end of file diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/ActionBarAccountAuthenticatorActivity.java b/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/ActionBarAccountAuthenticatorActivity.java new file mode 100644 index 0000000..d554a38 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/ActionBarAccountAuthenticatorActivity.java @@ -0,0 +1,72 @@ +package com.donnfelker.android.bootstrap.authenticator; + +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; + + +/** + * Base class for implementing an Activity that is used to help implement an + * AbstractAccountAuthenticator. If the AbstractAccountAuthenticator needs to use an activity + * to handle the request then it can have the activity extend ActionBarAccountAuthenticatorActivity. + * The AbstractAccountAuthenticator passes in the response to the intent using the following: + *

+ *      intent.putExtra({@link android.accounts.AccountManager#KEY_ACCOUNT_AUTHENTICATOR_RESPONSE}, response);
+ * 
+ * The activity then sets the result that is to be handed to the response via + * {@link #setAccountAuthenticatorResult(android.os.Bundle)}. + * This result will be sent as the result of the request when the activity finishes. If this + * is never set or if it is set to null then error + * {@link android.accounts.AccountManager#ERROR_CODE_CANCELED} + * will be called on the response. + */ +public class ActionBarAccountAuthenticatorActivity extends ActionBarActivity { + private AccountAuthenticatorResponse accountAuthenticatorResponse = null; + private Bundle resultBundle = null; + + /** + * Set the result that is to be sent as the result of the request that caused this + * Activity to be launched. If result is null or this method is never called then + * the request will be canceled. + * + * @param result this is returned as the result of the AbstractAccountAuthenticator request + */ + public final void setAccountAuthenticatorResult(Bundle result) { + resultBundle = result; + } + + /** + * Retreives the AccountAuthenticatorResponse from either the intent of the icicle, if the + * icicle is non-zero. + * + * @param icicle the save instance data of this Activity, may be null + */ + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + accountAuthenticatorResponse = + getIntent().getParcelableExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); + + if (accountAuthenticatorResponse != null) { + accountAuthenticatorResponse.onRequestContinued(); + } + } + + /** + * Sends the result or a Constants.ERROR_CODE_CANCELED error if a result isn't present. + */ + public void finish() { + if (accountAuthenticatorResponse != null) { + // send the result bundle back if set, otherwise send an error. + if (resultBundle != null) { + accountAuthenticatorResponse.onResult(resultBundle); + } else { + accountAuthenticatorResponse.onError(AccountManager.ERROR_CODE_CANCELED, + "canceled"); + } + accountAuthenticatorResponse = null; + } + super.finish(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/ApiKeyProvider.java b/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/ApiKeyProvider.java index 500a6f0..ae4acf3 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/ApiKeyProvider.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/ApiKeyProvider.java @@ -2,37 +2,51 @@ package com.donnfelker.android.bootstrap.authenticator; -import static android.accounts.AccountManager.KEY_AUTHTOKEN; - import android.accounts.AccountManager; import android.accounts.AccountManagerFuture; import android.accounts.AccountsException; import android.app.Activity; import android.os.Bundle; -import com.donnfelker.android.bootstrap.core.Constants; -import com.google.inject.Inject; - import java.io.IOException; +import javax.inject.Inject; + +import static android.accounts.AccountManager.KEY_AUTHTOKEN; +import static com.donnfelker.android.bootstrap.core.Constants.Auth.AUTHTOKEN_TYPE; +import static com.donnfelker.android.bootstrap.core.Constants.Auth.BOOTSTRAP_ACCOUNT_TYPE; + /** * Bridge class that obtains a API key for the currently configured account */ public class ApiKeyProvider { - @Inject private Activity activity; - @Inject private AccountManager accountManager; + private AccountManager accountManager; + + public ApiKeyProvider(AccountManager accountManager) { + this.accountManager = accountManager; + } /** - * This call blocks, so shouldn't be called on the UI thread + * This call blocks, so shouldn't be called on the UI thread. + * This call is what makes the login screen pop up. If the user has + * not logged in there will no accounts in the {@link android.accounts.AccountManager} + * and therefore the Activity that is referenced in the + * {@link com.donnfelker.android.bootstrap.authenticator.BootstrapAccountAuthenticator} will get started. + * If you want to remove the authentication then you can comment out the code below and return a string such as + * "foo" and the authentication process will not be kicked off. Alternatively, you can remove this class + * completely and clean up any references to the authenticator. + * * - * @return API key to be used for authorization with a {@link com.donnfelker.android.bootstrap.core.BootstrapService} instance + * @return API key to be used for authorization with a + * {@link com.donnfelker.android.bootstrap.core.BootstrapService} instance * @throws AccountsException * @throws IOException */ - public String getAuthKey() throws AccountsException, IOException { - AccountManagerFuture accountManagerFuture = accountManager.getAuthTokenByFeatures(Constants.Auth.BOOTSTRAP_ACCOUNT_TYPE, - Constants.Auth.AUTHTOKEN_TYPE, new String[0], activity, null, null, null, null); + public String getAuthKey(final Activity activity) throws AccountsException, IOException { + final AccountManagerFuture accountManagerFuture + = accountManager.getAuthTokenByFeatures(BOOTSTRAP_ACCOUNT_TYPE, + AUTHTOKEN_TYPE, new String[0], activity, null, null, null, null); return accountManagerFuture.getResult().getString(KEY_AUTHTOKEN); } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/BootstrapAccountAuthenticator.java b/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/BootstrapAccountAuthenticator.java index 1913d2b..81e9d18 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/BootstrapAccountAuthenticator.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/BootstrapAccountAuthenticator.java @@ -1,13 +1,6 @@ package com.donnfelker.android.bootstrap.authenticator; -import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE; -import static android.accounts.AccountManager.KEY_ACCOUNT_NAME; -import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE; -import static android.accounts.AccountManager.KEY_AUTHTOKEN; -import static android.accounts.AccountManager.KEY_BOOLEAN_RESULT; -import static android.accounts.AccountManager.KEY_INTENT; -import static com.donnfelker.android.bootstrap.authenticator.BootstrapAuthenticatorActivity.PARAM_AUTHTOKEN_TYPE; import android.accounts.AbstractAccountAuthenticator; import android.accounts.Account; import android.accounts.AccountAuthenticatorResponse; @@ -16,9 +9,17 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.util.Log; import com.donnfelker.android.bootstrap.core.Constants; +import com.donnfelker.android.bootstrap.util.Ln; + +import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE; +import static android.accounts.AccountManager.KEY_ACCOUNT_NAME; +import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE; +import static android.accounts.AccountManager.KEY_AUTHTOKEN; +import static android.accounts.AccountManager.KEY_BOOLEAN_RESULT; +import static android.accounts.AccountManager.KEY_INTENT; +import static com.donnfelker.android.bootstrap.authenticator.BootstrapAuthenticatorActivity.PARAM_AUTHTOKEN_TYPE; class BootstrapAccountAuthenticator extends AbstractAccountAuthenticator { @@ -26,42 +27,49 @@ class BootstrapAccountAuthenticator extends AbstractAccountAuthenticator { private final Context context; - public BootstrapAccountAuthenticator(Context context) { + public BootstrapAccountAuthenticator(final Context context) { super(context); this.context = context; } /* - * The user has requested to add a new account to the system. We return an intent that will launch our login screen - * if the user has not logged in yet, otherwise our activity will just pass the user's credentials on to the account - * manager. + * The user has requested to add a new account to the system. We return an intent that will + * launch our login screen if the user has not logged in yet, otherwise our activity will + * just pass the user's credentials on to the account manager. */ @Override - public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, - String[] requiredFeatures, Bundle options) throws NetworkErrorException { + public Bundle addAccount(final AccountAuthenticatorResponse response, final String accountType, + final String authTokenType, final String[] requiredFeatures, + final Bundle options) throws NetworkErrorException { final Intent intent = new Intent(context, BootstrapAuthenticatorActivity.class); intent.putExtra(PARAM_AUTHTOKEN_TYPE, authTokenType); intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); + final Bundle bundle = new Bundle(); bundle.putParcelable(KEY_INTENT, intent); + return bundle; } @Override - public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) { + public Bundle confirmCredentials(final AccountAuthenticatorResponse response, + final Account account, final Bundle options) { return null; } @Override - public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { + public Bundle editProperties(final AccountAuthenticatorResponse response, + final String accountType) { return null; } /** * This method gets called when the - * {@link com.donnfelker.android.bootstrap.authenticator.ApiKeyProvider#getAuthKey()} methods gets invoked. + * {@link com.donnfelker.android.bootstrap.authenticator.ApiKeyProvider#getAuthKey()} + * methods gets invoked. * This happens on a different process, so debugging it can be a beast. + * * @param response * @param account * @param authTokenType @@ -70,35 +78,39 @@ public Bundle editProperties(AccountAuthenticatorResponse response, String accou * @throws NetworkErrorException */ @Override - public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, - Bundle options) throws NetworkErrorException { + public Bundle getAuthToken(final AccountAuthenticatorResponse response, + final Account account, final String authTokenType, + final Bundle options) throws NetworkErrorException { + + Ln.d("Attempting to get authToken"); - Log.d("AccountAuthenticator", "Attempting to get authToken"); + final String authToken = AccountManager.get(context).peekAuthToken(account, authTokenType); - String authToken = AccountManager.get(context).peekAuthToken(account, authTokenType); - Bundle bundle = new Bundle(); + final Bundle bundle = new Bundle(); bundle.putString(KEY_ACCOUNT_NAME, account.name); bundle.putString(KEY_ACCOUNT_TYPE, Constants.Auth.BOOTSTRAP_ACCOUNT_TYPE); bundle.putString(KEY_AUTHTOKEN, authToken); + return bundle; } @Override - public String getAuthTokenLabel(String authTokenType) { + public String getAuthTokenLabel(final String authTokenType) { return authTokenType.equals(Constants.Auth.AUTHTOKEN_TYPE) ? authTokenType : null; } @Override - public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) - throws NetworkErrorException { + public Bundle hasFeatures(final AccountAuthenticatorResponse response, final Account account, + final String[] features) throws NetworkErrorException { final Bundle result = new Bundle(); result.putBoolean(KEY_BOOLEAN_RESULT, false); return result; } @Override - public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, - Bundle options) { + public Bundle updateCredentials(final AccountAuthenticatorResponse response, + final Account account, final String authTokenType, + final Bundle options) { return null; } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/BootstrapAuthenticatorActivity.java b/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/BootstrapAuthenticatorActivity.java index 84d5627..4d39073 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/BootstrapAuthenticatorActivity.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/BootstrapAuthenticatorActivity.java @@ -1,4 +1,3 @@ - package com.donnfelker.android.bootstrap.authenticator; import static android.R.layout.simple_dropdown_item_1line; @@ -9,13 +8,6 @@ import static android.view.KeyEvent.ACTION_DOWN; import static android.view.KeyEvent.KEYCODE_ENTER; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; -import static com.donnfelker.android.bootstrap.core.Constants.Http.HEADER_PARSE_APP_ID; -import static com.donnfelker.android.bootstrap.core.Constants.Http.HEADER_PARSE_REST_API_KEY; -import static com.donnfelker.android.bootstrap.core.Constants.Http.PARSE_APP_ID; -import static com.donnfelker.android.bootstrap.core.Constants.Http.PARSE_REST_API_KEY; -import static com.donnfelker.android.bootstrap.core.Constants.Http.URL_AUTH; -import static com.github.kevinsawicki.http.HttpRequest.get; -import static com.github.kevinsawicki.http.HttpRequest.post; import android.accounts.Account; import android.accounts.AccountManager; import android.app.Dialog; @@ -27,7 +19,6 @@ import android.text.Html; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; -import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.View.OnKeyListener; @@ -38,39 +29,39 @@ import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; -import com.donnfelker.android.bootstrap.core.Constants; -import com.donnfelker.android.bootstrap.core.User; -import com.github.kevinsawicki.http.HttpRequest; -import com.github.kevinsawicki.wishlist.Toaster; +import com.donnfelker.android.bootstrap.Injector; +import com.donnfelker.android.bootstrap.R; import com.donnfelker.android.bootstrap.R.id; import com.donnfelker.android.bootstrap.R.layout; import com.donnfelker.android.bootstrap.R.string; +import com.donnfelker.android.bootstrap.core.BootstrapService; +import com.donnfelker.android.bootstrap.core.Constants; +import com.donnfelker.android.bootstrap.core.User; +import com.donnfelker.android.bootstrap.events.UnAuthorizedErrorEvent; import com.donnfelker.android.bootstrap.ui.TextWatcherAdapter; -import com.github.rtyley.android.sherlock.roboguice.activity.RoboSherlockAccountAuthenticatorActivity; -import com.google.gson.Gson; +import com.donnfelker.android.bootstrap.util.Ln; +import com.donnfelker.android.bootstrap.util.SafeAsyncTask; +import com.github.kevinsawicki.wishlist.Toaster; +import com.squareup.otto.Bus; +import com.squareup.otto.Subscribe; -import java.net.URLEncoder; +import javax.inject.Inject; import java.util.ArrayList; import java.util.List; -import roboguice.inject.InjectView; -import roboguice.util.Ln; -import roboguice.util.RoboAsyncTask; -import roboguice.util.Strings; - -import static com.donnfelker.android.bootstrap.core.Constants.Http.USERNAME; -import static com.donnfelker.android.bootstrap.core.Constants.Http.PASSWORD; +import butterknife.InjectView; +import butterknife.Views; +import retrofit.RetrofitError; /** * Activity to authenticate the user against an API (example API on Parse.com) */ -public class BootstrapAuthenticatorActivity extends - RoboSherlockAccountAuthenticatorActivity { +public class BootstrapAuthenticatorActivity extends ActionBarAccountAuthenticatorActivity { /** - * PARAM_CONFIRMCREDENTIALS + * PARAM_CONFIRM_CREDENTIALS */ - public static final String PARAM_CONFIRMCREDENTIALS = "confirmCredentials"; + public static final String PARAM_CONFIRM_CREDENTIALS = "confirmCredentials"; /** * PARAM_PASSWORD @@ -90,18 +81,16 @@ public class BootstrapAuthenticatorActivity extends private AccountManager accountManager; - @InjectView(id.et_email) - private AutoCompleteTextView emailText; + @Inject BootstrapService bootstrapService; + @Inject Bus bus; - @InjectView(id.et_password) - private EditText passwordText; + @InjectView(id.et_email) protected AutoCompleteTextView emailText; + @InjectView(id.et_password) protected EditText passwordText; + @InjectView(id.b_signin) protected Button signInButton; - @InjectView(id.b_signin) - private Button signinButton; + private final TextWatcher watcher = validationTextWatcher(); - private TextWatcher watcher = validationTextWatcher(); - - private RoboAsyncTask authenticationTask; + private SafeAsyncTask authenticationTask; private String authToken; private String authTokenType; @@ -132,25 +121,30 @@ public class BootstrapAuthenticatorActivity extends public void onCreate(Bundle bundle) { super.onCreate(bundle); + Injector.inject(this); + accountManager = AccountManager.get(this); + final Intent intent = getIntent(); email = intent.getStringExtra(PARAM_USERNAME); authTokenType = intent.getStringExtra(PARAM_AUTHTOKEN_TYPE); + confirmCredentials = intent.getBooleanExtra(PARAM_CONFIRM_CREDENTIALS, false); + requestNewAccount = email == null; - confirmCredentials = intent.getBooleanExtra(PARAM_CONFIRMCREDENTIALS, - false); setContentView(layout.login_activity); + Views.inject(this); + emailText.setAdapter(new ArrayAdapter(this, simple_dropdown_item_1line, userEmailAccounts())); passwordText.setOnKeyListener(new OnKeyListener() { - public boolean onKey(View v, int keyCode, KeyEvent event) { + public boolean onKey(final View v, final int keyCode, final KeyEvent event) { if (event != null && ACTION_DOWN == event.getAction() - && keyCode == KEYCODE_ENTER && signinButton.isEnabled()) { - handleLogin(signinButton); + && keyCode == KEYCODE_ENTER && signInButton.isEnabled()) { + handleLogin(signInButton); return true; } return false; @@ -159,10 +153,10 @@ public boolean onKey(View v, int keyCode, KeyEvent event) { passwordText.setOnEditorActionListener(new OnEditorActionListener() { - public boolean onEditorAction(TextView v, int actionId, - KeyEvent event) { - if (actionId == IME_ACTION_DONE && signinButton.isEnabled()) { - handleLogin(signinButton); + public boolean onEditorAction(final TextView v, final int actionId, + final KeyEvent event) { + if (actionId == IME_ACTION_DONE && signInButton.isEnabled()) { + handleLogin(signInButton); return true; } return false; @@ -172,22 +166,23 @@ public boolean onEditorAction(TextView v, int actionId, emailText.addTextChangedListener(watcher); passwordText.addTextChangedListener(watcher); - TextView signupText = (TextView) findViewById(id.tv_signup); - signupText.setMovementMethod(LinkMovementMethod.getInstance()); - signupText.setText(Html.fromHtml(getString(string.signup_link))); + final TextView signUpText = (TextView) findViewById(id.tv_signup); + signUpText.setMovementMethod(LinkMovementMethod.getInstance()); + signUpText.setText(Html.fromHtml(getString(string.signup_link))); } private List userEmailAccounts() { - Account[] accounts = accountManager.getAccountsByType("com.google"); - List emailAddresses = new ArrayList(accounts.length); - for (Account account : accounts) + final Account[] accounts = accountManager.getAccountsByType("com.google"); + final List emailAddresses = new ArrayList(accounts.length); + for (final Account account : accounts) { emailAddresses.add(account.name); + } return emailAddresses; } private TextWatcher validationTextWatcher() { return new TextWatcherAdapter() { - public void afterTextChanged(Editable gitDirEditText) { + public void afterTextChanged(final Editable gitDirEditText) { updateUIWithValidation(); } @@ -197,15 +192,22 @@ public void afterTextChanged(Editable gitDirEditText) { @Override protected void onResume() { super.onResume(); + bus.register(this); updateUIWithValidation(); } + @Override + protected void onPause() { + super.onPause(); + bus.unregister(this); + } + private void updateUIWithValidation() { - boolean populated = populated(emailText) && populated(passwordText); - signinButton.setEnabled(populated); + final boolean populated = populated(emailText) && populated(passwordText); + signInButton.setEnabled(populated); } - private boolean populated(EditText editText) { + private boolean populated(final EditText editText) { return editText.length() > 0; } @@ -216,14 +218,21 @@ protected Dialog onCreateDialog(int id) { dialog.setIndeterminate(true); dialog.setCancelable(true); dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { - public void onCancel(DialogInterface dialog) { - if (authenticationTask != null) + public void onCancel(final DialogInterface dialog) { + if (authenticationTask != null) { authenticationTask.cancel(true); + } } }); return dialog; } + @Subscribe + public void onUnAuthorizedErrorEvent(UnAuthorizedErrorEvent unAuthorizedErrorEvent) { + // Could not authorize for some reason. + Toaster.showLong(BootstrapAuthenticatorActivity.this, R.string.message_bad_credentials); + } + /** * Handles onClick event on the Submit button. Sends username/password to * the server for authentication. @@ -232,53 +241,43 @@ public void onCancel(DialogInterface dialog) { * * @param view */ - public void handleLogin(View view) { - if (authenticationTask != null) + public void handleLogin(final View view) { + if (authenticationTask != null) { return; + } - if (requestNewAccount) + if (requestNewAccount) { email = emailText.getText().toString(); + } + password = passwordText.getText().toString(); showProgress(); - authenticationTask = new RoboAsyncTask(this) { + authenticationTask = new SafeAsyncTask() { public Boolean call() throws Exception { - final String query = String.format("%s=%s&%s=%s", PARAM_USERNAME, email, PARAM_PASSWORD, password); - - HttpRequest request = get(URL_AUTH + "?" + query) - .header(HEADER_PARSE_APP_ID, PARSE_APP_ID) - .header(HEADER_PARSE_REST_API_KEY, PARSE_REST_API_KEY); + final String query = String.format("%s=%s&%s=%s", + PARAM_USERNAME, email, PARAM_PASSWORD, password); + User loginResponse = bootstrapService.authenticate(email, password); + token = loginResponse.getSessionToken(); - Log.d("Auth", "response=" + request.code()); - - if(request.ok()) { - final User model = new Gson().fromJson(Strings.toString(request.buffer()), User.class); - token = model.getSessionToken(); - } - - return request.ok(); + return true; } @Override - protected void onException(Exception e) throws RuntimeException { - Throwable cause = e.getCause() != null ? e.getCause() : e; - - String message; - // A 404 is returned as an Exception with this message - if ("Received authentication challenge is null".equals(cause - .getMessage())) - message = getResources().getString( - string.message_bad_credentials); - else - message = cause.getMessage(); - - Toaster.showLong(BootstrapAuthenticatorActivity.this, message); + protected void onException(final Exception e) throws RuntimeException { + // Retrofit Errors are handled inside of the { + if(!(e instanceof RetrofitError)) { + final Throwable cause = e.getCause() != null ? e.getCause() : e; + if(cause != null) { + Toaster.showLong(BootstrapAuthenticatorActivity.this, cause.getMessage()); + } + } } @Override - public void onSuccess(Boolean authSuccess) { + public void onSuccess(final Boolean authSuccess) { onAuthenticationResult(authSuccess); } @@ -298,7 +297,7 @@ protected void onFinally() throws RuntimeException { * * @param result */ - protected void finishConfirmCredentials(boolean result) { + protected void finishConfirmCredentials(final boolean result) { final Account account = new Account(email, Constants.Auth.BOOTSTRAP_ACCOUNT_TYPE); accountManager.setPassword(account, password); @@ -319,17 +318,23 @@ protected void finishConfirmCredentials(boolean result) { protected void finishLogin() { final Account account = new Account(email, Constants.Auth.BOOTSTRAP_ACCOUNT_TYPE); - if (requestNewAccount) + if (requestNewAccount) { accountManager.addAccountExplicitly(account, password, null); - else + } else { accountManager.setPassword(account, password); - final Intent intent = new Intent(); + } + authToken = token; + + final Intent intent = new Intent(); intent.putExtra(KEY_ACCOUNT_NAME, email); intent.putExtra(KEY_ACCOUNT_TYPE, Constants.Auth.BOOTSTRAP_ACCOUNT_TYPE); + if (authTokenType != null - && authTokenType.equals(Constants.Auth.AUTHTOKEN_TYPE)) + && authTokenType.equals(Constants.Auth.AUTHTOKEN_TYPE)) { intent.putExtra(KEY_AUTHTOKEN, authToken); + } + setAccountAuthenticatorResult(intent.getExtras()); setResult(RESULT_OK, intent); finish(); @@ -356,20 +361,22 @@ protected void showProgress() { * * @param result */ - public void onAuthenticationResult(boolean result) { - if (result) - if (!confirmCredentials) + public void onAuthenticationResult(final boolean result) { + if (result) { + if (!confirmCredentials) { finishLogin(); - else + } else { finishConfirmCredentials(true); - else { + } + } else { Ln.d("onAuthenticationResult: failed to authenticate"); - if (requestNewAccount) + if (requestNewAccount) { Toaster.showLong(BootstrapAuthenticatorActivity.this, string.message_auth_failed_new_account); - else + } else { Toaster.showLong(BootstrapAuthenticatorActivity.this, string.message_auth_failed); + } } } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/LogoutService.java b/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/LogoutService.java index 871a018..603dd97 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/LogoutService.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/authenticator/LogoutService.java @@ -4,70 +4,76 @@ import android.accounts.AccountManager; import android.accounts.AccountManagerFuture; import android.content.Context; -import android.util.Log; import com.donnfelker.android.bootstrap.core.Constants; -import com.google.inject.Inject; +import com.donnfelker.android.bootstrap.util.Ln; +import com.donnfelker.android.bootstrap.util.SafeAsyncTask; -import java.util.concurrent.Callable; -import java.util.concurrent.Executor; +import javax.inject.Inject; -import roboguice.inject.ContextSingleton; -import roboguice.util.RoboAsyncTask; -@ContextSingleton +/** + * Class used for logging a user out. + */ public class LogoutService { - @Inject protected Context context; - @Inject protected AccountManager accountManager; - + protected final Context context; + protected final AccountManager accountManager; + @Inject + public LogoutService(final Context context, final AccountManager accountManager) { + this.context = context; + this.accountManager = accountManager; + } public void logout(final Runnable onSuccess) { - new LogoutTask(context, onSuccess).execute(); } - private static class LogoutTask extends RoboAsyncTask { + private static class LogoutTask extends SafeAsyncTask { - private Runnable onSuccess; + private final Context taskContext; + private final Runnable onSuccess; - protected LogoutTask(Context context, Runnable onSuccess) { - super(context); + protected LogoutTask(final Context context, final Runnable onSuccess) { + this.taskContext = context; this.onSuccess = onSuccess; } @Override public Boolean call() throws Exception { - final Account[] accounts = AccountManager.get(context).getAccountsByType(Constants.Auth.BOOTSTRAP_ACCOUNT_TYPE); - if(accounts.length > 0) { - AccountManagerFuture removeAccountFuture = AccountManager.get(context).removeAccount - (accounts[0], null, null); - if(removeAccountFuture.getResult() == true) { - return true; - } else { - return false; + final AccountManager accountManagerWithContext = AccountManager.get(taskContext); + if (accountManagerWithContext != null) { + final Account[] accounts = accountManagerWithContext + .getAccountsByType(Constants.Auth.BOOTSTRAP_ACCOUNT_TYPE); + if (accounts.length > 0) { + final AccountManagerFuture removeAccountFuture + = accountManagerWithContext.removeAccount(accounts[0], null, null); + + return removeAccountFuture.getResult(); } + } else { + Ln.w("accountManagerWithContext is null"); } return false; } @Override - protected void onSuccess(Boolean accountWasRemoved) throws Exception { + protected void onSuccess(final Boolean accountWasRemoved) throws Exception { super.onSuccess(accountWasRemoved); - Log.d("LOGOUT_SERVICE", "Logout succeeded:" + accountWasRemoved); + Ln.d("Logout succeeded: %s", accountWasRemoved); onSuccess.run(); } @Override - protected void onException(Exception e) throws RuntimeException { + protected void onException(final Exception e) throws RuntimeException { super.onException(e); - Log.e("LOGOUT_SERVICE", "Logout failed.", e.getCause()); - } - }; + Ln.e(e.getCause(), "Logout failed."); + } + } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/ApiError.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/ApiError.java new file mode 100644 index 0000000..317447a --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/ApiError.java @@ -0,0 +1,24 @@ +package com.donnfelker.android.bootstrap.core; + + +public class ApiError { + private int code; + private String error; + + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/AvatarLoader.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/AvatarLoader.java deleted file mode 100644 index 2d1a3b2..0000000 --- a/app/src/main/java/com/donnfelker/android/bootstrap/core/AvatarLoader.java +++ /dev/null @@ -1,408 +0,0 @@ -package com.donnfelker.android.bootstrap.core; - -import static android.graphics.Bitmap.CompressFormat.PNG; -import static android.graphics.Bitmap.Config.ARGB_8888; -import static android.view.View.VISIBLE; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.text.TextUtils; -import android.util.Log; -import android.widget.ImageView; - -import com.actionbarsherlock.app.ActionBar; -import com.donnfelker.android.bootstrap.R; -import com.github.kevinsawicki.http.HttpRequest; -import com.google.inject.Inject; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicReference; - -import roboguice.util.RoboAsyncTask; - -/** - * Avatar utilities - */ -public class AvatarLoader { - - private static final String TAG = "AvatarLoader"; - - private static final float CORNER_RADIUS_IN_DIP = 3; - - private static final int CACHE_SIZE = 75; - - private static abstract class FetchAvatarTask extends - RoboAsyncTask { - - private static final Executor EXECUTOR = Executors - .newFixedThreadPool(1); - - private FetchAvatarTask(Context context) { - super(context, EXECUTOR); - } - - @Override - protected void onException(Exception e) throws RuntimeException { - Log.d(TAG, "Avatar load failed", e); - } - } - - private final float cornerRadius; - - private final Map loaded = new LinkedHashMap( - CACHE_SIZE, 1.0F) { - - private static final long serialVersionUID = -4191624209581976720L; - - @Override - protected boolean removeEldestEntry( - Map.Entry eldest) { - return size() >= CACHE_SIZE; - } - }; - - private final Context context; - - private final File avatarDir; - - private final Drawable loadingAvatar; - - private final BitmapFactory.Options options; - - /** - * Create avatar helper - * - * @param context - */ - @Inject - public AvatarLoader(final Context context) { - this.context = context; - - loadingAvatar = context.getResources().getDrawable(R.drawable.gravatar_icon); - - avatarDir = new File(context.getCacheDir(), "avatars/" + context.getPackageName()); - if (!avatarDir.isDirectory()) - avatarDir.mkdirs(); - - float density = context.getResources().getDisplayMetrics().density; - cornerRadius = CORNER_RADIUS_IN_DIP * density; - - options = new BitmapFactory.Options(); - options.inDither = false; - options.inPreferredConfig = ARGB_8888; - } - - /** - * Get image for user - * - * @param user - * @return image - */ - protected BitmapDrawable getImage(final User user) { - File avatarFile = new File(avatarDir, user.getObjectId()); - - if (!avatarFile.exists() || avatarFile.length() == 0) - return null; - - Bitmap bitmap = decode(avatarFile); - if (bitmap != null) - return new BitmapDrawable(context.getResources(), bitmap); - else { - avatarFile.delete(); - return null; - } - } - -// /** -// * Get image for user -// * -// * @param user -// * @return image -// */ -// protected BitmapDrawable getImage(final CommitUser user) { -// File avatarFile = new File(avatarDir, user.getEmail()); -// -// if (!avatarFile.exists() || avatarFile.length() == 0) -// return null; -// -// Bitmap bitmap = decode(avatarFile); -// if (bitmap != null) -// return new BitmapDrawable(context.getResources(), bitmap); -// else { -// avatarFile.delete(); -// return null; -// } -// } - - /** - * Decode file to bitmap - * - * @param file - * @return bitmap - */ - protected Bitmap decode(final File file) { - return BitmapFactory.decodeFile(file.getAbsolutePath(), options); - } - - /** - * Fetch avatar from URL - * - * @param url - * @param userId - * @return bitmap - */ - protected BitmapDrawable fetchAvatar(final String url, final String userId) { - File rawAvatar = new File(avatarDir, userId + "-raw"); - HttpRequest request = HttpRequest.get(url); - if (request.ok()) - request.receive(rawAvatar); - - if (!rawAvatar.exists() || rawAvatar.length() == 0) - return null; - - Bitmap bitmap = decode(rawAvatar); - if (bitmap == null) { - rawAvatar.delete(); - return null; - } - - bitmap = ImageUtils.roundCorners(bitmap, cornerRadius); - if (bitmap == null) { - rawAvatar.delete(); - return null; - } - - File roundedAvatar = new File(avatarDir, userId.toString()); - FileOutputStream output = null; - try { - output = new FileOutputStream(roundedAvatar); - if (bitmap.compress(PNG, 100, output)) - return new BitmapDrawable(context.getResources(), bitmap); - else - return null; - } catch (IOException e) { - Log.d(TAG, "Exception writing rounded avatar", e); - return null; - } finally { - if (output != null) - try { - output.close(); - } catch (IOException e) { - // Ignored - } - rawAvatar.delete(); - } - } - - /** - * Sets the logo on the {@link com.actionbarsherlock.app.ActionBar} to the user's avatar. - * - * @param actionBar - * @param user - * @return this helper - */ - public AvatarLoader bind(final ActionBar actionBar, final User user) { - return bind(actionBar, new AtomicReference(user)); - } - - /** - * Sets the logo on the {@link ActionBar} to the user's avatar. - * - * @param actionBar - * @param userReference - * @return this helper - */ - public AvatarLoader bind(final ActionBar actionBar, - final AtomicReference userReference) { - if (userReference == null) - return this; - - final User user = userReference.get(); - if (user == null) - return this; - - final String avatarUrl = user.getAvatarUrl(); - if (TextUtils.isEmpty(avatarUrl)) - return this; - - final String userId = user.getObjectId(); - - BitmapDrawable loadedImage = loaded.get(userId); - if (loadedImage != null) { - actionBar.setLogo(loadedImage); - return this; - } - - new FetchAvatarTask(context) { - - @Override - public BitmapDrawable call() throws Exception { - final BitmapDrawable image = getImage(user); - if (image != null) - return image; - else - return fetchAvatar(avatarUrl, userId.toString()); - } - - @Override - protected void onSuccess(BitmapDrawable image) throws Exception { - final User current = userReference.get(); - if (current != null && userId.equals(current.getObjectId())) - actionBar.setLogo(image); - } - }.execute(); - - return this; - } - - private AvatarLoader setImage(final Drawable image, final ImageView view) { - return setImage(image, view, null); - } - - private AvatarLoader setImage(final Drawable image, final ImageView view, - Object tag) { - view.setImageDrawable(image); - view.setTag(R.id.iv_avatar, tag); - view.setVisibility(VISIBLE); - return this; - } - - private String getAvatarUrl(String id) { - if (!TextUtils.isEmpty(id)) - return "https://secure.gravatar.com/avatar/" + id + "?d=404"; - else - return null; - } - - private String getAvatarUrl(User user) { - String avatarUrl = user.getAvatarUrl(); - if (TextUtils.isEmpty(avatarUrl)) { - String gravatarId = user.getGravatarId(); - if (TextUtils.isEmpty(gravatarId)) - gravatarId = GravatarUtils.getHash(user.getUsername()); - avatarUrl = getAvatarUrl(gravatarId); - } - return avatarUrl; - } - -// private String getAvatarUrl(CommitUser user) { -// return getAvatarUrl(GravatarUtils.getHash(user.getEmail())); -// } - - /** - * Bind view to image at URL - * - * @param view - * @param user - * @return this helper - */ - public AvatarLoader bind(final ImageView view, final User user) { - if (user == null) - return setImage(loadingAvatar, view); - - String avatarUrl = getAvatarUrl(user); - - if (TextUtils.isEmpty(avatarUrl)) - return setImage(loadingAvatar, view); - - final String userId = user.getObjectId(); - - BitmapDrawable loadedImage = loaded.get(userId); - if (loadedImage != null) - return setImage(loadedImage, view); - - setImage(loadingAvatar, view, userId); - - final String loadUrl = avatarUrl; - new FetchAvatarTask(context) { - - @Override - public BitmapDrawable call() throws Exception { - if (!userId.equals(view.getTag(R.id.iv_avatar))) - return null; - - final BitmapDrawable image = getImage(user); - if (image != null) - return image; - else - return fetchAvatar(loadUrl, userId.toString()); - } - - @Override - protected void onSuccess(final BitmapDrawable image) - throws Exception { - if (image == null) - return; - loaded.put(userId, image); - if (userId.equals(view.getTag(R.id.iv_avatar))) - setImage(image, view); - } - - }.execute(); - - return this; - } - -// /** -// * Bind view to image at URL -// * -// * @param view -// * @param user -// * @return this helper -// */ -// public AvatarLoader bind(final ImageView view, final CommitUser user) { -// if (user == null) -// return setImage(loadingAvatar, view); -// -// String avatarUrl = getAvatarUrl(user); -// -// if (TextUtils.isEmpty(avatarUrl)) -// return setImage(loadingAvatar, view); -// -// final String userId = user.getEmail(); -// -// BitmapDrawable loadedImage = loaded.get(userId); -// if (loadedImage != null) -// return setImage(loadedImage, view); -// -// setImage(loadingAvatar, view, userId); -// -// final String loadUrl = avatarUrl; -// new FetchAvatarTask(context) { -// -// @Override -// public BitmapDrawable call() throws Exception { -// if (!userId.equals(view.getTag(id.iv_avatar))) -// return null; -// -// final BitmapDrawable image = getImage(user); -// if (image != null) -// return image; -// else -// return fetchAvatar(loadUrl, userId); -// } -// -// @Override -// protected void onSuccess(final BitmapDrawable image) -// throws Exception { -// if (image == null) -// return; -// loaded.put(userId, image); -// if (userId.equals(view.getTag(id.iv_avatar))) -// setImage(image, view); -// } -// -// }.execute(); -// -// return this; -// } -} - diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/BootstrapService.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/BootstrapService.java index 8ef2917..e592312 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/core/BootstrapService.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/BootstrapService.java @@ -1,232 +1,71 @@ package com.donnfelker.android.bootstrap.core; -import static com.donnfelker.android.bootstrap.core.Constants.Http.HEADER_PARSE_APP_ID; -import static com.donnfelker.android.bootstrap.core.Constants.Http.HEADER_PARSE_REST_API_KEY; -import static com.donnfelker.android.bootstrap.core.Constants.Http.PARSE_APP_ID; -import static com.donnfelker.android.bootstrap.core.Constants.Http.PARSE_REST_API_KEY; -import static com.donnfelker.android.bootstrap.core.Constants.Http.URL_CHECKINS; -import static com.donnfelker.android.bootstrap.core.Constants.Http.URL_NEWS; -import static com.donnfelker.android.bootstrap.core.Constants.Http.URL_USERS; - -import com.github.kevinsawicki.http.HttpRequest; -import com.github.kevinsawicki.http.HttpRequest.HttpRequestException; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonParseException; - -import java.io.IOException; -import java.io.Reader; -import java.util.Collections; import java.util.List; +import retrofit.RestAdapter; + /** * Bootstrap API service */ public class BootstrapService { - private UserAgentProvider userAgentProvider; - - /** - * GSON instance to use for all request with date format set up for proper parsing. - */ - public static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd").create(); - - /** - * You can also configure GSON with different naming policies for your API. Maybe your api is Rails - * api and all json values are lower case with an underscore, like this "first_name" instead of "firstName". - * You can configure GSON as such below. - * - * public static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd").setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES).create(); - * - */ - - - /** - * Read and connect timeout in milliseconds - */ - private static final int TIMEOUT = 30 * 1000; - - - private static class UsersWrapper { - - private List results; - } - - private static class NewsWrapper { - - private List results; - } - - private static class CheckInWrapper { - - private List results; - - } - - private static class JsonException extends IOException { - - private static final long serialVersionUID = 3774706606129390273L; - - /** - * Create exception from {@link JsonParseException} - * - * @param cause - */ - public JsonException(JsonParseException cause) { - super(cause.getMessage()); - initCause(cause); - } - } - - - private final String apiKey; - private final String username; - private final String password; + private RestAdapter restAdapter; /** * Create bootstrap service - * - * @param username - * @param password + * Default CTOR */ - public BootstrapService(final String username, final String password) { - this.username = username; - this.password = password; - this.apiKey = null; + public BootstrapService() { } /** * Create bootstrap service * - * @param userAgentProvider - * @param apiKey + * @param restAdapter The RestAdapter that allows HTTP Communication. */ - public BootstrapService(final String apiKey, final UserAgentProvider userAgentProvider) { - this.userAgentProvider = userAgentProvider; - this.username = null; - this.password = null; - this.apiKey = apiKey; + public BootstrapService(RestAdapter restAdapter) { + this.restAdapter = restAdapter; } - /** - * Execute request - * - * @param request - * @return request - * @throws IOException - */ - protected HttpRequest execute(HttpRequest request) throws IOException { - if (!configure(request).ok()) - throw new IOException("Unexpected response code: " + request.code()); - return request; - } - - private HttpRequest configure(final HttpRequest request) { - request.connectTimeout(TIMEOUT).readTimeout(TIMEOUT); - request.userAgent(userAgentProvider.get()); - - if(isPostOrPut(request)) - request.contentType(Constants.Http.CONTENT_TYPE_JSON); // All PUT & POST requests to Parse.com api must be in JSON - https://www.parse.com/docs/rest#general-requests - - return addCredentialsTo(request); + private UserService getUserService() { + return getRestAdapter().create(UserService.class); } - private boolean isPostOrPut(HttpRequest request) { - return request.getConnection().getRequestMethod().equals(HttpRequest.METHOD_POST) - || request.getConnection().getRequestMethod().equals(HttpRequest.METHOD_PUT); - + private NewsService getNewsService() { + return getRestAdapter().create(NewsService.class); } - private HttpRequest addCredentialsTo(HttpRequest request) { - - // Required params for - request.header(HEADER_PARSE_REST_API_KEY, PARSE_REST_API_KEY ); - request.header(HEADER_PARSE_APP_ID, PARSE_APP_ID); - - /** - * NOTE: This may be where you want to add a header for the api token that was saved when you - * logged in. In the bootstrap sample this is where we are saving the session id as the token. - * If you actually had received a token you'd take the "apiKey" (aka: token) and add it to the - * header or form values before you make your requests. - */ - - /** - * Add the user name and password to the request here if your service needs username or password for each - * request. You can do this like this: - * request.basic("myusername", "mypassword"); - */ - - return request; + private CheckInService getCheckInService() { + return getRestAdapter().create(CheckInService.class); } - private V fromJson(HttpRequest request, Class target) throws IOException { - Reader reader = request.bufferedReader(); - try { - return GSON.fromJson(reader, target); - } catch (JsonParseException e) { - throw new JsonException(e); - } finally { - try { - reader.close(); - } catch (IOException ignored) { - // Ignored - } - } + private RestAdapter getRestAdapter() { + return restAdapter; } /** - * Get all bootstrap Users that exist on Parse.com - * - * @return non-null but possibly empty list of bootstrap - * @throws IOException + * Get all bootstrap News that exists on Parse.com */ - public List getUsers() throws IOException { - try { - HttpRequest request = execute(HttpRequest.get(URL_USERS)); - UsersWrapper response = fromJson(request, UsersWrapper.class); - if (response != null && response.results != null) - return response.results; - return Collections.emptyList(); - } catch (HttpRequestException e) { - throw e.getCause(); - } + public List getNews() { + return getNewsService().getNews().getResults(); } /** - * Get all bootstrap News that exists on Parse.com - * - * @return non-null but possibly empty list of bootstrap - * @throws IOException + * Get all bootstrap Users that exist on Parse.com */ - public List getNews() throws IOException { - try { - HttpRequest request = execute(HttpRequest.get(URL_NEWS)); - NewsWrapper response = fromJson(request, NewsWrapper.class); - if (response != null && response.results != null) - return response.results; - return Collections.emptyList(); - } catch (HttpRequestException e) { - throw e.getCause(); - } + public List getUsers() { + return getUserService().getUsers().getResults(); } /** * Get all bootstrap Checkins that exists on Parse.com - * - * @return non-null but possibly empty list of bootstrap - * @throws IOException */ - public List getCheckIns() throws IOException { - try { - HttpRequest request = execute(HttpRequest.get(URL_CHECKINS)); - CheckInWrapper response = fromJson(request, CheckInWrapper.class); - if (response != null && response.results != null) - return response.results; - return Collections.emptyList(); - } catch (HttpRequestException e) { - throw e.getCause(); - } + public List getCheckIns() { + return getCheckInService().getCheckIns().getResults(); } -} + public User authenticate(String email, String password) { + return getUserService().authenticate(email, password); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/CheckIn.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/CheckIn.java index 4bee35e..fda0cd9 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/core/CheckIn.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/CheckIn.java @@ -6,12 +6,11 @@ public class CheckIn { private String name; private String objectId; - public Location getLocation() { return location; } - public void setLocation(Location location) { + public void setLocation(final Location location) { this.location = location; } @@ -19,7 +18,7 @@ public String getName() { return name; } - public void setName(String name) { + public void setName(final String name) { this.name = name; } @@ -27,7 +26,7 @@ public String getObjectId() { return objectId; } - public void setObjectId(String objectId) { + public void setObjectId(final String objectId) { this.objectId = objectId; } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/CheckInService.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/CheckInService.java new file mode 100644 index 0000000..a455c2c --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/CheckInService.java @@ -0,0 +1,11 @@ +package com.donnfelker.android.bootstrap.core; + +import java.util.List; + +import retrofit.http.GET; + +public interface CheckInService { + + @GET(Constants.Http.URL_CHECKINS_FRAG) + CheckInWrapper getCheckIns(); +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/CheckInWrapper.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/CheckInWrapper.java new file mode 100644 index 0000000..0478eae --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/CheckInWrapper.java @@ -0,0 +1,11 @@ +package com.donnfelker.android.bootstrap.core; + +import java.util.List; + +public class CheckInWrapper { + private List results; + + public List getResults() { + return results; + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/Constants.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/Constants.java index 81954ce..67a8a57 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/core/Constants.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/Constants.java @@ -5,9 +5,10 @@ /** * Bootstrap constants */ -public class Constants { +public final class Constants { + private Constants() {} - public static class Auth { + public static final class Auth { private Auth() {} /** @@ -35,35 +36,48 @@ private Auth() {} * All HTTP is done through a REST style API built for demonstration purposes on Parse.com * Thanks to the nice people at Parse for creating such a nice system for us to use for bootstrap! */ - public static class Http { + public static final class Http { private Http() {} - /** * Base URL for all requests */ public static final String URL_BASE = "https://api.parse.com"; + /** * Authentication URL */ - public static final String URL_AUTH = URL_BASE + "/1/login"; + public static final String URL_AUTH_FRAG = "/1/login"; + public static final String URL_AUTH = URL_BASE + URL_AUTH_FRAG; /** * List Users URL */ - public static final String URL_USERS = URL_BASE + "/1/users"; + public static final String URL_USERS_FRAG = "/1/users"; + public static final String URL_USERS = URL_BASE + URL_USERS_FRAG; + /** * List News URL */ - public static final String URL_NEWS = URL_BASE + "/1/classes/News"; + public static final String URL_NEWS_FRAG = "/1/classes/News"; + public static final String URL_NEWS = URL_BASE + URL_NEWS_FRAG; + /** * List Checkin's URL */ - public static final String URL_CHECKINS = URL_BASE + "/1/classes/Locations"; + public static final String URL_CHECKINS_FRAG = "/1/classes/Locations"; + public static final String URL_CHECKINS = URL_BASE + URL_CHECKINS_FRAG; + + /** + * PARAMS for auth + */ + public static final String PARAM_USERNAME = "username"; + public static final String PARAM_PASSWORD = "password"; + public static final String PARSE_APP_ID = "zHb2bVia6kgilYRWWdmTiEJooYA17NnkBSUVsr4H"; public static final String PARSE_REST_API_KEY = "N2kCY1T3t3Jfhf9zpJ5MCURn3b25UpACILhnf5u9"; @@ -78,7 +92,7 @@ private Http() {} } - public static class Extra { + public static final class Extra { private Extra() {} public static final String NEWS_ITEM = "news_item"; @@ -87,7 +101,7 @@ private Extra() {} } - public static class Intent { + public static final class Intent { private Intent() {} /** @@ -97,6 +111,13 @@ private Intent() {} } + public static class Notification { + private Notification() { + } + + public static final int TIMER_NOTIFICATION_ID = 1000; // Why 1000? Why not? :) + } + } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/GravatarUtils.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/GravatarUtils.java index 5ebb517..0b2cd19 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/core/GravatarUtils.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/GravatarUtils.java @@ -1,8 +1,6 @@ package com.donnfelker.android.bootstrap.core; - -import static java.util.Locale.US; import android.text.TextUtils; import java.io.UnsupportedEncodingException; @@ -11,6 +9,8 @@ import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import static java.util.Locale.US; + /** * Helper to get a gravatar hash for an email */ @@ -32,22 +32,23 @@ public class GravatarUtils { public static final String CHARSET = "CP1252"; //$NON-NLS-1$ private static String digest(final String value) { - byte[] digested; + final byte[] digested; try { digested = MessageDigest.getInstance(HASH_ALGORITHM).digest( value.getBytes(CHARSET)); - } catch (NoSuchAlgorithmException e) { + } catch (final NoSuchAlgorithmException e) { return null; - } catch (UnsupportedEncodingException e) { + } catch (final UnsupportedEncodingException e) { return null; } - String hashed = new BigInteger(1, digested).toString(16); - int padding = HASH_LENGTH - hashed.length(); - if (padding == 0) + final String hashed = new BigInteger(1, digested).toString(16); + final int padding = HASH_LENGTH - hashed.length(); + if (padding == 0) { return hashed; + } - char[] zeros = new char[padding]; + final char[] zeros = new char[padding]; Arrays.fill(zeros, '0'); return new StringBuilder(HASH_LENGTH).append(zeros).append(hashed) .toString(); @@ -59,10 +60,11 @@ private static String digest(final String value) { * @param email * @return hash */ - public static String getHash(String email) { - if (TextUtils.isEmpty(email)) + public static String getHash(final String email) { + if (TextUtils.isEmpty(email)) { return null; - email = email.trim().toLowerCase(US); - return email.length() > 0 ? digest(email) : null; + } + final String tmpEmail = email.trim().toLowerCase(US); + return tmpEmail.length() > 0 ? digest(tmpEmail) : null; } } \ No newline at end of file diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/ImageUtils.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/ImageUtils.java index 08f9543..e56cc6c 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/core/ImageUtils.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/ImageUtils.java @@ -1,10 +1,6 @@ package com.donnfelker.android.bootstrap.core; - -import static android.graphics.Bitmap.Config.ARGB_8888; -import static android.graphics.Color.WHITE; -import static android.graphics.PorterDuff.Mode.DST_IN; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; @@ -12,19 +8,29 @@ import android.graphics.Point; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; -import android.util.Log; import android.widget.ImageView; +import com.donnfelker.android.bootstrap.util.Ln; + import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; +import static android.graphics.Bitmap.Config.ARGB_8888; +import static android.graphics.Color.WHITE; +import static android.graphics.PorterDuff.Mode.DST_IN; + /** * Image utilities */ -public class ImageUtils { +public final class ImageUtils { - private static final String TAG = "ImageUtils"; + /** + * This is a utility class. + */ + private ImageUtils() { + //never called + } /** * Get a bitmap from the image path @@ -43,7 +49,7 @@ public static Bitmap getBitmap(final String imagePath) { * @param sampleSize * @return bitmap or null if read fails */ - public static Bitmap getBitmap(final String imagePath, int sampleSize) { + public static Bitmap getBitmap(final String imagePath, final int sampleSize) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inDither = false; options.inSampleSize = sampleSize; @@ -54,14 +60,14 @@ public static Bitmap getBitmap(final String imagePath, int sampleSize) { return BitmapFactory.decodeFileDescriptor(file.getFD(), null, options); } catch (IOException e) { - Log.d(TAG, e.getMessage(), e); + Ln.d(e, "Could not get cached bitmap."); return null; } finally { if (file != null) try { file.close(); } catch (IOException e) { - Log.d(TAG, e.getMessage(), e); + Ln.d(e, "Could not get cached bitmap."); } } } @@ -81,16 +87,17 @@ public static Point getSize(final String imagePath) { file = new RandomAccessFile(imagePath, "r"); BitmapFactory.decodeFileDescriptor(file.getFD(), null, options); return new Point(options.outWidth, options.outHeight); - } catch (IOException e) { - Log.d(TAG, e.getMessage(), e); + } catch (final IOException e) { + Ln.d(e, "Could not get size."); return null; } finally { - if (file != null) + if (file != null) { try { file.close(); - } catch (IOException e) { - Log.d(TAG, e.getMessage(), e); + } catch (final IOException e) { + Ln.d(e, "Could not get size."); } + } } } @@ -102,8 +109,8 @@ public static Point getSize(final String imagePath) { * @param height * @return image */ - public static Bitmap getBitmap(final String imagePath, int width, int height) { - Point size = getSize(imagePath); + public static Bitmap getBitmap(final String imagePath, final int width, final int height) { + final Point size = getSize(imagePath); int currWidth = size.x; int currHeight = size.y; @@ -125,7 +132,7 @@ public static Bitmap getBitmap(final String imagePath, int width, int height) { * @param height * @return image */ - public static Bitmap getBitmap(final File image, int width, int height) { + public static Bitmap getBitmap(final File image, final int width, final int height) { return getBitmap(image.getAbsolutePath(), width, height); } @@ -158,9 +165,10 @@ public static void setImage(final String imagePath, final ImageView view) { * @param view */ public static void setImage(final File image, final ImageView view) { - Bitmap bitmap = getBitmap(image); - if (bitmap != null) + final Bitmap bitmap = getBitmap(image); + if (bitmap != null) { view.setImageBitmap(bitmap); + } } /** @@ -171,20 +179,20 @@ public static void setImage(final File image, final ImageView view) { * @return rounded corner bitmap */ public static Bitmap roundCorners(final Bitmap source, final float radius) { - int width = source.getWidth(); - int height = source.getHeight(); + final int width = source.getWidth(); + final int height = source.getHeight(); - Paint paint = new Paint(); + final Paint paint = new Paint(); paint.setAntiAlias(true); paint.setColor(WHITE); - Bitmap clipped = Bitmap.createBitmap(width, height, ARGB_8888); + final Bitmap clipped = Bitmap.createBitmap(width, height, ARGB_8888); Canvas canvas = new Canvas(clipped); canvas.drawRoundRect(new RectF(0, 0, width, height), radius, radius, paint); paint.setXfermode(new PorterDuffXfermode(DST_IN)); - Bitmap rounded = Bitmap.createBitmap(width, height, ARGB_8888); + final Bitmap rounded = Bitmap.createBitmap(width, height, ARGB_8888); canvas = new Canvas(rounded); canvas.drawBitmap(source, 0, 0, null); canvas.drawBitmap(clipped, 0, 0, paint); diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/IntentFactory.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/IntentFactory.java new file mode 100644 index 0000000..f7504b2 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/IntentFactory.java @@ -0,0 +1,5 @@ +package com.donnfelker.android.bootstrap.core; + +public class IntentFactory { + //TODO implement an Activity and Fragment delegate pattern +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/Location.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/Location.java index 3f511af..fe9f6e3 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/core/Location.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/Location.java @@ -9,7 +9,7 @@ public double getLatitude() { return latitude; } - public void setLatitude(double latitude) { + public void setLatitude(final double latitude) { this.latitude = latitude; } @@ -17,7 +17,7 @@ public double getLongitude() { return longitude; } - public void setLongitude(double longitude) { + public void setLongitude(final double longitude) { this.longitude = longitude; } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/News.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/News.java index 391b517..7036bc4 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/core/News.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/News.java @@ -14,7 +14,7 @@ public String getTitle() { return title; } - public void setTitle(String title) { + public void setTitle(final String title) { this.title = title; } @@ -22,7 +22,7 @@ public String getContent() { return content; } - public void setContent(String content) { + public void setContent(final String content) { this.content = content; } @@ -30,7 +30,7 @@ public String getObjectId() { return objectId; } - public void setObjectId(String objectId) { + public void setObjectId(final String objectId) { this.objectId = objectId; } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/NewsService.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/NewsService.java new file mode 100644 index 0000000..cee18c4 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/NewsService.java @@ -0,0 +1,14 @@ +package com.donnfelker.android.bootstrap.core; + +import retrofit.http.GET; + + +/** + * Interface for defining the news service to communicate with Parse.com + */ +public interface NewsService { + + @GET(Constants.Http.URL_NEWS_FRAG) + NewsWrapper getNews(); + +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/NewsWrapper.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/NewsWrapper.java new file mode 100644 index 0000000..4d85792 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/NewsWrapper.java @@ -0,0 +1,12 @@ +package com.donnfelker.android.bootstrap.core; + + +import java.util.List; + +public class NewsWrapper { + private List results; + + public List getResults() { + return results; + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/PauseTimerEvent.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/PauseTimerEvent.java new file mode 100644 index 0000000..bd98769 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/PauseTimerEvent.java @@ -0,0 +1,7 @@ +package com.donnfelker.android.bootstrap.core; + +/** + * Marker class for Otto for a pause event for the timer. + */ +public class PauseTimerEvent { +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/PostFromAnyThreadBus.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/PostFromAnyThreadBus.java new file mode 100644 index 0000000..e83a76e --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/PostFromAnyThreadBus.java @@ -0,0 +1,57 @@ +package com.donnfelker.android.bootstrap.core; + +import android.os.Handler; +import android.os.Looper; + +import com.donnfelker.android.bootstrap.util.Ln; +import com.squareup.otto.Bus; +import com.squareup.otto.ThreadEnforcer; + +/** + * This message bus allows you to post a message from any thread and it will get handled and then + * posted to the main thread for you. + */ +public class PostFromAnyThreadBus extends Bus +{ + public PostFromAnyThreadBus() + { + super(ThreadEnforcer.MAIN); + } + + @Override + public void post(final Object event) + { + if (Looper.myLooper() != Looper.getMainLooper()) + { + // We're not in the main loop, so we need to get into it. + (new Handler(Looper.getMainLooper())).post(new Runnable() + { + @Override + public void run() + { + // We're now in the main loop, we can post now + PostFromAnyThreadBus.super.post(event); + } + }); + } + else + { + super.post(event); + } + } + + @Override + public void unregister(final Object object) + { + // Lots of edge cases with register/unregister that sometimes throw. + try + { + super.unregister(object); + } + catch (IllegalArgumentException e) + { + // TODO: use Crashlytics unhandled exception logging + Ln.e(e); + } + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/RestAdapterRequestInterceptor.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/RestAdapterRequestInterceptor.java new file mode 100644 index 0000000..3097088 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/RestAdapterRequestInterceptor.java @@ -0,0 +1,32 @@ +package com.donnfelker.android.bootstrap.core; + + +import static com.donnfelker.android.bootstrap.core.Constants.Http.HEADER_PARSE_APP_ID; +import static com.donnfelker.android.bootstrap.core.Constants.Http.HEADER_PARSE_REST_API_KEY; +import static com.donnfelker.android.bootstrap.core.Constants.Http.PARSE_APP_ID; +import static com.donnfelker.android.bootstrap.core.Constants.Http.PARSE_REST_API_KEY; + +import retrofit.RequestInterceptor; + +public class RestAdapterRequestInterceptor implements RequestInterceptor { + + private UserAgentProvider userAgentProvider; + + public RestAdapterRequestInterceptor(UserAgentProvider userAgentProvider) { + this.userAgentProvider = userAgentProvider; + } + + @Override + public void intercept(RequestFacade request) { + + // Add header to set content type of JSON + request.addHeader("Content-Type", "application/json"); + + // Add auth info for PARSE, normally this is where you'd add your auth info for this request (if needed). + request.addHeader(HEADER_PARSE_REST_API_KEY, PARSE_REST_API_KEY); + request.addHeader(HEADER_PARSE_APP_ID, PARSE_APP_ID); + + // Add the user agent to the request. + request.addHeader("User-Agent", userAgentProvider.get()); + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/RestErrorHandler.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/RestErrorHandler.java new file mode 100644 index 0000000..6cc812e --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/RestErrorHandler.java @@ -0,0 +1,75 @@ +package com.donnfelker.android.bootstrap.core; + +import com.donnfelker.android.bootstrap.events.NetworkErrorEvent; +import com.donnfelker.android.bootstrap.events.RestAdapterErrorEvent; +import com.donnfelker.android.bootstrap.events.UnAuthorizedErrorEvent; +import com.squareup.otto.Bus; + +import retrofit.ErrorHandler; +import retrofit.RetrofitError; + +public class RestErrorHandler implements ErrorHandler { + + public static final int HTTP_NOT_FOUND = 404; + public static final int INVALID_LOGIN_PARAMETERS = 101; + + private Bus bus; + + public RestErrorHandler(Bus bus) { + this.bus = bus; + } + + @Override + public Throwable handleError(RetrofitError cause) { + if(cause != null) { + if (cause.isNetworkError()) { + bus.post(new NetworkErrorEvent(cause)); + } else if(isUnAuthorized(cause)) { + bus.post(new UnAuthorizedErrorEvent(cause)); + } else { + bus.post(new RestAdapterErrorEvent(cause)); + } + } + + // Example of how you'd check for a unauthorized result + // if (cause != null && cause.getStatus() == 401) { + // return new UnauthorizedException(cause); + // } + + // You could also put some generic error handling in here so you can start + // getting analytics on error rates/etc. Perhaps ship your logs off to + // Splunk, Loggly, etc + + return cause; + } + + /** + * If a user passes an incorrect username/password combo in we could + * get a unauthorized error back from the API. On parse.com this means + * we get back a HTTP 404 with an error as JSON in the body as such: + * + * { + * code: 101, + * error: "invalid login parameters" + * } + * + * } + * + * Therefore we need to check for the 101 and the 404. + * + * @param cause The initial error. + * @return + */ + private boolean isUnAuthorized(RetrofitError cause) { + boolean authFailed = false; + + if(cause.getResponse().getStatus() == HTTP_NOT_FOUND) { + final ApiError err = (ApiError) cause.getBodyAs(ApiError.class); + if(err != null && err.getCode() == INVALID_LOGIN_PARAMETERS) { + authFailed = true; + } + } + + return authFailed; + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/ResumeTimerEvent.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/ResumeTimerEvent.java new file mode 100644 index 0000000..c767c71 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/ResumeTimerEvent.java @@ -0,0 +1,7 @@ +package com.donnfelker.android.bootstrap.core; + +/** + * Marker class for resuming a timer through Otto + */ +public class ResumeTimerEvent { +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/StopTimerEvent.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/StopTimerEvent.java new file mode 100644 index 0000000..672e2c4 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/StopTimerEvent.java @@ -0,0 +1,7 @@ +package com.donnfelker.android.bootstrap.core; + +/** + * Marker class for the stop timer event in Otto. + */ +public class StopTimerEvent { +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/TimerPausedEvent.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/TimerPausedEvent.java new file mode 100644 index 0000000..a419c17 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/TimerPausedEvent.java @@ -0,0 +1,14 @@ +package com.donnfelker.android.bootstrap.core; + +public class TimerPausedEvent { + + private final boolean timerIsPaused; + + public TimerPausedEvent(boolean timerIsPaused) { + this.timerIsPaused = timerIsPaused; + } + + public boolean isTimerIsPaused() { + return timerIsPaused; + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/TimerService.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/TimerService.java new file mode 100644 index 0000000..7df94a2 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/TimerService.java @@ -0,0 +1,212 @@ +package com.donnfelker.android.bootstrap.core; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.SystemClock; +import android.support.v4.app.NotificationCompat; + +import com.donnfelker.android.bootstrap.Injector; +import com.donnfelker.android.bootstrap.R; +import com.donnfelker.android.bootstrap.ui.BootstrapTimerActivity; +import com.donnfelker.android.bootstrap.util.Ln; +import com.squareup.otto.Bus; +import com.squareup.otto.Produce; +import com.squareup.otto.Subscribe; + +import javax.inject.Inject; + +import static com.donnfelker.android.bootstrap.core.Constants.Notification.TIMER_NOTIFICATION_ID; + +public class TimerService extends Service { + + @Inject protected Bus eventBus; + @Inject NotificationManager notificationManager; + + private boolean timerRunning = false; + private boolean timerStarted; + private long base; + private long currentRunningTimeInMillis; + private long pausedBaseTime; + private boolean isPaused; + + public static final int TICK_WHAT = 2; + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + + Injector.inject(this); + + // Register the bus so we can send notifications. + eventBus.register(this); + + } + + @Override + public void onDestroy() { + + // Unregister bus, since its not longer needed as the service is shutting down + eventBus.unregister(this); + + notificationManager.cancel(TIMER_NOTIFICATION_ID); + + Ln.d("Service has been destroyed"); + + super.onDestroy(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + + if (!timerStarted) { + + timerStarted = true; + + startTimer(); + + // Run as foreground service: http://stackoverflow.com/a/3856940/5210 + // Another example: https://github.com/commonsguy/cw-android/blob/master/Notifications/FakePlayer/src/com/commonsware/android/fakeplayerfg/PlayerService.java + startForeground(TIMER_NOTIFICATION_ID, getNotification(getString(R.string.timer_running))); + } + + return START_NOT_STICKY; + } + + @Produce + public TimerTickEvent produceTickEvent() { + return new TimerTickEvent(currentRunningTimeInMillis); + } + + @Produce + public TimerPausedEvent produceTimerIsPausedEvent() { + return new TimerPausedEvent(isPaused); + } + + @Subscribe + public void onStopEvent(StopTimerEvent stopEvent) { + + timerHandler.removeMessages(TICK_WHAT); + stopSelf(); + } + + @Subscribe + public void onPauseEvent(PauseTimerEvent pauseEvent) { + pauseTimer(); + } + + /** + * Pauses the active running timer and updates the notification in the status bar. + */ + private void pauseTimer() { + + updateNotification(getString(R.string.timer_is_paused)); + + timerHandler.removeMessages(TICK_WHAT); + pausedBaseTime = SystemClock.elapsedRealtime() - base; + timerRunning = false; + isPaused = true; + + produceTimerIsPausedEvent(); + } + + @Subscribe + public void onResumeTimerEvent(ResumeTimerEvent resumeTimerEvent) { + startTimer(); + } + + private void startTimer() { + startChronoTimer(); + notifyTimerRunning(); + } + + private void startChronoTimer() { + base = SystemClock.elapsedRealtime(); + + // If coming from a paused state, then find our true base. + if (pausedBaseTime > 0) + base = base - pausedBaseTime; + + isPaused = false; + + updateRunning(); + } + + /** + * Starts the generic timer. + */ + private void updateRunning() { + if (timerStarted != timerRunning) { + if (timerStarted) { + dispatchTimerUpdate(SystemClock.elapsedRealtime()); + timerHandler.sendMessageDelayed(Message.obtain(timerHandler, TICK_WHAT), 1000); + } else { + timerHandler.removeMessages(TICK_WHAT); + } + timerRunning = timerStarted; + } + } + + private Handler timerHandler = new Handler() { + public void handleMessage(Message m) { + if (timerRunning) { + dispatchTimerUpdate(SystemClock.elapsedRealtime()); + sendMessageDelayed(Message.obtain(this, TICK_WHAT), 1000); + } + } + }; + + private void dispatchTimerUpdate(long now) { + + currentRunningTimeInMillis = now - base; + Ln.d("Elapsed Seconds: " + currentRunningTimeInMillis / 1000); + + eventBus.post(produceTickEvent()); + + } + + + private void notifyTimerRunning() { + updateNotification(getString(R.string.timer_running)); + produceTimerIsPausedEvent(); + } + + + private void updateNotification(String message) { + notificationManager.notify(TIMER_NOTIFICATION_ID, getNotification(message)); + + } + + /** + * Creates a notification to show in the notification bar + * + * @param message the message to display in the notification bar + * @return a new {@link Notification} + */ + private Notification getNotification(String message) { + final Intent i = new Intent(this, BootstrapTimerActivity.class); + + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, i, 0); + + return new NotificationCompat.Builder(this) + .setContentTitle(getString(R.string.app_name)) + .setSmallIcon(R.drawable.ic_stat_ab_notification) + .setContentText(message) + .setAutoCancel(false) + .setOnlyAlertOnce(true) + .setOngoing(true) + .setWhen(System.currentTimeMillis()) + .setContentIntent(pendingIntent) + .getNotification(); + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/TimerTickEvent.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/TimerTickEvent.java new file mode 100644 index 0000000..6b03e37 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/TimerTickEvent.java @@ -0,0 +1,35 @@ +package com.donnfelker.android.bootstrap.core; + + +/** + * Event used to pass tick events around through the message bus. + * This is mainly used in the {@link BootstrapTimer} to show the updates on the timer + * as the background service runs the timer. + */ +public class TimerTickEvent { + + private final long millis; + + public TimerTickEvent(long millis) { + this.millis = millis; + } + + public long getMillis() { + return millis; + } + + public long getSeconds() { + return (millis / 1000); + } + + @Override + public String toString() { + return new StringBuilder("") + .append("Millis: ").append(getMillis()) + .append(", ") + .append("Seconds: ").append(getSeconds()) + .toString(); + } + +} + diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/User.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/User.java index 4dfcb44..44dd480 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/core/User.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/User.java @@ -1,10 +1,11 @@ package com.donnfelker.android.bootstrap.core; +import android.text.TextUtils; + import java.io.Serializable; public class User implements Serializable { - private static final long serialVersionUID = -7495897652017488896L; protected String firstName; @@ -21,7 +22,7 @@ public String getUsername() { return username; } - public void setUsername(String username) { + public void setUsername(final String username) { this.username = username; } @@ -29,7 +30,7 @@ public String getPhone() { return phone; } - public void setPhone(String phone) { + public void setPhone(final String phone) { this.phone = phone; } @@ -37,7 +38,7 @@ public String getObjectId() { return objectId; } - public void setObjectId(String objectId) { + public void setObjectId(final String objectId) { this.objectId = objectId; } @@ -53,7 +54,7 @@ public String getFirstName() { return firstName; } - public void setFirstName(String firstName) { + public void setFirstName(final String firstName) { this.firstName = firstName; } @@ -61,7 +62,7 @@ public String getLastName() { return lastName; } - public void setLastName(String lastName) { + public void setLastName(final String lastName) { this.lastName = lastName; } @@ -70,6 +71,19 @@ public String getGravatarId() { } public String getAvatarUrl() { + if (TextUtils.isEmpty(avatarUrl)) { + String gravatarId = getGravatarId(); + if (TextUtils.isEmpty(gravatarId)) + gravatarId = GravatarUtils.getHash(getUsername()); + avatarUrl = getAvatarUrl(gravatarId); + } return avatarUrl; } + + private String getAvatarUrl(String id) { + if (!TextUtils.isEmpty(id)) + return "https://secure.gravatar.com/avatar/" + id + "?d=404"; + else + return null; + } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/UserAgentProvider.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/UserAgentProvider.java index f6b1fb2..5fefb13 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/core/UserAgentProvider.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/UserAgentProvider.java @@ -1,35 +1,57 @@ package com.donnfelker.android.bootstrap.core; -import android.app.Application; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.os.Build; import android.telephony.TelephonyManager; -import com.google.inject.Inject; -import com.google.inject.Provider; +import com.donnfelker.android.bootstrap.util.Ln; +import com.donnfelker.android.bootstrap.util.Strings; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Locale; -import roboguice.util.Ln; -import roboguice.util.Strings; +import javax.inject.Inject; +import javax.inject.Provider; + +/** + * Class that builds a User-Agent that is set on all HTTP calls. + * + * The user agent will change depending on the version of Android that + * the user is running, the device their running and the version of the + * app that they're running. This will allow your remote API to perform + * User-Agent inspection to provide different logic routes or analytics + * based upon the User-Agent. + * + * Example of what is generated when running the Genymotion Nexus 4 Emulator: + * + * Android Bootstrap/1.0 (Android 4.2.2; Genymotion Vbox86p / Generic Galaxy Nexus - 4.2.2 - API 17 - 720x1280; )[preload=false;locale=en_US;clientidbase=] + * + * The value "preload" means that the app has been preloaded by the manufacturer. + * Instances of when this might happen is if you partner with a telecom company + * to ship your app with their new device. + * + * If clientidbase is available you "should" be getting the telecom that is operating + * the device. This is not reliable, but is still useful. + */ public class UserAgentProvider implements Provider { - @Inject protected Application app; + + private static final String APP_NAME = "Android Bootstrap"; + + @Inject protected ApplicationInfo appInfo; @Inject protected PackageInfo info; @Inject protected TelephonyManager telephonyManager; + @Inject protected ClassLoader classLoader; protected String userAgent; - private static final String APP_NAME = "Android Bootstrap"; - @Override public String get() { - if( userAgent==null ) { + if (userAgent == null) { synchronized (UserAgentProvider.class) { - if( userAgent==null ) { + if (userAgent == null) { userAgent = String.format("%s/%s (Android %s; %s %s / %s %s; %s)", APP_NAME, info.versionName, @@ -38,26 +60,26 @@ public String get() { Strings.capitalize(Build.DEVICE), Strings.capitalize(Build.BRAND), Strings.capitalize(Build.MODEL), - Strings.capitalize( telephonyManager == null ? "not-found" : telephonyManager.getSimOperatorName()) + Strings.capitalize(telephonyManager == null ? "not-found" : telephonyManager.getSimOperatorName()) ); final ArrayList params = new ArrayList(); - params.add( "preload=" + ((app.getApplicationInfo().flags& ApplicationInfo.FLAG_SYSTEM)==1) ); // Determine if this app was a preloaded app - params.add( "locale=" + Locale.getDefault() ); + params.add("preload=" + ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 1)); // Determine if this app was a preloaded app + params.add("locale=" + Locale.getDefault()); // http://stackoverflow.com/questions/2641111/where-is-android-os-systemproperties - try{ - final Class SystemProperties = app.getClassLoader().loadClass("android.os.SystemProperties"); + try { + final Class SystemProperties = classLoader.loadClass("android.os.SystemProperties"); final Method get = SystemProperties.getMethod("get", String.class); - params.add( "clientidbase=" + get.invoke(SystemProperties, "ro.com.google.clientidbase")); - }catch( Exception ignored ){ + params.add("clientidbase=" + get.invoke(SystemProperties, "ro.com.google.clientidbase")); + } catch (Exception ignored) { Ln.d(ignored); } - if( params.size()>0 ) - userAgent += "["+ Strings.join(";", params) +"]"; + if (params.size() > 0) + userAgent += "[" + Strings.join(";", params) + "]"; } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/UserService.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/UserService.java new file mode 100644 index 0000000..a0a040d --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/UserService.java @@ -0,0 +1,28 @@ +package com.donnfelker.android.bootstrap.core; + +import java.util.List; + +import retrofit.http.GET; +import retrofit.http.Query; + +/** + * User service for connecting the the REST API and + * getting the users. + */ +public interface UserService { + + @GET(Constants.Http.URL_USERS_FRAG) + UsersWrapper getUsers(); + + /** + * The {@link retrofit.http.Query} values will be transform into query string paramters + * via Retrofit + * + * @param email The users email + * @param password The users password + * @return A login response. + */ + @GET(Constants.Http.URL_AUTH_FRAG) + User authenticate(@Query(Constants.Http.PARAM_USERNAME) String email, + @Query(Constants.Http.PARAM_PASSWORD) String password); +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/core/UsersWrapper.java b/app/src/main/java/com/donnfelker/android/bootstrap/core/UsersWrapper.java new file mode 100644 index 0000000..7b898a5 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/core/UsersWrapper.java @@ -0,0 +1,14 @@ +package com.donnfelker.android.bootstrap.core; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class UsersWrapper { + + private List results; + + public List getResults() { + return results; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/events/NavItemSelectedEvent.java b/app/src/main/java/com/donnfelker/android/bootstrap/events/NavItemSelectedEvent.java new file mode 100644 index 0000000..d0462c4 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/events/NavItemSelectedEvent.java @@ -0,0 +1,17 @@ +package com.donnfelker.android.bootstrap.events; + +/** + * Pub/Sub event used to communicate between fragment and activity. + * Subscription occurs in the {@link com.donnfelker.android.bootstrap.ui.MainActivity} + */ +public class NavItemSelectedEvent { + private int itemPosition; + + public NavItemSelectedEvent(int itemPosition) { + this.itemPosition = itemPosition; + } + + public int getItemPosition() { + return itemPosition; + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/events/NetworkErrorEvent.java b/app/src/main/java/com/donnfelker/android/bootstrap/events/NetworkErrorEvent.java new file mode 100644 index 0000000..40fff4d --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/events/NetworkErrorEvent.java @@ -0,0 +1,20 @@ +package com.donnfelker.android.bootstrap.events; + +import retrofit.RetrofitError; + +/** + * The event that is posted when a network error event occurs. + * TODO: Consume this event in the {@link com.donnfelker.android.bootstrap.ui.BootstrapActivity} and + * show a dialog that something went wrong. + */ +public class NetworkErrorEvent { + private RetrofitError cause; + + public NetworkErrorEvent(RetrofitError cause) { + this.cause = cause; + } + + public RetrofitError getCause() { + return cause; + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/events/RestAdapterErrorEvent.java b/app/src/main/java/com/donnfelker/android/bootstrap/events/RestAdapterErrorEvent.java new file mode 100644 index 0000000..795cdb2 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/events/RestAdapterErrorEvent.java @@ -0,0 +1,18 @@ +package com.donnfelker.android.bootstrap.events; + +import retrofit.RetrofitError; + +/** + * Error that is posted when a non-network error event occurs in the {@link retrofit.RestAdapter} + */ +public class RestAdapterErrorEvent { + private RetrofitError cause; + + public RestAdapterErrorEvent(RetrofitError cause) { + this.cause = cause; + } + + public RetrofitError getCause() { + return cause; + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/events/UnAuthorizedErrorEvent.java b/app/src/main/java/com/donnfelker/android/bootstrap/events/UnAuthorizedErrorEvent.java new file mode 100644 index 0000000..9444b76 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/events/UnAuthorizedErrorEvent.java @@ -0,0 +1,17 @@ +package com.donnfelker.android.bootstrap.events; + +import java.io.Serializable; + +import retrofit.RetrofitError; + +public class UnAuthorizedErrorEvent { + private Serializable cause; + + public UnAuthorizedErrorEvent(Serializable cause) { + this.cause = cause; + } + + public Serializable getCause() { + return cause; + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/AlternatingColorListAdapter.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/AlternatingColorListAdapter.java index 3a94b5a..aab0835 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/AlternatingColorListAdapter.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/AlternatingColorListAdapter.java @@ -3,9 +3,9 @@ import android.view.LayoutInflater; -import com.actionbarsherlock.R.color; -import com.github.kevinsawicki.wishlist.SingleTypeAdapter; +import com.donnfelker.android.bootstrap.R; import com.donnfelker.android.bootstrap.R.drawable; +import com.github.kevinsawicki.wishlist.SingleTypeAdapter; import java.util.List; @@ -28,8 +28,8 @@ public abstract class AlternatingColorListAdapter extends * @param inflater * @param items */ - public AlternatingColorListAdapter(final int layoutId, - final LayoutInflater inflater, final List items) { + public AlternatingColorListAdapter(final int layoutId, final LayoutInflater inflater, + final List items) { this(layoutId, inflater, items, true); } @@ -41,16 +41,16 @@ public AlternatingColorListAdapter(final int layoutId, * @param items * @param selectable */ - public AlternatingColorListAdapter(final int layoutId, - LayoutInflater inflater, final List items, boolean selectable) { + public AlternatingColorListAdapter(final int layoutId, final LayoutInflater inflater, + final List items, final boolean selectable) { super(inflater, layoutId); if (selectable) { primaryResource = drawable.table_background_selector; secondaryResource = drawable.table_background_alternate_selector; } else { - primaryResource = color.pager_background; - secondaryResource = color.pager_background_alternate; + primaryResource = R.color.pager_background; + secondaryResource = R.color.pager_background_alternate; } setItems(items); @@ -59,8 +59,8 @@ public AlternatingColorListAdapter(final int layoutId, @Override protected void update(final int position, final V item) { if (position % 2 != 0) - view.setBackgroundResource(primaryResource); + updater.view.setBackgroundResource(primaryResource); else - view.setBackgroundResource(secondaryResource); + updater.view.setBackgroundResource(secondaryResource); } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/AsyncLoader.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/AsyncLoader.java index 9371d00..46db305 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/AsyncLoader.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/AsyncLoader.java @@ -11,11 +11,11 @@ *

* Based on CursorLoader.java in the Fragment compatibility package * - * @param - * data type + * @param data type * @author Alexander Blom (me@alexanderblom.se) */ public abstract class AsyncLoader extends AsyncTaskLoader { + private D data; /** @@ -23,15 +23,16 @@ public abstract class AsyncLoader extends AsyncTaskLoader { * * @param context */ - public AsyncLoader(Context context) { + public AsyncLoader(final Context context) { super(context); } @Override - public void deliverResult(D data) { - if (isReset()) + public void deliverResult(final D data) { + if (isReset()) { // An async query came in while the loader is stopped return; + } this.data = data; @@ -40,11 +41,13 @@ public void deliverResult(D data) { @Override protected void onStartLoading() { - if (data != null) + if (data != null) { deliverResult(data); + } - if (takeContentChanged() || data == null) + if (takeContentChanged() || data == null) { forceLoad(); + } } @Override diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/BarGraphDrawable.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/BarGraphDrawable.java index 8ff4502..01a9401 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/BarGraphDrawable.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/BarGraphDrawable.java @@ -32,9 +32,11 @@ public BarGraphDrawable(final long[][] data, final int[][] colors) { super(android.R.color.transparent); this.data = data; this.colors = colors; - for (int i = 0; i < data.length; i++) - for (int j = 0; j < data[i].length; j++) + for (int i = 0; i < data.length; i++) { + for (int j = 0; j < data[i].length; j++) { max = Math.max(max, data[i][j]); + } + } } @Override diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapActivity.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapActivity.java index 52be6dc..9d5665f 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapActivity.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapActivity.java @@ -1,23 +1,45 @@ package com.donnfelker.android.bootstrap.ui; -import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; -import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.view.MenuItem; -import com.actionbarsherlock.view.MenuItem; -import com.github.rtyley.android.sherlock.roboguice.activity.RoboSherlockActivity; +import com.donnfelker.android.bootstrap.Injector; + +import butterknife.Views; + +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; +import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; /** * Base activity for a Bootstrap activity which does not use fragments. */ -public abstract class BootstrapActivity extends RoboSherlockActivity { +public abstract class BootstrapActivity extends ActionBarActivity { + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Injector.inject(this); + } + + @Override + public void setContentView(final int layoutResId) { + super.setContentView(layoutResId); + + // Used to inject views with the Butterknife library + Views.inject(this); + } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { - case android.R.id.home: // This is the home button in the top left corner of the screen. - // Dont call finish! Because activity could have been started by an outside activity and the home button would not operated as expected! - Intent homeIntent = new Intent(this, CarouselActivity.class); + // This is the home button in the top left corner of the screen. + case android.R.id.home: + // Don't call finish! Because activity could have been started by an + // outside activity and the home button would not operated as expected! + final Intent homeIntent = new Intent(this, MainActivity.class); homeIntent.addFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP); startActivity(homeIntent); return true; diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapFragmentActivity.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapFragmentActivity.java new file mode 100644 index 0000000..d1e096a --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapFragmentActivity.java @@ -0,0 +1,47 @@ +package com.donnfelker.android.bootstrap.ui; + +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; + +import com.donnfelker.android.bootstrap.Injector; +import com.squareup.otto.Bus; + +import javax.inject.Inject; + +import butterknife.InjectView; +import butterknife.Views; + +/** + * Base class for all Bootstrap Activities that need fragments. + */ +public class BootstrapFragmentActivity extends ActionBarActivity { + + @Inject + protected Bus eventBus; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Injector.inject(this); + } + + @Override + public void setContentView(final int layoutResId) { + super.setContentView(layoutResId); + + Views.inject(this); + } + + @Override + protected void onResume() { + super.onResume(); + eventBus.register(this); + } + + @Override + protected void onPause() { + super.onPause(); + eventBus.unregister(this); + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapPagerAdapter.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapPagerAdapter.java index f896477..cf81fa1 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapPagerAdapter.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapPagerAdapter.java @@ -23,7 +23,7 @@ public class BootstrapPagerAdapter extends FragmentPagerAdapter { * @param resources * @param fragmentManager */ - public BootstrapPagerAdapter(Resources resources, FragmentManager fragmentManager) { + public BootstrapPagerAdapter(final Resources resources, final FragmentManager fragmentManager) { super(fragmentManager); this.resources = resources; } @@ -34,37 +34,39 @@ public int getCount() { } @Override - public Fragment getItem(int position) { - Bundle bundle = new Bundle(); + public Fragment getItem(final int position) { + final Fragment result; switch (position) { - case 0: - NewsListFragment newsFragment = new NewsListFragment(); - newsFragment.setArguments(bundle); - return newsFragment; - case 1: - UserListFragment userListFragment = new UserListFragment(); - userListFragment.setArguments(bundle); - return userListFragment; - case 2: - CheckInsListFragment checkInsFragment = new CheckInsListFragment(); - checkInsFragment.setArguments(bundle); - return checkInsFragment; - default: - return null; + case 0: + result = new NewsListFragment(); + break; + case 1: + result = new UserListFragment(); + break; + case 2: + result = new CheckInsListFragment(); + break; + default: + result = null; + break; } + if (result != null) { + result.setArguments(new Bundle()); //TODO do we need this? + } + return result; } @Override - public CharSequence getPageTitle(int position) { + public CharSequence getPageTitle(final int position) { switch (position) { - case 0: - return resources.getString(R.string.page_news); - case 1: - return resources.getString(R.string.page_users); - case 2: - return resources.getString(R.string.page_checkins); - default: - return null; + case 0: + return resources.getString(R.string.page_news); + case 1: + return resources.getString(R.string.page_users); + case 2: + return resources.getString(R.string.page_checkins); + default: + return null; } } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapTimerActivity.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapTimerActivity.java new file mode 100644 index 0000000..1520f86 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/BootstrapTimerActivity.java @@ -0,0 +1,237 @@ +package com.donnfelker.android.bootstrap.ui; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.NavUtils; +import android.support.v4.app.TaskStackBuilder; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import com.donnfelker.android.bootstrap.R; +import com.donnfelker.android.bootstrap.core.PauseTimerEvent; +import com.donnfelker.android.bootstrap.core.ResumeTimerEvent; +import com.donnfelker.android.bootstrap.core.StopTimerEvent; +import com.donnfelker.android.bootstrap.core.TimerPausedEvent; +import com.donnfelker.android.bootstrap.core.TimerService; +import com.donnfelker.android.bootstrap.core.TimerTickEvent; +import com.squareup.otto.Bus; +import com.squareup.otto.Subscribe; + +import javax.inject.Inject; + +import butterknife.InjectView; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +public class BootstrapTimerActivity extends BootstrapFragmentActivity implements View.OnClickListener { + + @Inject Bus eventBus; + + @InjectView(R.id.chronometer) protected TextView chronometer; + @InjectView(R.id.start) protected Button start; + @InjectView(R.id.stop) protected Button stop; + @InjectView(R.id.pause) protected Button pause; + @InjectView(R.id.resume) protected Button resume; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.bootstrap_timer); + + setTitle(R.string.title_timer); + + getSupportActionBar().setHomeButtonEnabled(true); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + start.setOnClickListener(this); + stop.setOnClickListener(this); + pause.setOnClickListener(this); + resume.setOnClickListener(this); + + } + + @Override + public void onClick(final View v) { + switch (v.getId()) { + case R.id.start: + startTimer(); + break; + case R.id.stop: + produceStopEvent(); + break; + case R.id.pause: + producePauseEvent(); + break; + case R.id.resume: + produceResumeEvent(); + break; + } + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + // Source: + // http://developer.android.com/training/implementing-navigation/ancestral.html + // This is the home button in the top left corner of the screen. + case android.R.id.home: + final Intent upIntent = NavUtils.getParentActivityIntent(this); + // If parent is not properly defined in AndroidManifest.xml upIntent will be null + // TODO hanlde upIntent == null + if (NavUtils.shouldUpRecreateTask(this, upIntent)) { + // This activity is NOT part of this app's task, so create a new task + // when navigating up, with a synthesized back stack. + TaskStackBuilder.create(this) + // Add all of this activity's parents to the back stack + .addNextIntentWithParentStack(upIntent) + // Navigate up to the closest parent + .startActivities(); + } else { + // This activity is part of this app's task, so simply + // navigate up to the logical parent activity. + NavUtils.navigateUpTo(this, upIntent); + } + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + /** + * Starts the timer service + */ + private void startTimer() { + if (!isTimerServiceRunning()) { + final Intent i = new Intent(this, TimerService.class); + startService(i); + + setButtonVisibility(GONE, VISIBLE, GONE, VISIBLE); + } + } + + /** + * Posts a {@link StopTimerEvent} message to the {@link Bus} + */ + private void produceStopEvent() { + eventBus.post(new StopTimerEvent()); + } + + /** + * Posts a {@link PauseTimerEvent} message to the {@link Bus} + */ + private void producePauseEvent() { + eventBus.post(new PauseTimerEvent()); + } + + /** + * Posts a {@link ResumeTimerEvent} message to the {@link Bus} + */ + private void produceResumeEvent() { + eventBus.post(new ResumeTimerEvent()); + } + + @Subscribe + public void onTimerPausedEvent(final TimerPausedEvent event) { + if (event.isTimerIsPaused()) { + setButtonVisibility(GONE, VISIBLE, VISIBLE, GONE); + } else if (isTimerServiceRunning()) { + setButtonVisibility(GONE, VISIBLE, GONE, VISIBLE); + } + } + + /** + * Called by {@link Bus} when a tick event occurs. + * + * @param event The event + */ + @Subscribe + public void onTickEvent(final TimerTickEvent event) { + setFormattedTime(event.getMillis()); + } + + /** + * Called by {@link Bus} when a tick event occurs. + * + * @param event The event + */ + @Subscribe + public void onPauseEvent(final PauseTimerEvent event) { + setButtonVisibility(GONE, VISIBLE, VISIBLE, GONE); + } + + /** + * Called by {@link Bus} when a tick event occurs. + * + * @param event The event + */ + @Subscribe + public void onResumeEvent(final ResumeTimerEvent event) { + setButtonVisibility(GONE, VISIBLE, GONE, VISIBLE); + } + + /** + * Called by {@link Bus} when a tick event occurs. + * + * @param event The event + */ + @Subscribe + public void onStopEvent(final StopTimerEvent event) { + setButtonVisibility(VISIBLE, GONE, GONE, GONE); + setFormattedTime(0); // Since its stopped, zero out the timer. + } + + /** + * Checks to see if the timer service is running or not. + * + * @return true if the service is running otherwise false. + */ + private boolean isTimerServiceRunning() { + final ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { + if (TimerService.class.getName().equals(service.service.getClassName())) { + return true; + } + } + return false; + } + + private void setButtonVisibility(final int start, final int stop, + final int resume, final int pause) { + this.start.setVisibility(start); + this.stop.setVisibility(stop); + this.resume.setVisibility(resume); + this.pause.setVisibility(pause); + } + + /** + * Sets the formatted time + * + * @param millis the elapsed time + */ + private void setFormattedTime(long millis) { + final String formattedTime = formatTime(millis); + chronometer.setText(formattedTime); + } + + /** + * Formats the time to look like "HH:MM:SS" + * + * @param millis The number of elapsed milliseconds + * @return A formatted time value + */ + public static String formatTime(final long millis) { + //TODO does not support hour>=100 (4.1 days) + return String.format("%02d:%02d:%02d", + millis / (1000 * 60 * 60), + (millis / (1000 * 60)) % 60, + (millis / 1000) % 60 + ); + } + +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/CarouselActivity.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/CarouselActivity.java deleted file mode 100644 index a5b6745..0000000 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/CarouselActivity.java +++ /dev/null @@ -1,43 +0,0 @@ - - -package com.donnfelker.android.bootstrap.ui; - -import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; -import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; -import android.content.Intent; -import android.os.Bundle; -import android.support.v4.view.ViewPager; - -import com.actionbarsherlock.view.MenuItem; -import com.actionbarsherlock.view.Window; -import com.donnfelker.android.bootstrap.R; -import com.donnfelker.android.bootstrap.R.id; -import com.github.rtyley.android.sherlock.roboguice.activity.RoboSherlockFragmentActivity; -import com.viewpagerindicator.TitlePageIndicator; - -import roboguice.inject.InjectView; - -/** - * Activity to view the carousel and view pager indicator with fragments. - */ -public class CarouselActivity extends RoboSherlockFragmentActivity { - - @InjectView(id.tpi_header) private TitlePageIndicator indicator; - @InjectView(id.vp_pages) private ViewPager pager; - - @Override - protected void onCreate(Bundle savedInstanceState) { - - requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); - - super.onCreate(savedInstanceState); - setContentView(R.layout.carousel_view); - - pager.setAdapter(new BootstrapPagerAdapter(getResources(), getSupportFragmentManager())); - - indicator.setViewPager(pager); - pager.setCurrentItem(1); - } - - -} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/CarouselFragment.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/CarouselFragment.java new file mode 100644 index 0000000..f093e07 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/CarouselFragment.java @@ -0,0 +1,43 @@ +package com.donnfelker.android.bootstrap.ui; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.view.ViewPager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.donnfelker.android.bootstrap.R; +import com.viewpagerindicator.TitlePageIndicator; + +import butterknife.InjectView; +import butterknife.Views; + +/** + * Fragment which houses the View pager. + */ +public class CarouselFragment extends Fragment { + + @InjectView(R.id.tpi_header) + protected TitlePageIndicator indicator; + + @InjectView(R.id.vp_pages) + protected ViewPager pager; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_carousel, container, false); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + Views.inject(this, getView()); + + pager.setAdapter(new BootstrapPagerAdapter(getResources(), getChildFragmentManager())); + indicator.setViewPager(pager); + pager.setCurrentItem(1); + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/CheckInsListAdapter.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/CheckInsListAdapter.java index 6761c54..2c5d9b6 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/CheckInsListAdapter.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/CheckInsListAdapter.java @@ -1,15 +1,12 @@ package com.donnfelker.android.bootstrap.ui; -import android.util.Log; import android.view.LayoutInflater; import com.donnfelker.android.bootstrap.R; import com.donnfelker.android.bootstrap.core.CheckIn; -import com.donnfelker.android.bootstrap.core.News; import java.util.List; -import roboguice.util.Strings; public class CheckInsListAdapter extends AlternatingColorListAdapter { /** @@ -17,8 +14,8 @@ public class CheckInsListAdapter extends AlternatingColorListAdapter { * @param items * @param selectable */ - public CheckInsListAdapter(LayoutInflater inflater, List items, - boolean selectable) { + public CheckInsListAdapter(final LayoutInflater inflater, final List items, + final boolean selectable) { super(R.layout.checkin_list_item, inflater, items, selectable); } @@ -26,19 +23,19 @@ public CheckInsListAdapter(LayoutInflater inflater, List items, * @param inflater * @param items */ - public CheckInsListAdapter(LayoutInflater inflater, List items) { + public CheckInsListAdapter(final LayoutInflater inflater, final List items) { super(R.layout.checkin_list_item, inflater, items); } @Override protected int[] getChildViewIds() { - return new int[] { R.id.tv_name, R.id.tv_date }; + return new int[]{R.id.tv_name, R.id.tv_date}; } @Override - protected void update(int position, CheckIn item) { + protected void update(final int position, final CheckIn item) { super.update(position, item); - setText(R.id.tv_name, item.getName()); + setText(0, item.getName()); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/CheckInsListFragment.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/CheckInsListFragment.java index 0769ff8..1ac95f1 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/CheckInsListFragment.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/CheckInsListFragment.java @@ -10,19 +10,30 @@ import android.widget.ListView; import com.donnfelker.android.bootstrap.BootstrapServiceProvider; +import com.donnfelker.android.bootstrap.Injector; import com.donnfelker.android.bootstrap.R; +import com.donnfelker.android.bootstrap.authenticator.LogoutService; import com.donnfelker.android.bootstrap.core.CheckIn; import com.github.kevinsawicki.wishlist.SingleTypeAdapter; -import com.google.inject.Inject; +import java.util.Collections; import java.util.List; +import javax.inject.Inject; + public class CheckInsListFragment extends ItemListFragment { @Inject protected BootstrapServiceProvider serviceProvider; + @Inject protected LogoutService logoutService; + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Injector.inject(this); + } @Override - protected void configureList(Activity activity, ListView listView) { + protected void configureList(final Activity activity, final ListView listView) { super.configureList(activity, listView); listView.setFastScrollEnabled(true); @@ -33,6 +44,11 @@ protected void configureList(Activity activity, ListView listView) { .inflate(R.layout.checkins_list_item_labels, null)); } + @Override + protected LogoutService getLogoutService() { + return logoutService; + } + @Override public void onDestroyView() { setListAdapter(null); @@ -41,16 +57,20 @@ public void onDestroyView() { } @Override - public Loader> onCreateLoader(int id, Bundle args) { + public Loader> onCreateLoader(final int id, final Bundle args) { final List initialItems = items; return new ThrowableLoader>(getActivity(), items) { @Override public List loadData() throws Exception { try { - return serviceProvider.getService().getCheckIns(); - } catch (OperationCanceledException e) { - Activity activity = getActivity(); + if (getActivity() != null) { + return serviceProvider.getService(getActivity()).getCheckIns(); + } else { + return Collections.emptyList(); + } + } catch (final OperationCanceledException e) { + final Activity activity = getActivity(); if (activity != null) activity.finish(); return initialItems; @@ -60,24 +80,26 @@ public List loadData() throws Exception { } @Override - protected SingleTypeAdapter createAdapter(List items) { + protected SingleTypeAdapter createAdapter(final List items) { return new CheckInsListAdapter(getActivity().getLayoutInflater(), items); } - public void onListItemClick(ListView l, View v, int position, long id) { - CheckIn checkIn = ((CheckIn) l.getItemAtPosition(position)); + public void onListItemClick(final ListView l, final View v, final int position, final long id) { + final CheckIn checkIn = ((CheckIn) l.getItemAtPosition(position)); - String uri = String.format("geo:%s,%s?q=%s", + final String uri = String.format("geo:%s,%s?q=%s", checkIn.getLocation().getLatitude(), checkIn.getLocation().getLongitude(), checkIn.getName()); // Show a chooser that allows the user to decide how to display this data, in this case, map data. - startActivity(Intent.createChooser(new Intent(Intent.ACTION_VIEW, Uri.parse(uri)), getString(R.string.choose))); + startActivity(Intent.createChooser( + new Intent(Intent.ACTION_VIEW, Uri.parse(uri)), getString(R.string.choose)) + ); } @Override - protected int getErrorMessage(Exception exception) { + protected int getErrorMessage(final Exception exception) { return R.string.error_loading_checkins; } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/HeaderFooterListAdapter.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/HeaderFooterListAdapter.java index d2be941..e7b331d 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/HeaderFooterListAdapter.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/HeaderFooterListAdapter.java @@ -31,13 +31,13 @@ public class HeaderFooterListAdapter extends * @param view * @param adapter */ - public HeaderFooterListAdapter(ListView view, E adapter) { + public HeaderFooterListAdapter(final ListView view, final E adapter) { this(new ArrayList(), new ArrayList(), view, adapter); } - private HeaderFooterListAdapter(ArrayList headerViewInfos, - ArrayList footerViewInfos, ListView view, E adapter) { + private HeaderFooterListAdapter(final ArrayList headerViewInfos, + final ArrayList footerViewInfos, final ListView view, final E adapter) { super(headerViewInfos, footerViewInfos, adapter); headers = headerViewInfos; @@ -49,11 +49,11 @@ private HeaderFooterListAdapter(ArrayList headerViewInfos, /** * Add non-selectable header view with no data * - * @see #addHeader(View, Object, boolean) * @param view * @return this adapter + * @see #addHeader(View, Object, boolean) */ - public HeaderFooterListAdapter addHeader(View view) { + public HeaderFooterListAdapter addHeader(final View view) { return addHeader(view, null, false); } @@ -65,9 +65,9 @@ public HeaderFooterListAdapter addHeader(View view) { * @param isSelectable * @return this adapter */ - public HeaderFooterListAdapter addHeader(View view, Object data, - boolean isSelectable) { - FixedViewInfo info = list.new FixedViewInfo(); + public HeaderFooterListAdapter addHeader(final View view, final Object data, + final boolean isSelectable) { + final FixedViewInfo info = list.new FixedViewInfo(); info.view = view; info.data = data; info.isSelectable = isSelectable; @@ -80,11 +80,11 @@ public HeaderFooterListAdapter addHeader(View view, Object data, /** * Add non-selectable footer view with no data * - * @see #addFooter(View, Object, boolean) * @param view * @return this adapter + * @see #addFooter(View, Object, boolean) */ - public HeaderFooterListAdapter addFooter(View view) { + public HeaderFooterListAdapter addFooter(final View view) { return addFooter(view, null, false); } @@ -96,9 +96,9 @@ public HeaderFooterListAdapter addFooter(View view) { * @param isSelectable * @return this adapter */ - public HeaderFooterListAdapter addFooter(View view, Object data, - boolean isSelectable) { - FixedViewInfo info = list.new FixedViewInfo(); + public HeaderFooterListAdapter addFooter(final View view, final Object data, + final boolean isSelectable) { + final FixedViewInfo info = list.new FixedViewInfo(); info.view = view; info.data = data; info.isSelectable = isSelectable; @@ -109,10 +109,11 @@ public HeaderFooterListAdapter addFooter(View view, Object data, } @Override - public boolean removeHeader(View v) { - boolean removed = super.removeHeader(v); - if (removed) + public boolean removeHeader(final View v) { + final boolean removed = super.removeHeader(v); + if (removed) { wrapped.notifyDataSetChanged(); + } return removed; } @@ -124,13 +125,15 @@ public boolean removeHeader(View v) { public boolean clearHeaders() { boolean removed = false; if (!headers.isEmpty()) { - FixedViewInfo[] infos = headers.toArray(new FixedViewInfo[headers + final FixedViewInfo[] infos = headers.toArray(new FixedViewInfo[headers .size()]); - for (FixedViewInfo info : infos) + for (final FixedViewInfo info : infos) { removed = super.removeHeader(info.view) || removed; + } } - if (removed) + if (removed) { wrapped.notifyDataSetChanged(); + } return removed; } @@ -142,21 +145,24 @@ public boolean clearHeaders() { public boolean clearFooters() { boolean removed = false; if (!footers.isEmpty()) { - FixedViewInfo[] infos = footers.toArray(new FixedViewInfo[footers + final FixedViewInfo[] infos = footers.toArray(new FixedViewInfo[footers .size()]); - for (FixedViewInfo info : infos) + for (final FixedViewInfo info : infos) { removed = super.removeFooter(info.view) || removed; + } } - if (removed) + if (removed) { wrapped.notifyDataSetChanged(); + } return removed; } @Override - public boolean removeFooter(View v) { - boolean removed = super.removeFooter(v); - if (removed) + public boolean removeFooter(final View v) { + final boolean removed = super.removeFooter(v); + if (removed) { wrapped.notifyDataSetChanged(); + } return removed; } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/ItemListFragment.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/ItemListFragment.java index 06370b6..e950599 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/ItemListFragment.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/ItemListFragment.java @@ -1,12 +1,16 @@ package com.donnfelker.android.bootstrap.ui; -import android.accounts.AccountManager; import android.app.Activity; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; +import android.support.v7.app.ActionBarActivity; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; @@ -17,25 +21,17 @@ import android.widget.ProgressBar; import android.widget.TextView; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; import com.donnfelker.android.bootstrap.R; +import com.donnfelker.android.bootstrap.R.id; +import com.donnfelker.android.bootstrap.R.layout; import com.donnfelker.android.bootstrap.authenticator.LogoutService; import com.github.kevinsawicki.wishlist.SingleTypeAdapter; import com.github.kevinsawicki.wishlist.Toaster; import com.github.kevinsawicki.wishlist.ViewUtils; -import com.donnfelker.android.bootstrap.R.id; -import com.donnfelker.android.bootstrap.R.layout; -import com.donnfelker.android.bootstrap.R.menu; -import com.github.rtyley.android.sherlock.roboguice.fragment.RoboSherlockFragment; -import com.google.inject.Inject; import java.util.Collections; import java.util.List; -import java.util.concurrent.Callable; -import roboguice.util.RoboAsyncTask; /** * Base fragment for displaying a list of items that loads with a progress bar @@ -43,20 +39,17 @@ * * @param */ -public abstract class ItemListFragment extends RoboSherlockFragment +public abstract class ItemListFragment extends Fragment implements LoaderCallbacks> { - @Inject protected LogoutService logoutService; - private static final String FORCE_REFRESH = "forceRefresh"; /** - * @param args - * bundle passed to the loader by the LoaderManager + * @param args bundle passed to the loader by the LoaderManager * @return true if the bundle indicates a requested forced refresh of the - * items + * items */ - protected static boolean isForceRefresh(Bundle args) { + protected static boolean isForceRefresh(final Bundle args) { return args != null && args.getBoolean(FORCE_REFRESH, false); } @@ -86,18 +79,19 @@ protected static boolean isForceRefresh(Bundle args) { protected boolean listShown; @Override - public void onActivityCreated(Bundle savedInstanceState) { + public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - if (!items.isEmpty()) + if (!items.isEmpty()) { setListShown(true, false); + } getLoaderManager().initLoader(0, null, this); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { return inflater.inflate(layout.item_list, null); } @@ -115,7 +109,7 @@ public void onDestroyView() { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(final View view, final Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); listView = (ListView) view.findViewById(android.R.id.list); @@ -123,7 +117,7 @@ public void onViewCreated(View view, Bundle savedInstanceState) { @Override public void onItemClick(AdapterView parent, View view, - int position, long id) { + int position, long id) { onListItemClick((ListView) parent, view, position, id); } }); @@ -140,40 +134,43 @@ public void onItemClick(AdapterView parent, View view, * @param activity * @param listView */ - protected void configureList(Activity activity, ListView listView) { + protected void configureList(final Activity activity, final ListView listView) { listView.setAdapter(createAdapter()); } @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); } @Override - public void onCreateOptionsMenu(Menu optionsMenu, MenuInflater inflater) { + public void onCreateOptionsMenu(final Menu optionsMenu, final MenuInflater inflater) { inflater.inflate(R.menu.bootstrap, optionsMenu); } @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (!isUsable()) + public boolean onOptionsItemSelected(final MenuItem item) { + if (!isUsable()) { return false; + } switch (item.getItemId()) { - case id.refresh: - forceRefresh(); - return true; - case R.id.logout: - logout(); - return true; - default: - return super.onOptionsItemSelected(item); + case id.refresh: + forceRefresh(); + return true; + case R.id.logout: + logout(); + return true; + default: + return super.onOptionsItemSelected(item); } } + protected abstract LogoutService getLogoutService(); + private void logout() { - logoutService.logout(new Runnable() { + getLogoutService().logout(new Runnable() { @Override public void run() { // Calling a refresh will force the service to look for a logged in user @@ -187,7 +184,7 @@ public void run() { * Force a refresh of the items displayed ignoring any cached items */ protected void forceRefresh() { - Bundle bundle = new Bundle(); + final Bundle bundle = new Bundle(); bundle.putBoolean(FORCE_REFRESH, true); refresh(bundle); } @@ -200,27 +197,32 @@ public void refresh() { } private void refresh(final Bundle args) { - if (!isUsable()) + if (!isUsable()) { return; + } - getSherlockActivity().setSupportProgressBarIndeterminateVisibility(true); + getActionBarActivity().setSupportProgressBarIndeterminateVisibility(true); getLoaderManager().restartLoader(0, args, this); } + private ActionBarActivity getActionBarActivity() { + return ((ActionBarActivity) getActivity()); + } + /** * Get error message to display for exception * * @param exception * @return string resource id */ - protected abstract int getErrorMessage(Exception exception); + protected abstract int getErrorMessage(final Exception exception); - public void onLoadFinished(Loader> loader, List items) { + public void onLoadFinished(final Loader> loader, final List items) { - getSherlockActivity().setSupportProgressBarIndeterminateVisibility(false); + getActionBarActivity().setSupportProgressBarIndeterminateVisibility(false); - Exception exception = getException(loader); + final Exception exception = getException(loader); if (exception != null) { showError(getErrorMessage(exception)); showList(); @@ -238,7 +240,7 @@ public void onLoadFinished(Loader> loader, List items) { * @return adapter */ protected HeaderFooterListAdapter> createAdapter() { - SingleTypeAdapter wrapped = createAdapter(items); + final SingleTypeAdapter wrapped = createAdapter(items); return new HeaderFooterListAdapter>(getListView(), wrapped); } @@ -259,7 +261,7 @@ protected void showList() { } @Override - public void onLoaderReset(Loader> loader) { + public void onLoaderReset(final Loader> loader) { // Intentionally left blank } @@ -280,10 +282,11 @@ protected void showError(final int message) { * @return exception or null if none provided */ protected Exception getException(final Loader> loader) { - if (loader instanceof ThrowableLoader) + if (loader instanceof ThrowableLoader) { return ((ThrowableLoader>) loader).clearException(); - else + } else { return null; + } } /** @@ -311,11 +314,11 @@ public ListView getListView() { */ @SuppressWarnings("unchecked") protected HeaderFooterListAdapter> getListAdapter() { - if (listView != null) + if (listView != null) { return (HeaderFooterListAdapter>) listView .getAdapter(); - else - return null; + } + return null; } /** @@ -325,18 +328,21 @@ protected HeaderFooterListAdapter> getListAdapter() { * @return this fragment */ protected ItemListFragment setListAdapter(final ListAdapter adapter) { - if (listView != null) + if (listView != null) { listView.setAdapter(adapter); + } return this; } private ItemListFragment fadeIn(final View view, final boolean animate) { - if (view != null) - if (animate) + if (view != null) { + if (animate) { view.startAnimation(AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in)); - else + } else { view.clearAnimation(); + } + } return this; } @@ -367,35 +373,38 @@ public ItemListFragment setListShown(final boolean shown) { * @param animate * @return this fragment */ - public ItemListFragment setListShown(final boolean shown, - final boolean animate) { - if (!isUsable()) + public ItemListFragment setListShown(final boolean shown, final boolean animate) { + if (!isUsable()) { return this; + } if (shown == listShown) { - if (shown) + if (shown) { // List has already been shown so hide/show the empty view with // no fade effect - if (items.isEmpty()) + if (items.isEmpty()) { hide(listView).show(emptyView); - else + } else { hide(emptyView).show(listView); + } + } return this; } listShown = shown; - if (shown) - if (!items.isEmpty()) + if (shown) { + if (!items.isEmpty()) { hide(progressBar).hide(emptyView).fadeIn(listView, animate) .show(listView); - else + } else { hide(progressBar).hide(listView).fadeIn(emptyView, animate) .show(emptyView); - else + } + } else { hide(listView).hide(emptyView).fadeIn(progressBar, animate) .show(progressBar); - + } return this; } @@ -406,8 +415,9 @@ public ItemListFragment setListShown(final boolean shown, * @return this fragment */ protected ItemListFragment setEmptyText(final String message) { - if (emptyView != null) + if (emptyView != null) { emptyView.setText(message); + } return this; } @@ -418,8 +428,9 @@ protected ItemListFragment setEmptyText(final String message) { * @return this fragment */ protected ItemListFragment setEmptyText(final int resId) { - if (emptyView != null) + if (emptyView != null) { emptyView.setText(resId); + } return this; } @@ -431,7 +442,8 @@ protected ItemListFragment setEmptyText(final int resId) { * @param position * @param id */ - public void onListItemClick(ListView l, View v, int position, long id) { + public void onListItemClick(final ListView l, final View v, + final int position, final long id) { } /** diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/MainActivity.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/MainActivity.java new file mode 100644 index 0000000..ef0d818 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/MainActivity.java @@ -0,0 +1,214 @@ + + +package com.donnfelker.android.bootstrap.ui; + +import android.accounts.OperationCanceledException; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.support.v4.app.ActionBarDrawerToggle; +import android.support.v4.app.FragmentManager; +import android.support.v4.widget.DrawerLayout; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; + +import com.donnfelker.android.bootstrap.BootstrapServiceProvider; +import com.donnfelker.android.bootstrap.R; +import com.donnfelker.android.bootstrap.core.BootstrapService; +import com.donnfelker.android.bootstrap.events.NavItemSelectedEvent; +import com.donnfelker.android.bootstrap.util.Ln; +import com.donnfelker.android.bootstrap.util.SafeAsyncTask; +import com.donnfelker.android.bootstrap.util.UIUtils; +import com.squareup.otto.Subscribe; + +import javax.inject.Inject; + +import butterknife.Views; + + +/** + * Initial activity for the application. + * + * If you need to remove the authentication from the application please see + * {@link com.donnfelker.android.bootstrap.authenticator.ApiKeyProvider#getAuthKey(android.app.Activity)} + */ +public class MainActivity extends BootstrapFragmentActivity { + + @Inject protected BootstrapServiceProvider serviceProvider; + + private boolean userHasAuthenticated = false; + + private DrawerLayout drawerLayout; + private ActionBarDrawerToggle drawerToggle; + private CharSequence drawerTitle; + private CharSequence title; + private NavigationDrawerFragment navigationDrawerFragment; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + + super.onCreate(savedInstanceState); + + if(isTablet()) { + setContentView(R.layout.main_activity_tablet); + } else { + setContentView(R.layout.main_activity); + } + + // View injection with Butterknife + Views.inject(this); + + // Set up navigation drawer + title = drawerTitle = getTitle(); + + if(!isTablet()) { + drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); + drawerToggle = new ActionBarDrawerToggle( + this, /* Host activity */ + drawerLayout, /* DrawerLayout object */ + R.drawable.ic_drawer, /* nav drawer icon to replace 'Up' caret */ + R.string.navigation_drawer_open, /* "open drawer" description */ + R.string.navigation_drawer_close) { /* "close drawer" description */ + + /** Called when a drawer has settled in a completely closed state. */ + public void onDrawerClosed(View view) { + getSupportActionBar().setTitle(title); + supportInvalidateOptionsMenu(); // creates call to onPrepareOptionsMenu() + } + + /** Called when a drawer has settled in a completely open state. */ + public void onDrawerOpened(View drawerView) { + getSupportActionBar().setTitle(drawerTitle); + supportInvalidateOptionsMenu(); // creates call to onPrepareOptionsMenu() + } + }; + + // Set the drawer toggle as the DrawerListener + drawerLayout.setDrawerListener(drawerToggle); + + navigationDrawerFragment = (NavigationDrawerFragment) + getSupportFragmentManager().findFragmentById(R.id.navigation_drawer); + + // Set up the drawer. + navigationDrawerFragment.setUp( + R.id.navigation_drawer, + (DrawerLayout) findViewById(R.id.drawer_layout)); + } + + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + + + checkAuth(); + + } + + private boolean isTablet() { + return UIUtils.isTablet(this); + } + + @Override + protected void onPostCreate(final Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + + if(!isTablet()) { + // Sync the toggle state after onRestoreInstanceState has occurred. + drawerToggle.syncState(); + } + } + + + @Override + public void onConfigurationChanged(final Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if(!isTablet()) { + drawerToggle.onConfigurationChanged(newConfig); + } + } + + + private void initScreen() { + if (userHasAuthenticated) { + + Ln.d("Foo"); + final FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.container, new CarouselFragment()) + .commit(); + } + + } + + private void checkAuth() { + new SafeAsyncTask() { + + @Override + public Boolean call() throws Exception { + final BootstrapService svc = serviceProvider.getService(MainActivity.this); + return svc != null; + } + + @Override + protected void onException(final Exception e) throws RuntimeException { + super.onException(e); + if (e instanceof OperationCanceledException) { + // User cancelled the authentication process (back button, etc). + // Since auth could not take place, lets finish this activity. + finish(); + } + } + + @Override + protected void onSuccess(final Boolean hasAuthenticated) throws Exception { + super.onSuccess(hasAuthenticated); + userHasAuthenticated = true; + initScreen(); + } + }.execute(); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + + if (!isTablet() && drawerToggle.onOptionsItemSelected(item)) { + return true; + } + + switch (item.getItemId()) { + case android.R.id.home: + //menuDrawer.toggleMenu(); + return true; + case R.id.timer: + navigateToTimer(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void navigateToTimer() { + final Intent i = new Intent(this, BootstrapTimerActivity.class); + startActivity(i); + } + + @Subscribe + public void onNavigationItemSelected(NavItemSelectedEvent event) { + + Ln.d("Selected: %1$s", event.getItemPosition()); + + switch(event.getItemPosition()) { + case 0: + // Home + // do nothing as we're already on the home screen. + break; + case 1: + // Timer + navigateToTimer(); + break; + } + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/NavigationDrawerFragment.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/NavigationDrawerFragment.java new file mode 100644 index 0000000..178fdaf --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/NavigationDrawerFragment.java @@ -0,0 +1,267 @@ +package com.donnfelker.android.bootstrap.ui; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.os.Bundle; +import android.support.v4.app.ActionBarDrawerToggle; +import android.support.v4.app.Fragment; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import com.donnfelker.android.bootstrap.Injector; +import com.donnfelker.android.bootstrap.R; +import com.donnfelker.android.bootstrap.events.NavItemSelectedEvent; +import com.donnfelker.android.bootstrap.util.UIUtils; +import com.squareup.otto.Bus; + +import javax.inject.Inject; + +/** + * Fragment used for managing interactions for and presentation of a navigation drawer. + * See the + * design guidelines for a complete explanation of the behaviors implemented here. + */ +public class NavigationDrawerFragment extends Fragment { + + /** + * Remember the position of the selected item. + */ + private static final String STATE_SELECTED_POSITION = "selected_navigation_drawer_position"; + + /** + * Per the design guidelines, you should show the drawer on launch until the user manually + * expands it. This shared preference tracks this. + */ + private static final String PREF_USER_LEARNED_DRAWER = "navigation_drawer_learned"; + + /** + * Helper component that ties the action bar to the navigation drawer. + */ + private ActionBarDrawerToggle drawerToggle; + + private DrawerLayout drawerLayout; + private ListView drawerListView; + private View fragmentContainerView; + + private int currentSelectedPosition = 0; + private boolean fromSavedInstanceState; + private boolean userLearnedDrawer; + + @Inject protected SharedPreferences prefs; + @Inject protected Bus bus; + + + public NavigationDrawerFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Injector.inject(this); + + // Read in the flag indicating whether or not the user has demonstrated awareness of the + // drawer. See PREF_USER_LEARNED_DRAWER for details. + userLearnedDrawer = prefs.getBoolean(PREF_USER_LEARNED_DRAWER, false); + + if (savedInstanceState != null) { + currentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION); + fromSavedInstanceState = true; + } + + // Select either the default item (0) or the last selected item. + selectItem(currentSelectedPosition); + + + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Indicate that this fragment would like to influence the set of actions in the action bar. + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + drawerListView = (ListView) inflater.inflate(R.layout.fragment_navigation_drawer, container, false); + drawerListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + selectItem(position); + } + }); + drawerListView.setAdapter(new ArrayAdapter( + getActionBar().getThemedContext(), + android.R.layout.simple_list_item_1, + android.R.id.text1, + new String[] { + getString(R.string.title_home), + getString(R.string.title_timer) + })); + drawerListView.setItemChecked(currentSelectedPosition, true); + return drawerListView; + } + + public boolean isDrawerOpen() { + return drawerLayout != null && drawerLayout.isDrawerOpen(fragmentContainerView); + } + + /** + * Users of this fragment must call this method to set up the navigation drawer interactions. + * + * @param fragmentId The android:id of this fragment in its activity's layout. + * @param drawerLayout The DrawerLayout containing this fragment's UI. + */ + public void setUp(int fragmentId, DrawerLayout drawerLayout) { + fragmentContainerView = getActivity().findViewById(fragmentId); + this.drawerLayout = drawerLayout; + + // set a custom shadow that overlays the main content when the drawer opens + //drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); + // set up the drawer's list view with items and click listener + + ActionBar actionBar = getActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeButtonEnabled(true); + + // ActionBarDrawerToggle ties together the the proper interactions + // between the navigation drawer and the action bar app icon. + drawerToggle = new ActionBarDrawerToggle( + getActivity(), /* host Activity */ + NavigationDrawerFragment.this.drawerLayout, /* DrawerLayout object */ + R.drawable.ic_drawer, /* nav drawer image to replace 'Up' caret */ + R.string.navigation_drawer_open, /* "open drawer" description for accessibility */ + R.string.navigation_drawer_close /* "close drawer" description for accessibility */ + ) { + @Override + public void onDrawerClosed(View drawerView) { + super.onDrawerClosed(drawerView); + if (!isAdded()) { + return; + } + + getActivity().supportInvalidateOptionsMenu(); // calls onPrepareOptionsMenu() + } + + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + if (!isAdded()) { + return; + } + + if (!userLearnedDrawer) { + // The user manually opened the drawer; store this flag to prevent auto-showing + // the navigation drawer automatically in the future. + userLearnedDrawer = true; + prefs.edit().putBoolean(PREF_USER_LEARNED_DRAWER, true).apply(); + } + + getActivity().supportInvalidateOptionsMenu(); // calls onPrepareOptionsMenu() + } + }; + + // If the user hasn't 'learned' about the drawer, open it to introduce them to the drawer, + // per the navigation drawer design guidelines. + if (!userLearnedDrawer && !fromSavedInstanceState) { + this.drawerLayout.openDrawer(fragmentContainerView); + } + + // Defer code dependent on restoration of previous instance state. + this.drawerLayout.post(new Runnable() { + @Override + public void run() { + drawerToggle.syncState(); + } + }); + + this.drawerLayout.setDrawerListener(drawerToggle); + } + + private void selectItem(int position) { + currentSelectedPosition = position; + if (drawerListView != null) { + drawerListView.setItemChecked(position, true); + } + if (drawerLayout != null) { + drawerLayout.closeDrawer(fragmentContainerView); + } + + // Fire the event off to the bus which. + bus.post(new NavItemSelectedEvent(position)); + + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(STATE_SELECTED_POSITION, currentSelectedPosition); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + if(!isTablet()) { + // Forward the new configuration the drawer toggle component. + drawerToggle.onConfigurationChanged(newConfig); + } + } + + private boolean isTablet() { + if(getActivity() != null) { + return UIUtils.isTablet(getActivity()); + } + return false; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + // If the drawer is open, show the global app actions in the action bar. See also + // showGlobalContextActionBar, which controls the top-left area of the action bar. + if (drawerLayout != null && isDrawerOpen()) { + inflater.inflate(R.menu.global, menu); + showGlobalContextActionBar(); + } + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (drawerToggle.onOptionsItemSelected(item)) { + return true; + } + + return super.onOptionsItemSelected(item); + } + + /** + * Per the navigation drawer design guidelines, updates the action bar to show the global app + * 'context', rather than just what's in the current screen. + */ + private void showGlobalContextActionBar() { + ActionBar actionBar = getActionBar(); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); + actionBar.setTitle(R.string.app_name); + } + + private ActionBar getActionBar() { + return ((ActionBarActivity) getActivity()).getSupportActionBar(); + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/NewsActivity.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/NewsActivity.java index bac4b8c..30ccec4 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/NewsActivity.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/NewsActivity.java @@ -1,36 +1,32 @@ package com.donnfelker.android.bootstrap.ui; -import android.content.Intent; import android.os.Bundle; import android.widget.TextView; -import com.actionbarsherlock.R; -import com.actionbarsherlock.view.MenuItem; -import com.donnfelker.android.bootstrap.BootstrapServiceProvider; +import com.donnfelker.android.bootstrap.R; import com.donnfelker.android.bootstrap.core.News; -import com.github.rtyley.android.sherlock.roboguice.activity.RoboSherlockActivity; -import com.google.inject.Inject; -import roboguice.inject.InjectExtra; -import roboguice.inject.InjectView; +import butterknife.InjectView; -import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; -import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; import static com.donnfelker.android.bootstrap.core.Constants.Extra.NEWS_ITEM; public class NewsActivity extends BootstrapActivity { - @InjectExtra(NEWS_ITEM) protected News newsItem; + private News newsItem; @InjectView(R.id.tv_title) protected TextView title; @InjectView(R.id.tv_content) protected TextView content; @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.news); + if (getIntent() != null && getIntent().getExtras() != null) { + newsItem = (News) getIntent().getExtras().getSerializable(NEWS_ITEM); + } + getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setHomeButtonEnabled(true); diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/NewsListAdapter.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/NewsListAdapter.java index 20f9567..144176c 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/NewsListAdapter.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/NewsListAdapter.java @@ -4,7 +4,6 @@ import com.donnfelker.android.bootstrap.R; import com.donnfelker.android.bootstrap.core.News; -import com.donnfelker.android.bootstrap.ui.AlternatingColorListAdapter; import java.util.List; @@ -14,8 +13,8 @@ public class NewsListAdapter extends AlternatingColorListAdapter { * @param items * @param selectable */ - public NewsListAdapter(LayoutInflater inflater, List items, - boolean selectable) { + public NewsListAdapter(final LayoutInflater inflater, final List items, + final boolean selectable) { super(R.layout.news_list_item, inflater, items, selectable); } @@ -23,22 +22,22 @@ public NewsListAdapter(LayoutInflater inflater, List items, * @param inflater * @param items */ - public NewsListAdapter(LayoutInflater inflater, List items) { + public NewsListAdapter(final LayoutInflater inflater, final List items) { super(R.layout.news_list_item, inflater, items); } @Override protected int[] getChildViewIds() { - return new int[] { R.id.tv_title, R.id.tv_summary, - R.id.tv_date }; + return new int[]{R.id.tv_title, R.id.tv_summary, + R.id.tv_date}; } @Override - protected void update(int position, News item) { + protected void update(final int position, final News item) { super.update(position, item); - setText(R.id.tv_title, item.getTitle()); - setText(R.id.tv_summary, item.getContent()); + setText(0, item.getTitle()); + setText(1, item.getContent()); //setNumber(R.id.tv_date, item.getCreatedAt()); } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/NewsListFragment.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/NewsListFragment.java index 430fb73..18e11a9 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/NewsListFragment.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/NewsListFragment.java @@ -1,6 +1,5 @@ package com.donnfelker.android.bootstrap.ui; -import static com.donnfelker.android.bootstrap.core.Constants.Extra.NEWS_ITEM; import android.accounts.OperationCanceledException; import android.app.Activity; import android.content.Intent; @@ -10,16 +9,29 @@ import android.widget.ListView; import com.donnfelker.android.bootstrap.BootstrapServiceProvider; +import com.donnfelker.android.bootstrap.Injector; import com.donnfelker.android.bootstrap.R; +import com.donnfelker.android.bootstrap.authenticator.LogoutService; import com.donnfelker.android.bootstrap.core.News; import com.github.kevinsawicki.wishlist.SingleTypeAdapter; -import com.google.inject.Inject; +import java.util.Collections; import java.util.List; +import javax.inject.Inject; + +import static com.donnfelker.android.bootstrap.core.Constants.Extra.NEWS_ITEM; + public class NewsListFragment extends ItemListFragment { @Inject protected BootstrapServiceProvider serviceProvider; + @Inject protected LogoutService logoutService; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Injector.inject(this); + } @Override public void onActivityCreated(Bundle savedInstanceState) { @@ -40,6 +52,11 @@ protected void configureList(Activity activity, ListView listView) { .inflate(R.layout.news_list_item_labels, null)); } + @Override + protected LogoutService getLogoutService() { + return logoutService; + } + @Override public void onDestroyView() { setListAdapter(null); @@ -55,7 +72,12 @@ public Loader> onCreateLoader(int id, Bundle args) { @Override public List loadData() throws Exception { try { - return serviceProvider.getService().getNews(); + if (getActivity() != null) { + return serviceProvider.getService(getActivity()).getNews(); + } else { + return Collections.emptyList(); + } + } catch (OperationCanceledException e) { Activity activity = getActivity(); if (activity != null) diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/TextWatcherAdapter.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/TextWatcherAdapter.java index 695624e..7e4a283 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/TextWatcherAdapter.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/TextWatcherAdapter.java @@ -10,12 +10,14 @@ */ public class TextWatcherAdapter implements TextWatcher { - public void afterTextChanged(Editable s) { + public void afterTextChanged(final Editable s) { } - public void beforeTextChanged(CharSequence s, int start, int count, int after) { + public void beforeTextChanged(final CharSequence s, final int start, + final int count, final int after) { } - public void onTextChanged(CharSequence s, int start, int before, int count) { + public void onTextChanged(final CharSequence s, final int start, + final int before, final int count) { } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/ThrowableLoader.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/ThrowableLoader.java index f4a6d61..a224e9f 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/ThrowableLoader.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/ThrowableLoader.java @@ -2,9 +2,9 @@ package com.donnfelker.android.bootstrap.ui; import android.content.Context; -import android.util.Log; -import roboguice.util.Ln; +import com.donnfelker.android.bootstrap.util.Ln; + /** * Loader that support throwing an exception when loading in the background @@ -13,7 +13,6 @@ */ public abstract class ThrowableLoader extends AsyncLoader { - private final D data; private Exception exception; @@ -24,7 +23,7 @@ public abstract class ThrowableLoader extends AsyncLoader { * @param context * @param data */ - public ThrowableLoader(Context context, D data) { + public ThrowableLoader(final Context context, final D data) { super(context); this.data = data; @@ -35,7 +34,7 @@ public D loadInBackground() { exception = null; try { return loadData(); - } catch (Exception e) { + } catch (final Exception e) { Ln.d(e, "Exception loading data"); exception = e; return data; diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/UserActivity.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/UserActivity.java index 86e7f37..ee18e4d 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/UserActivity.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/UserActivity.java @@ -1,37 +1,41 @@ package com.donnfelker.android.bootstrap.ui; -import static com.donnfelker.android.bootstrap.core.Constants.Extra.USER; import android.os.Bundle; import android.widget.ImageView; import android.widget.TextView; import com.donnfelker.android.bootstrap.R; -import com.donnfelker.android.bootstrap.core.AvatarLoader; import com.donnfelker.android.bootstrap.core.User; -import com.google.inject.Inject; +import com.squareup.picasso.Picasso; + +import butterknife.InjectView; -import roboguice.inject.InjectExtra; -import roboguice.inject.InjectView; +import static com.donnfelker.android.bootstrap.core.Constants.Extra.USER; public class UserActivity extends BootstrapActivity { @InjectView(R.id.iv_avatar) protected ImageView avatar; @InjectView(R.id.tv_name) protected TextView name; - @InjectExtra(USER) protected User user; - - @Inject protected AvatarLoader avatarLoader; + private User user; @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.user_view); + if (getIntent() != null && getIntent().getExtras() != null) { + user = (User) getIntent().getExtras().getSerializable(USER); + } + getSupportActionBar().setHomeButtonEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - avatarLoader.bind(avatar, user); + Picasso.with(this).load(user.getAvatarUrl()) + .placeholder(R.drawable.gravatar_icon) + .into(avatar); + name.setText(String.format("%s %s", user.getFirstName(), user.getLastName())); } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/UserListAdapter.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/UserListAdapter.java index 5830f3b..b546d3e 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/UserListAdapter.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/UserListAdapter.java @@ -3,10 +3,11 @@ import android.text.TextUtils; import android.view.LayoutInflater; +import com.donnfelker.android.bootstrap.BootstrapApplication; import com.donnfelker.android.bootstrap.R; -import com.donnfelker.android.bootstrap.core.AvatarLoader; import com.donnfelker.android.bootstrap.core.User; import com.github.kevinsawicki.wishlist.SingleTypeAdapter; +import com.squareup.picasso.Picasso; import java.text.SimpleDateFormat; import java.util.List; @@ -17,24 +18,22 @@ public class UserListAdapter extends SingleTypeAdapter { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MMMM dd"); - private final AvatarLoader avatars; /** * @param inflater * @param items */ - public UserListAdapter(LayoutInflater inflater, List items, AvatarLoader avatars) { + public UserListAdapter(final LayoutInflater inflater, final List items) { super(inflater, R.layout.user_list_item); - this.avatars = avatars; setItems(items); } /** * @param inflater */ - public UserListAdapter(LayoutInflater inflater, AvatarLoader avatars) { - this(inflater, null, avatars); + public UserListAdapter(final LayoutInflater inflater) { + this(inflater, null); } @@ -47,15 +46,18 @@ public long getItemId(final int position) { @Override protected int[] getChildViewIds() { - return new int[] { R.id.iv_avatar, R.id.tv_name }; + return new int[]{R.id.iv_avatar, R.id.tv_name}; } @Override - protected void update(int position, User user) { + protected void update(final int position, final User user) { - avatars.bind(imageView(R.id.iv_avatar), user); + Picasso.with(BootstrapApplication.getInstance()) + .load(user.getAvatarUrl()) + .placeholder(R.drawable.gravatar_icon) + .into(imageView(0)); - setText(R.id.tv_name, String.format("%1$s %2$s", user.getFirstName(), user.getLastName())); + setText(1, String.format("%1$s %2$s", user.getFirstName(), user.getLastName())); } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/UserListFragment.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/UserListFragment.java index ab53827..09272ea 100644 --- a/app/src/main/java/com/donnfelker/android/bootstrap/ui/UserListFragment.java +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/UserListFragment.java @@ -1,7 +1,5 @@ package com.donnfelker.android.bootstrap.ui; -import static com.donnfelker.android.bootstrap.core.Constants.Extra.NEWS_ITEM; -import static com.donnfelker.android.bootstrap.core.Constants.Extra.USER; import android.accounts.OperationCanceledException; import android.app.Activity; import android.content.Intent; @@ -11,58 +9,78 @@ import android.widget.ListView; import com.donnfelker.android.bootstrap.BootstrapServiceProvider; +import com.donnfelker.android.bootstrap.Injector; import com.donnfelker.android.bootstrap.R; -import com.donnfelker.android.bootstrap.core.AvatarLoader; -import com.donnfelker.android.bootstrap.core.News; +import com.donnfelker.android.bootstrap.authenticator.LogoutService; import com.donnfelker.android.bootstrap.core.User; import com.github.kevinsawicki.wishlist.SingleTypeAdapter; -import com.google.inject.Inject; import java.util.Collections; import java.util.List; -public class UserListFragment extends ItemListFragment { +import javax.inject.Inject; + +import static com.donnfelker.android.bootstrap.core.Constants.Extra.USER; + +public class UserListFragment extends ItemListFragment { + + @Inject protected BootstrapServiceProvider serviceProvider; + @Inject protected LogoutService logoutService; - @Inject private BootstrapServiceProvider serviceProvider; - @Inject private AvatarLoader avatars; @Override - public void onActivityCreated(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Injector.inject(this); + } + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setEmptyText(R.string.no_users); } @Override - protected void configureList(Activity activity, ListView listView) { + protected void configureList(final Activity activity, final ListView listView) { super.configureList(activity, listView); listView.setFastScrollEnabled(true); listView.setDividerHeight(0); getListAdapter().addHeader(activity.getLayoutInflater() - .inflate(R.layout.user_list_item_labels, null)); + .inflate(R.layout.user_list_item_labels, null)); } - + @Override + protected LogoutService getLogoutService() { + return logoutService; + } @Override - public Loader> onCreateLoader(int id, Bundle args) { + public Loader> onCreateLoader(final int id, final Bundle args) { final List initialItems = items; return new ThrowableLoader>(getActivity(), items) { @Override public List loadData() throws Exception { try { - List latest = serviceProvider.getService().getUsers(); - if (latest != null) + List latest = null; + + if (getActivity() != null) { + latest = serviceProvider.getService(getActivity()).getUsers(); + } + + if (latest != null) { return latest; - else + } else { return Collections.emptyList(); - } catch (OperationCanceledException e) { - Activity activity = getActivity(); - if (activity != null) + } + } catch (final OperationCanceledException e) { + final Activity activity = getActivity(); + if (activity != null) { activity.finish(); + } return initialItems; } } @@ -70,25 +88,25 @@ public List loadData() throws Exception { } - public void onListItemClick(ListView l, View v, int position, long id) { - User user = ((User) l.getItemAtPosition(position)); + public void onListItemClick(final ListView l, final View v, final int position, final long id) { + final User user = ((User) l.getItemAtPosition(position)); startActivity(new Intent(getActivity(), UserActivity.class).putExtra(USER, user)); } @Override - public void onLoadFinished(Loader> loader, List items) { + public void onLoadFinished(final Loader> loader, final List items) { super.onLoadFinished(loader, items); } @Override - protected int getErrorMessage(Exception exception) { + protected int getErrorMessage(final Exception exception) { return R.string.error_loading_users; } @Override - protected SingleTypeAdapter createAdapter(List items) { - return new UserListAdapter(getActivity().getLayoutInflater(), items, avatars); + protected SingleTypeAdapter createAdapter(final List items) { + return new UserListAdapter(getActivity().getLayoutInflater(), items); } } diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/ui/view/CapitalizedTextView.java b/app/src/main/java/com/donnfelker/android/bootstrap/ui/view/CapitalizedTextView.java new file mode 100644 index 0000000..9bfac6d --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/ui/view/CapitalizedTextView.java @@ -0,0 +1,81 @@ +package com.donnfelker.android.bootstrap.ui.view; + +import android.content.Context; +import android.graphics.Typeface; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.Button; + +import com.donnfelker.android.bootstrap.util.Strings; +import android.util.Log; +import java.util.Hashtable; +import java.util.Locale; + +/** + * A button who's text is always uppercase which uses the roboto font. + * Inspired by com.actionbarsherlock.internal.widget.CapitalizingTextView + */ +public class CapitalizedTextView extends Button { + + private static final boolean IS_GINGERBREAD + = Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD; + + private static final String TAG = "Typefaces"; + private static final Hashtable cache = new Hashtable(); + + public CapitalizedTextView(Context context) { + super(context); + + setTF(context); + } + + public CapitalizedTextView(Context context, AttributeSet attrs) { + super(context, attrs); + + setTF(context); + } + + public CapitalizedTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + setTF(context); + + } + + @Override + public void setText(CharSequence text, BufferType type) { + if (IS_GINGERBREAD) { + try { + super.setText(text.toString().toUpperCase(Locale.ROOT), type); + } catch (NoSuchFieldError e) { + //Some manufacturer broke Locale.ROOT. See #572. + super.setText(text.toString().toUpperCase(), type); + } + } else { + super.setText(text.toString().toUpperCase(), type); + } + } + + public static Typeface getTypeFace(Context c, String assetPath) { + synchronized (cache) { + if (!cache.containsKey(assetPath)) { + try { + Typeface t = Typeface.createFromAsset(c.getAssets(), + assetPath); + cache.put(assetPath, t); + } catch (Exception e) { + Log.e(TAG, "Could not get typeface '" + assetPath + + "' because " + e.getMessage()); + return null; + } + } + return cache.get(assetPath); + } + } + + private void setTF(Context context) { + Typeface tf = getTypeFace(context, "fonts/Roboto-Regular.ttf"); + setTypeface(tf); + } +} diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/util/Ln.java b/app/src/main/java/com/donnfelker/android/bootstrap/util/Ln.java new file mode 100644 index 0000000..2f52135 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/util/Ln.java @@ -0,0 +1,300 @@ +package com.donnfelker.android.bootstrap.util; + + +import android.app.Application; +import android.content.pm.ApplicationInfo; +import android.util.Log; + +import javax.inject.Inject; + + +/** + * Originally from RoboGuice: + * https://github.com/roboguice/roboguice/blob/master/roboguice/src/main/java/roboguice/util/Ln.java + *

+ * A more natural android logging facility. + *

+ * WARNING: CHECK OUT COMMON PITFALLS BELOW + *

+ * Unlike {@link android.util.Log}, Log provides sensible defaults. + * Debug and Verbose logging is enabled for applications that + * have "android:debuggable=true" in their AndroidManifest.xml. + * For apps built using SDK Tools r8 or later, this means any debug + * build. Release builds built with r8 or later will have verbose + * and debug log messages turned off. + *

+ * The default tag is automatically set to your app's packagename, + * and the current context (eg. activity, service, application, etc) + * is appended as well. You can add an additional parameter to the + * tag using {@link #Log(String)}. + *

+ * Log-levels can be programatically overridden for specific instances + * using {@link #Log(String, boolean, boolean)}. + *

+ * Log messages may optionally use {@link String#format(String, Object...)} + * formatting, which will not be evaluated unless the log statement is output. + * Additional parameters to the logging statement are treated as varrgs parameters + * to {@link String#format(String, Object...)} + *

+ * Also, the current file and line is automatically appended to the tag + * (this is only done if debug is enabled for performance reasons). + *

+ * COMMON PITFALLS: + * * Make sure you put the exception FIRST in the call. A common + * mistake is to place it last as is the android.util.Log convention, + * but then it will get treated as varargs parameter. + * * vararg parameters are not appended to the log message! You must + * insert them into the log message using %s or another similar + * format parameter + *

+ * Usage Examples: + *

+ * Ln.v("hello there"); + * Ln.d("%s %s", "hello", "there"); + * Ln.e( exception, "Error during some operation"); + * Ln.w( exception, "Error during %s operation", "some other"); + */ +@SuppressWarnings({"ImplicitArrayToString"}) +public class Ln { + /** + * config is initially set to BaseConfig() with sensible defaults, then replaced + * by BaseConfig(ContextSingleton) during guice static injection pass. + */ + @Inject + protected static BaseConfig config = new BaseConfig(); + + /** + * print is initially set to Print(), then replaced by guice during + * static injection pass. This allows overriding where the log message is delivered to. + */ + @Inject + protected static Print print = new Print(); + + + private Ln() { + } + + + public static int v(Throwable t) { + return config.minimumLogLevel <= Log.VERBOSE ? print.println(Log.VERBOSE, + Log.getStackTraceString(t)) : 0; + } + + public static int v(Object s1, Object... args) { + if (config.minimumLogLevel > Log.VERBOSE) + return 0; + + final String s = Strings.toString(s1); + final String message = args.length > 0 ? String.format(s, args) : s; + return print.println(Log.VERBOSE, message); + } + + public static int v(Throwable throwable, Object s1, Object... args) { + if (config.minimumLogLevel > Log.VERBOSE) + return 0; + + final String s = Strings.toString(s1); + final String message = (args.length > 0 ? String.format(s, args) : s) + '\n' + + Log.getStackTraceString(throwable); + return print.println(Log.VERBOSE, message); + } + + public static int d(Throwable t) { + return config.minimumLogLevel <= Log.DEBUG ? print.println(Log.DEBUG, + Log.getStackTraceString(t)) : 0; + } + + public static int d(Object s1, Object... args) { + if (config.minimumLogLevel > Log.DEBUG) + return 0; + + final String s = Strings.toString(s1); + final String message = args.length > 0 ? String.format(s, args) : s; + return print.println(Log.DEBUG, message); + } + + public static int d(Throwable throwable, Object s1, Object... args) { + if (config.minimumLogLevel > Log.DEBUG) + return 0; + + final String s = Strings.toString(s1); + final String message = (args.length > 0 ? String.format(s, args) : s) + '\n' + + Log.getStackTraceString(throwable); + return print.println(Log.DEBUG, message); + } + + public static int i(Throwable t) { + return config.minimumLogLevel <= Log.INFO ? print.println(Log.INFO, + Log.getStackTraceString(t)) : 0; + } + + public static int i(Object s1, Object... args) { + if (config.minimumLogLevel > Log.INFO) + return 0; + + final String s = Strings.toString(s1); + final String message = args.length > 0 ? String.format(s, args) : s; + return print.println(Log.INFO, message); + } + + public static int i(Throwable throwable, Object s1, Object... args) { + if (config.minimumLogLevel > Log.INFO) + return 0; + + final String s = Strings.toString(s1); + final String message = (args.length > 0 ? String.format(s, args) : s) + '\n' + + Log.getStackTraceString(throwable); + return print.println(Log.INFO, message); + } + + public static int w(Throwable t) { + return config.minimumLogLevel <= Log.WARN ? print.println(Log.WARN, + Log.getStackTraceString(t)) : 0; + } + + public static int w(Object s1, Object... args) { + if (config.minimumLogLevel > Log.WARN) + return 0; + + final String s = Strings.toString(s1); + final String message = args.length > 0 ? String.format(s, args) : s; + return print.println(Log.WARN, message); + } + + public static int w(Throwable throwable, Object s1, Object... args) { + if (config.minimumLogLevel > Log.WARN) + return 0; + + final String s = Strings.toString(s1); + final String message = (args.length > 0 ? String.format(s, args) : s) + '\n' + + Log.getStackTraceString(throwable); + return print.println(Log.WARN, message); + } + + public static int e(Throwable t) { + return config.minimumLogLevel <= Log.ERROR ? print.println(Log.ERROR, + Log.getStackTraceString(t)) : 0; + } + + public static int e(Object s1, Object... args) { + if (config.minimumLogLevel > Log.ERROR) + return 0; + + final String s = Strings.toString(s1); + final String message = args.length > 0 ? String.format(s, args) : s; + return print.println(Log.ERROR, message); + } + + public static int e(Throwable throwable, Object s1, Object... args) { + if (config.minimumLogLevel > Log.ERROR) + return 0; + + final String s = Strings.toString(s1); + final String message = (args.length > 0 ? String.format(s, args) : s) + '\n' + + Log.getStackTraceString(throwable); + return print.println(Log.ERROR, message); + } + + public static boolean isDebugEnabled() { + return config.minimumLogLevel <= Log.DEBUG; + } + + public static boolean isVerboseEnabled() { + return config.minimumLogLevel <= Log.VERBOSE; + } + + public static Config getConfig() { + return config; + } + + + public static interface Config { + public int getLoggingLevel(); + + public void setLoggingLevel(int level); + } + + public static class BaseConfig implements Config { + protected int minimumLogLevel = Log.VERBOSE; + protected String packageName = ""; + protected String scope = ""; + + protected BaseConfig() { + } + + @Inject + public BaseConfig(Application context) { + try { + packageName = context.getPackageName(); + final int flags = context.getPackageManager() + .getApplicationInfo(packageName, 0).flags; + minimumLogLevel = (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 + ? Log.VERBOSE : Log.INFO; + scope = packageName.toUpperCase(); + + Ln.d("Configuring Logging, minimum log level is %s", + logLevelToString(minimumLogLevel)); + + } catch (Exception e) { + try { + Log.e(packageName, "Error configuring logger", e); + } catch (RuntimeException f) { + // HACK ignore Stub! errors in mock objects during testing + } + } + } + + public int getLoggingLevel() { + return minimumLogLevel; + } + + public void setLoggingLevel(int level) { + minimumLogLevel = level; + } + } + + public static String logLevelToString(int loglevel) { + switch (loglevel) { + case Log.VERBOSE: + return "VERBOSE"; + case Log.DEBUG: + return "DEBUG"; + case Log.INFO: + return "INFO"; + case Log.WARN: + return "WARN"; + case Log.ERROR: + return "ERROR"; + case Log.ASSERT: + return "ASSERT"; + } + + return "UNKNOWN"; + } + + + /** + * Default implementation logs to android.util.Log + */ + public static class Print { + public int println(int priority, String msg) { + return Log.println(priority, getScope(5), processMessage(msg)); + } + + protected String processMessage(String msg) { + if (config.minimumLogLevel <= Log.DEBUG) + msg = String.format("%s %s", Thread.currentThread().getName(), msg); + return msg; + } + + protected static String getScope(int skipDepth) { + if (config.minimumLogLevel <= Log.DEBUG) { + final StackTraceElement trace = Thread.currentThread().getStackTrace()[skipDepth]; + return config.scope + "/" + trace.getFileName() + ":" + trace.getLineNumber(); + } + + return config.scope; + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/util/SafeAsyncTask.java b/app/src/main/java/com/donnfelker/android/bootstrap/util/SafeAsyncTask.java new file mode 100644 index 0000000..1ec0ff6 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/util/SafeAsyncTask.java @@ -0,0 +1,304 @@ +package com.donnfelker.android.bootstrap.util; + +import android.os.Handler; +import android.os.Looper; + +import java.io.InterruptedIOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; + +/** + * Originally from RoboGuice: + * https://github.com/roboguice/roboguice/blob/master/roboguice/src/main/java/roboguice/util/SafeAsyncTask.java + *

+ * A class similar but unrelated to android's {@link android.os.AsyncTask}. + *

+ * Unlike AsyncTask, this class properly propagates exceptions. + *

+ * If you're familiar with AsyncTask and are looking for {@link android.os.AsyncTask#doInBackground(Object[])}, + * we've named it {@link #call()} here to conform with java 1.5's {@link java.util.concurrent.Callable} interface. + *

+ * Current limitations: does not yet handle progress, although it shouldn't be + * hard to add. + *

+ * If using your own executor, you must call future() to get a runnable you can execute. + * + * @param + */ +public abstract class SafeAsyncTask implements Callable { + public static final int DEFAULT_POOL_SIZE = 25; + protected static final Executor DEFAULT_EXECUTOR = Executors.newFixedThreadPool(DEFAULT_POOL_SIZE); + + protected Handler handler; + protected Executor executor; + protected StackTraceElement[] launchLocation; + protected FutureTask future; + + + /** + * Sets executor to Executors.newFixedThreadPool(DEFAULT_POOL_SIZE) and + * Handler to new Handler() + */ + public SafeAsyncTask() { + this.executor = DEFAULT_EXECUTOR; + } + + /** + * Sets executor to Executors.newFixedThreadPool(DEFAULT_POOL_SIZE) + */ + public SafeAsyncTask(Handler handler) { + this.handler = handler; + this.executor = DEFAULT_EXECUTOR; + } + + /** + * Sets Handler to new Handler() + */ + public SafeAsyncTask(Executor executor) { + this.executor = executor; + } + + public SafeAsyncTask(Handler handler, Executor executor) { + this.handler = handler; + this.executor = executor; + } + + + public FutureTask future() { + future = new FutureTask(newTask()); + return future; + } + + public SafeAsyncTask executor(Executor executor) { + this.executor = executor; + return this; + } + + public Executor executor() { + return executor; + } + + public SafeAsyncTask handler(Handler handler) { + this.handler = handler; + return this; + } + + public Handler handler() { + return handler; + } + + public void execute() { + execute(Thread.currentThread().getStackTrace()); + } + + protected void execute(StackTraceElement[] launchLocation) { + this.launchLocation = launchLocation; + executor.execute(future()); + } + + public boolean cancel(boolean mayInterruptIfRunning) { + if (future == null) + throw new UnsupportedOperationException("You cannot cancel this task before calling future()"); + + return future.cancel(mayInterruptIfRunning); + } + + + /** + * @throws Exception, captured on passed to onException() if present. + */ + protected void onPreExecute() throws Exception { + } + + /** + * @param t the result of {@link #call()} + * @throws Exception, captured on passed to onException() if present. + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onSuccess(ResultT t) throws Exception { + } + + /** + * Called when the thread has been interrupted, likely because + * the task was canceled. + *

+ * By default, calls {@link #onException(Exception)}, but this method + * may be overridden to handle interruptions differently than other + * exceptions. + * + * @param e an InterruptedException or InterruptedIOException + */ + protected void onInterrupted(Exception e) { + onException(e); + } + + /** + * Logs the exception as an Error by default, but this method may + * be overridden by subclasses. + * + * @param e the exception thrown from {@link #onPreExecute()}, {@link #call()}, or {@link #onSuccess(Object)} + * @throws RuntimeException, ignored + */ + protected void onException(Exception e) throws RuntimeException { + onThrowable(e); + } + + protected void onThrowable(Throwable t) throws RuntimeException { + Ln.e(t, "Throwable caught during background processing"); + } + + /** + * @throws RuntimeException, ignored + */ + protected void onFinally() throws RuntimeException { + } + + + protected Task newTask() { + return new Task(this); + } + + + public static class Task implements Callable { + protected final SafeAsyncTask parent; + protected final Handler handler; + + public Task(SafeAsyncTask parent) { + this.parent = parent; + this.handler = parent.handler != null ? parent.handler : new Handler(Looper.getMainLooper()); + } + + public Void call() throws Exception { + try { + doPreExecute(); + doSuccess(doCall()); + + } catch (final Exception e) { + try { + doException(e); + } catch (Exception f) { + // logged but ignored + Ln.e(f); + } + + } catch (final Throwable t) { + try { + doThrowable(t); + } catch (Exception f) { + // logged but ignored + Ln.e(f); + } + } finally { + doFinally(); + } + + return null; + } + + protected void doPreExecute() throws Exception { + postToUiThreadAndWait(new Callable() { + public Object call() throws Exception { + parent.onPreExecute(); + return null; + } + }); + } + + protected ResultT doCall() throws Exception { + return parent.call(); + } + + protected void doSuccess(final ResultT r) throws Exception { + postToUiThreadAndWait(new Callable() { + public Object call() throws Exception { + parent.onSuccess(r); + return null; + } + }); + } + + protected void doException(final Exception e) throws Exception { + if (parent.launchLocation != null) { + final ArrayList stack = new ArrayList(Arrays.asList(e.getStackTrace())); + stack.addAll(Arrays.asList(parent.launchLocation)); + e.setStackTrace(stack.toArray(new StackTraceElement[stack.size()])); + } + postToUiThreadAndWait(new Callable() { + public Object call() throws Exception { + if (e instanceof InterruptedException || e instanceof InterruptedIOException) + parent.onInterrupted(e); + else + parent.onException(e); + return null; + } + }); + } + + protected void doThrowable(final Throwable e) throws Exception { + if (parent.launchLocation != null) { + final ArrayList stack = new ArrayList(Arrays.asList(e.getStackTrace())); + stack.addAll(Arrays.asList(parent.launchLocation)); + e.setStackTrace(stack.toArray(new StackTraceElement[stack.size()])); + } + postToUiThreadAndWait(new Callable() { + public Object call() throws Exception { + parent.onThrowable(e); + return null; + } + }); + } + + protected void doFinally() throws Exception { + postToUiThreadAndWait(new Callable() { + public Object call() throws Exception { + parent.onFinally(); + return null; + } + }); + } + + + /** + * Posts the specified runnable to the UI thread using a handler, + * and waits for operation to finish. If there's an exception, + * it captures it and rethrows it. + * + * @param c the callable to post + * @throws Exception on error + */ + protected void postToUiThreadAndWait(final Callable c) throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + final Exception[] exceptions = new Exception[1]; + + // Execute onSuccess in the UI thread, but wait + // for it to complete. + // If it throws an exception, capture that exception + // and rethrow it later. + handler.post(new Runnable() { + public void run() { + try { + c.call(); + } catch (Exception e) { + exceptions[0] = e; + } finally { + latch.countDown(); + } + } + }); + + // Wait for onSuccess to finish + latch.await(); + + if (exceptions[0] != null) + throw exceptions[0]; + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/util/Strings.java b/app/src/main/java/com/donnfelker/android/bootstrap/util/Strings.java new file mode 100644 index 0000000..6c9ba97 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/util/Strings.java @@ -0,0 +1,197 @@ +package com.donnfelker.android.bootstrap.util; + + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.security.InvalidParameterException; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class Strings { + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + /** + * Originally from RoboGuice: + * https://github.com/roboguice/roboguice/blob/master/roboguice/src/main/java/roboguice/util/Strings.java + * Like join, but allows for a distinct final delimiter. For english sentences such + * as "Alice, Bob and Charlie" use ", " and " and " as the delimiters. + * + * @param delimiter usually ", " + * @param lastDelimiter usually " and " + * @param objs the objects + * @param the type + * @return a string + */ + public static String joinAnd(final String delimiter, final String lastDelimiter, + final Collection objs) { + if (objs == null || objs.isEmpty()) + return ""; + + final Iterator iter = objs.iterator(); + final StringBuilder buffer = new StringBuilder(Strings.toString(iter.next())); + int i = 1; + while (iter.hasNext()) { + final T obj = iter.next(); + if (notEmpty(obj)) + buffer.append(++i == objs.size() ? lastDelimiter : delimiter). + append(Strings.toString(obj)); + } + return buffer.toString(); + } + + public static String joinAnd(final String delimiter, final String lastDelimiter, + final T... objs) { + return joinAnd(delimiter, lastDelimiter, Arrays.asList(objs)); + } + + public static String join(final String delimiter, final Collection objs) { + if (objs == null || objs.isEmpty()) + return ""; + + final Iterator iter = objs.iterator(); + final StringBuilder buffer = new StringBuilder(Strings.toString(iter.next())); + + while (iter.hasNext()) { + final T obj = iter.next(); + if (notEmpty(obj)) buffer.append(delimiter).append(Strings.toString(obj)); + } + return buffer.toString(); + } + + public static String join(final String delimiter, final T... objects) { + return join(delimiter, Arrays.asList(objects)); + } + + public static String toString(InputStream input) { + StringWriter sw = new StringWriter(); + copy(new InputStreamReader(input), sw); + return sw.toString(); + } + + public static String toString(Reader input) { + StringWriter sw = new StringWriter(); + copy(input, sw); + return sw.toString(); + } + + public static int copy(Reader input, Writer output) { + long count = copyLarge(input, output); + return count > Integer.MAX_VALUE ? -1 : (int) count; + } + + public static long copyLarge(Reader input, Writer output) throws RuntimeException { + try { + char[] buffer = new char[DEFAULT_BUFFER_SIZE]; + long count = 0; + int n; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static String toString(final Object o) { + return toString(o, ""); + } + + public static String toString(final Object o, final String def) { + return o == null ? def : + o instanceof InputStream ? toString((InputStream) o) : + o instanceof Reader ? toString((Reader) o) : + o instanceof Object[] ? Strings.join(", ", (Object[]) o) : + o instanceof Collection ? Strings.join(", ", (Collection) o) : o.toString(); + } + + public static boolean isEmpty(final Object o) { + return toString(o).trim().length() == 0; + } + + public static boolean notEmpty(final Object o) { + return toString(o).trim().length() != 0; + } + + public static String md5(String s) { + // http://stackoverflow.com/questions/1057041/difference-between-java-and-php5-md5-hash + // http://code.google.com/p/roboguice/issues/detail?id=89 + try { + + final byte[] hash = MessageDigest.getInstance("MD5").digest(s.getBytes("UTF-8")); + final StringBuilder hashString = new StringBuilder(); + + for (byte aHash : hash) { + String hex = Integer.toHexString(aHash); + + if (hex.length() == 1) { + hashString.append('0'); + hashString.append(hex.charAt(hex.length() - 1)); + } else { + hashString.append(hex.substring(hex.length() - 2)); + } + } + + return hashString.toString(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static String capitalize(String s) { + final String c = Strings.toString(s); + return c.length() >= 2 ? c.substring(0, 1).toUpperCase() + c.substring(1) : + c.length() >= 1 ? c.toUpperCase() : c; + } + + public static boolean equals(Object a, Object b) { + return Strings.toString(a).equals(Strings.toString(b)); + } + + public static boolean equalsIgnoreCase(Object a, Object b) { + return Strings.toString(a).toLowerCase().equals(Strings.toString(b).toLowerCase()); + } + + public static String[] chunk(String str, int chunkSize) { + if (isEmpty(str) || chunkSize == 0) + return new String[0]; + + final int len = str.length(); + final int arrayLen = ((len - 1) / chunkSize) + 1; + final String[] array = new String[arrayLen]; + for (int i = 0; i < arrayLen; ++i) + array[i] = str.substring(i * chunkSize, (i * chunkSize) + chunkSize < len + ? (i * chunkSize) + chunkSize : len); + + return array; + } + + public static String namedFormat(String str, Map substitutions) { + for (String key : substitutions.keySet()) + str = str.replace('$' + key, substitutions.get(key)); + + return str; + } + + public static String namedFormat(String str, Object... nameValuePairs) { + if (nameValuePairs.length % 2 != 0) + throw new InvalidParameterException("You must include one value for each parameter"); + + final HashMap map = new HashMap(nameValuePairs.length / 2); + for (int i = 0; i < nameValuePairs.length; i += 2) + map.put(Strings.toString(nameValuePairs[i]), Strings.toString(nameValuePairs[i + 1])); + + return namedFormat(str, map); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/donnfelker/android/bootstrap/util/UIUtils.java b/app/src/main/java/com/donnfelker/android/bootstrap/util/UIUtils.java new file mode 100644 index 0000000..f80afe2 --- /dev/null +++ b/app/src/main/java/com/donnfelker/android/bootstrap/util/UIUtils.java @@ -0,0 +1,19 @@ +package com.donnfelker.android.bootstrap.util; + +import android.content.Context; +import android.content.res.Configuration; + +public class UIUtils { + + /** + * Helps determine if the app is running in a Tablet context. + * + * @param context + * @return + */ + public static boolean isTablet(Context context) { + return (context.getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) + >= Configuration.SCREENLAYOUT_SIZE_LARGE; + } +} diff --git a/app/src/main/res/color/nav_text_selector.xml b/app/src/main/res/color/nav_text_selector.xml new file mode 100644 index 0000000..2fcef81 --- /dev/null +++ b/app/src/main/res/color/nav_text_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/res/color/table_text_light_selector.xml b/app/src/main/res/color/table_text_light_selector.xml similarity index 100% rename from app/res/color/table_text_light_selector.xml rename to app/src/main/res/color/table_text_light_selector.xml diff --git a/app/res/color/table_text_selector.xml b/app/src/main/res/color/table_text_selector.xml similarity index 100% rename from app/res/color/table_text_selector.xml rename to app/src/main/res/color/table_text_selector.xml diff --git a/app/res/color/text_light_selector.xml b/app/src/main/res/color/text_light_selector.xml similarity index 100% rename from app/res/color/text_light_selector.xml rename to app/src/main/res/color/text_light_selector.xml diff --git a/app/res/color/text_selector.xml b/app/src/main/res/color/text_selector.xml similarity index 100% rename from app/res/color/text_selector.xml rename to app/src/main/res/color/text_selector.xml diff --git a/app/res/color/text_title_selector.xml b/app/src/main/res/color/text_title_selector.xml similarity index 100% rename from app/res/color/text_title_selector.xml rename to app/src/main/res/color/text_title_selector.xml diff --git a/app/src/main/res/drawable-hdpi-v11/ic_stat_ab_notification.png b/app/src/main/res/drawable-hdpi-v11/ic_stat_ab_notification.png new file mode 100755 index 0000000..3b28155 Binary files /dev/null and b/app/src/main/res/drawable-hdpi-v11/ic_stat_ab_notification.png differ diff --git a/app/src/main/res/drawable-hdpi-v9/ic_stat_ab_notification.png b/app/src/main/res/drawable-hdpi-v9/ic_stat_ab_notification.png new file mode 100755 index 0000000..72d24f6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi-v9/ic_stat_ab_notification.png differ diff --git a/app/res/drawable-hdpi/ic_action_refresh.png b/app/src/main/res/drawable-hdpi/ic_action_refresh.png similarity index 100% rename from app/res/drawable-hdpi/ic_action_refresh.png rename to app/src/main/res/drawable-hdpi/ic_action_refresh.png diff --git a/app/src/main/res/drawable-hdpi/ic_action_timer.png b/app/src/main/res/drawable-hdpi/ic_action_timer.png new file mode 100755 index 0000000..dda32bd Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_timer.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_drawer.png b/app/src/main/res/drawable-hdpi/ic_drawer.png new file mode 100644 index 0000000..ec8c51f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_drawer.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_home.png b/app/src/main/res/drawable-hdpi/ic_home.png new file mode 100755 index 0000000..eb11224 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_home.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_stat_ab_notification.png b/app/src/main/res/drawable-hdpi/ic_stat_ab_notification.png new file mode 100755 index 0000000..d3d4d39 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_ab_notification.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_timer.png b/app/src/main/res/drawable-hdpi/ic_timer.png new file mode 100755 index 0000000..0febe1d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_timer.png differ diff --git a/app/res/drawable-hdpi/icon.png b/app/src/main/res/drawable-hdpi/icon.png similarity index 100% rename from app/res/drawable-hdpi/icon.png rename to app/src/main/res/drawable-hdpi/icon.png diff --git a/app/src/main/res/drawable-ldpi-v11/ic_stat_ab_notification.png b/app/src/main/res/drawable-ldpi-v11/ic_stat_ab_notification.png new file mode 100755 index 0000000..b8685af Binary files /dev/null and b/app/src/main/res/drawable-ldpi-v11/ic_stat_ab_notification.png differ diff --git a/app/src/main/res/drawable-ldpi-v9/ic_stat_ab_notification.png b/app/src/main/res/drawable-ldpi-v9/ic_stat_ab_notification.png new file mode 100755 index 0000000..944778f Binary files /dev/null and b/app/src/main/res/drawable-ldpi-v9/ic_stat_ab_notification.png differ diff --git a/app/res/drawable-ldpi/ic_action_refresh.png b/app/src/main/res/drawable-ldpi/ic_action_refresh.png similarity index 100% rename from app/res/drawable-ldpi/ic_action_refresh.png rename to app/src/main/res/drawable-ldpi/ic_action_refresh.png diff --git a/app/res/drawable-ldpi/icon.png b/app/src/main/res/drawable-ldpi/icon.png similarity index 100% rename from app/res/drawable-ldpi/icon.png rename to app/src/main/res/drawable-ldpi/icon.png diff --git a/app/src/main/res/drawable-mdpi-v11/ic_stat_ab_notification.png b/app/src/main/res/drawable-mdpi-v11/ic_stat_ab_notification.png new file mode 100755 index 0000000..d4087e5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi-v11/ic_stat_ab_notification.png differ diff --git a/app/src/main/res/drawable-mdpi-v9/ic_stat_ab_notification.png b/app/src/main/res/drawable-mdpi-v9/ic_stat_ab_notification.png new file mode 100755 index 0000000..6011074 Binary files /dev/null and b/app/src/main/res/drawable-mdpi-v9/ic_stat_ab_notification.png differ diff --git a/app/res/drawable-mdpi/ic_action_refresh.png b/app/src/main/res/drawable-mdpi/ic_action_refresh.png similarity index 100% rename from app/res/drawable-mdpi/ic_action_refresh.png rename to app/src/main/res/drawable-mdpi/ic_action_refresh.png diff --git a/app/src/main/res/drawable-mdpi/ic_action_timer.png b/app/src/main/res/drawable-mdpi/ic_action_timer.png new file mode 100755 index 0000000..475b0c6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_timer.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_drawer.png b/app/src/main/res/drawable-mdpi/ic_drawer.png new file mode 100644 index 0000000..7420a68 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_drawer.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_home.png b/app/src/main/res/drawable-mdpi/ic_home.png new file mode 100755 index 0000000..ad925f8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_home.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_ab_notification.png b/app/src/main/res/drawable-mdpi/ic_stat_ab_notification.png new file mode 100755 index 0000000..d763f38 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_ab_notification.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_timer.png b/app/src/main/res/drawable-mdpi/ic_timer.png new file mode 100755 index 0000000..74037a2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_timer.png differ diff --git a/app/res/drawable-mdpi/icon.png b/app/src/main/res/drawable-mdpi/icon.png similarity index 100% rename from app/res/drawable-mdpi/icon.png rename to app/src/main/res/drawable-mdpi/icon.png diff --git a/app/res/drawable-nodpi/gravatar_icon.png b/app/src/main/res/drawable-nodpi/gravatar_icon.png similarity index 100% rename from app/res/drawable-nodpi/gravatar_icon.png rename to app/src/main/res/drawable-nodpi/gravatar_icon.png diff --git a/app/res/drawable-nodpi/spinner_inner.png b/app/src/main/res/drawable-nodpi/spinner_inner.png similarity index 100% rename from app/res/drawable-nodpi/spinner_inner.png rename to app/src/main/res/drawable-nodpi/spinner_inner.png diff --git a/app/res/drawable-nodpi/spinner_outer.png b/app/src/main/res/drawable-nodpi/spinner_outer.png similarity index 100% rename from app/res/drawable-nodpi/spinner_outer.png rename to app/src/main/res/drawable-nodpi/spinner_outer.png diff --git a/app/src/main/res/drawable-xhdpi-v11/ic_stat_ab_notification.png b/app/src/main/res/drawable-xhdpi-v11/ic_stat_ab_notification.png new file mode 100755 index 0000000..b321d3a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi-v11/ic_stat_ab_notification.png differ diff --git a/app/src/main/res/drawable-xhdpi-v9/ic_stat_ab_notification.png b/app/src/main/res/drawable-xhdpi-v9/ic_stat_ab_notification.png new file mode 100755 index 0000000..1f33587 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi-v9/ic_stat_ab_notification.png differ diff --git a/app/res/drawable-xhdpi/ic_action_refresh.png b/app/src/main/res/drawable-xhdpi/ic_action_refresh.png similarity index 100% rename from app/res/drawable-xhdpi/ic_action_refresh.png rename to app/src/main/res/drawable-xhdpi/ic_action_refresh.png diff --git a/app/src/main/res/drawable-xhdpi/ic_action_timer.png b/app/src/main/res/drawable-xhdpi/ic_action_timer.png new file mode 100755 index 0000000..1c2ca4f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_timer.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drawer.png b/app/src/main/res/drawable-xhdpi/ic_drawer.png new file mode 100644 index 0000000..7ffbe5c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_drawer.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_home.png b/app/src/main/res/drawable-xhdpi/ic_home.png new file mode 100755 index 0000000..a7031e4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_home.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_ab_notification.png b/app/src/main/res/drawable-xhdpi/ic_stat_ab_notification.png new file mode 100755 index 0000000..8d8c0e9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_ab_notification.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_timer.png b/app/src/main/res/drawable-xhdpi/ic_timer.png new file mode 100755 index 0000000..0e15946 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_timer.png differ diff --git a/app/res/drawable-xhdpi/icon.png b/app/src/main/res/drawable-xhdpi/icon.png similarity index 100% rename from app/res/drawable-xhdpi/icon.png rename to app/src/main/res/drawable-xhdpi/icon.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_home.png b/app/src/main/res/drawable-xxhdpi/ic_home.png new file mode 100755 index 0000000..547a4e1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_home.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_timer.png b/app/src/main/res/drawable-xxhdpi/ic_timer.png new file mode 100755 index 0000000..43ce820 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_timer.png differ diff --git a/app/res/drawable/actionbar_background.xml b/app/src/main/res/drawable/actionbar_background.xml similarity index 100% rename from app/res/drawable/actionbar_background.xml rename to app/src/main/res/drawable/actionbar_background.xml diff --git a/app/res/drawable/bootstrap_divider.xml b/app/src/main/res/drawable/bootstrap_divider.xml similarity index 100% rename from app/res/drawable/bootstrap_divider.xml rename to app/src/main/res/drawable/bootstrap_divider.xml diff --git a/app/res/drawable/button_background_disabled.xml b/app/src/main/res/drawable/button_background_disabled.xml similarity index 100% rename from app/res/drawable/button_background_disabled.xml rename to app/src/main/res/drawable/button_background_disabled.xml diff --git a/app/res/drawable/button_background_enabled.xml b/app/src/main/res/drawable/button_background_enabled.xml similarity index 100% rename from app/res/drawable/button_background_enabled.xml rename to app/src/main/res/drawable/button_background_enabled.xml diff --git a/app/src/main/res/drawable/button_background_pressed.xml b/app/src/main/res/drawable/button_background_pressed.xml new file mode 100644 index 0000000..6dcb77f --- /dev/null +++ b/app/src/main/res/drawable/button_background_pressed.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/res/drawable/button_background_states.xml b/app/src/main/res/drawable/button_background_states.xml similarity index 73% rename from app/res/drawable/button_background_states.xml rename to app/src/main/res/drawable/button_background_states.xml index 6aebde7..eafea6f 100644 --- a/app/res/drawable/button_background_states.xml +++ b/app/src/main/res/drawable/button_background_states.xml @@ -2,7 +2,8 @@ - + + \ No newline at end of file diff --git a/app/res/drawable/edit_text_background.xml b/app/src/main/res/drawable/edit_text_background.xml similarity index 100% rename from app/res/drawable/edit_text_background.xml rename to app/src/main/res/drawable/edit_text_background.xml diff --git a/app/res/drawable/edit_text_cursor.xml b/app/src/main/res/drawable/edit_text_cursor.xml similarity index 100% rename from app/res/drawable/edit_text_cursor.xml rename to app/src/main/res/drawable/edit_text_cursor.xml diff --git a/app/res/drawable/list_item_background.xml b/app/src/main/res/drawable/list_item_background.xml similarity index 100% rename from app/res/drawable/list_item_background.xml rename to app/src/main/res/drawable/list_item_background.xml diff --git a/app/res/drawable/main_background.xml b/app/src/main/res/drawable/main_background.xml similarity index 100% rename from app/res/drawable/main_background.xml rename to app/src/main/res/drawable/main_background.xml diff --git a/app/res/drawable/map_header_background.xml b/app/src/main/res/drawable/map_header_background.xml similarity index 100% rename from app/res/drawable/map_header_background.xml rename to app/src/main/res/drawable/map_header_background.xml diff --git a/app/src/main/res/drawable/nav_menu_button_background_disabled.xml b/app/src/main/res/drawable/nav_menu_button_background_disabled.xml new file mode 100755 index 0000000..1045bb3 --- /dev/null +++ b/app/src/main/res/drawable/nav_menu_button_background_disabled.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/nav_menu_button_background_enabled.xml b/app/src/main/res/drawable/nav_menu_button_background_enabled.xml new file mode 100755 index 0000000..97384c3 --- /dev/null +++ b/app/src/main/res/drawable/nav_menu_button_background_enabled.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/nav_menu_button_background_pressed.xml b/app/src/main/res/drawable/nav_menu_button_background_pressed.xml new file mode 100755 index 0000000..b6813e6 --- /dev/null +++ b/app/src/main/res/drawable/nav_menu_button_background_pressed.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/nav_menu_button_background_selector.xml b/app/src/main/res/drawable/nav_menu_button_background_selector.xml new file mode 100755 index 0000000..f5a007b --- /dev/null +++ b/app/src/main/res/drawable/nav_menu_button_background_selector.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/res/drawable/spinner.xml b/app/src/main/res/drawable/spinner.xml similarity index 100% rename from app/res/drawable/spinner.xml rename to app/src/main/res/drawable/spinner.xml diff --git a/app/res/drawable/stripe.png b/app/src/main/res/drawable/stripe.png similarity index 100% rename from app/res/drawable/stripe.png rename to app/src/main/res/drawable/stripe.png diff --git a/app/res/drawable/stripe_repeat.xml b/app/src/main/res/drawable/stripe_repeat.xml similarity index 100% rename from app/res/drawable/stripe_repeat.xml rename to app/src/main/res/drawable/stripe_repeat.xml diff --git a/app/res/drawable/table_background_alternate_selector.xml b/app/src/main/res/drawable/table_background_alternate_selector.xml similarity index 100% rename from app/res/drawable/table_background_alternate_selector.xml rename to app/src/main/res/drawable/table_background_alternate_selector.xml diff --git a/app/res/drawable/table_background_selector.xml b/app/src/main/res/drawable/table_background_selector.xml similarity index 100% rename from app/res/drawable/table_background_selector.xml rename to app/src/main/res/drawable/table_background_selector.xml diff --git a/app/src/main/res/layout/bootstrap_timer.xml b/app/src/main/res/layout/bootstrap_timer.xml new file mode 100644 index 0000000..5367a92 --- /dev/null +++ b/app/src/main/res/layout/bootstrap_timer.xml @@ -0,0 +1,51 @@ + + + + + + +