├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── icon_large.png │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable │ │ │ │ ├── scrim_top.xml │ │ │ │ ├── shape_circle.xml │ │ │ │ ├── line_divider.xml │ │ │ │ ├── ic_send.xml │ │ │ │ ├── ic_arrow_upward.xml │ │ │ │ ├── ic_terrain.xml │ │ │ │ ├── ic_compass.xml │ │ │ │ ├── ic_map.xml │ │ │ │ ├── ic_archive.xml │ │ │ │ ├── ic_pencil_box.xml │ │ │ │ ├── ic_extension.xml │ │ │ │ ├── ic_cube.xml │ │ │ │ └── ic_speedometer.xml │ │ │ ├── anim │ │ │ │ └── rotation.xml │ │ │ ├── values-w820dp │ │ │ │ └── dimens.xml │ │ │ ├── menu │ │ │ │ ├── menu_cache_detail.xml │ │ │ │ └── menu_cache_list.xml │ │ │ ├── values │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ ├── values-v21 │ │ │ │ └── styles.xml │ │ │ └── layout │ │ │ │ ├── fragment_cache_list.xml │ │ │ │ ├── library_item.xml │ │ │ │ ├── empty_view.xml │ │ │ │ ├── activity_cache_list.xml │ │ │ │ ├── content_compass_loading.xml │ │ │ │ ├── activity_compass.xml │ │ │ │ ├── activity_map.xml │ │ │ │ └── content_compass.xml │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── plastix │ │ │ └── forage │ │ │ ├── ForApplication.java │ │ │ ├── data │ │ │ ├── local │ │ │ │ ├── RealmInitWrapper.java │ │ │ │ ├── pref │ │ │ │ │ ├── SharedPrefConstants.java │ │ │ │ │ ├── OAuthUserToken.java │ │ │ │ │ ├── OAuthUserTokenSecret.java │ │ │ │ │ ├── PrefsModule.java │ │ │ │ │ └── StringPreference.java │ │ │ │ ├── RealmInitWrapperImpl.java │ │ │ │ ├── model │ │ │ │ │ ├── RealmLocation.java │ │ │ │ │ └── Cache.java │ │ │ │ └── DatabaseModule.java │ │ │ ├── api │ │ │ │ ├── EndpointQualifier.java │ │ │ │ ├── response │ │ │ │ │ └── SubmitLogResponse.java │ │ │ │ ├── ApiConstants.java │ │ │ │ ├── gson │ │ │ │ │ ├── HtmlAdapter.java │ │ │ │ │ ├── StringCapitalizerAdapter.java │ │ │ │ │ ├── RealmLocationAdapter.java │ │ │ │ │ ├── ListTypeAdapterFactory.java │ │ │ │ │ └── ListTypeAdapter.java │ │ │ │ ├── HostSelectionInterceptor.java │ │ │ │ ├── OkApiService.java │ │ │ │ ├── auth │ │ │ │ │ └── OAuthSigningInterceptor.java │ │ │ │ └── OkApiInteractor.java │ │ │ ├── network │ │ │ │ ├── NetworkUnavailableException.java │ │ │ │ ├── NetworkModule.java │ │ │ │ └── NetworkInteractor.java │ │ │ ├── location │ │ │ │ ├── LocationUnavailableException.java │ │ │ │ ├── LocationModule.java │ │ │ │ └── LocationAsyncEmitter.java │ │ │ └── sensor │ │ │ │ ├── SensorUnavailableException.java │ │ │ │ ├── SensorModule.java │ │ │ │ ├── AzimuthInteractor.java │ │ │ │ └── AzimuthAsyncEmitter.java │ │ │ ├── dev_tools │ │ │ └── DevMetricsProxy.java │ │ │ ├── ui │ │ │ ├── ActivityScope.java │ │ │ ├── FragmentScope.java │ │ │ ├── navigate │ │ │ │ ├── NavigateView.java │ │ │ │ ├── NavigateComponent.java │ │ │ │ ├── NavigateModule.java │ │ │ │ ├── NavigatePresenter.java │ │ │ │ └── NavigateActivity.java │ │ │ ├── log │ │ │ │ ├── LogComponent.java │ │ │ │ ├── LogModule.java │ │ │ │ └── LogView.java │ │ │ ├── login │ │ │ │ ├── LoginComponent.java │ │ │ │ ├── LoginView.java │ │ │ │ └── LoginModule.java │ │ │ ├── cachelist │ │ │ │ ├── CacheListView.java │ │ │ │ ├── CacheListComponent.java │ │ │ │ ├── CacheListModule.java │ │ │ │ └── CacheAdapter.java │ │ │ ├── map │ │ │ │ ├── MapComponent.java │ │ │ │ ├── MapActivityView.java │ │ │ │ ├── MapModule.java │ │ │ │ └── MapPresenter.java │ │ │ ├── cachedetail │ │ │ │ ├── CacheDetailView.java │ │ │ │ ├── CacheDetailComponent.java │ │ │ │ ├── CacheDetailModule.java │ │ │ │ └── CacheDetailPresenter.java │ │ │ ├── compass │ │ │ │ ├── CompassView.java │ │ │ │ ├── CompassComponent.java │ │ │ │ ├── CompassModule.java │ │ │ │ └── LocationUnavailableDialog.java │ │ │ ├── base │ │ │ │ ├── FragmentModule.java │ │ │ │ ├── ActivityModule.java │ │ │ │ ├── Presenter.java │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── PresenterLoader.java │ │ │ │ ├── BaseFragment.java │ │ │ │ ├── PresenterFragment.java │ │ │ │ ├── PresenterActivity.java │ │ │ │ └── RxPresenter.java │ │ │ ├── misc │ │ │ │ ├── SimpleDividerItemDecoration.java │ │ │ │ └── PermissionRationaleDialog.java │ │ │ └── about │ │ │ │ └── AboutActivity.java │ │ │ ├── util │ │ │ ├── ViewUtils.java │ │ │ ├── MenuUtils.java │ │ │ ├── UnitUtils.java │ │ │ ├── AngleUtils.java │ │ │ ├── PermissionUtils.java │ │ │ ├── StringUtils.java │ │ │ └── ActivityUtils.java │ │ │ ├── ApplicationModule.java │ │ │ └── ForageApplication.java │ ├── unitTests │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── plastix │ │ │ └── forage │ │ │ ├── data │ │ │ ├── local │ │ │ │ ├── DatabaseModuleForTest.java │ │ │ │ ├── DatabaseInteractorTest.java │ │ │ │ └── RealmLocationTest.java │ │ │ ├── api │ │ │ │ ├── gson │ │ │ │ │ ├── HtmlAdapterTest.java │ │ │ │ │ ├── StringCaptializerTest.java │ │ │ │ │ └── RealmLocationAdapterTest.java │ │ │ │ └── HostSelectionInterceptorTest.java │ │ │ ├── location │ │ │ │ └── LocationInteractorTest.java │ │ │ └── network │ │ │ │ └── NetworkInteractorTest.java │ │ │ ├── ui │ │ │ ├── base │ │ │ │ ├── BaseActivityTest.java │ │ │ │ └── PresenterTest.java │ │ │ ├── cachedetail │ │ │ │ └── CacheDetailPresenterTest.java │ │ │ └── navigate │ │ │ │ └── NavigatePresenterTest.java │ │ │ ├── util │ │ │ ├── ViewUtilsTest.java │ │ │ ├── UnitUtilsTest.java │ │ │ ├── PermissionUtilsTest.java │ │ │ ├── ActivityUtilsTest.java │ │ │ ├── AngleUtilsTest.java │ │ │ └── RxUtilsTest.java │ │ │ ├── ForageUnitTestApplication.java │ │ │ └── ForageRoboelectricUnitTestRunner.java │ ├── androidTest │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── plastix │ │ │ └── forage │ │ │ ├── util │ │ │ ├── TestUtils.java │ │ │ └── UiAutomatorUtils.java │ │ │ ├── ForageFunctionalTestRunner.java │ │ │ ├── ForageFunctionalTestApplication.java │ │ │ ├── screens │ │ │ ├── MapScreen.java │ │ │ ├── NavigateScreen.java │ │ │ └── CacheListScreen.java │ │ │ ├── rules │ │ │ ├── NeedsMockWebServer.java │ │ │ └── MockWebServerRule.java │ │ │ └── espresso │ │ │ └── ViewMatchers.java │ ├── debug │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── plastix │ │ │ └── forage │ │ │ └── dev_tools │ │ │ ├── DebugToolsModule.java │ │ │ └── DevMetricsProxyImpl.java │ ├── release │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── plastix │ │ │ └── forage │ │ │ └── dev_tools │ │ │ └── DebugToolsModule.java │ ├── integrationTests │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── plastix │ │ │ └── forage │ │ │ ├── ForageIntegrationTestApplication.java │ │ │ ├── ForageRoboelectricIntegrationTestRunner.java │ │ │ └── data │ │ │ ├── local │ │ │ └── CacheTest.java │ │ │ └── api │ │ │ └── HttpCodes.java │ └── releaseUnitTests │ │ └── java │ │ └── io │ │ └── github │ │ └── plastix │ │ └── forage │ │ └── permission │ │ └── PermissionsTest.java └── proguard-rules.pro ├── settings.gradle ├── art ├── screenshots │ ├── Screenshot_20160804-074527_framed.png │ ├── forage_map.png │ ├── forage_detail.png │ ├── forage_cache_list.png │ └── forage_cache_list_empty.png └── logo.ai ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── signing.properties.sample ├── ci.sh ├── circle.yml ├── gradle.properties ├── environmentSetup.sh ├── gradlew.bat └── .gitignore /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /art/screenshots/Screenshot_20160804-074527_framed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /art/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Forage/HEAD/art/logo.ai -------------------------------------------------------------------------------- /art/screenshots/forage_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Forage/HEAD/art/screenshots/forage_map.png -------------------------------------------------------------------------------- /art/screenshots/forage_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Forage/HEAD/art/screenshots/forage_detail.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Forage/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /art/screenshots/forage_cache_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Forage/HEAD/art/screenshots/forage_cache_list.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Forage/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Forage/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /art/screenshots/forage_cache_list_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Forage/HEAD/art/screenshots/forage_cache_list_empty.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Forage/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Forage/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/icon_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Forage/HEAD/app/src/main/res/mipmap-xxxhdpi/icon_large.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Forage/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /signing.properties.sample: -------------------------------------------------------------------------------- 1 | STORE_FILE=/path/to/your.keystore 2 | STORE_PASSWORD=yourkeystorepass 3 | KEY_ALIAS=projectkeyalias 4 | KEY_PASSWORD=keyaliaspassword -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ForApplication.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage; 2 | 3 | import javax.inject.Qualifier; 4 | 5 | @Qualifier 6 | public @interface ForApplication { 7 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/local/RealmInitWrapper.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local; 2 | 3 | public interface RealmInitWrapper { 4 | 5 | void init(); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/api/EndpointQualifier.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api; 2 | 3 | import javax.inject.Qualifier; 4 | 5 | @Qualifier 6 | public @interface EndpointQualifier { 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/dev_tools/DevMetricsProxy.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.dev_tools; 2 | 3 | public interface DevMetricsProxy { 4 | /** 5 | * Applies performance metrics library. 6 | */ 7 | void apply(); 8 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/ActivityScope.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui; 2 | 3 | import javax.inject.Scope; 4 | 5 | /** 6 | * Custom Dagger 2 scope for Activities 7 | */ 8 | @Scope 9 | public @interface ActivityScope { 10 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/FragmentScope.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui; 2 | 3 | import javax.inject.Scope; 4 | 5 | /** 6 | * Custom Dagger 2 scope for Fragments 7 | */ 8 | @Scope 9 | public @interface FragmentScope { 10 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Mar 04 08:33:35 EST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/navigate/NavigateView.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.navigate; 2 | 3 | public interface NavigateView { 4 | 5 | void errorParsingLatitude(); 6 | 7 | void errorParsingLongitude(); 8 | 9 | void openCompassScreen(double lat, double lon); 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/scrim_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/local/pref/SharedPrefConstants.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local.pref; 2 | 3 | public class SharedPrefConstants { 4 | 5 | public static final String KEY_OAUTH_USER_TOKEN = "OAUTH_USER_TOKEN"; 6 | public static final String KEY_OAUTH_USER_TOKEN_SECRET = "OAUTH_USER_TOKEN_SECRET"; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/line_divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/local/pref/OAuthUserToken.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local.pref; 2 | 3 | import java.lang.annotation.Retention; 4 | 5 | import javax.inject.Qualifier; 6 | 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Qualifier 10 | @Retention(RUNTIME) 11 | public @interface OAuthUserToken { 12 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/network/NetworkUnavailableException.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.network; 2 | 3 | public class NetworkUnavailableException extends Throwable { 4 | public NetworkUnavailableException(String message) { 5 | super(message); 6 | } 7 | 8 | public NetworkUnavailableException() { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/res/anim/rotation.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/log/LogComponent.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.log; 2 | 3 | import dagger.Subcomponent; 4 | import io.github.plastix.forage.ui.ActivityScope; 5 | 6 | @ActivityScope 7 | @Subcomponent(modules = { 8 | LogModule.class 9 | }) 10 | public interface LogComponent { 11 | void injectTo(LogActivity activity); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/local/pref/OAuthUserTokenSecret.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local.pref; 2 | 3 | import java.lang.annotation.Retention; 4 | 5 | import javax.inject.Qualifier; 6 | 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Qualifier 10 | @Retention(RUNTIME) 11 | public @interface OAuthUserTokenSecret { 12 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_send.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/log/LogModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.log; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | 5 | import dagger.Module; 6 | import io.github.plastix.forage.ui.base.ActivityModule; 7 | 8 | @Module 9 | public class LogModule extends ActivityModule { 10 | public LogModule(AppCompatActivity activity) { 11 | super(activity); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_upward.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_terrain.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/login/LoginComponent.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.login; 2 | 3 | import dagger.Subcomponent; 4 | import io.github.plastix.forage.ui.ActivityScope; 5 | 6 | @ActivityScope 7 | @Subcomponent( 8 | modules = { 9 | LoginModule.class 10 | } 11 | ) 12 | public interface LoginComponent { 13 | void injectTo(LoginActivity loginActivity); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/login/LoginView.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.login; 2 | 3 | public interface LoginView { 4 | 5 | void openBrowser(String authUrl); 6 | 7 | void onErrorRequestToken(); 8 | 9 | void onErrorAccessToken(); 10 | 11 | void onErrorNoInternet(); 12 | 13 | void onAuthSuccess(); 14 | 15 | void showLoading(); 16 | 17 | void stopLoading(); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/login/LoginModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.login; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | 5 | import dagger.Module; 6 | import io.github.plastix.forage.ui.base.ActivityModule; 7 | 8 | @Module 9 | public class LoginModule extends ActivityModule { 10 | public LoginModule(AppCompatActivity activity) { 11 | super(activity); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/navigate/NavigateComponent.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.navigate; 2 | 3 | import dagger.Subcomponent; 4 | import io.github.plastix.forage.ui.ActivityScope; 5 | 6 | @ActivityScope 7 | @Subcomponent( 8 | modules = { 9 | NavigateModule.class 10 | } 11 | ) 12 | public interface NavigateComponent { 13 | void injectTo(NavigateActivity activity); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/location/LocationUnavailableException.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.location; 2 | 3 | /** 4 | * Exception to indicate that location is not available. 5 | */ 6 | public class LocationUnavailableException extends Throwable { 7 | public LocationUnavailableException(String message) { 8 | super(message); 9 | } 10 | 11 | public LocationUnavailableException() { 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/navigate/NavigateModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.navigate; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | 5 | import dagger.Module; 6 | import io.github.plastix.forage.ui.base.ActivityModule; 7 | 8 | @Module 9 | public class NavigateModule extends ActivityModule { 10 | public NavigateModule(AppCompatActivity activity) { 11 | super(activity); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_cache_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 256dp 3 | 88dp 4 | 5 | 6 | 8dp 7 | 8 | 16dp 9 | 16dp 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/cachelist/CacheListView.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.cachelist; 2 | 3 | /** 4 | * Interface implemented by {@link CacheListFragment} to define callbacks used by 5 | * {@link CacheListPresenter}. 6 | */ 7 | public interface CacheListView { 8 | 9 | void onErrorInternet(); 10 | 11 | void onErrorFetch(); 12 | 13 | void onErrorLocation(); 14 | 15 | void setRefreshing(); 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/sensor/SensorUnavailableException.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.sensor; 2 | 3 | /** 4 | * Exception to indicate that an Android sensor is not available 5 | */ 6 | public class SensorUnavailableException extends Throwable { 7 | 8 | public SensorUnavailableException() { 9 | } 10 | 11 | public SensorUnavailableException(String message) { 12 | super(message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/data/local/DatabaseModuleForTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local; 2 | 3 | import android.app.Application; 4 | import android.support.annotation.NonNull; 5 | 6 | public class DatabaseModuleForTest extends DatabaseModule { 7 | 8 | @NonNull 9 | @Override 10 | public RealmInitWrapper provideRealmProxy(Application application) { 11 | return () -> { 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/map/MapComponent.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.map; 2 | 3 | import dagger.Subcomponent; 4 | import io.github.plastix.forage.ui.ActivityScope; 5 | 6 | /** 7 | * Dagger component to inject all required dependencies into {@link MapActivity}. 8 | */ 9 | @ActivityScope 10 | @Subcomponent( 11 | modules = { 12 | MapModule.class 13 | } 14 | ) 15 | public interface MapComponent { 16 | void injectTo(MapActivity mapActivity); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/map/MapActivityView.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.map; 2 | 3 | import android.location.Location; 4 | 5 | import java.util.List; 6 | 7 | import io.github.plastix.forage.data.local.model.Cache; 8 | 9 | /** 10 | * Interface implemented by {@link MapActivity} to define callbacks used by 11 | * {@link MapPresenter}. 12 | */ 13 | public interface MapActivityView { 14 | 15 | void addMapMarkers(List caches); 16 | 17 | void animateMapCamera(Location location); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/cachedetail/CacheDetailView.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.cachedetail; 2 | 3 | import io.github.plastix.forage.data.local.model.Cache; 4 | 5 | /** 6 | * Interface implemented by {@link CacheDetailActivity} to define callbacks used by 7 | * {@link CacheDetailPresenter}. 8 | */ 9 | public interface CacheDetailView { 10 | 11 | void returnedGeocache(Cache cache); 12 | 13 | void onError(); 14 | 15 | void openLogScreen(); 16 | 17 | void onErrorRequiresLogin(); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/local/RealmInitWrapperImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local; 2 | 3 | import android.app.Application; 4 | 5 | import io.realm.Realm; 6 | 7 | public class RealmInitWrapperImpl implements RealmInitWrapper { 8 | 9 | private Application application; 10 | 11 | public RealmInitWrapperImpl(Application application) { 12 | this.application = application; 13 | } 14 | 15 | @Override 16 | public void init() { 17 | Realm.init(application); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/compass/CompassView.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.compass; 2 | 3 | /** 4 | * Interface implemented by {@link CompassActivity} to define callbacks used by 5 | * {@link CompassPresenter}. 6 | */ 7 | public interface CompassView { 8 | 9 | void rotateCompass(float degrees); 10 | 11 | void setDistance(float distanceInMeters); 12 | 13 | void setAccuracy(float accuracyInMeters); 14 | 15 | void showLocationUnavailableDialog(); 16 | 17 | void showCompass(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/log/LogView.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.log; 2 | 3 | import android.support.annotation.ArrayRes; 4 | import android.support.annotation.StringRes; 5 | 6 | public interface LogView { 7 | 8 | void showSubmittingDialog(); 9 | 10 | void showErrorDialog(String message); 11 | 12 | void showErrorDialog(@StringRes int resId); 13 | 14 | void showErrorInternetDialog(); 15 | 16 | void showSuccessfulSubmit(); 17 | 18 | void setLogTypes(@ArrayRes int options); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/compass/CompassComponent.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.compass; 2 | 3 | import dagger.Subcomponent; 4 | import io.github.plastix.forage.ui.ActivityScope; 5 | 6 | /** 7 | * Dagger component to inject all required dependencies into {@link CompassActivity}. 8 | */ 9 | @ActivityScope 10 | @Subcomponent( 11 | modules = { 12 | CompassModule.class, 13 | } 14 | ) 15 | public interface CompassComponent { 16 | void injectTo(CompassActivity compassActivity); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/cachelist/CacheListComponent.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.cachelist; 2 | 3 | 4 | import dagger.Subcomponent; 5 | import io.github.plastix.forage.ui.FragmentScope; 6 | 7 | /** 8 | * Dagger component to inject all required dependencies into {@link CacheListFragment}. 9 | */ 10 | @FragmentScope 11 | @Subcomponent( 12 | modules = { 13 | CacheListModule.class, 14 | } 15 | ) 16 | public interface CacheListComponent { 17 | void injectTo(CacheListFragment cacheListFragment); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_compass.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/cachedetail/CacheDetailComponent.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.cachedetail; 2 | 3 | import dagger.Subcomponent; 4 | import io.github.plastix.forage.ui.ActivityScope; 5 | 6 | /** 7 | * Dagger component to inject all required dependencies into {@link CacheDetailActivity}. 8 | */ 9 | @ActivityScope 10 | @Subcomponent( 11 | modules = { 12 | CacheDetailModule.class 13 | } 14 | ) 15 | public interface CacheDetailComponent { 16 | void injectTo(CacheDetailActivity cacheDetailActivity); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_map.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/debug/java/io/github/plastix/forage/dev_tools/DebugToolsModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.dev_tools; 2 | 3 | import android.app.Application; 4 | import android.support.annotation.NonNull; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | 11 | @Module 12 | public class DebugToolsModule { 13 | 14 | @NonNull 15 | @Singleton 16 | @Provides 17 | public DevMetricsProxy provideDevMetricsProxy(@NonNull Application application) { 18 | return new DevMetricsProxyImpl(application); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/map/MapModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.map; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | 5 | import dagger.Module; 6 | import io.github.plastix.forage.ui.base.ActivityModule; 7 | 8 | /** 9 | * Dagger module that provides dependencies for {@link MapActivity}. 10 | * This is used to inject the cache list presenter and view into each other. 11 | */ 12 | @Module 13 | public class MapModule extends ActivityModule { 14 | public MapModule(AppCompatActivity activity) { 15 | super(activity); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/cachedetail/CacheDetailModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.cachedetail; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | 5 | import dagger.Module; 6 | import io.github.plastix.forage.ui.base.ActivityModule; 7 | 8 | /** 9 | * Dagger module that provides dependencies for {@link CacheDetailPresenter} and {@link CacheDetailActivity}. 10 | */ 11 | @Module 12 | public class CacheDetailModule extends ActivityModule { 13 | public CacheDetailModule(AppCompatActivity activity) { 14 | super(activity); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | # You can run it from any directory. 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | 7 | # This will: 8 | # 1. Clean the project 9 | # 2. Run Android Lint 10 | # 3. Run tests under JVM 11 | 12 | # We run each step separately to consume less memory (free CI sometimes fails with OOM) 13 | 14 | GRADLE=""$DIR"/gradlew -PdisablePreDex --no-daemon" 15 | 16 | # 1 17 | eval "$GRADLE clean" 18 | 19 | # 2 20 | eval "$GRADLE lintDebug" 21 | eval "$GRADLE lintRelease" 22 | 23 | # 3 24 | eval "$GRADLE testDebugUnitTest" 25 | eval "$GRADLE testReleaseUnitTest" -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/plastix/forage/util/TestUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.test.InstrumentationRegistry; 5 | 6 | import io.github.plastix.forage.ForageApplication; 7 | 8 | public class TestUtils { 9 | 10 | private TestUtils() { 11 | throw new IllegalStateException("No instantiation!"); 12 | } 13 | 14 | @NonNull 15 | public static ForageApplication app() { 16 | return (ForageApplication) InstrumentationRegistry.getTargetContext().getApplicationContext(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_archive.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/util/ViewUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.view.View; 5 | 6 | public class ViewUtils { 7 | 8 | private ViewUtils() { 9 | } 10 | 11 | public static void show(@NonNull View view) { 12 | view.setVisibility(View.VISIBLE); 13 | } 14 | 15 | public static void hide(@NonNull View view) { 16 | view.setVisibility(View.GONE); 17 | } 18 | 19 | public static void invis(@NonNull View view) { 20 | view.setVisibility(View.INVISIBLE); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/api/response/SubmitLogResponse.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api.response; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class SubmitLogResponse { 6 | 7 | @SerializedName("success") 8 | public boolean isSuccessful; 9 | 10 | @SerializedName("message") 11 | public String message; 12 | 13 | 14 | @Override 15 | public String toString() { 16 | return "SubmitLogResponse{" + 17 | "isSuccessful=" + isSuccessful + 18 | ", message='" + message + '\'' + 19 | '}'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pencil_box.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | > 2 | 3 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_extension.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/debug/java/io/github/plastix/forage/dev_tools/DevMetricsProxyImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.dev_tools; 2 | 3 | import android.app.Application; 4 | import android.support.annotation.NonNull; 5 | 6 | import com.frogermcs.androiddevmetrics.AndroidDevMetrics; 7 | 8 | public class DevMetricsProxyImpl implements DevMetricsProxy { 9 | 10 | @NonNull 11 | private final Application application; 12 | 13 | public DevMetricsProxyImpl(@NonNull Application application) { 14 | this.application = application; 15 | } 16 | 17 | @Override 18 | public void apply() { 19 | AndroidDevMetrics.initWith(application); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cube.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/base/FragmentModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.base; 2 | 3 | import android.content.Context; 4 | import android.support.v4.app.Fragment; 5 | 6 | import dagger.Module; 7 | import dagger.Provides; 8 | 9 | @Module 10 | public abstract class FragmentModule { 11 | 12 | private final Fragment fragment; 13 | 14 | public FragmentModule(Fragment fragment) { 15 | this.fragment = fragment; 16 | } 17 | 18 | @Provides 19 | public Fragment provideFragment() { 20 | return fragment; 21 | } 22 | 23 | @Provides 24 | public Context provideContext() { 25 | return fragment.getContext(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/release/java/io/github/plastix/forage/dev_tools/DebugToolsModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.dev_tools; 2 | 3 | import android.app.Application; 4 | import android.support.annotation.NonNull; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | 11 | @Module 12 | public class DebugToolsModule { 13 | 14 | @NonNull 15 | @Singleton 16 | @Provides 17 | public DevMetricsProxy provideDevMetricsProxy(@NonNull Application application) { 18 | return new DevMetricsProxy() { 19 | @Override 20 | public void apply() { 21 | //No Op 22 | } 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | java: 3 | version: oraclejdk8 4 | environment: 5 | GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx3500m -XX:+HeapDumpOnOutOfMemoryError"' 6 | 7 | dependencies: 8 | pre: 9 | - echo y | android update sdk --no-ui --all --filter "platform-tools,tools,android-24,android-25,extra-android-m2repository,extra-google-google_play_services,extra-google-m2repository,extra-android-support" 10 | # Build tools should be installed after "tools", uh. 11 | - echo y | android update sdk --no-ui --all --filter "build-tools-25.0.2" 12 | # Generate gradle.properties with API keys 13 | - source environmentSetup.sh && copyEnvVarsToGradleProperties 14 | 15 | test: 16 | override: 17 | - sh ci.sh 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/base/ActivityModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.base; 2 | 3 | import android.content.Context; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import dagger.Module; 7 | import dagger.Provides; 8 | 9 | @Module 10 | public abstract class ActivityModule { 11 | 12 | private final AppCompatActivity activity; 13 | 14 | public ActivityModule(AppCompatActivity activity) { 15 | this.activity = activity; 16 | } 17 | 18 | @Provides 19 | public AppCompatActivity provideActivity() { 20 | return activity; 21 | } 22 | 23 | @Provides 24 | public Context provideContext() { 25 | return activity.getBaseContext(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/network/NetworkModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.network; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.support.annotation.NonNull; 6 | 7 | import javax.inject.Singleton; 8 | 9 | import dagger.Module; 10 | import dagger.Provides; 11 | import io.github.plastix.forage.ForApplication; 12 | 13 | @Module 14 | public class NetworkModule { 15 | 16 | @NonNull 17 | @Provides 18 | @Singleton 19 | public static ConnectivityManager provideConnectivityManager(@NonNull @ForApplication Context context) { 20 | return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/compass/CompassModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.compass; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.view.animation.LinearInterpolator; 5 | 6 | import dagger.Module; 7 | import dagger.Provides; 8 | import io.github.plastix.forage.ui.base.ActivityModule; 9 | 10 | /** 11 | * Dagger module that provides dependencies for {@link CompassActivity}. 12 | * This is used to inject the cache list presenter and view into each other. 13 | */ 14 | @Module 15 | public class CompassModule extends ActivityModule { 16 | public CompassModule(AppCompatActivity activity) { 17 | super(activity); 18 | } 19 | 20 | @Provides 21 | public LinearInterpolator provideLinearInterpolator() { 22 | return new LinearInterpolator(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @color/cyan 5 | @color/cyan_dark 6 | @color/pink 7 | 8 | 9 | #FFFFFF 10 | #212121 11 | #727272 12 | #B6B6B6 13 | #e5e5e5 14 | 15 | 16 | #00BCD4 17 | #0097A7 18 | #FF4081 19 | 20 | 21 | #80000000 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/plastix/forage/ForageFunctionalTestRunner.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage; 2 | 3 | import android.app.Application; 4 | import android.app.Instrumentation; 5 | import android.content.Context; 6 | import android.support.annotation.NonNull; 7 | import android.support.test.runner.AndroidJUnitRunner; 8 | 9 | // Custom runner allows us set config in one place instead of setting it in each test class. 10 | public class ForageFunctionalTestRunner extends AndroidJUnitRunner { 11 | 12 | @Override 13 | @NonNull 14 | public Application newApplication(@NonNull ClassLoader cl, @NonNull String className, @NonNull Context context) 15 | throws InstantiationException, IllegalAccessException, ClassNotFoundException { 16 | return Instrumentation.newApplication(ForageFunctionalTestApplication.class, context); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/local/model/RealmLocation.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local.model; 2 | 3 | import android.location.Location; 4 | 5 | import io.github.plastix.forage.util.LocationUtils; 6 | import io.realm.RealmObject; 7 | 8 | /** 9 | * Realm Model "wrapper" to store location data in a Realm. This is required because Realm doesn't 10 | * allow third party types such as Android's location type. 11 | */ 12 | public class RealmLocation extends RealmObject { 13 | 14 | public double latitude; 15 | public double longitude; 16 | 17 | /** 18 | * Returns a new Android Location object with the corresponding data from the realm object. 19 | * 20 | * @return New Location object. 21 | */ 22 | public Location toLocation() { 23 | return LocationUtils.buildLocation(latitude, longitude); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_speedometer.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/location/LocationModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.location; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | 6 | import com.google.android.gms.common.api.GoogleApiClient; 7 | import com.google.android.gms.location.LocationServices; 8 | 9 | import javax.inject.Singleton; 10 | 11 | import dagger.Module; 12 | import dagger.Provides; 13 | import io.github.plastix.forage.ForApplication; 14 | 15 | /** 16 | * Dagger module to provide a location dependencies. 17 | */ 18 | @Module 19 | public class LocationModule { 20 | 21 | @NonNull 22 | @Provides 23 | @Singleton 24 | public static GoogleApiClient provideGoogleApiClient(@NonNull @ForApplication Context context) { 25 | return new GoogleApiClient.Builder(context).addApi(LocationServices.API).build(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/ui/base/BaseActivityTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.base; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import io.github.plastix.forage.ApplicationComponent; 9 | 10 | import static com.google.common.truth.Truth.assertThat; 11 | 12 | public class BaseActivityTest { 13 | 14 | private FakeBaseActivity baseActivity; 15 | 16 | @Before 17 | public void beforeEachTest() { 18 | baseActivity = new FakeBaseActivity(); 19 | } 20 | 21 | @Test 22 | public void shouldBeInstanceOfAppCompat() { 23 | assertThat(baseActivity).isInstanceOf(AppCompatActivity.class); 24 | } 25 | 26 | private class FakeBaseActivity extends BaseActivity { 27 | @Override 28 | protected void injectDependencies(ApplicationComponent component) { 29 | 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/base/Presenter.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.base; 2 | 3 | /** 4 | * Base Presenter class. 5 | * 6 | * @param Generic type of view that the presenter interacts with. 7 | */ 8 | public abstract class Presenter { 9 | 10 | protected V view; 11 | 12 | public void onViewAttached(V view) { 13 | this.view = view; 14 | } 15 | 16 | public void onViewDetached() { 17 | this.view = null; 18 | } 19 | 20 | /** 21 | * Called when the Presenter instance is being removed. Make sure to release any resources used 22 | * by the presenter here. 23 | */ 24 | public abstract void onDestroyed(); 25 | 26 | /** 27 | * Checks if the view is currently attached to the presenter. You should call this method before 28 | * accessing {@link #view} to avoid NPEs! 29 | */ 30 | protected boolean isViewAttached() { 31 | return view != null; 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/api/ApiConstants.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api; 2 | 3 | /** 4 | * OkApi URL routes and other constants 5 | * See http://www.opencaching.us/okapi/introduction.html 6 | */ 7 | public class ApiConstants { 8 | 9 | public static final String BASE_ENDPOINT = "http://www.opencaching.us/okapi/"; 10 | public static final String REQUEST_TOKEN_ENDPOINT = BASE_ENDPOINT + "services/oauth/request_token"; 11 | public static final String ACCESS_TOKEN_ENDPOINT = BASE_ENDPOINT + "services/oauth/access_token"; 12 | public static final String AUTHORIZATION_WEBSITE_URL = BASE_ENDPOINT + "services/oauth/authorize"; 13 | public static final String ENDPOINT_GEOCACHES = "services/caches/geocaches"; 14 | public static final String ENDPOINT_NEAREST = "services/caches/search/nearest"; 15 | 16 | public static final String OAUTH_CALLBACK = "app://forage"; 17 | public static final String OAUTH_ENABLE_HEADER = "OAuth"; 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/sensor/SensorModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.sensor; 2 | 3 | import android.content.Context; 4 | import android.hardware.SensorManager; 5 | import android.support.annotation.NonNull; 6 | import android.view.WindowManager; 7 | 8 | import javax.inject.Singleton; 9 | 10 | import dagger.Module; 11 | import dagger.Provides; 12 | import io.github.plastix.forage.ForApplication; 13 | 14 | @Module 15 | public class SensorModule { 16 | 17 | @NonNull 18 | @Provides 19 | @Singleton 20 | public static SensorManager provideSensorManager(@NonNull @ForApplication Context context) { 21 | return (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); 22 | } 23 | 24 | @NonNull 25 | @Provides 26 | @Singleton 27 | public static WindowManager provideWindowManager(@NonNull @ForApplication Context context) { 28 | return (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/plastix/forage/ForageFunctionalTestApplication.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage; 2 | 3 | import android.app.Application; 4 | import android.support.annotation.NonNull; 5 | 6 | import io.github.plastix.forage.dev_tools.DebugToolsModule; 7 | import io.github.plastix.forage.dev_tools.DevMetricsProxy; 8 | 9 | public class ForageFunctionalTestApplication extends ForageApplication { 10 | 11 | @NonNull 12 | @Override 13 | protected DaggerApplicationComponent.Builder prepareApplicationComponent() { 14 | return super.prepareApplicationComponent() 15 | .debugToolsModule(new DebugToolsModule() { 16 | @NonNull 17 | @Override 18 | public DevMetricsProxy provideDevMetricsProxy(@NonNull Application application) { 19 | return () -> { 20 | //No Op 21 | }; 22 | } 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/plastix/forage/screens/MapScreen.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.screens; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import io.github.plastix.forage.R; 6 | 7 | import static android.support.test.espresso.Espresso.onView; 8 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 9 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 10 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 11 | import static android.support.test.espresso.matcher.ViewMatchers.withParent; 12 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 13 | import static org.hamcrest.core.AllOf.allOf; 14 | 15 | public class MapScreen { 16 | 17 | @NonNull 18 | public MapScreen shouldDisplayTitle(@NonNull String title) { 19 | onView(allOf(withText(title), withParent(withId(R.id.map_toolbar)))) 20 | .check(matches(isDisplayed())); 21 | return this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/integrationTests/java/io/github/plastix/forage/ForageIntegrationTestApplication.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage; 2 | 3 | import android.app.Application; 4 | import android.support.annotation.NonNull; 5 | 6 | import io.github.plastix.forage.dev_tools.DebugToolsModule; 7 | import io.github.plastix.forage.dev_tools.DevMetricsProxy; 8 | 9 | public class ForageIntegrationTestApplication extends ForageApplication { 10 | 11 | @NonNull 12 | @Override 13 | protected DaggerApplicationComponent.Builder prepareApplicationComponent() { 14 | return super.prepareApplicationComponent() 15 | .debugToolsModule(new DebugToolsModule() { 16 | @NonNull 17 | @Override 18 | public DevMetricsProxy provideDevMetricsProxy(@NonNull Application application) { 19 | return () -> { 20 | //No Op 21 | }; 22 | } 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/util/ViewUtilsTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import android.view.View; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.Mock; 8 | import org.mockito.MockitoAnnotations; 9 | 10 | import static org.mockito.Mockito.verify; 11 | 12 | public class ViewUtilsTest { 13 | 14 | @Mock 15 | View view; 16 | 17 | @Before 18 | public void setUp() { 19 | MockitoAnnotations.initMocks(this); 20 | } 21 | 22 | @Test 23 | public void show_callsCorrectMethod() { 24 | ViewUtils.show(view); 25 | 26 | verify(view).setVisibility(View.VISIBLE); 27 | } 28 | 29 | @Test 30 | public void gone_callsCorrectMethod() { 31 | ViewUtils.hide(view); 32 | 33 | verify(view).setVisibility(View.GONE); 34 | } 35 | 36 | @Test 37 | public void invis_callsCorrectMethod() { 38 | ViewUtils.invis(view); 39 | 40 | verify(view).setVisibility(View.INVISIBLE); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/plastix/forage/screens/NavigateScreen.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.screens; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import io.github.plastix.forage.R; 6 | 7 | import static android.support.test.espresso.Espresso.onView; 8 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 9 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 10 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 11 | import static android.support.test.espresso.matcher.ViewMatchers.withParent; 12 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 13 | import static org.hamcrest.core.AllOf.allOf; 14 | 15 | public class NavigateScreen { 16 | 17 | @NonNull 18 | public NavigateScreen shouldDisplayTitle(@NonNull String title) { 19 | onView(allOf(withText(title), withParent(withId(R.id.navigate_toolbar)))) 20 | .check(matches(isDisplayed())); 21 | return this; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_cache_list.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/plastix/forage/screens/CacheListScreen.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.screens; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import io.github.plastix.forage.R; 6 | 7 | import static android.support.test.espresso.Espresso.onView; 8 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 9 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 10 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 11 | import static android.support.test.espresso.matcher.ViewMatchers.withParent; 12 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 13 | import static org.hamcrest.core.AllOf.allOf; 14 | 15 | public class CacheListScreen { 16 | 17 | @NonNull 18 | public CacheListScreen shouldDisplayTitle(@NonNull String title) { 19 | onView(allOf(withText(title), withParent(withId(R.id.cachelist_toolbar)))) 20 | .check(matches(isDisplayed())); 21 | return this; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_cache_list.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 16 | 17 | 21 | 22 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/plastix/forage/rules/NeedsMockWebServer.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.rules; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target(ElementType.METHOD) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | /** 13 | * From https://github.com/artem-zinnatullin/qualitymatters 14 | * @see MockWebServerRule 15 | */ 16 | public @interface NeedsMockWebServer { 17 | 18 | /** 19 | * Optional specifier for a setupMethod that needs to be invoked during initialization 20 | * of {@link okhttp3.mockwebserver.MockWebServer}. 21 | *

22 | * Useful for setting up responses that you simply can not define in the test code because app already hit the server. 23 | * 24 | * @return Empty string if no method invocation required or public method name that needs to be called. 25 | */ 26 | @NonNull String setupMethod() default ""; 27 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/sensor/AzimuthInteractor.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.sensor; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import javax.inject.Inject; 6 | import javax.inject.Provider; 7 | 8 | import rx.AsyncEmitter; 9 | import rx.Observable; 10 | 11 | 12 | /** 13 | * A Reactive wrapper around the Android compass sensor. 14 | */ 15 | public class AzimuthInteractor { 16 | 17 | private Provider azimuthProvider; 18 | 19 | @Inject 20 | public AzimuthInteractor(@NonNull Provider azimuthProvider) { 21 | this.azimuthProvider = azimuthProvider; 22 | } 23 | 24 | /** 25 | * Returns an Observable that emits the Azimuth of the Android compass. 26 | * 27 | * @return Observable 28 | */ 29 | @NonNull 30 | public Observable getAzimuthObservable() { 31 | AzimuthAsyncEmitter azimuthAsyncEmitter = azimuthProvider.get(); 32 | return Observable.fromEmitter(azimuthAsyncEmitter, AsyncEmitter.BackpressureMode.LATEST); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/ForageUnitTestApplication.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage; 2 | 3 | import android.app.Application; 4 | import android.support.annotation.NonNull; 5 | 6 | import io.github.plastix.forage.data.local.DatabaseModuleForTest; 7 | import io.github.plastix.forage.dev_tools.DebugToolsModule; 8 | import io.github.plastix.forage.dev_tools.DevMetricsProxy; 9 | 10 | public class ForageUnitTestApplication extends ForageApplication { 11 | 12 | @NonNull 13 | @Override 14 | protected DaggerApplicationComponent.Builder prepareApplicationComponent() { 15 | return super.prepareApplicationComponent() 16 | .debugToolsModule(new DebugToolsModule() { 17 | @NonNull 18 | @Override 19 | public DevMetricsProxy provideDevMetricsProxy(@NonNull Application application) { 20 | return () -> { 21 | //No Op 22 | }; 23 | } 24 | }) 25 | .databaseModule(new DatabaseModuleForTest()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/util/MenuUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import android.graphics.drawable.Drawable; 4 | import android.support.annotation.ColorInt; 5 | import android.support.v4.graphics.drawable.DrawableCompat; 6 | import android.view.MenuItem; 7 | 8 | public class MenuUtils { 9 | 10 | private MenuUtils() { 11 | throw new UnsupportedOperationException("No Instantiation!"); 12 | } 13 | 14 | /** 15 | * Tints the color of the icon of the specified MenuItem. 16 | * See http://stackoverflow.com/questions/28219178/toolbar-icon-tinting-on-android 17 | * 18 | * @param color Color to tint 19 | * @param item MenuItem to tint. 20 | */ 21 | public static void tintMenuItemIcon(@ColorInt int color, MenuItem item) { 22 | final Drawable drawable = item.getIcon(); 23 | if (drawable != null) { 24 | final Drawable wrapped = DrawableCompat.wrap(drawable); 25 | drawable.mutate(); 26 | 27 | DrawableCompat.setTint(wrapped, color); 28 | item.setIcon(drawable); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/api/gson/HtmlAdapter.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api.gson; 2 | 3 | import com.google.gson.TypeAdapter; 4 | import com.google.gson.stream.JsonReader; 5 | import com.google.gson.stream.JsonWriter; 6 | 7 | import java.io.IOException; 8 | 9 | import javax.inject.Inject; 10 | 11 | import io.github.plastix.forage.util.StringUtils; 12 | 13 | /** 14 | * Gson adapter for converting OkApi's HTML to raw text. 15 | */ 16 | public class HtmlAdapter extends TypeAdapter { 17 | 18 | /** 19 | * Single @Inject annotated constructor so Dagger knows how to instantiate the adapter. 20 | */ 21 | @Inject 22 | public HtmlAdapter() { 23 | } 24 | 25 | @Override 26 | public void write(JsonWriter out, String value) throws IOException { 27 | // Nothing 28 | } 29 | 30 | @Override 31 | public String read(JsonReader in) throws IOException { 32 | String raw = in.nextString(); 33 | if (raw != null) { 34 | return StringUtils.stripHtml(raw); 35 | } else { 36 | return null; 37 | } 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/api/gson/StringCapitalizerAdapter.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api.gson; 2 | 3 | import com.google.gson.TypeAdapter; 4 | import com.google.gson.stream.JsonReader; 5 | import com.google.gson.stream.JsonWriter; 6 | 7 | import java.io.IOException; 8 | 9 | import javax.inject.Inject; 10 | 11 | import io.github.plastix.forage.util.StringUtils; 12 | 13 | /** 14 | * Gson adapter for automatically capitalizing a String input. 15 | */ 16 | public class StringCapitalizerAdapter extends TypeAdapter { 17 | 18 | /** 19 | * Single @Inject annotated constructor so Dagger knows how to instantiate the adapter. 20 | */ 21 | @Inject 22 | public StringCapitalizerAdapter() { 23 | } 24 | 25 | @Override 26 | public void write(JsonWriter out, String value) throws IOException { 27 | // Nothing 28 | } 29 | 30 | @Override 31 | public String read(JsonReader in) throws IOException { 32 | String raw = in.nextString(); 33 | if (raw != null) { 34 | return StringUtils.capitalize(raw); 35 | } else { 36 | return null; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/api/HostSelectionInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api; 2 | 3 | import java.io.IOException; 4 | 5 | import okhttp3.HttpUrl; 6 | import okhttp3.Interceptor; 7 | import okhttp3.Request; 8 | 9 | /** 10 | * An interceptor that allows runtime changes to the URL hostname. 11 | * Adapted from https://gist.github.com/swankjesse/8571a8207a5815cca1fb 12 | */ 13 | public final class HostSelectionInterceptor implements Interceptor { 14 | 15 | private volatile String host; 16 | 17 | public void setHost(String host) { 18 | this.host = host; 19 | } 20 | 21 | @Override 22 | public okhttp3.Response intercept(Chain chain) throws IOException { 23 | Request request = chain.request(); 24 | String host = this.host; 25 | if (host != null) { 26 | HttpUrl newUrl = HttpUrl.parse(host); 27 | 28 | // Only intercept the URL if we parsed a valid HttpUrl 29 | if (newUrl != null) { 30 | request = request.newBuilder() 31 | .url(newUrl) 32 | .build(); 33 | } 34 | } 35 | return chain.proceed(request); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/local/DatabaseModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local; 2 | 3 | import android.app.Application; 4 | import android.support.annotation.NonNull; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | import io.realm.Realm; 11 | import io.realm.RealmConfiguration; 12 | 13 | /** 14 | * Dagger module to provide a {@link Realm} instance. 15 | */ 16 | @Module 17 | public class DatabaseModule { 18 | 19 | @NonNull 20 | @Provides 21 | public static Realm provideRealm(@NonNull RealmConfiguration realmConfiguration) { 22 | return Realm.getInstance(realmConfiguration); 23 | } 24 | 25 | @NonNull 26 | @Provides 27 | @Singleton 28 | public static RealmConfiguration provideDefaultRealmConfig() { 29 | return new RealmConfiguration.Builder() 30 | .deleteRealmIfMigrationNeeded() 31 | .name(Realm.DEFAULT_REALM_NAME) 32 | .build(); 33 | } 34 | 35 | @NonNull 36 | @Provides 37 | @Singleton 38 | public RealmInitWrapper provideRealmProxy(Application application) { 39 | return new RealmInitWrapperImpl(application); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/network/NetworkInteractor.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.network; 2 | 3 | import android.net.ConnectivityManager; 4 | import android.net.NetworkInfo; 5 | import android.support.annotation.NonNull; 6 | 7 | import javax.inject.Inject; 8 | 9 | import rx.Completable; 10 | 11 | /** 12 | * Wrapper around Android network services. 13 | */ 14 | public class NetworkInteractor { 15 | 16 | private ConnectivityManager connectivityManager; 17 | 18 | @Inject 19 | public NetworkInteractor(@NonNull ConnectivityManager connectivityManager) { 20 | this.connectivityManager = connectivityManager; 21 | } 22 | 23 | public boolean hasInternetConnection() { 24 | NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo(); 25 | return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); 26 | } 27 | 28 | @NonNull 29 | public Completable hasInternetConnectionCompletable() { 30 | if (hasInternetConnection()) { 31 | return Completable.complete(); 32 | } else { 33 | return Completable.error(new NetworkUnavailableException("Network unavailable!")); 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/navigate/NavigatePresenter.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.navigate; 2 | 3 | import javax.inject.Inject; 4 | 5 | import io.github.plastix.forage.ui.base.Presenter; 6 | import io.github.plastix.forage.util.LocationUtils; 7 | 8 | public class NavigatePresenter extends Presenter { 9 | 10 | @Inject 11 | public NavigatePresenter() { 12 | } 13 | 14 | public void navigate(String latitude, String longitude) { 15 | if (!isViewAttached()) { 16 | return; 17 | } 18 | 19 | boolean error = false; 20 | 21 | if (!LocationUtils.isValidLatitude(latitude)) { 22 | view.errorParsingLatitude(); 23 | error = true; 24 | } 25 | 26 | if (!LocationUtils.isValidLongitude(longitude)) { 27 | view.errorParsingLongitude(); 28 | error = true; 29 | } 30 | 31 | if (!error) { 32 | double lat = Double.valueOf(latitude); 33 | double lon = Double.valueOf(longitude); 34 | 35 | view.openCompassScreen(lat, lon); 36 | } 37 | 38 | } 39 | 40 | @Override 41 | public void onDestroyed() { 42 | // Nothing 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/local/pref/PrefsModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local.pref; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.preference.PreferenceManager; 6 | 7 | import javax.inject.Singleton; 8 | 9 | import dagger.Module; 10 | import dagger.Provides; 11 | import io.github.plastix.forage.ForApplication; 12 | 13 | @Module 14 | public class PrefsModule { 15 | 16 | @Provides 17 | @Singleton 18 | public static SharedPreferences provideSharedPreferences(@ForApplication Context context) { 19 | return PreferenceManager.getDefaultSharedPreferences(context); 20 | } 21 | 22 | @OAuthUserToken 23 | @Singleton 24 | @Provides 25 | public static StringPreference provideOAuthUserToken(SharedPreferences sharedPreferences) { 26 | return new StringPreference(sharedPreferences, SharedPrefConstants.KEY_OAUTH_USER_TOKEN, null); 27 | } 28 | 29 | @OAuthUserTokenSecret 30 | @Singleton 31 | @Provides 32 | public static StringPreference provideOAuthUserTokenSecret(SharedPreferences sharedPreferences) { 33 | return new StringPreference(sharedPreferences, SharedPrefConstants.KEY_OAUTH_USER_TOKEN_SECRET, null); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/library_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 19 | 20 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/cachelist/CacheListModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.cachelist; 2 | 3 | import android.content.Context; 4 | import android.support.v4.app.Fragment; 5 | import android.support.v7.widget.LinearLayoutManager; 6 | 7 | import dagger.Module; 8 | import dagger.Provides; 9 | import io.github.plastix.forage.data.local.model.Cache; 10 | import io.github.plastix.forage.ui.FragmentScope; 11 | import io.github.plastix.forage.ui.base.FragmentModule; 12 | import io.realm.Realm; 13 | import io.realm.RealmResults; 14 | 15 | /** 16 | * Dagger module that provides dependencies for {@link CacheListFragment} and {@link CacheListActivity}. 17 | * This is used to inject the cache list presenter and view into each other. 18 | */ 19 | @Module 20 | public class CacheListModule extends FragmentModule { 21 | public CacheListModule(Fragment fragment) { 22 | super(fragment); 23 | } 24 | 25 | @Provides 26 | @FragmentScope 27 | public CacheAdapter provideCacheAdapter(Context context, Realm realm) { 28 | RealmResults caches = realm.where(Cache.class).findAllAsync(); 29 | return new CacheAdapter(context, caches, true); 30 | } 31 | 32 | @Provides 33 | @FragmentScope 34 | public LinearLayoutManager provideLinearLayoutManager(Context context) { 35 | return new LinearLayoutManager(context); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/api/gson/RealmLocationAdapter.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api.gson; 2 | 3 | import com.google.gson.TypeAdapter; 4 | import com.google.gson.stream.JsonReader; 5 | import com.google.gson.stream.JsonWriter; 6 | 7 | import java.io.IOException; 8 | import java.util.regex.Pattern; 9 | 10 | import io.github.plastix.forage.data.local.model.RealmLocation; 11 | 12 | public class RealmLocationAdapter extends TypeAdapter { 13 | 14 | private static final Pattern SPLIT_PATTERN = Pattern.compile("\\|"); 15 | 16 | @Override 17 | public void write(JsonWriter out, RealmLocation value) throws IOException { 18 | 19 | } 20 | 21 | @Override 22 | public RealmLocation read(JsonReader in) throws IOException { 23 | String raw = in.nextString(); 24 | 25 | if (raw == null || raw.isEmpty()) { 26 | return null; 27 | } 28 | final String[] parts = SPLIT_PATTERN.split(raw, 2); 29 | 30 | if (parts.length < 2) { 31 | return null; 32 | } 33 | 34 | try { 35 | RealmLocation location = new RealmLocation(); 36 | location.latitude = Double.parseDouble(parts[0]); 37 | location.longitude = Double.parseDouble(parts[1]); 38 | return location; 39 | } catch (NumberFormatException e) { 40 | return null; 41 | } 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/data/local/DatabaseInteractorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.powermock.api.mockito.PowerMockito; 7 | import org.powermock.core.classloader.annotations.PrepareForTest; 8 | import org.powermock.modules.junit4.PowerMockRunner; 9 | 10 | import dagger.Lazy; 11 | import io.realm.Realm; 12 | 13 | import static org.mockito.Mockito.verify; 14 | 15 | /** 16 | * Todo instrumentation tests for Realm 17 | * Realm relies heavily on native code on Android. Since we can't actually run a realm during the 18 | * unit tests we can only verify a few interactions with the mock. Testing the functionality of other 19 | * DatabaseInteractor methods must be done with instrumentation tests. 20 | */ 21 | @RunWith(PowerMockRunner.class) 22 | @PrepareForTest(Realm.class) 23 | public class DatabaseInteractorTest { 24 | 25 | private DatabaseInteractor databaseInteractor; 26 | private Realm realm; 27 | 28 | @Before 29 | public void beforeEachTest() { 30 | realm = PowerMockito.mock(Realm.class); 31 | Lazy realmLazy = () -> realm; 32 | databaseInteractor = new DatabaseInteractor(realmLazy); 33 | } 34 | 35 | @Test 36 | public void onDestroy_shouldCallRealmClose() { 37 | databaseInteractor.onDestroy(); 38 | verify(realm).close(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/empty_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 22 | 23 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_cache_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/local/model/Cache.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local.model; 2 | 3 | import com.google.gson.annotations.JsonAdapter; 4 | import com.google.gson.annotations.SerializedName; 5 | 6 | import io.github.plastix.forage.data.api.gson.HtmlAdapter; 7 | import io.github.plastix.forage.data.api.gson.RealmLocationAdapter; 8 | import io.github.plastix.forage.data.api.gson.StringCapitalizerAdapter; 9 | import io.realm.RealmObject; 10 | import io.realm.annotations.PrimaryKey; 11 | 12 | /** 13 | * Realm model of a Geocache object. 14 | */ 15 | public class Cache extends RealmObject { 16 | 17 | @PrimaryKey 18 | @SerializedName("code") 19 | public String cacheCode; // Opencaching ID 20 | 21 | public String name; //Name of the Cache 22 | 23 | @JsonAdapter(RealmLocationAdapter.class) 24 | public RealmLocation location; 25 | 26 | @JsonAdapter(StringCapitalizerAdapter.class) 27 | public String type; // Cache type such as "Traditional, Multi, Quiz, Virtual" 28 | 29 | @JsonAdapter(StringCapitalizerAdapter.class) 30 | public String status; //Cache Status "Available, etc" 31 | 32 | public float terrain; //Terrain rating of cache 33 | public float difficulty; //Difficulty rating of cache 34 | 35 | @SerializedName("size2") 36 | public String size; // String size of container "none, nano" 37 | 38 | @JsonAdapter(HtmlAdapter.class) 39 | public String description; //HTML Description of the cache 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_compass_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 22 | 23 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/local/pref/StringPreference.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local.pref; 2 | 3 | 4 | import android.content.SharedPreferences; 5 | import android.support.annotation.NonNull; 6 | import android.support.annotation.Nullable; 7 | 8 | /** 9 | * SharedPreference wrapper based on U2020's preference architecture 10 | * https://medium.com/google-developer-experts/persist-your-data-elegantly-u2020-way-c50be19acf9#.8j3qerkk3 11 | */ 12 | public class StringPreference { 13 | 14 | private final SharedPreferences preferences; 15 | private final String key; 16 | private final String defaultValue; 17 | 18 | public StringPreference(@NonNull SharedPreferences preferences, @NonNull String key) { 19 | this(preferences, key, null); 20 | } 21 | 22 | public StringPreference(@NonNull SharedPreferences preferences, @NonNull String key, @Nullable String defaultValue) { 23 | this.preferences = preferences; 24 | this.key = key; 25 | this.defaultValue = defaultValue; 26 | } 27 | 28 | @Nullable 29 | public String get() { 30 | return preferences.getString(key, defaultValue); 31 | } 32 | 33 | public boolean isSet() { 34 | return preferences.contains(key); 35 | } 36 | 37 | public void set(@Nullable String value) { 38 | preferences.edit().putString(key, value).apply(); 39 | } 40 | 41 | public void delete() { 42 | preferences.edit().remove(key).apply(); 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/api/OkApiService.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api; 2 | 3 | import java.util.List; 4 | 5 | import io.github.plastix.forage.data.api.response.SubmitLogResponse; 6 | import io.github.plastix.forage.data.local.model.Cache; 7 | import retrofit2.http.GET; 8 | import retrofit2.http.Headers; 9 | import retrofit2.http.Query; 10 | import rx.Single; 11 | 12 | /** 13 | * Retrofit service definition of the Open Caching API. 14 | * http://www.opencaching.us/okapi/introduction.html 15 | */ 16 | public interface OkApiService { 17 | 18 | @GET("/okapi/services/caches/shortcuts/search_and_retrieve") 19 | Single> searchAndRetrieve(@Query("search_method") String searchMethod, 20 | @Query("search_params") String searchParams, 21 | @Query("retr_method") String retrMethod, 22 | @Query("retr_params") String retrParams, 23 | @Query("wrap") boolean wrap, 24 | @Query("consumer_key") String consumerKey 25 | ); 26 | 27 | @Headers("OAuth: ENABLED") 28 | @GET("/okapi/services/logs/submit") 29 | Single submitLog(@Query("cache_code") String cacheCode, 30 | @Query("logtype") String logType, 31 | @Query("comment") String comment 32 | ); 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/plastix/forage/espresso/ViewMatchers.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.espresso; 2 | 3 | import android.support.design.widget.TextInputLayout; 4 | import android.view.View; 5 | 6 | import org.hamcrest.Description; 7 | import org.hamcrest.Matcher; 8 | import org.hamcrest.TypeSafeMatcher; 9 | 10 | public class ViewMatchers { 11 | 12 | private ViewMatchers() { 13 | throw new IllegalStateException("No instantiation!"); 14 | } 15 | 16 | /** 17 | * Espresso ViewMatcher for checking the error text of a TextInputLayout 18 | * Source: http://stackoverflow.com/questions/34285782/android-espresso-how-to-check-errortext-in-textinputlayout 19 | */ 20 | public static Matcher hasTextInputLayoutErrorText(final String expectedErrorText) { 21 | return new TypeSafeMatcher() { 22 | 23 | @Override 24 | public boolean matchesSafely(View view) { 25 | if (!(view instanceof TextInputLayout)) { 26 | return false; 27 | } 28 | 29 | CharSequence error = ((TextInputLayout) view).getError(); 30 | 31 | if (error == null) { 32 | return false; 33 | } 34 | 35 | String hint = error.toString(); 36 | 37 | return expectedErrorText.equals(hint); 38 | } 39 | 40 | @Override 41 | public void describeTo(Description description) { 42 | } 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_compass.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 | 20 | 21 | 31 | 32 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/api/gson/ListTypeAdapterFactory.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api.gson; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.TypeAdapter; 5 | import com.google.gson.TypeAdapterFactory; 6 | import com.google.gson.internal.$Gson$Types; 7 | import com.google.gson.reflect.TypeToken; 8 | 9 | import java.lang.reflect.Type; 10 | import java.util.List; 11 | 12 | import javax.inject.Inject; 13 | 14 | /** 15 | * Factory for {@link ListTypeAdapter}. Uses some hacky Gson internal APis for figuring out the 16 | * correct type needed for the list. 17 | */ 18 | public class ListTypeAdapterFactory implements TypeAdapterFactory { 19 | 20 | /** 21 | * No argument @Inject constructor so Dagger can instantiate this for us. 22 | */ 23 | @Inject 24 | public ListTypeAdapterFactory() { 25 | } 26 | 27 | @Override 28 | public TypeAdapter create(Gson gson, TypeToken type) { 29 | 30 | if (!List.class.equals(type.getRawType())) { 31 | return null; 32 | } 33 | 34 | // Extract the collection type. WARNING: Uses Gson's internal APIs. 35 | Type collectionType = 36 | $Gson$Types.getCollectionElementType(type.getType(), type.getRawType()); 37 | // Create a TypeAdapter for the collection type. 38 | TypeAdapter delegateAdapter = gson.getAdapter(TypeToken.get(collectionType)); 39 | 40 | //noinspection unchecked 41 | return (TypeAdapter) new ListTypeAdapter<>(delegateAdapter).nullSafe(); 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/compass/LocationUnavailableDialog.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.compass; 2 | 3 | 4 | import android.app.Dialog; 5 | import android.os.Bundle; 6 | import android.support.annotation.NonNull; 7 | import android.support.v4.app.DialogFragment; 8 | import android.support.v4.app.FragmentActivity; 9 | import android.support.v4.app.FragmentManager; 10 | 11 | import com.afollestad.materialdialogs.MaterialDialog; 12 | 13 | import io.github.plastix.forage.R; 14 | 15 | public class LocationUnavailableDialog extends DialogFragment { 16 | 17 | private static final String ID = "LocationDialog"; 18 | 19 | public static void show(FragmentActivity context) { 20 | LocationUnavailableDialog dialog = new LocationUnavailableDialog(); 21 | FragmentManager fragmentManager = context.getSupportFragmentManager(); 22 | // Only show a new Dialog if we don't already have one in the manager 23 | if (fragmentManager.findFragmentByTag(ID) == null) { 24 | dialog.show(context.getSupportFragmentManager(), ID); 25 | } 26 | } 27 | 28 | @NonNull 29 | @Override 30 | public Dialog onCreateDialog(Bundle savedInstanceState) { 31 | return new MaterialDialog.Builder(getActivity()) 32 | .title(R.string.compass_location_unavailable) 33 | .content(R.string.compass_location_unavailable_dialog) 34 | .positiveText(R.string.location_dialog_ok) 35 | .onPositive((dialog, which) -> getActivity().finish()) 36 | .show(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/util/UnitUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | /** 4 | * Utility class for unit conversions. 5 | */ 6 | public class UnitUtils { 7 | 8 | private UnitUtils() { 9 | throw new UnsupportedOperationException("No Instantiation!"); 10 | } 11 | 12 | public static final double MILES_PER_KILOMETER = 0.621371; 13 | public static final double KILOMETER_PER_METER = 0.001; 14 | public static final int FEET_PER_MILE = 5280; 15 | 16 | /** 17 | * Converts miles to kilometers. 18 | * 19 | * @param miles Unit in miles. 20 | * @return Unit in kilometers. 21 | */ 22 | public static double milesToKilometer(double miles) { 23 | return miles / MILES_PER_KILOMETER; 24 | } 25 | 26 | /** 27 | * Converts meters to kilometers. 28 | * 29 | * @param meter Unit in meters. 30 | * @return Unit in kilometers. 31 | */ 32 | public static double metersToKilometers(double meter) { 33 | return meter * KILOMETER_PER_METER; 34 | } 35 | 36 | 37 | /** 38 | * Converts meters to miles. 39 | * 40 | * @param meters Unit in meters. 41 | * @return Unit in miles. 42 | */ 43 | public static double metersToMiles(double meters) { 44 | return metersToKilometers(meters) * MILES_PER_KILOMETER; 45 | } 46 | 47 | /** 48 | * Converts miles to feet. 49 | * 50 | * @param miles Unit in miles. 51 | * @return Unit in feet. 52 | */ 53 | public static double milesToFeet(double miles) { 54 | return miles * FEET_PER_MILE; 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/ui/base/PresenterTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.base; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static com.google.common.truth.Truth.assertThat; 7 | 8 | public class PresenterTest { 9 | 10 | private FakePresenter presenter; 11 | private FakeView view; 12 | 13 | @Before 14 | public void beforeTest() { 15 | presenter = new FakePresenter(); 16 | view = new FakeView(); 17 | } 18 | 19 | @Test 20 | public void onViewAttached_shouldSetView() { 21 | presenter.onViewAttached(view); 22 | 23 | assertThat(presenter.getView()).isSameAs(view); 24 | } 25 | 26 | @Test 27 | public void onViewDetached_shouldClearView() { 28 | presenter.onViewAttached(view); 29 | presenter.onViewDetached(); 30 | 31 | assertThat(presenter.getView()).isNull(); 32 | } 33 | 34 | @Test 35 | public void isViewAttached_shouldReturnCorrectState() { 36 | presenter.onViewAttached(view); 37 | 38 | assertThat(presenter.isViewAttached()).isTrue(); 39 | 40 | presenter.onViewDetached(); 41 | 42 | assertThat(presenter.isViewAttached()).isFalse(); 43 | } 44 | 45 | private class FakeView { 46 | } 47 | 48 | private class FakePresenter extends Presenter { 49 | 50 | @Override 51 | public void onDestroyed() { 52 | // No op 53 | } 54 | 55 | /** 56 | * Getter for tests 57 | */ 58 | public FakeView getView() { 59 | return view; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/util/AngleUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.support.annotation.NonNull; 5 | import android.view.Surface; 6 | import android.view.WindowManager; 7 | 8 | public class AngleUtils { 9 | 10 | private AngleUtils() { 11 | throw new UnsupportedOperationException("No Instantiation!"); 12 | } 13 | 14 | @SuppressLint("SwitchIntDef") 15 | public static int getRotationOffset(@NonNull WindowManager windowManager) { 16 | switch (windowManager.getDefaultDisplay().getRotation()) { 17 | case Surface.ROTATION_90: 18 | return 90; 19 | case Surface.ROTATION_180: 20 | return 180; 21 | case Surface.ROTATION_270: 22 | return 270; 23 | default: 24 | return 0; 25 | } 26 | } 27 | 28 | /** 29 | * Calculates the difference between two angles. 30 | * 31 | * @param from The origin angle in degrees 32 | * @param to The target angle in degrees 33 | * @return Degrees in [-180,180] range 34 | */ 35 | public static float difference(final float from, final float to) { 36 | return normalize(to - from + 180) - 180; 37 | } 38 | 39 | /** 40 | * Normalize an angle so that it is between 0 and 360. 41 | * 42 | * @param angle Angle in degrees to normalize 43 | * @return Normalized angle. 44 | */ 45 | public static float normalize(final float angle) { 46 | return (angle >= 0 ? angle : (360 - ((-angle) % 360))) % 360; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/api/gson/ListTypeAdapter.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api.gson; 2 | 3 | import com.google.gson.TypeAdapter; 4 | import com.google.gson.stream.JsonReader; 5 | import com.google.gson.stream.JsonToken; 6 | import com.google.gson.stream.JsonWriter; 7 | 8 | import java.io.IOException; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import javax.inject.Inject; 13 | 14 | /** 15 | * Gson Type Adapter for dealing with OkApi's awful JSON response format. For some reason 16 | * OkApi returns JSON Object with each each geocache as a key instead of a JSON list. 17 | *

18 | * This adapter converts the OkApi JSON object format to a nice list. 19 | * 20 | * @param Object type of list. 21 | */ 22 | public class ListTypeAdapter extends TypeAdapter> { 23 | 24 | private TypeAdapter delegateAdapter; 25 | 26 | @Inject 27 | public ListTypeAdapter(TypeAdapter delegateAdapter) { 28 | this.delegateAdapter = delegateAdapter; 29 | } 30 | 31 | @Override 32 | public void write(JsonWriter out, List value) throws IOException { 33 | // Do nothing 34 | } 35 | 36 | @Override 37 | public List read(JsonReader in) throws IOException { 38 | List list = new ArrayList<>(); 39 | 40 | in.beginObject(); 41 | while (in.peek() != JsonToken.END_OBJECT) { 42 | // Ignore the JSON Key 43 | in.nextName(); 44 | 45 | // Parse the object using the delegate's type adapter 46 | list.add(delegateAdapter.read(in)); 47 | } 48 | 49 | return list; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ApplicationModule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.content.res.Resources; 6 | import android.support.annotation.NonNull; 7 | 8 | import javax.inject.Singleton; 9 | 10 | import dagger.Module; 11 | import dagger.Provides; 12 | import timber.log.Timber; 13 | 14 | /** 15 | * Dagger module that provides (singleton) application wide dependencies. 16 | */ 17 | @Module 18 | public class ApplicationModule { 19 | 20 | @NonNull 21 | private final ForageApplication app; 22 | 23 | public ApplicationModule(@NonNull ForageApplication app) { 24 | this.app = app; 25 | } 26 | 27 | @NonNull 28 | @Provides 29 | @Singleton 30 | public static Resources provideResources(@NonNull @ForApplication Context context) { 31 | return context.getResources(); 32 | } 33 | 34 | @NonNull 35 | @Provides 36 | @Singleton 37 | public static Timber.DebugTree provideDebugTree() { 38 | return new Timber.DebugTree(); 39 | } 40 | 41 | @NonNull 42 | @Provides 43 | @Singleton 44 | public Application provideApplication() { 45 | return app; 46 | } 47 | 48 | /** 49 | * Allow the application context to be injected but require that it be annotated with 50 | * {@link ForApplication @Annotation} to explicitly differentiate it from an activity context. 51 | */ 52 | @NonNull 53 | @Provides 54 | @Singleton 55 | @ForApplication 56 | public Context provideApplicationContext() { 57 | return app.getApplicationContext(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/ForageRoboelectricUnitTestRunner.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import org.robolectric.RobolectricTestRunner; 6 | import org.robolectric.annotation.Config; 7 | 8 | import java.lang.reflect.Method; 9 | 10 | // Custom runner allows us set config in one place instead of setting it in each test class. 11 | public class ForageRoboelectricUnitTestRunner extends RobolectricTestRunner { 12 | 13 | // This value should be changed as soon as Robolectric will support newer api. 14 | private static final int SDK_EMULATE_LEVEL = 23; 15 | 16 | public ForageRoboelectricUnitTestRunner(@NonNull Class klass) throws Exception { 17 | super(klass); 18 | } 19 | 20 | @Override 21 | public Config getConfig(@NonNull Method method) { 22 | final Config defaultConfig = super.getConfig(method); 23 | return new Config.Implementation( 24 | new int[]{SDK_EMULATE_LEVEL}, 25 | defaultConfig.manifest(), 26 | defaultConfig.qualifiers(), 27 | defaultConfig.packageName(), 28 | defaultConfig.abiSplit(), 29 | defaultConfig.resourceDir(), 30 | defaultConfig.assetDir(), 31 | defaultConfig.buildDir(), 32 | defaultConfig.shadows(), 33 | defaultConfig.instrumentedPackages(), 34 | ForageUnitTestApplication.class, 35 | defaultConfig.libraries(), 36 | defaultConfig.constants() == Void.class ? BuildConfig.class : defaultConfig.constants() 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/plastix/forage/util/UiAutomatorUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import android.os.Build; 4 | import android.support.test.uiautomator.UiDevice; 5 | import android.support.test.uiautomator.UiObject; 6 | import android.support.test.uiautomator.UiObjectNotFoundException; 7 | import android.support.test.uiautomator.UiSelector; 8 | 9 | public class UiAutomatorUtils { 10 | 11 | public static final String TEXT_ALLOW = "Allow"; 12 | public static final String TEXT_DENY = "Deny"; 13 | public static final String TEXT_NEVER_ASK_AGAIN = "Never ask again"; 14 | 15 | public UiAutomatorUtils() { 16 | throw new IllegalStateException("No instantiation!"); 17 | } 18 | 19 | public static void allowPermissionsIfNeeded(UiDevice device) { 20 | if (Build.VERSION.SDK_INT >= 23) { 21 | UiObject allowPermissions = device.findObject(new UiSelector().text(TEXT_ALLOW)); 22 | 23 | if (allowPermissions.exists()) { 24 | try { 25 | allowPermissions.click(); 26 | } catch (UiObjectNotFoundException ignored) { 27 | } 28 | } 29 | } 30 | } 31 | 32 | public static void denyPermissions(UiDevice device) { 33 | if (Build.VERSION.SDK_INT >= 23) { 34 | UiObject denyPermissions = device.findObject(new UiSelector().text(TEXT_DENY)); 35 | 36 | if (denyPermissions.exists()) { 37 | try { 38 | denyPermissions.click(); 39 | } catch (UiObjectNotFoundException ignored) { 40 | } 41 | } 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/misc/SimpleDividerItemDecoration.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.misc; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.drawable.Drawable; 6 | import android.support.v4.content.ContextCompat; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.View; 9 | 10 | import javax.inject.Inject; 11 | 12 | import io.github.plastix.forage.ForApplication; 13 | import io.github.plastix.forage.R; 14 | 15 | /** 16 | * Simple divider decorator for a RecyclerView. 17 | * Based on https://gist.github.com/polbins/e37206fbc444207c0e92. 18 | */ 19 | public class SimpleDividerItemDecoration extends RecyclerView.ItemDecoration { 20 | 21 | private Drawable divider; 22 | 23 | @Inject 24 | public SimpleDividerItemDecoration(@ForApplication Context context) { 25 | divider = ContextCompat.getDrawable(context, R.drawable.line_divider); 26 | } 27 | 28 | @Override 29 | public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { 30 | int left = parent.getPaddingLeft(); 31 | int right = parent.getWidth() - parent.getPaddingRight(); 32 | 33 | int childCount = parent.getChildCount(); 34 | for (int i = 0; i < childCount; i++) { 35 | View child = parent.getChildAt(i); 36 | 37 | RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); 38 | 39 | int top = child.getBottom() + params.bottomMargin; 40 | int bottom = top + divider.getIntrinsicHeight(); 41 | 42 | divider.setBounds(left, top, right, bottom); 43 | divider.draw(c); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /environmentSetup.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Circle CI & gradle.properties live in harmony 3 | # 4 | # Android convention is to store your API keys in a local, non-versioned 5 | # gradle.properties file. Circle CI doesn't allow users to upload pre-populated 6 | # gradle.properties files to store this secret information, but instaed allows 7 | # users to store such information as environment variables. 8 | # 9 | # This script creates a local gradle.properties file on current the Circle CI 10 | # instance. It then reads environment variables which a user 11 | # has defined in their Circle CI project settings environment variables, and 12 | # writes this value to the Circle CI instance's gradle.properties file. 13 | # 14 | # You must execute this script via your circle.yml as a pre-process dependency, 15 | # so your gradle build process has access to all variables. 16 | # 17 | # dependencies: 18 | # pre: 19 | # - source environmentSetup.sh && copyEnvVarsToGradleProperties 20 | # 21 | # Adapted from https://gist.github.com/KioKrofovitch/716e6a681acb33859d16 22 | 23 | #!/usr/bin/env bash 24 | 25 | function copyEnvVarsToGradleProperties { 26 | GRADLE_PROPERTIES=$HOME"/.gradle/gradle.properties" 27 | export GRADLE_PROPERTIES 28 | echo "Gradle Properties should exist at $GRADLE_PROPERTIES" 29 | 30 | if [ ! -f "$GRADLE_PROPERTIES" ]; then 31 | echo "Gradle Properties does not exist" 32 | 33 | echo "Creating Gradle Properties file..." 34 | touch $GRADLE_PROPERTIES 35 | 36 | echo "Writing API keys to gradle.properties..." 37 | echo "OKAPI_US_CONSUMER_KEY=$OKAPI_US_CONSUMER_KEY" >> $GRADLE_PROPERTIES 38 | echo "OKAPI_US_CONSUMER_SECRET=$OKAPI_US_CONSUMER_SECRET" >> $GRADLE_PROPERTIES 39 | echo "GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY" >> $GRADLE_PROPERTIES 40 | fi 41 | } -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/data/api/gson/HtmlAdapterTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api.gson; 2 | 3 | import com.google.gson.stream.JsonReader; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.powermock.api.mockito.PowerMockito; 9 | import org.powermock.core.classloader.annotations.PrepareForTest; 10 | import org.powermock.modules.junit4.PowerMockRunner; 11 | 12 | import io.github.plastix.forage.util.StringUtils; 13 | 14 | import static com.google.common.truth.Truth.assertThat; 15 | import static org.mockito.Mockito.mock; 16 | import static org.mockito.Mockito.when; 17 | 18 | @RunWith(PowerMockRunner.class) 19 | @PrepareForTest(StringUtils.class) 20 | public class HtmlAdapterTest { 21 | 22 | private HtmlAdapter htmlAdapter; 23 | private JsonReader jsonReader; 24 | 25 | @Before 26 | public void beforeEachTest() { 27 | htmlAdapter = new HtmlAdapter(); 28 | jsonReader = mock(JsonReader.class); 29 | PowerMockito.mockStatic(StringUtils.class); 30 | } 31 | 32 | @Test 33 | public void read_shouldCallStringUtils() throws Exception { 34 | String input = "regular string"; 35 | when(jsonReader.nextString()).thenReturn(input); 36 | 37 | htmlAdapter.read(jsonReader); 38 | 39 | PowerMockito.verifyStatic(); 40 | StringUtils.stripHtml(input); 41 | } 42 | 43 | @Test 44 | public void read_returnsConvertedString() throws Exception { 45 | String input = "regular string"; 46 | when(jsonReader.nextString()).thenReturn(input); 47 | 48 | String newString = "new string"; 49 | when(StringUtils.stripHtml(input)).thenReturn(newString); 50 | 51 | String result = htmlAdapter.read(jsonReader); 52 | assertThat(result).isEqualTo(newString); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/base/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.base; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.LayoutRes; 5 | import android.support.annotation.Nullable; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.view.MenuItem; 8 | 9 | import butterknife.ButterKnife; 10 | import icepick.Icepick; 11 | import io.github.plastix.forage.ApplicationComponent; 12 | import io.github.plastix.forage.ForageApplication; 13 | 14 | /** 15 | * AppCompatActivity base to extend all other activities from. 16 | * This provides hooks for Butterknife and Icepick automatically. 17 | */ 18 | public abstract class BaseActivity extends AppCompatActivity { 19 | 20 | @Override 21 | protected void onCreate(@Nullable Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | injectDependencies(ForageApplication.getComponent(this)); 24 | Icepick.restoreInstanceState(this, savedInstanceState); 25 | } 26 | 27 | protected abstract void injectDependencies(ApplicationComponent component); 28 | 29 | @Override 30 | protected void onSaveInstanceState(Bundle outState) { 31 | super.onSaveInstanceState(outState); 32 | Icepick.saveInstanceState(this, outState); 33 | } 34 | 35 | @Override 36 | public void setContentView(@LayoutRes int layoutResID) { 37 | super.setContentView(layoutResID); 38 | ButterKnife.bind(this); 39 | } 40 | 41 | @Override 42 | public boolean onOptionsItemSelected(MenuItem item) { 43 | switch (item.getItemId()) { 44 | // Respond to the action bar's Up/Home button 45 | case android.R.id.home: 46 | supportFinishAfterTransition(); 47 | return true; 48 | } 49 | return super.onOptionsItemSelected(item); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/util/UnitUtilsTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import org.junit.Test; 4 | 5 | import static com.google.common.truth.Truth.assertThat; 6 | 7 | public class UnitUtilsTest { 8 | 9 | @Test 10 | public void milesToKilometer_isCorrect() { 11 | assertThat(UnitUtils.milesToKilometer(0)).isWithin(0).of(0); 12 | assertThat(UnitUtils.milesToKilometer(1)).isWithin(0).of(1.6093444978925633); 13 | assertThat(UnitUtils.milesToKilometer(0.5)).isWithin(0).of(0.8046722489462816); 14 | assertThat(UnitUtils.milesToKilometer(2)).isWithin(0).of(3.2186889957851266); 15 | 16 | } 17 | 18 | @Test 19 | public void metersToKilometers_isCorrect() { 20 | assertThat(UnitUtils.metersToKilometers(0)).isWithin(0).of(0); 21 | assertThat(UnitUtils.metersToKilometers(1)).isWithin(0).of(0.001); 22 | assertThat(UnitUtils.metersToKilometers(500)).isWithin(0).of(0.500); 23 | assertThat(UnitUtils.metersToKilometers(1000)).isWithin(0).of(1); 24 | assertThat(UnitUtils.metersToKilometers(1500)).isWithin(0).of(1.5); 25 | } 26 | 27 | @Test 28 | public void metersToMiles_isCorrect() { 29 | assertThat(UnitUtils.metersToMiles(0)).isWithin(0).of(0); 30 | assertThat(UnitUtils.metersToMiles(804.672)).isWithin(0.1).of(0.5); 31 | assertThat(UnitUtils.metersToMiles(1609.34)).isWithin(0.1).of(1); 32 | assertThat(UnitUtils.metersToMiles(3218.69)).isWithin(0.1).of(2); 33 | } 34 | 35 | @Test 36 | public void milesToFeet_isCorrect() { 37 | assertThat(UnitUtils.milesToFeet(1)).isWithin(0).of(5280); 38 | assertThat(UnitUtils.milesToFeet(0.5)).isWithin(0).of(2640); 39 | assertThat(UnitUtils.milesToFeet(0)).isWithin(0).of(0); 40 | assertThat(UnitUtils.milesToFeet(3)).isWithin(0).of(15840); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/integrationTests/java/io/github/plastix/forage/ForageRoboelectricIntegrationTestRunner.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import org.robolectric.RobolectricTestRunner; 6 | import org.robolectric.RuntimeEnvironment; 7 | import org.robolectric.annotation.Config; 8 | 9 | import java.lang.reflect.Method; 10 | 11 | // Custom runner allows us set config in one place instead of setting it in each test class. 12 | public class ForageRoboelectricIntegrationTestRunner extends RobolectricTestRunner { 13 | 14 | // This value should be changed as soon as Robolectric will support newer api. 15 | private static final int SDK_EMULATE_LEVEL = 23; 16 | 17 | public ForageRoboelectricIntegrationTestRunner(@NonNull Class klass) throws Exception { 18 | super(klass); 19 | } 20 | 21 | @NonNull 22 | public static ForageApplication forageApplication() { 23 | return (ForageApplication) RuntimeEnvironment.application; 24 | } 25 | 26 | @Override 27 | public Config getConfig(@NonNull Method method) { 28 | final Config defaultConfig = super.getConfig(method); 29 | return new Config.Implementation( 30 | new int[]{SDK_EMULATE_LEVEL}, 31 | defaultConfig.manifest(), 32 | defaultConfig.qualifiers(), 33 | defaultConfig.packageName(), 34 | defaultConfig.abiSplit(), 35 | defaultConfig.resourceDir(), 36 | defaultConfig.assetDir(), 37 | defaultConfig.buildDir(), 38 | defaultConfig.shadows(), 39 | defaultConfig.instrumentedPackages(), 40 | ForageUnitTestApplication.class, 41 | defaultConfig.libraries(), 42 | defaultConfig.constants() == Void.class ? BuildConfig.class : defaultConfig.constants() 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/base/PresenterLoader.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.base; 2 | 3 | import android.content.Context; 4 | import android.support.v4.content.Loader; 5 | 6 | import javax.inject.Inject; 7 | import javax.inject.Provider; 8 | 9 | import io.github.plastix.forage.ui.ActivityScope; 10 | 11 | /** 12 | * Synchronous Loader used to hold presenter instances outside of the Activity/Fragment lifecycle. 13 | * Adapted from https://medium.com/@czyrux/presenter-surviving-orientation-changes-with-loaders-6da6d86ffbbf#.x1x4xfxc7 14 | * 15 | * @param Type of Presenter 16 | */ 17 | public class PresenterLoader extends Loader { 18 | 19 | private T presenter; 20 | private Provider presenterFactory; 21 | 22 | @Inject 23 | public PresenterLoader(@ActivityScope Context context, Provider presenterFactory) { 24 | super(context); 25 | this.presenterFactory = presenterFactory; 26 | } 27 | 28 | @Override 29 | protected void onStartLoading() { 30 | super.onStartLoading(); 31 | 32 | // If we already have a presenter instance, simply deliver it. 33 | if (presenter != null) { 34 | deliverResult(presenter); 35 | } else { 36 | // Otherwise, force a load 37 | forceLoad(); 38 | } 39 | 40 | } 41 | 42 | @Override 43 | protected void onForceLoad() { 44 | super.onForceLoad(); 45 | 46 | // Grab an instance of the presenter from the provider 47 | presenter = presenterFactory.get(); 48 | 49 | // Deliver the presenter 50 | deliverResult(presenter); 51 | 52 | } 53 | 54 | @Override 55 | protected void onReset() { 56 | super.onReset(); 57 | 58 | // Clean up presenter resources if we have one 59 | if (presenter != null) { 60 | presenter.onDestroyed(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/releaseUnitTests/java/io/github/plastix/forage/permission/PermissionsTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.permission; 2 | 3 | import android.Manifest; 4 | 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.robolectric.RobolectricTestRunner; 8 | import org.robolectric.annotation.Config; 9 | import org.robolectric.manifest.AndroidManifest; 10 | import org.robolectric.res.Fs; 11 | 12 | import java.util.HashSet; 13 | 14 | import static com.google.common.truth.Truth.assertThat; 15 | 16 | @RunWith(RobolectricTestRunner.class) 17 | @Config(manifest = Config.NONE) 18 | public final class PermissionsTest { 19 | 20 | /** 21 | * This list must be kept updated as new permissions/libraries are added! 22 | */ 23 | private static final String[] EXPECTED_PERMISSIONS = { 24 | // App permissions 25 | Manifest.permission.INTERNET, 26 | Manifest.permission.ACCESS_FINE_LOCATION, 27 | // Google Maps Permission 28 | Manifest.permission.ACCESS_NETWORK_STATE 29 | }; 30 | 31 | /** 32 | * Merged manifest location. 33 | * Only check the release manifest because the debug version might have debug libraries that 34 | * require special permissions that won't show up in the release. 35 | */ 36 | private static final String MERGED_MANIFEST = 37 | "build/intermediates/manifests/full/release/AndroidManifest.xml"; 38 | 39 | /** 40 | * Test to check if libraries are adding extra permissions to the app manifest. 41 | */ 42 | @Test 43 | public void shouldMatchPermissions() { 44 | AndroidManifest manifest = new AndroidManifest( 45 | Fs.fileFromPath(MERGED_MANIFEST), 46 | null, 47 | null 48 | ); 49 | 50 | assertThat(new HashSet<>(manifest.getUsedPermissions())). 51 | containsExactly((Object[]) EXPECTED_PERMISSIONS); 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/cachedetail/CacheDetailPresenter.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.cachedetail; 2 | 3 | import javax.inject.Inject; 4 | 5 | import io.github.plastix.forage.data.api.auth.OAuthInteractor; 6 | import io.github.plastix.forage.data.local.DatabaseInteractor; 7 | import io.github.plastix.forage.ui.base.RxPresenter; 8 | import io.github.plastix.rxdelay.RxDelay; 9 | 10 | public class CacheDetailPresenter extends RxPresenter { 11 | 12 | private DatabaseInteractor databaseInteractor; 13 | private OAuthInteractor oAuthInteractor; 14 | 15 | @Inject 16 | public CacheDetailPresenter(DatabaseInteractor databaseInteractor, OAuthInteractor oAuthInteractor) { 17 | this.databaseInteractor = databaseInteractor; 18 | this.oAuthInteractor = oAuthInteractor; 19 | } 20 | 21 | public void getGeocache(String cacheCode) { 22 | addSubscription(databaseInteractor.getGeocacheCopy(cacheCode) 23 | .toObservable() 24 | .compose(RxDelay.delayFirst(getViewState())) 25 | .toSingle() 26 | .subscribe(cache -> { 27 | if (isViewAttached()) { 28 | view.returnedGeocache(cache); 29 | } 30 | }, throwable -> { 31 | if (isViewAttached()) { 32 | view.onError(); 33 | } 34 | }) 35 | ); 36 | } 37 | 38 | public void openLogScreen() { 39 | if (oAuthInteractor.hasSavedOAuthTokens()) { 40 | if (isViewAttached()) { 41 | view.openLogScreen(); 42 | } 43 | } else { 44 | if (isViewAttached()) { 45 | view.onErrorRequiresLogin(); 46 | } 47 | } 48 | } 49 | 50 | @Override 51 | public void onDestroyed() { 52 | databaseInteractor.onDestroy(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/data/api/gson/StringCaptializerTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api.gson; 2 | 3 | import com.google.gson.stream.JsonReader; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.powermock.api.mockito.PowerMockito; 9 | import org.powermock.core.classloader.annotations.PrepareForTest; 10 | import org.powermock.modules.junit4.PowerMockRunner; 11 | 12 | import io.github.plastix.forage.util.StringUtils; 13 | 14 | import static com.google.common.truth.Truth.assertThat; 15 | import static org.mockito.Mockito.mock; 16 | import static org.mockito.Mockito.when; 17 | 18 | @RunWith(PowerMockRunner.class) 19 | @PrepareForTest(StringUtils.class) 20 | public class StringCaptializerTest { 21 | 22 | private StringCapitalizerAdapter stringCapitalizerAdapter; 23 | private JsonReader jsonReader; 24 | 25 | @Before 26 | public void beforeEachTest() { 27 | stringCapitalizerAdapter = new StringCapitalizerAdapter(); 28 | jsonReader = mock(JsonReader.class); 29 | PowerMockito.mockStatic(StringUtils.class); 30 | } 31 | 32 | @Test 33 | public void read_shouldCallStringUtils() throws Exception { 34 | String input = "regular string"; 35 | when(jsonReader.nextString()).thenReturn(input); 36 | 37 | stringCapitalizerAdapter.read(jsonReader); 38 | 39 | PowerMockito.verifyStatic(); 40 | StringUtils.capitalize(input); 41 | } 42 | 43 | @Test 44 | public void read_returnsConvertedString() throws Exception { 45 | String input = "regular string"; 46 | when(jsonReader.nextString()).thenReturn(input); 47 | 48 | String newString = "Regular string"; 49 | when(StringUtils.capitalize(input)).thenReturn(newString); 50 | 51 | String result = stringCapitalizerAdapter.read(jsonReader); 52 | assertThat(result).isEqualTo(newString); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/integrationTests/java/io/github/plastix/forage/data/local/CacheTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | 8 | import io.github.plastix.forage.ForageRoboelectricIntegrationTestRunner; 9 | import io.github.plastix.forage.data.local.model.Cache; 10 | 11 | import static com.google.common.truth.Truth.assertThat; 12 | 13 | @RunWith(ForageRoboelectricIntegrationTestRunner.class) 14 | public class CacheTest { 15 | 16 | @Test 17 | public void fromJson() { 18 | 19 | Gson gson = ForageRoboelectricIntegrationTestRunner.forageApplication().getComponent().gson(); 20 | 21 | String input = "{\n" + 22 | " \"code\": \"CACHE_CODE\",\n" + 23 | " \"name\": \"Cache Name\",\n" + 24 | " \"location\": \"40.7095|-74.0116\",\n" + 25 | " \"type\": \"Traditional\",\n" + 26 | " \"status\": \"Available\",\n" + 27 | " \"terrain\": 1,\n" + 28 | " \"difficulty\": 2.5,\n" + 29 | " \"size2\": \"micro\",\n" + 30 | " \"description\": \"

HTML Description

\"\n" + 31 | "}"; 32 | Cache cache = gson.fromJson(input, Cache.class); 33 | 34 | assertThat(cache.cacheCode).isEqualTo("CACHE_CODE"); 35 | assertThat(cache.name).isEqualTo("Cache Name"); 36 | assertThat(cache.location.latitude).isWithin(0).of(40.7095); 37 | assertThat(cache.location.longitude).isWithin(0).of(-74.0116); 38 | assertThat(cache.type).isEqualTo("Traditional"); 39 | assertThat(cache.status).isEqualTo("Available"); 40 | assertThat(cache.terrain).isWithin(0).of(1); 41 | assertThat(cache.difficulty).isWithin(0).of(2.5f); 42 | assertThat(cache.size).isEqualTo("Micro"); 43 | assertThat(cache.description).isEqualTo("HTML Description"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ForageApplication.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.support.annotation.NonNull; 6 | 7 | import com.squareup.leakcanary.LeakCanary; 8 | 9 | import javax.inject.Inject; 10 | 11 | import dagger.Lazy; 12 | import io.github.plastix.forage.data.local.RealmInitWrapper; 13 | import io.github.plastix.forage.dev_tools.DevMetricsProxy; 14 | import timber.log.Timber; 15 | 16 | public class ForageApplication extends Application { 17 | 18 | @Inject 19 | Lazy realmProxy; 20 | 21 | @Inject 22 | Lazy devMetricsProxy; 23 | 24 | @Inject 25 | Lazy debugTree; 26 | 27 | private ApplicationComponent component; 28 | 29 | @NonNull 30 | public static ApplicationComponent getComponent(Context context) { 31 | return ((ForageApplication) context.getApplicationContext()).getComponent(); 32 | } 33 | 34 | @NonNull 35 | public ApplicationComponent getComponent() { 36 | return component; 37 | } 38 | 39 | @Override 40 | public void onCreate() { 41 | super.onCreate(); 42 | 43 | this.component = prepareApplicationComponent().build(); 44 | this.component.injectTo(this); 45 | 46 | // Init realm db 47 | 48 | realmProxy.get().init(); 49 | 50 | //Use debug tools only in debug builds 51 | if (BuildConfig.DEBUG) { 52 | setupDebugTools(); 53 | } 54 | } 55 | 56 | private void setupDebugTools() { 57 | Timber.plant(debugTree.get()); 58 | LeakCanary.install(this); 59 | devMetricsProxy.get().apply(); 60 | } 61 | 62 | @NonNull 63 | protected DaggerApplicationComponent.Builder prepareApplicationComponent() { 64 | return DaggerApplicationComponent.builder() 65 | .applicationModule(new ApplicationModule(this)); 66 | } 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/data/local/RealmLocationTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.local; 2 | 3 | import android.location.Location; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.powermock.api.mockito.PowerMockito; 9 | import org.powermock.core.classloader.annotations.PrepareForTest; 10 | import org.powermock.modules.junit4.PowerMockRunner; 11 | 12 | import io.github.plastix.forage.data.local.model.RealmLocation; 13 | import io.github.plastix.forage.util.LocationUtils; 14 | 15 | import static com.google.common.truth.Truth.assertThat; 16 | import static org.mockito.Mockito.mock; 17 | import static org.mockito.Mockito.when; 18 | 19 | @RunWith(PowerMockRunner.class) 20 | @PrepareForTest(LocationUtils.class) 21 | public class RealmLocationTest { 22 | 23 | @Before 24 | public void beforeEachTest() { 25 | PowerMockito.mockStatic(LocationUtils.class); 26 | } 27 | 28 | @Test 29 | public void toLocation_shouldCallLocationUtils() { 30 | double lat = 40.7127; 31 | double lon = 74.0059; 32 | 33 | RealmLocation realmLocation = new RealmLocation(); 34 | realmLocation.latitude = lat; 35 | realmLocation.longitude = lon; 36 | 37 | realmLocation.toLocation(); 38 | 39 | PowerMockito.verifyStatic(); 40 | LocationUtils.buildLocation(lat, lon); 41 | } 42 | 43 | @Test 44 | public void toLocation_shouldReturnFromLocationUtils() { 45 | double lat = 40.7127; 46 | double lon = 74.0059; 47 | 48 | RealmLocation realmLocation = new RealmLocation(); 49 | realmLocation.latitude = lat; 50 | realmLocation.longitude = lon; 51 | 52 | Location location = mock(Location.class); 53 | when(LocationUtils.buildLocation(lat, lon)).thenReturn(location); 54 | 55 | Location result = realmLocation.toLocation(); 56 | 57 | assertThat(result).isSameAs(location); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_map.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 25 | 26 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/util/PermissionUtilsTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import org.junit.Test; 4 | 5 | import static com.google.common.truth.Truth.assertThat; 6 | 7 | public class PermissionUtilsTest { 8 | 9 | @Test 10 | public void isPermissionRequestCancelled_shouldBeCancelled() { 11 | assertThat(PermissionUtils.isPermissionRequestCancelled(new int[]{})).isTrue(); 12 | } 13 | 14 | @Test 15 | public void isPermissionRequestCancelled_isNotCancelled() { 16 | assertThat(PermissionUtils.isPermissionRequestCancelled(new int[]{-1, -1})).isFalse(); 17 | assertThat(PermissionUtils.isPermissionRequestCancelled(new int[]{0, 0})).isFalse(); 18 | assertThat(PermissionUtils.isPermissionRequestCancelled(new int[]{0, -1})).isFalse(); 19 | assertThat(PermissionUtils.isPermissionRequestCancelled(new int[]{-1, 0})).isFalse(); 20 | assertThat(PermissionUtils.isPermissionRequestCancelled(new int[]{-1})).isFalse(); 21 | assertThat(PermissionUtils.isPermissionRequestCancelled(new int[]{0})).isFalse(); 22 | 23 | 24 | } 25 | 26 | @Test 27 | public void hasAllPermissionsGranted_allGranted() { 28 | assertThat(PermissionUtils.hasAllPermissionsGranted(new int[]{0})).isTrue(); 29 | assertThat(PermissionUtils.hasAllPermissionsGranted(new int[]{0, 0})).isTrue(); 30 | assertThat(PermissionUtils.hasAllPermissionsGranted(new int[]{0, 0, 0})).isTrue(); 31 | } 32 | 33 | 34 | @Test 35 | public void hasAllPermissionsGranted_notAllGranted() { 36 | assertThat(PermissionUtils.hasAllPermissionsGranted(new int[]{})).isFalse(); 37 | assertThat(PermissionUtils.hasAllPermissionsGranted(new int[]{-1})).isFalse(); 38 | assertThat(PermissionUtils.hasAllPermissionsGranted(new int[]{0, -1})).isFalse(); 39 | assertThat(PermissionUtils.hasAllPermissionsGranted(new int[]{0, -1, 0})).isFalse(); 40 | assertThat(PermissionUtils.hasAllPermissionsGranted(new int[]{-1, -1, 0})).isFalse(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/base/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.base; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import butterknife.ButterKnife; 11 | import butterknife.Unbinder; 12 | import icepick.Icepick; 13 | import io.github.plastix.forage.ApplicationComponent; 14 | import io.github.plastix.forage.ForageApplication; 15 | 16 | /** 17 | * Support Fragment base to extend all other Fragments from. 18 | * This provides hooks for Butterknife and Icepick automatically. 19 | */ 20 | public abstract class BaseFragment extends Fragment { 21 | 22 | private Unbinder unbinder; 23 | 24 | @Override 25 | public void onCreate(@Nullable Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | injectDependencies(ForageApplication.getComponent(getContext())); 28 | Icepick.restoreInstanceState(this, savedInstanceState); 29 | } 30 | 31 | protected abstract void injectDependencies(ApplicationComponent component); 32 | 33 | @Nullable 34 | @Override 35 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 36 | return inflater.inflate(getFragmentLayout(), container, false); 37 | } 38 | 39 | protected abstract int getFragmentLayout(); 40 | 41 | @Override 42 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 43 | super.onViewCreated(view, savedInstanceState); 44 | unbinder = ButterKnife.bind(this, view); 45 | } 46 | 47 | @Override 48 | public void onSaveInstanceState(Bundle outState) { 49 | super.onSaveInstanceState(outState); 50 | Icepick.saveInstanceState(this, outState); 51 | } 52 | 53 | @Override 54 | public void onDestroyView() { 55 | super.onDestroyView(); 56 | unbinder.unbind(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/util/PermissionUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageManager; 5 | import android.support.annotation.NonNull; 6 | import android.support.v4.content.ContextCompat; 7 | 8 | /** 9 | * Utility class for handling Android Marshmallow runtime permissions. 10 | */ 11 | public class PermissionUtils { 12 | 13 | private PermissionUtils() { 14 | throw new UnsupportedOperationException("No Instantiation!"); 15 | } 16 | 17 | /** 18 | * Returns whether the application has access to a specific permission. 19 | * 20 | * @param permission Permission to check. 21 | */ 22 | public static boolean hasPermission(@NonNull Context context, @NonNull String permission) { 23 | return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED; 24 | } 25 | 26 | /** 27 | * Returns whether all permissions have been granted based on the specified permission results. 28 | * 29 | * @param grantResults Permission grant results. 30 | * @return True if all permissions have been granted, else false. 31 | */ 32 | public static boolean hasAllPermissionsGranted(@NonNull int[] grantResults) { 33 | // If the request is canceled the results array will be empty 34 | if (isPermissionRequestCancelled(grantResults)) { 35 | return false; 36 | } 37 | 38 | for (int grantResult : grantResults) { 39 | if (grantResult == PackageManager.PERMISSION_DENIED) { 40 | return false; 41 | } 42 | } 43 | return true; 44 | } 45 | 46 | /** 47 | * Returns whether the permission request has been cancelled. 48 | * 49 | * @param grantResults Permission grant results. 50 | * @return True if request has been cancelled, else false. 51 | */ 52 | public static boolean isPermissionRequestCancelled(@NonNull int[] grantResults) { 53 | return grantResults.length == 0; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/data/location/LocationInteractorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.location; 2 | 3 | import com.google.android.gms.location.LocationRequest; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | 9 | import javax.inject.Provider; 10 | 11 | import io.github.plastix.forage.ForageRoboelectricUnitTestRunner; 12 | 13 | import static org.mockito.Matchers.any; 14 | import static org.mockito.Mockito.mock; 15 | import static org.mockito.Mockito.times; 16 | import static org.mockito.Mockito.verify; 17 | 18 | @RunWith(ForageRoboelectricUnitTestRunner.class) 19 | public class LocationInteractorTest { 20 | 21 | private Provider locationAsyncEmitterProvider; 22 | private Provider locationAvailableProvider; 23 | private LocationInteractor locationInteractor; 24 | private LocationAsyncEmitter locationAsyncEmitter; 25 | private LocationAvailableAsyncEmitter locationAvailableAsyncEmitter; 26 | 27 | @Before 28 | public void beforeEachTest() { 29 | locationAsyncEmitter = mock(LocationAsyncEmitter.class); 30 | locationAsyncEmitterProvider = () -> locationAsyncEmitter; 31 | 32 | locationAvailableAsyncEmitter = mock(LocationAvailableAsyncEmitter.class); 33 | locationAvailableProvider = () -> locationAvailableAsyncEmitter; 34 | 35 | locationInteractor = new LocationInteractor(locationAsyncEmitterProvider, locationAvailableProvider); 36 | } 37 | 38 | @Test 39 | public void getUpdatedLocation_returnsLocationSingle() { 40 | locationInteractor.getUpdatedLocation().toBlocking(); 41 | 42 | verify(locationAsyncEmitter, times(1)).setLocationRequest(any(LocationRequest.class)); 43 | } 44 | 45 | @Test 46 | public void getLocationObservable_returnsLocationObservable() { 47 | locationInteractor.getLocationObservable(1000).toBlocking(); 48 | 49 | verify(locationAsyncEmitter, times(1)).setLocationRequest(any(LocationRequest.class)); 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/api/auth/OAuthSigningInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api.auth; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.inject.Inject; 6 | 7 | import io.github.plastix.forage.data.api.ApiConstants; 8 | import io.github.plastix.forage.data.local.pref.OAuthUserToken; 9 | import io.github.plastix.forage.data.local.pref.OAuthUserTokenSecret; 10 | import io.github.plastix.forage.data.local.pref.StringPreference; 11 | import oauth.signpost.exception.OAuthException; 12 | import okhttp3.Interceptor; 13 | import okhttp3.Request; 14 | import okhttp3.Response; 15 | import se.akerfeldt.okhttp.signpost.OkHttpOAuthConsumer; 16 | 17 | /** 18 | * An OKHttp interceptor that signs requests using oauth-signpost. 19 | * Adapted from https://github.com/pakerfeldt/okhttp-signpost 20 | * This Interceptor allows oauth signing to be enabled and disabled using a custom request 21 | * header 22 | */ 23 | public class OAuthSigningInterceptor implements Interceptor { 24 | 25 | private final OkHttpOAuthConsumer consumer; 26 | 27 | @Inject 28 | public OAuthSigningInterceptor(OkHttpOAuthConsumer consumer, 29 | @OAuthUserToken StringPreference userToken, 30 | @OAuthUserTokenSecret StringPreference userTokenSecret) { 31 | this.consumer = consumer; 32 | 33 | // Set the signing token and secret if we already have one saved in shared prefs 34 | if (userToken.isSet() && userTokenSecret.isSet()) { 35 | consumer.setTokenWithSecret(userToken.get(), userTokenSecret.get()); 36 | } 37 | } 38 | 39 | @Override 40 | public Response intercept(Chain chain) throws IOException { 41 | Request original = chain.request(); 42 | 43 | 44 | if (original.header(ApiConstants.OAUTH_ENABLE_HEADER) != null) { 45 | try { 46 | original = (Request) consumer.sign(original).unwrap(); 47 | } catch (OAuthException e) { 48 | throw new IOException("Could not sign request", e); 49 | } 50 | } 51 | return chain.proceed(original); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/util/ActivityUtilsTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import android.support.annotation.StringRes; 4 | import android.support.v7.app.ActionBar; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.app.AppCompatDelegate; 7 | 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | 11 | import static org.mockito.Mockito.mock; 12 | import static org.mockito.Mockito.times; 13 | import static org.mockito.Mockito.verify; 14 | import static org.mockito.Mockito.when; 15 | 16 | 17 | public class ActivityUtilsTest { 18 | 19 | 20 | private AppCompatActivity activity; 21 | 22 | @Before 23 | public void beforeEachTest() { 24 | activity = mock(AppCompatActivity.class); 25 | } 26 | 27 | @Test 28 | public void setSupportActionBarTitle_setStringTitleHasActionBar() { 29 | String title = "title"; 30 | ActionBar actionBar = mock(ActionBar.class); 31 | when(activity.getSupportActionBar()).thenReturn(actionBar); 32 | 33 | ActivityUtils.setSupportActionBarTitle(activity, title); 34 | 35 | verify(actionBar, times(1)).setTitle(title); 36 | } 37 | 38 | @Test 39 | public void setSupportActionBarTitle_stringResID() { 40 | String title = "title"; 41 | @StringRes int fakeId = 0; 42 | when(activity.getString(fakeId)).thenReturn(title); 43 | ActionBar actionBar = mock(ActionBar.class); 44 | when(activity.getSupportActionBar()).thenReturn(actionBar); 45 | 46 | ActivityUtils.setSupportActionBarTitle(activity, fakeId); 47 | 48 | verify(actionBar, times(1)).setTitle(title); 49 | } 50 | 51 | @Test 52 | public void setSupportActionBarBack_setBackButtonEnabled() { 53 | AppCompatDelegate delegate = mock(AppCompatDelegate.class); 54 | ActionBar actionBar = mock(ActionBar.class); 55 | when(delegate.getSupportActionBar()).thenReturn(actionBar); 56 | 57 | ActivityUtils.setSupportActionBarBack(delegate); 58 | 59 | verify(actionBar, times(1)).setDisplayShowHomeEnabled(true); 60 | verify(actionBar, times(1)).setDisplayHomeAsUpEnabled(true); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Applications/android-sdk-mac_x86/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Butterknife 20 | # Retain generated class which implement ViewBinder. 21 | -keep public class * implements butterknife.internal.ViewBinder { public (); } 22 | 23 | # Prevent obfuscation of types which use ButterKnife annotations since the simple name 24 | # is used to reflectively look up the generated ViewBinder. 25 | -keep class butterknife.* 26 | -keepclasseswithmembernames class * { @butterknife.* ; } 27 | -keepclasseswithmembernames class * { @butterknife.* ; } 28 | 29 | # Retrofit 30 | -dontwarn retrofit2.** 31 | -keep class retrofit2.** { *; } 32 | -keepattributes Signature 33 | -keepattributes Exceptions 34 | 35 | # Google Maps API 36 | 37 | # The Maps API uses custom Parcelables. 38 | # Use this rule (which is slightly broader than the standard recommended one) 39 | # to avoid obfuscating them. 40 | -keepclassmembers class * implements android.os.Parcelable { 41 | static *** CREATOR; 42 | } 43 | 44 | # The Maps API uses serialization. 45 | -keepclassmembers class * implements java.io.Serializable { 46 | static final long serialVersionUID; 47 | static final java.io.ObjectStreamField[] serialPersistentFields; 48 | private void writeObject(java.io.ObjectOutputStream); 49 | private void readObject(java.io.ObjectInputStream); 50 | java.lang.Object writeReplace(); 51 | java.lang.Object readResolve(); 52 | } 53 | 54 | # Ice Pick 55 | -dontwarn icepick.** 56 | -keep class **$$Icepick { *; } 57 | -keepclasseswithmembernames class * { 58 | @icepick.* ; 59 | } 60 | 61 | # Retrolambda 62 | -dontwarn java.lang.invoke.* -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/about/AboutActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.about; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v7.widget.Toolbar; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.widget.LinearLayout; 10 | import android.widget.TextView; 11 | 12 | import butterknife.BindView; 13 | import io.github.plastix.forage.ApplicationComponent; 14 | import io.github.plastix.forage.BuildConfig; 15 | import io.github.plastix.forage.R; 16 | import io.github.plastix.forage.ui.base.BaseActivity; 17 | import io.github.plastix.forage.util.ActivityUtils; 18 | 19 | public class AboutActivity extends BaseActivity { 20 | 21 | @BindView(R.id.about_toolbar) 22 | Toolbar toolbar; 23 | 24 | @BindView(R.id.about_version) 25 | TextView version; 26 | 27 | @BindView(R.id.about_linearlayout) 28 | LinearLayout linearLayout; 29 | 30 | 31 | public static Intent newIntent(Context context) { 32 | return new Intent(context, AboutActivity.class); 33 | } 34 | 35 | @Override 36 | protected void onCreate(Bundle savedInstanceState) { 37 | super.onCreate(savedInstanceState); 38 | setContentView(R.layout.activity_about); 39 | setSupportActionBar(toolbar); 40 | ActivityUtils.setSupportActionBarBack(getDelegate()); 41 | 42 | setupUI(); 43 | } 44 | 45 | private void setupUI() { 46 | String raw = getString(R.string.about_version_info); 47 | String ver = String.format(raw, 48 | BuildConfig.VERSION_NAME, 49 | BuildConfig.BUILD_TYPE, 50 | BuildConfig.VERSION_CODE); 51 | version.setText(ver); 52 | 53 | LayoutInflater inflater = LayoutInflater.from(this); 54 | for (String library : getResources().getStringArray(R.array.about_libraries_list)) { 55 | View view = inflater.inflate(R.layout.library_item, linearLayout, false); 56 | TextView title = ((TextView) view.findViewById(R.id.library_title)); 57 | title.setText(library); 58 | linearLayout.addView(view); 59 | } 60 | } 61 | 62 | @Override 63 | protected void injectDependencies(ApplicationComponent component) { 64 | // No injections 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/base/PresenterFragment.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.base; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.CallSuper; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.app.LoaderManager; 7 | import android.support.v4.content.Loader; 8 | 9 | import javax.inject.Inject; 10 | import javax.inject.Provider; 11 | 12 | /** 13 | * Base Fragment that holds a Presenter and manages its lifecycle using a {@link PresenterLoader}. 14 | * 15 | * @param Type of Presenter. 16 | */ 17 | public abstract class PresenterFragment, V> extends BaseFragment 18 | implements LoaderManager.LoaderCallbacks { 19 | 20 | // Internal ID to reference the Loader from the LoaderManager 21 | private static final int LOADER_ID = 1; 22 | 23 | protected T presenter; 24 | 25 | @Inject 26 | protected Provider> presenterLoaderProvider; 27 | 28 | @Override 29 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 30 | super.onActivityCreated(savedInstanceState); 31 | 32 | initLoader(); 33 | } 34 | 35 | private void initLoader() { 36 | getLoaderManager().initLoader(LOADER_ID, null, this); 37 | } 38 | 39 | @Override 40 | public Loader onCreateLoader(int id, Bundle args) { 41 | return presenterLoaderProvider.get(); 42 | } 43 | 44 | @Override 45 | public void onLoadFinished(Loader loader, T presenter) { 46 | onPresenterProvided(presenter); 47 | } 48 | 49 | @CallSuper 50 | protected void onPresenterProvided(T presenter) { 51 | this.presenter = presenter; 52 | } 53 | 54 | @Override 55 | public void onLoaderReset(Loader loader) { 56 | onPresenterDestroyed(); 57 | } 58 | 59 | @CallSuper 60 | protected void onPresenterDestroyed() { 61 | presenter = null; 62 | } 63 | 64 | @Override 65 | public void onResume() { 66 | super.onResume(); 67 | presenter.onViewAttached(getPresenterView()); 68 | } 69 | 70 | // Override if Fragment does not implement Presenter interface 71 | protected V getPresenterView() { 72 | //noinspection unchecked 73 | return (V) this; 74 | } 75 | 76 | @Override 77 | public void onPause() { 78 | super.onPause(); 79 | presenter.onViewDetached(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/map/MapPresenter.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.map; 2 | 3 | import android.Manifest; 4 | import android.support.annotation.RequiresPermission; 5 | 6 | import javax.inject.Inject; 7 | 8 | import io.github.plastix.forage.data.local.DatabaseInteractor; 9 | import io.github.plastix.forage.data.location.LocationInteractor; 10 | import io.github.plastix.forage.ui.base.RxPresenter; 11 | import io.github.plastix.rxdelay.RxDelay; 12 | import timber.log.Timber; 13 | 14 | public class MapPresenter extends RxPresenter { 15 | 16 | private DatabaseInteractor databaseInteractor; 17 | private LocationInteractor locationInteractor; 18 | 19 | @Inject 20 | public MapPresenter(DatabaseInteractor databaseInteractor, 21 | LocationInteractor locationInteractor) { 22 | this.databaseInteractor = databaseInteractor; 23 | this.locationInteractor = locationInteractor; 24 | } 25 | 26 | public void setupMap() { 27 | addSubscription( 28 | databaseInteractor.getGeocaches() 29 | .compose(RxDelay.delaySingle(getViewState())) 30 | .subscribe(caches -> { 31 | if (isViewAttached()) { 32 | view.addMapMarkers(caches); 33 | } 34 | }, throwable -> { 35 | // TODO Dialog 36 | }) 37 | ); 38 | 39 | } 40 | 41 | @RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) 42 | void centerMapOnLocation() { 43 | addSubscription( 44 | locationInteractor.isLocationAvailable() 45 | .andThen(locationInteractor.getUpdatedLocation()) 46 | .compose(RxDelay.delaySingle(getViewState())) 47 | .subscribe(location -> { 48 | if (isViewAttached()) { 49 | view.animateMapCamera(location); 50 | } 51 | }, throwable -> Timber.e(throwable, "Error fetching location!") 52 | 53 | ) 54 | ); 55 | } 56 | 57 | @Override 58 | public void onDestroyed() { 59 | databaseInteractor.onDestroy(); 60 | databaseInteractor = null; 61 | locationInteractor = null; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/data/api/HostSelectionInterceptorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import java.io.IOException; 8 | 9 | import okhttp3.Call; 10 | import okhttp3.OkHttpClient; 11 | import okhttp3.Request; 12 | import okhttp3.Response; 13 | import okhttp3.mockwebserver.MockResponse; 14 | import okhttp3.mockwebserver.MockWebServer; 15 | 16 | import static com.google.common.truth.Truth.assertThat; 17 | 18 | public class HostSelectionInterceptorTest { 19 | 20 | private MockWebServer mockWebServer; 21 | private HostSelectionInterceptor interceptor; 22 | private OkHttpClient okHttpClient; 23 | private String hostURL; 24 | 25 | @Before 26 | public void beforeEachTest() throws IOException { 27 | mockWebServer = new MockWebServer(); 28 | mockWebServer.enqueue(new MockResponse().setBody("Hello!")); 29 | mockWebServer.start(); 30 | hostURL = mockWebServer.url("").toString(); 31 | interceptor = new HostSelectionInterceptor(); 32 | okHttpClient = new OkHttpClient.Builder() 33 | .addInterceptor(interceptor) 34 | .build(); 35 | } 36 | 37 | @After 38 | public void afterEachTest() throws IOException { 39 | mockWebServer.shutdown(); 40 | } 41 | 42 | @Test 43 | public void responseShouldBeIntercepted() throws IOException { 44 | String interceptURL = mockWebServer.url("/intercept/").toString(); 45 | interceptor.setHost(interceptURL); 46 | 47 | Request request = new Request.Builder() 48 | .url(hostURL) 49 | .build(); 50 | Call call = okHttpClient.newCall(request); 51 | 52 | // Execute the call synchronously 53 | Response response = call.execute(); 54 | assertThat(response.request().url().toString()).isEqualTo(interceptURL); 55 | } 56 | 57 | @Test 58 | public void responseShouldNotBeIntercepted() throws IOException { 59 | interceptor.setHost(null); 60 | 61 | Request request = new Request.Builder() 62 | .url(hostURL) 63 | .build(); 64 | Call call = okHttpClient.newCall(request); 65 | 66 | // Execute the call synchronously 67 | Response response = call.execute(); 68 | assertThat(response.request().url().toString()).isEqualTo(hostURL); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/plastix/forage/rules/MockWebServerRule.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.rules; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import org.junit.rules.TestRule; 6 | import org.junit.runner.Description; 7 | import org.junit.runners.model.Statement; 8 | 9 | import java.lang.reflect.Method; 10 | 11 | import io.github.plastix.forage.data.api.HostSelectionInterceptor; 12 | import io.github.plastix.forage.util.TestUtils; 13 | import okhttp3.mockwebserver.MockWebServer; 14 | 15 | /** 16 | * Adapted from https://github.com/artem-zinnatullin/qualitymatters 17 | */ 18 | public class MockWebServerRule implements TestRule { 19 | 20 | @NonNull 21 | private final Object testClassInstance; 22 | 23 | @NonNull 24 | private HostSelectionInterceptor interceptor; 25 | 26 | public MockWebServerRule(@NonNull Object testClassInstance) { 27 | this.testClassInstance = testClassInstance; 28 | interceptor = TestUtils.app().getComponent().hostInteceptor(); 29 | } 30 | 31 | @Override 32 | public Statement apply(final Statement base, final Description description) { 33 | 34 | return new Statement() { 35 | @Override 36 | public void evaluate() throws Throwable { 37 | 38 | final NeedsMockWebServer needsMockWebServer = description.getAnnotation(NeedsMockWebServer.class); 39 | 40 | if (needsMockWebServer != null) { 41 | final MockWebServer mockWebServer = new MockWebServer(); 42 | mockWebServer.start(); 43 | 44 | interceptor.setHost(mockWebServer.url("").toString()); 45 | 46 | if (!needsMockWebServer.setupMethod().isEmpty()) { 47 | final Method setupMethod = testClassInstance.getClass().getDeclaredMethod(needsMockWebServer.setupMethod(), MockWebServer.class); 48 | setupMethod.invoke(testClassInstance, mockWebServer); 49 | } 50 | 51 | // Try to evaluate the test and anyway shutdown the MockWebServer. 52 | try { 53 | base.evaluate(); 54 | } finally { 55 | mockWebServer.shutdown(); 56 | } 57 | } else { 58 | // No need to setup a MockWebServer, just evaluate the test. 59 | base.evaluate(); 60 | } 61 | } 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/base/PresenterActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.base; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.CallSuper; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.app.LoaderManager; 7 | import android.support.v4.content.Loader; 8 | 9 | import javax.inject.Inject; 10 | import javax.inject.Provider; 11 | 12 | 13 | /** 14 | * Base Activity that holds a Presenter and manages its lifecycle using a {@link PresenterLoader}. 15 | * See {@link PresenterFragment} if you want similar functionality but with a Fragment instead of an 16 | * activity. 17 | * 18 | * @param Type of Presenter. 19 | */ 20 | public abstract class PresenterActivity, V> extends BaseActivity 21 | implements LoaderManager.LoaderCallbacks { 22 | 23 | // Internal ID to reference the Loader from the LoaderManager 24 | private static final int LOADER_ID = 1; 25 | protected T presenter; 26 | 27 | @Inject 28 | protected Provider> presenterLoaderProvider; 29 | 30 | @Override 31 | protected void onCreate(@Nullable Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | 34 | initLoader(); 35 | } 36 | 37 | private void initLoader() { 38 | getSupportLoaderManager().initLoader(LOADER_ID, null, this); 39 | } 40 | 41 | @Override 42 | public void onLoadFinished(Loader loader, T presenter) { 43 | onPresenterProvided(presenter); 44 | } 45 | 46 | @CallSuper 47 | protected void onPresenterProvided(T presenter) { 48 | this.presenter = presenter; 49 | } 50 | 51 | @Override 52 | public void onLoaderReset(Loader loader) { 53 | onPresenterDestroyed(); 54 | } 55 | 56 | @CallSuper 57 | protected void onPresenterDestroyed() { 58 | presenter = null; 59 | } 60 | 61 | @Override 62 | public Loader onCreateLoader(int id, Bundle args) { 63 | return presenterLoaderProvider.get(); 64 | } 65 | 66 | @Override 67 | protected void onStart() { 68 | super.onStart(); 69 | presenter.onViewAttached(getPresenterView()); 70 | } 71 | 72 | // Override if Activity does not implement Presenter interface 73 | protected V getPresenterView() { 74 | //noinspection unchecked 75 | return (V) this; 76 | } 77 | 78 | @Override 79 | protected void onStop() { 80 | super.onStop(); 81 | presenter.onViewDetached(); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/src/integrationTests/java/io/github/plastix/forage/data/api/HttpCodes.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import static java.util.Arrays.asList; 9 | import static java.util.Collections.unmodifiableList; 10 | 11 | /** 12 | * From https://github.com/artem-zinnatullin/qualitymatters/blob/master/app/src/integrationTests/ 13 | * java/com/artemzin/qualitymatters/integration_tests/api/HttpCodes.java 14 | */ 15 | public class HttpCodes { 16 | 17 | // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error 18 | private static final List CLIENT_SIDE_ERROR_CODES = unmodifiableList(asList( 19 | 400, 20 | 401, 21 | 402, 22 | 403, 23 | 404, 24 | 405, 25 | 406, 26 | // 408, OkHttp will do silent retry, so either we need to test that separately or be 27 | // able to include it in generic tests somehow. 28 | 409, 29 | 410, 30 | 411, 31 | 412, 32 | 413, 33 | 414, 34 | 415, 35 | 416, 36 | 417, 37 | 418, // I'm teapot. (Really). 38 | 422, 39 | 423, 40 | 424, 41 | 426, 42 | 428, 43 | 429, 44 | 431, 45 | 440 46 | )); 47 | 48 | // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#5xx_Server_Error 49 | private static final List SERVER_SIDE_ERROR_CODES = unmodifiableList(asList( 50 | 500, 51 | 501, 52 | 502, 53 | 503, 54 | 504, 55 | 505, 56 | 506, 57 | 507, 58 | 508, 59 | 509, 60 | 510, 61 | 511 62 | )); 63 | 64 | private static final List CLIENT_AND_SERVER_ERROR_CODES; 65 | 66 | static { 67 | List clientAndServerErrorCodes = new ArrayList<>(CLIENT_SIDE_ERROR_CODES.size() + SERVER_SIDE_ERROR_CODES.size()); 68 | clientAndServerErrorCodes.addAll(CLIENT_SIDE_ERROR_CODES); 69 | clientAndServerErrorCodes.addAll(SERVER_SIDE_ERROR_CODES); 70 | 71 | CLIENT_AND_SERVER_ERROR_CODES = unmodifiableList(clientAndServerErrorCodes); 72 | } 73 | 74 | private HttpCodes() { 75 | throw new IllegalStateException("No instances!"); 76 | } 77 | 78 | @NonNull 79 | public static List clientAndServerSideErrorCodes() { 80 | return CLIENT_AND_SERVER_ERROR_CODES; 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/data/network/NetworkInteractorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.network; 2 | 3 | import android.net.ConnectivityManager; 4 | import android.net.NetworkInfo; 5 | 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | import rx.Completable; 10 | 11 | import static com.google.common.truth.Truth.assertThat; 12 | import static org.mockito.Mockito.mock; 13 | import static org.mockito.Mockito.when; 14 | 15 | public class NetworkInteractorTest { 16 | 17 | private NetworkInteractor networkInteractor; 18 | private ConnectivityManager connectivityManager; 19 | private NetworkInfo networkInfo; 20 | 21 | @Before 22 | public void beforeEachTest() { 23 | networkInfo = mock(NetworkInfo.class); 24 | connectivityManager = mock(ConnectivityManager.class); 25 | networkInteractor = new NetworkInteractor(connectivityManager); 26 | } 27 | 28 | @Test 29 | public void hasInternetConnection_shouldReturnFalseWhenNoNetwork() { 30 | when(connectivityManager.getActiveNetworkInfo()).thenReturn(null); 31 | 32 | assertThat(networkInteractor.hasInternetConnection()).isFalse(); 33 | } 34 | 35 | @Test 36 | public void hasInternetConnection_shouldReturnFalseWhenNotConnected() { 37 | when(networkInfo.isConnectedOrConnecting()).thenReturn(false); 38 | when(connectivityManager.getActiveNetworkInfo()).thenReturn(networkInfo); 39 | 40 | assertThat(networkInteractor.hasInternetConnection()).isFalse(); 41 | } 42 | 43 | @Test 44 | public void hasInternetConnection_shouldReturnTrueWhenConnected() { 45 | when(networkInfo.isConnectedOrConnecting()).thenReturn(true); 46 | when(connectivityManager.getActiveNetworkInfo()).thenReturn(networkInfo); 47 | 48 | assertThat(networkInteractor.hasInternetConnection()).isTrue(); 49 | } 50 | 51 | @Test 52 | public void hasInternetConnectionCompletable_shouldCompleteWhenConnected() { 53 | when(networkInfo.isConnectedOrConnecting()).thenReturn(true); 54 | when(connectivityManager.getActiveNetworkInfo()).thenReturn(networkInfo); 55 | 56 | assertThat(networkInteractor.hasInternetConnectionCompletable()).isEqualTo(Completable.complete()); 57 | } 58 | 59 | @Test 60 | public void hasInternetConnectionCompletable_shouldErrorWhenNotConnected() { 61 | when(networkInfo.isConnectedOrConnecting()).thenReturn(false); 62 | when(connectivityManager.getActiveNetworkInfo()).thenReturn(networkInfo); 63 | 64 | assertThat(networkInteractor.hasInternetConnectionCompletable().get()).isInstanceOf(Throwable.class); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/android,intellij,osx 3 | 4 | ### Android ### 5 | # Built application files 6 | *.apk 7 | *.ap_ 8 | 9 | # Files for the Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | 38 | ### Android Patch ### 39 | gen-external-apklibs 40 | 41 | 42 | ### Intellij ### 43 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 44 | 45 | *.iml 46 | 47 | ## Directory-based project format: 48 | .idea/ 49 | # if you remove the above rule, at least ignore the following: 50 | 51 | # User-specific stuff: 52 | # .idea/workspace.xml 53 | # .idea/tasks.xml 54 | # .idea/dictionaries 55 | # .idea/shelf 56 | 57 | # Sensitive or high-churn files: 58 | # .idea/dataSources.ids 59 | # .idea/dataSources.xml 60 | # .idea/sqlDataSources.xml 61 | # .idea/dynamic.xml 62 | # .idea/uiDesigner.xml 63 | 64 | # Gradle: 65 | # .idea/gradle.xml 66 | # .idea/libraries 67 | 68 | # Mongo Explorer plugin: 69 | # .idea/mongoSettings.xml 70 | 71 | ## File-based project format: 72 | *.ipr 73 | *.iws 74 | 75 | ## Plugin-specific files: 76 | 77 | # IntelliJ 78 | /out/ 79 | 80 | # mpeltonen/sbt-idea plugin 81 | .idea_modules/ 82 | 83 | # JIRA plugin 84 | atlassian-ide-plugin.xml 85 | 86 | # Crashlytics plugin (for Android Studio and IntelliJ) 87 | com_crashlytics_export_strings.xml 88 | crashlytics.properties 89 | crashlytics-build.properties 90 | fabric.properties 91 | 92 | 93 | ### OSX ### 94 | .DS_Store 95 | .AppleDouble 96 | .LSOverride 97 | 98 | # Icon must end with two \r 99 | Icon 100 | 101 | 102 | # Thumbnails 103 | ._* 104 | 105 | # Files that might appear in the root of a volume 106 | .DocumentRevisions-V100 107 | .fseventsd 108 | .Spotlight-V100 109 | .TemporaryItems 110 | .Trashes 111 | .VolumeIcon.icns 112 | 113 | # Directories potentially created on remote AFP share 114 | .AppleDB 115 | .AppleDesktop 116 | Network Trash Folder 117 | Temporary Items 118 | .apdisk 119 | 120 | # Allow anything in the gradle wrapper folder 121 | !gradle/wrapper/* 122 | !gradle/wrapper/gradle-wrapper.jar 123 | !gradle/wrapper/gradle-wrapper.properties 124 | 125 | # Hide signing config from VSC 126 | signing.properties 127 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/base/RxPresenter.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.base; 2 | 3 | import rx.Observable; 4 | import rx.Subscription; 5 | import rx.subjects.BehaviorSubject; 6 | import rx.subscriptions.CompositeSubscription; 7 | 8 | /** 9 | * Presenter that provides support for "pausing" and "resuming" Observables and automatically 10 | * unsubscribing from subscriptions when the presenter is destroyed. 11 | *

12 | * Slightly adapted Code from https://github.com/alapshin/arctor 13 | * 14 | * @param Generic type of view that the presenter interacts with. 15 | */ 16 | public abstract class RxPresenter extends Presenter { 17 | 18 | private CompositeSubscription subscriptions = new CompositeSubscription(); 19 | private BehaviorSubject viewLifecycle = BehaviorSubject.create(); 20 | 21 | @Override 22 | public void onViewAttached(V view) { 23 | super.onViewAttached(view); 24 | viewLifecycle.onNext(true); 25 | } 26 | 27 | @Override 28 | public void onViewDetached() { 29 | super.onViewDetached(); 30 | viewLifecycle.onNext(false); 31 | } 32 | 33 | @Override 34 | public void onDestroyed() { 35 | viewLifecycle.onCompleted(); 36 | clearSubscriptions(); 37 | } 38 | 39 | /** 40 | * Removes and unsubscribes from all subscriptions that have been registered with 41 | * {@link #addSubscription(Subscription)} previously. 42 | * See {@link CompositeSubscription#clear() for details.} 43 | */ 44 | public void clearSubscriptions() { 45 | subscriptions.clear(); 46 | } 47 | 48 | /** 49 | * Registers a subscription to automatically be unsubscribed on onDestroy. 50 | * See {@link CompositeSubscription#add(Subscription) for details.} 51 | * 52 | * @param subscription A Subscription to add. 53 | */ 54 | public void addSubscription(Subscription subscription) { 55 | subscriptions.add(subscription); 56 | } 57 | 58 | /** 59 | * Removes and unsubscribes a subscription that has been previously registered with 60 | * {@link #addSubscription(Subscription)}. 61 | * See {@link CompositeSubscription#remove(Subscription) for details.} 62 | * 63 | * @param subscription a subscription to remove. 64 | */ 65 | public void removeSubscription(Subscription subscription) { 66 | subscriptions.remove(subscription); 67 | } 68 | 69 | /** 70 | * Exposes the state of the attached view as a boolean Observabele. True is emitted when the view 71 | * is attached and false is emitted when the view detatches. 72 | */ 73 | public Observable getViewState() { 74 | return viewLifecycle.asObservable(); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/util/AngleUtilsTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import android.view.Display; 4 | import android.view.Surface; 5 | import android.view.WindowManager; 6 | 7 | import org.junit.Test; 8 | 9 | import static com.google.common.truth.Truth.assertThat; 10 | import static org.mockito.Mockito.mock; 11 | import static org.mockito.Mockito.when; 12 | 13 | public class AngleUtilsTest { 14 | 15 | @Test 16 | public void normalize_doesNotModifyNormalAngles() { 17 | float a = 0; 18 | float b = 360; 19 | float c = 180; 20 | float d = 145.86f; 21 | 22 | assertThat(AngleUtils.normalize(a)).isWithin(0).of(a); 23 | assertThat(AngleUtils.normalize(b)).isWithin(0).of(a); 24 | assertThat(AngleUtils.normalize(c)).isWithin(0).of(c); 25 | assertThat(AngleUtils.normalize(d)).isWithin(0).of(d); 26 | } 27 | 28 | @Test 29 | public void normalize_normalizesPositiveAngles() { 30 | float a = 361; 31 | float b = 456; 32 | float c = 720; 33 | float d = 945.5f; 34 | 35 | assertThat(AngleUtils.normalize(a)).isWithin(0).of(1); 36 | assertThat(AngleUtils.normalize(b)).isWithin(0).of(96); 37 | assertThat(AngleUtils.normalize(c)).isWithin(0).of(0); 38 | assertThat(AngleUtils.normalize(d)).isWithin(0).of(225.5f); 39 | } 40 | 41 | @Test 42 | public void normalize_normalizesNegativeAngles() { 43 | float a = -361; 44 | float b = -456; 45 | float c = -720; 46 | float d = -945.5f; 47 | 48 | assertThat(AngleUtils.normalize(a)).isWithin(0).of(359); 49 | assertThat(AngleUtils.normalize(b)).isWithin(0).of(264); 50 | assertThat(AngleUtils.normalize(c)).isWithin(0).of(0); 51 | assertThat(AngleUtils.normalize(d)).isWithin(0).of(134.5f); 52 | } 53 | 54 | @Test 55 | public void getRotationOffset_returnsCorrectRotation() { 56 | WindowManager windowManager = mock(WindowManager.class); 57 | Display display = mock(Display.class); 58 | when(windowManager.getDefaultDisplay()).thenReturn(display); 59 | 60 | when(display.getRotation()).thenReturn(Surface.ROTATION_90); 61 | assertThat(AngleUtils.getRotationOffset(windowManager)).isEqualTo(90); 62 | 63 | when(display.getRotation()).thenReturn(Surface.ROTATION_180); 64 | assertThat(AngleUtils.getRotationOffset(windowManager)).isEqualTo(180); 65 | 66 | when(display.getRotation()).thenReturn(Surface.ROTATION_270); 67 | assertThat(AngleUtils.getRotationOffset(windowManager)).isEqualTo(270); 68 | 69 | when(display.getRotation()).thenReturn(Surface.ROTATION_0); 70 | assertThat(AngleUtils.getRotationOffset(windowManager)).isEqualTo(0); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/ui/cachedetail/CacheDetailPresenterTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.cachedetail; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.mockito.Mock; 6 | import org.mockito.MockitoAnnotations; 7 | 8 | import io.github.plastix.forage.data.api.auth.OAuthInteractor; 9 | import io.github.plastix.forage.data.local.DatabaseInteractor; 10 | import io.github.plastix.forage.data.local.model.Cache; 11 | import rx.Single; 12 | 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.only; 15 | import static org.mockito.Mockito.verify; 16 | import static org.mockito.Mockito.when; 17 | 18 | public class CacheDetailPresenterTest { 19 | 20 | private CacheDetailPresenter cacheDetailPresenter; 21 | 22 | @Mock 23 | private DatabaseInteractor databaseInteractor; 24 | 25 | @Mock 26 | private OAuthInteractor oAuthInteractor; 27 | 28 | @Mock 29 | private CacheDetailView view; 30 | 31 | @Before 32 | public void beforeEachTest() { 33 | MockitoAnnotations.initMocks(this); 34 | cacheDetailPresenter = new CacheDetailPresenter(databaseInteractor, oAuthInteractor); 35 | cacheDetailPresenter.onViewAttached(view); 36 | } 37 | 38 | @Test 39 | public void onDestroy_shouldCallDatabaseInteractorDestroy() { 40 | cacheDetailPresenter.onDestroyed(); 41 | verify(databaseInteractor, only()).onDestroy(); 42 | } 43 | 44 | @Test 45 | public void getGeocache_shouldCallViewReturnedGeocache() { 46 | Cache cache = mock(Cache.class); 47 | String cacheCode = "geocache code"; 48 | when(databaseInteractor.getGeocacheCopy(cacheCode)).thenReturn(Single.just(cache)); 49 | 50 | cacheDetailPresenter.getGeocache(cacheCode); 51 | 52 | verify(view, only()).returnedGeocache(cache); 53 | } 54 | 55 | @Test 56 | public void getGeocache_shouldCallViewError() { 57 | String cacheCode = "geocache code"; 58 | when(databaseInteractor.getGeocacheCopy(cacheCode)) 59 | .thenReturn(Single.error(new Throwable("Error"))); 60 | 61 | cacheDetailPresenter.getGeocache(cacheCode); 62 | 63 | verify(view, only()).onError(); 64 | } 65 | 66 | @Test 67 | public void openLogScreen_shouldOpenActivity() { 68 | when(oAuthInteractor.hasSavedOAuthTokens()).thenReturn(true); 69 | 70 | cacheDetailPresenter.openLogScreen(); 71 | 72 | verify(view, only()).openLogScreen(); 73 | } 74 | 75 | @Test 76 | public void openLogScreen_shouldShowError() { 77 | when(oAuthInteractor.hasSavedOAuthTokens()).thenReturn(false); 78 | 79 | cacheDetailPresenter.openLogScreen(); 80 | 81 | verify(view, only()).onErrorRequiresLogin(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/sensor/AzimuthAsyncEmitter.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.sensor; 2 | 3 | import android.hardware.Sensor; 4 | import android.hardware.SensorEvent; 5 | import android.hardware.SensorEventListener; 6 | import android.hardware.SensorManager; 7 | import android.support.annotation.NonNull; 8 | import android.view.WindowManager; 9 | 10 | import javax.inject.Inject; 11 | 12 | import io.github.plastix.forage.util.AngleUtils; 13 | import rx.AsyncEmitter; 14 | import rx.functions.Action1; 15 | 16 | public class AzimuthAsyncEmitter implements Action1> { 17 | 18 | private SensorManager sensorManager; 19 | private WindowManager windowManager; 20 | private Sensor compass; 21 | private float[] orientation; 22 | private float[] rMat; 23 | 24 | @Inject 25 | public AzimuthAsyncEmitter(@NonNull SensorManager sensorManager, 26 | @NonNull WindowManager windowManager) { 27 | this.sensorManager = sensorManager; 28 | this.windowManager = windowManager; 29 | this.orientation = new float[3]; 30 | this.rMat = new float[9]; 31 | this.compass = null; 32 | } 33 | 34 | 35 | @Override 36 | public void call(AsyncEmitter emitter) { 37 | compass = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); 38 | 39 | if (compass != null) { 40 | SensorEventListener sensorEventListener = new SensorEventListener() { 41 | @Override 42 | public void onSensorChanged(SensorEvent event) { 43 | 44 | if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) { 45 | // Calculate the rotation matrix 46 | SensorManager.getRotationMatrixFromVector(rMat, event.values); 47 | 48 | // Compass direction is result[0] 49 | float[] result = SensorManager.getOrientation(rMat, orientation); 50 | float azimuth = (float) (Math.toDegrees(result[0])); 51 | azimuth += AngleUtils.getRotationOffset(windowManager); 52 | 53 | emitter.onNext(azimuth); 54 | } 55 | } 56 | 57 | @Override 58 | public void onAccuracyChanged(Sensor sensor, int i) { 59 | // No op 60 | } 61 | }; 62 | 63 | sensorManager.registerListener(sensorEventListener, compass, SensorManager.SENSOR_DELAY_GAME); 64 | 65 | emitter.setCancellation(() -> sensorManager.unregisterListener(sensorEventListener, compass)); 66 | } else { 67 | emitter.onError(new SensorUnavailableException("Compass Sensor not available!")); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/misc/PermissionRationaleDialog.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.misc; 2 | 3 | 4 | import android.app.Dialog; 5 | import android.os.Bundle; 6 | import android.support.annotation.NonNull; 7 | import android.support.annotation.Nullable; 8 | import android.support.annotation.StringRes; 9 | import android.support.v4.app.DialogFragment; 10 | import android.support.v4.app.FragmentActivity; 11 | import android.support.v4.app.FragmentManager; 12 | import android.support.v4.app.FragmentTransaction; 13 | 14 | import com.afollestad.materialdialogs.MaterialDialog; 15 | 16 | import io.github.plastix.forage.R; 17 | import io.github.plastix.forage.util.ActivityUtils; 18 | 19 | public class PermissionRationaleDialog extends DialogFragment { 20 | 21 | private static final String ID = "PermissionRationaleDialog"; 22 | private static final String KEY_CONTENT_ID = "ContentID"; 23 | 24 | @StringRes 25 | private int contentId; 26 | 27 | public static void show(FragmentActivity context, @StringRes int contentId) { 28 | FragmentManager fragmentManager = context.getSupportFragmentManager(); 29 | 30 | if (fragmentManager.findFragmentByTag(ID) == null) { 31 | PermissionRationaleDialog dialog = newInstance(contentId); 32 | FragmentTransaction transaction = fragmentManager.beginTransaction(); 33 | transaction.add(dialog, ID); 34 | transaction.commitAllowingStateLoss(); 35 | } 36 | } 37 | 38 | public static PermissionRationaleDialog newInstance(@StringRes int contentId) { 39 | PermissionRationaleDialog dialog = new PermissionRationaleDialog(); 40 | dialog.setCancelable(false); 41 | 42 | Bundle args = new Bundle(); 43 | args.putInt(KEY_CONTENT_ID, contentId); 44 | dialog.setArguments(args); 45 | 46 | return dialog; 47 | } 48 | 49 | @Override 50 | public void onCreate(@Nullable Bundle savedInstanceState) { 51 | super.onCreate(savedInstanceState); 52 | contentId = getArguments().getInt(KEY_CONTENT_ID); 53 | } 54 | 55 | @NonNull 56 | @Override 57 | public Dialog onCreateDialog(Bundle savedInstanceState) { 58 | return new MaterialDialog.Builder(getActivity()) 59 | .title(R.string.permission_dialog_permission_missing) 60 | .content(contentId) 61 | .positiveText(R.string.permission_dialog_settings) 62 | .onPositive((dialog1, which) -> 63 | startActivity(ActivityUtils.getApplicationSettingsIntent(getActivity()))) 64 | .negativeText(R.string.permission_dialog_exit) 65 | .onNegative((dialog1, which) -> getActivity().finish()) 66 | .cancelable(false) 67 | .build(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/api/OkApiInteractor.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import com.google.gson.JsonObject; 6 | 7 | import java.util.List; 8 | 9 | import javax.inject.Inject; 10 | 11 | import dagger.Lazy; 12 | import io.github.plastix.forage.BuildConfig; 13 | import io.github.plastix.forage.data.api.response.SubmitLogResponse; 14 | import io.github.plastix.forage.data.local.model.Cache; 15 | import io.github.plastix.forage.util.RxUtils; 16 | import io.github.plastix.forage.util.StringUtils; 17 | import io.github.plastix.forage.util.UnitUtils; 18 | import rx.Single; 19 | 20 | /** 21 | * A Reactive wrapper around {@link OkApiService}. 22 | */ 23 | public class OkApiInteractor { 24 | 25 | private Lazy apiService; 26 | private String[] geocacheFields = {"code", "name", "location", "type", "status", "terrain", 27 | "difficulty", "size2", "description"}; 28 | 29 | @Inject 30 | public OkApiInteractor(@NonNull Lazy apiService) { 31 | this.apiService = apiService; 32 | } 33 | 34 | /** 35 | * Gets a JSON array of Geocaches near the specified Location from {@link OkApiService}. 36 | * 37 | * @param lat Latitude of specified location. 38 | * @param lon Longitude of specified location. 39 | * @param radius Boundary radius. 40 | * @return A rx.Single JsonArray. 41 | */ 42 | public Single> getNearbyCaches(double lat, double lon, double radius) { 43 | 44 | JsonObject searchParams = new JsonObject(); 45 | searchParams.addProperty("center", String.format("%s|%s", lat, lon)); 46 | searchParams.addProperty("radius", UnitUtils.milesToKilometer(radius)); 47 | 48 | JsonObject returnParams = new JsonObject(); 49 | returnParams.addProperty("fields", StringUtils.join("|", geocacheFields)); 50 | 51 | return apiService.get().searchAndRetrieve( 52 | ApiConstants.ENDPOINT_NEAREST, 53 | searchParams.toString(), 54 | ApiConstants.ENDPOINT_GEOCACHES, 55 | returnParams.toString(), 56 | false, 57 | BuildConfig.OKAPI_US_CONSUMER_KEY 58 | ) 59 | .compose(RxUtils.>subscribeOnIoThreadTransformerSingle()) 60 | .compose(RxUtils.>observeOnUIThreadTransformerSingle()); 61 | } 62 | 63 | public Single submitLog(String cacheCode, String type, String comment) { 64 | return apiService.get() 65 | .submitLog(cacheCode, type, comment) 66 | .compose(RxUtils.subscribeOnIoThreadTransformerSingle()) 67 | .compose(RxUtils.observeOnUIThreadTransformerSingle()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/data/api/gson/RealmLocationAdapterTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.api.gson; 2 | 3 | import com.google.gson.stream.JsonReader; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import io.github.plastix.forage.data.local.model.RealmLocation; 9 | 10 | import static com.google.common.truth.Truth.assertThat; 11 | import static org.mockito.Mockito.mock; 12 | import static org.mockito.Mockito.when; 13 | 14 | public class RealmLocationAdapterTest { 15 | 16 | private RealmLocationAdapter realmLocationAdapter; 17 | private JsonReader jsonReader; 18 | 19 | @Before 20 | public void beforeEachTest() { 21 | realmLocationAdapter = new RealmLocationAdapter(); 22 | jsonReader = mock(JsonReader.class); 23 | } 24 | 25 | @Test 26 | public void read_shouldReturnRealmLocation() throws Exception { 27 | String input = "0|0"; 28 | when(jsonReader.nextString()).thenReturn(input); 29 | 30 | assertThat(realmLocationAdapter.read(jsonReader)).isInstanceOf(RealmLocation.class); 31 | } 32 | 33 | @Test 34 | public void read_shouldReturnCorrectData() throws Exception { 35 | double lat = 43; 36 | double lon = 72; 37 | String input = lat + "|" + lon; 38 | when(jsonReader.nextString()).thenReturn(input); 39 | 40 | RealmLocation location = realmLocationAdapter.read(jsonReader); 41 | assertThat(location.latitude).isWithin(0).of(lat); 42 | assertThat(location.longitude).isWithin(0).of(lon); 43 | } 44 | 45 | @Test 46 | public void read_shouldReturnNullOnEmptyString() throws Exception { 47 | when(jsonReader.nextString()).thenReturn(""); 48 | assertThat(realmLocationAdapter.read(jsonReader)).isNull(); 49 | } 50 | 51 | @Test 52 | public void read_shouldReturnNullOnNullString() throws Exception { 53 | when(jsonReader.nextString()).thenReturn(null); 54 | assertThat(realmLocationAdapter.read(jsonReader)).isNull(); 55 | } 56 | 57 | @Test 58 | public void read_shouldReturnNullInvalidString() throws Exception { 59 | when(jsonReader.nextString()).thenReturn(" "); 60 | assertThat(realmLocationAdapter.read(jsonReader)).isNull(); 61 | 62 | when(jsonReader.nextString()).thenReturn("regular string"); 63 | assertThat(realmLocationAdapter.read(jsonReader)).isNull(); 64 | 65 | when(jsonReader.nextString()).thenReturn("hello|world"); 66 | assertThat(realmLocationAdapter.read(jsonReader)).isNull(); 67 | 68 | when(jsonReader.nextString()).thenReturn("1234"); 69 | assertThat(realmLocationAdapter.read(jsonReader)).isNull(); 70 | 71 | when(jsonReader.nextString()).thenReturn("42|world"); 72 | assertThat(realmLocationAdapter.read(jsonReader)).isNull(); 73 | 74 | when(jsonReader.nextString()).thenReturn("hello|42"); 75 | assertThat(realmLocationAdapter.read(jsonReader)).isNull(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/ui/navigate/NavigatePresenterTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.navigate; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.mockito.Mock; 6 | import org.mockito.MockitoAnnotations; 7 | 8 | import static org.mockito.Matchers.anyDouble; 9 | import static org.mockito.Mockito.never; 10 | import static org.mockito.Mockito.times; 11 | import static org.mockito.Mockito.verify; 12 | import static org.mockito.Mockito.verifyZeroInteractions; 13 | 14 | public class NavigatePresenterTest { 15 | 16 | private NavigatePresenter navigatePresenter; 17 | 18 | @Mock 19 | private NavigateView navigateView; 20 | 21 | @Before 22 | public void beforeEachTest() { 23 | MockitoAnnotations.initMocks(this); 24 | navigatePresenter = new NavigatePresenter(); 25 | navigatePresenter.onViewAttached(navigateView); 26 | } 27 | 28 | @Test 29 | public void navigate_invalidLatitudeShouldOnlyCallErrorLatitude() { 30 | String lat = "latitude"; 31 | String lon = "0"; 32 | navigatePresenter.navigate(lat, lon); 33 | 34 | verify(navigateView, times(1)).errorParsingLatitude(); 35 | verify(navigateView, never()).errorParsingLongitude(); 36 | verify(navigateView, never()).openCompassScreen(anyDouble(), anyDouble()); 37 | } 38 | 39 | @Test 40 | public void navigate_invalidLongitudeShouldOnlyCallErrorLongitude() { 41 | String lat = "0"; 42 | String lon = "longitude"; 43 | navigatePresenter.navigate(lat, lon); 44 | 45 | verify(navigateView, never()).errorParsingLatitude(); 46 | verify(navigateView, times(1)).errorParsingLongitude(); 47 | verify(navigateView, never()).openCompassScreen(anyDouble(), anyDouble()); 48 | } 49 | 50 | 51 | @Test 52 | public void navigate_invalidLocationShouldErrorBoth() { 53 | String lat = "latitude"; 54 | String lon = "longitude"; 55 | navigatePresenter.navigate(lat, lon); 56 | 57 | verify(navigateView, times(1)).errorParsingLatitude(); 58 | verify(navigateView, times(1)).errorParsingLongitude(); 59 | verify(navigateView, never()).openCompassScreen(anyDouble(), anyDouble()); 60 | } 61 | 62 | @Test 63 | public void navigate_validDataShouldOpenCompassScreen() { 64 | String lat = "0"; 65 | String lon = "0"; 66 | navigatePresenter.navigate(lat, lon); 67 | 68 | verify(navigateView, never()).errorParsingLatitude(); 69 | verify(navigateView, never()).errorParsingLongitude(); 70 | verify(navigateView, times(1)).openCompassScreen(Double.valueOf(lat), Double.valueOf(lon)); 71 | } 72 | 73 | @Test 74 | public void navigate_noViewAttached() { 75 | navigatePresenter.onViewDetached(); 76 | String lat = "latitude"; 77 | String lon = "longitude"; 78 | navigatePresenter.navigate(lat, lon); 79 | 80 | verifyZeroInteractions(navigateView); 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/unitTests/java/io/github/plastix/forage/util/RxUtilsTest.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.mockito.Mock; 6 | import org.mockito.MockitoAnnotations; 7 | 8 | import rx.Observable; 9 | import rx.Subscription; 10 | import rx.functions.Action1; 11 | import rx.observers.TestSubscriber; 12 | 13 | import static com.google.common.truth.Truth.assert_; 14 | import static org.mockito.Matchers.anyInt; 15 | import static org.mockito.Mockito.mock; 16 | import static org.mockito.Mockito.never; 17 | import static org.mockito.Mockito.times; 18 | import static org.mockito.Mockito.verify; 19 | import static org.mockito.Mockito.when; 20 | 21 | public class RxUtilsTest { 22 | 23 | TestSubscriber testSubscriber; 24 | 25 | @Mock 26 | Action1 func; 27 | 28 | @Before 29 | public void setUp() { 30 | MockitoAnnotations.initMocks(this); 31 | testSubscriber = new TestSubscriber<>(); 32 | } 33 | 34 | @Test 35 | public void safeUnsubscribe_handleNullSubscription() { 36 | try { 37 | RxUtils.safeUnsubscribe(null); 38 | } catch (Exception e) { 39 | assert_().fail("RxUtils safeUnsubscribe threw an unexpected error!", e); 40 | } 41 | } 42 | 43 | @Test 44 | public void safeUnsubscribe_unsubscribeSubscriptionCorrectly() { 45 | Subscription subscription = mock(Subscription.class); 46 | 47 | RxUtils.safeUnsubscribe(subscription); 48 | 49 | verify(subscription, times(1)).unsubscribe(); 50 | } 51 | 52 | @Test 53 | public void safeUnsubscribe_onlyUnsubscribeActiveSubscriptions() { 54 | Subscription subscription = mock(Subscription.class); 55 | when(subscription.isUnsubscribed()).thenReturn(true); 56 | 57 | RxUtils.safeUnsubscribe(subscription); 58 | 59 | verify(subscription, never()).unsubscribe(); 60 | } 61 | 62 | @Test 63 | public void doOnFirst_singleEmissionCallsFunction() { 64 | Observable.just(0) 65 | .compose(RxUtils.doOnFirst(func)) 66 | .subscribe(testSubscriber); 67 | 68 | verify(func, times(1)).call(0); 69 | } 70 | 71 | @Test 72 | public void doOnFirst_doubleEmissionCallsFunction() { 73 | Observable.just(0,1) 74 | .compose(RxUtils.doOnFirst(func)) 75 | .subscribe(testSubscriber); 76 | 77 | verify(func, times(1)).call(0); 78 | 79 | } 80 | 81 | @Test 82 | public void doOnFirst_emptyCallsNothing() { 83 | Observable.empty() 84 | .compose(RxUtils.doOnFirst(func)) 85 | .subscribe(testSubscriber); 86 | 87 | verify(func, never()).call(anyInt()); 88 | } 89 | 90 | @Test 91 | public void doOnFirst_neverCallsNothing() { 92 | Observable.never() 93 | .compose(RxUtils.doOnFirst(func)) 94 | .subscribe(testSubscriber); 95 | 96 | verify(func, never()).call(anyInt()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/data/location/LocationAsyncEmitter.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.data.location; 2 | 3 | import android.location.Location; 4 | import android.os.Bundle; 5 | import android.support.annotation.NonNull; 6 | import android.support.annotation.Nullable; 7 | 8 | import com.google.android.gms.common.api.GoogleApiClient; 9 | import com.google.android.gms.location.LocationListener; 10 | import com.google.android.gms.location.LocationRequest; 11 | import com.google.android.gms.location.LocationServices; 12 | 13 | import javax.inject.Inject; 14 | 15 | import rx.AsyncEmitter; 16 | import rx.functions.Action1; 17 | 18 | public class LocationAsyncEmitter implements Action1> { 19 | 20 | private final GoogleApiClient googleApiClient; 21 | private LocationRequest locationRequest; 22 | 23 | @Inject 24 | public LocationAsyncEmitter(@NonNull GoogleApiClient googleApiClient) { 25 | this.googleApiClient = googleApiClient; 26 | this.locationRequest = null; 27 | } 28 | 29 | public void setLocationRequest(LocationRequest locationRequest) { 30 | this.locationRequest = locationRequest; 31 | } 32 | 33 | @Override 34 | public void call(AsyncEmitter locationAsyncEmitter) { 35 | 36 | LocationListener locationListener = locationAsyncEmitter::onNext; 37 | 38 | GoogleApiClient.OnConnectionFailedListener onConnectionFailedListener = connectionResult -> 39 | locationAsyncEmitter.onError(new LocationUnavailableException("Failed to connect to Google Play Services!")); 40 | 41 | GoogleApiClient.ConnectionCallbacks connectionCallbacks = new GoogleApiClient.ConnectionCallbacks() { 42 | @Override 43 | public void onConnected(@Nullable Bundle bundle) { 44 | try { 45 | LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, locationListener); 46 | } catch (SecurityException e) { 47 | locationAsyncEmitter.onError(new LocationUnavailableException("Location permission not available!")); 48 | } 49 | } 50 | 51 | @Override 52 | public void onConnectionSuspended(int i) { 53 | locationAsyncEmitter.onError(new LocationUnavailableException("Connection lost to Google Play Services")); 54 | 55 | } 56 | }; 57 | 58 | googleApiClient.registerConnectionCallbacks(connectionCallbacks); 59 | googleApiClient.registerConnectionFailedListener(onConnectionFailedListener); 60 | googleApiClient.connect(); 61 | 62 | locationAsyncEmitter.setCancellation(() -> { 63 | LocationAsyncEmitter.this.locationRequest = null; 64 | LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, locationListener); 65 | googleApiClient.unregisterConnectionCallbacks(connectionCallbacks); 66 | googleApiClient.unregisterConnectionFailedListener(onConnectionFailedListener); 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_compass.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 22 | 23 | 34 | 35 | 46 | 47 | 55 | 56 | 62 | 63 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/util/StringUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import android.content.res.Resources; 4 | import android.support.annotation.NonNull; 5 | import android.text.Html; 6 | 7 | import java.util.Locale; 8 | 9 | import io.github.plastix.forage.R; 10 | 11 | public class StringUtils { 12 | 13 | private StringUtils() { 14 | throw new UnsupportedOperationException("No Instantiation!"); 15 | } 16 | 17 | /** 18 | * Joins an array of strings into a single string separated by the specified delimiter. 19 | * 20 | * @param delimiter String in between joined elements. 21 | * @param elements Elements to join together. 22 | * @return Single joined string. 23 | */ 24 | @NonNull 25 | public static String join(@NonNull String delimiter, @NonNull String[] elements) { 26 | StringBuilder builder = new StringBuilder(); 27 | String separator = ""; 28 | for (String element : elements) { 29 | builder.append(separator).append(element); 30 | separator = delimiter; 31 | } 32 | 33 | return builder.toString(); 34 | } 35 | 36 | /** 37 | * Parses HTML into a Plain text string 38 | * 39 | * @param input String in HTML form 40 | * @return Plain text string 41 | *

42 | * Based on http://stackoverflow.com/questions/8560045/android-getting-obj-using-textview-settextcharactersequence 43 | */ 44 | @NonNull 45 | public static String stripHtml(@NonNull String input) { 46 | return Html.fromHtml(input).toString() 47 | .replace((char) 160, (char) 32).replace((char) 65532, (char) 32).trim(); 48 | } 49 | 50 | /** 51 | * Capitalizes the first character of the given string. 52 | * 53 | * @param input String to capitalize. 54 | * @return Capitalized string. 55 | */ 56 | public static String capitalize(@NonNull String input) { 57 | // Don't process empty strings 58 | if (input.length() == 0) { 59 | return input; 60 | } 61 | 62 | return input.substring(0, 1).toUpperCase(Locale.getDefault()) + input.substring(1); 63 | } 64 | 65 | /** 66 | * Turns the distance in miles to a human-readable String. For example: 67 | *

68 | * 5.2 -> 5.2 mi 69 | * 0.3 -> 5280 ft 70 | * 71 | * @param resources Resources object to fetch the string resources. 72 | * @param miles Miles to convert. 73 | * @return Human readable string. 74 | */ 75 | @NonNull 76 | public static String humanReadableImperialDistance(@NonNull Resources resources, double miles) { 77 | String distance; 78 | String units; 79 | 80 | if (miles > 1) { 81 | distance = String.format(Locale.getDefault(), "%.2f", miles); 82 | units = resources.getString(R.string.unit_miles); 83 | } else { 84 | distance = String.format(Locale.getDefault(), "%.2f", UnitUtils.milesToFeet(miles)); 85 | units = resources.getString(R.string.unit_feet); 86 | } 87 | 88 | return distance + " " + units; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/cachelist/CacheAdapter.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.cachelist; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.res.Resources; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.TextView; 11 | 12 | import butterknife.BindView; 13 | import butterknife.ButterKnife; 14 | import io.github.plastix.forage.R; 15 | import io.github.plastix.forage.data.local.model.Cache; 16 | import io.github.plastix.forage.ui.cachedetail.CacheDetailActivity; 17 | import io.realm.OrderedRealmCollection; 18 | import io.realm.RealmRecyclerViewAdapter; 19 | 20 | /** 21 | * RecyclerView adapter to get {@link Cache}s from Realm and display them. 22 | */ 23 | public class CacheAdapter extends RealmRecyclerViewAdapter { 24 | 25 | public CacheAdapter(Context context, OrderedRealmCollection data, boolean autoUpdate) { 26 | super(context, data, autoUpdate); 27 | } 28 | 29 | @Override 30 | public CacheHolder onCreateViewHolder(ViewGroup parent, int viewType) { 31 | LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 32 | View cacheView = inflater.inflate(R.layout.cache_item, parent, false); 33 | 34 | return new CacheHolder(cacheView); 35 | } 36 | 37 | @Override 38 | public void onBindViewHolder(final CacheHolder holder, int position) { 39 | Cache cache = getItem(position); 40 | Resources resources = holder.itemView.getContext().getResources(); 41 | 42 | holder.cacheName.setText(cache.name); 43 | holder.cacheType.setText(resources.getString(R.string.cacheitem_type, cache.type)); 44 | holder.cacheTerrain.setText(String.valueOf(cache.terrain)); 45 | holder.cacheDifficulty.setText(String.valueOf(cache.difficulty)); 46 | holder.cacheSize.setText(cache.size); 47 | } 48 | 49 | 50 | public class CacheHolder extends RecyclerView.ViewHolder implements View.OnClickListener { 51 | 52 | @BindView(R.id.cache_name) 53 | TextView cacheName; 54 | 55 | @BindView(R.id.cache_terrain) 56 | TextView cacheTerrain; 57 | 58 | @BindView(R.id.cache_difficulty) 59 | TextView cacheDifficulty; 60 | 61 | @BindView(R.id.cache_size) 62 | TextView cacheSize; 63 | 64 | @BindView(R.id.cache_type) 65 | TextView cacheType; 66 | 67 | public CacheHolder(View itemView) { 68 | super(itemView); 69 | itemView.setOnClickListener(this); 70 | ButterKnife.bind(this, itemView); 71 | } 72 | 73 | @Override 74 | public void onClick(View v) { 75 | CacheAdapter adapter = CacheAdapter.this; 76 | Context context = adapter.context; 77 | 78 | Cache item = getItem(getLayoutPosition()); 79 | if (item != null) { 80 | 81 | Intent intent = CacheDetailActivity.newIntent(context, item.cacheCode); 82 | context.startActivity(intent); 83 | } 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/ui/navigate/NavigateActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.ui.navigate; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.location.Location; 6 | import android.os.Bundle; 7 | import android.support.design.widget.TextInputLayout; 8 | import android.support.v7.widget.Toolbar; 9 | import android.widget.EditText; 10 | 11 | import butterknife.BindView; 12 | import butterknife.OnClick; 13 | import io.github.plastix.forage.ApplicationComponent; 14 | import io.github.plastix.forage.R; 15 | import io.github.plastix.forage.ui.base.PresenterActivity; 16 | import io.github.plastix.forage.ui.compass.CompassActivity; 17 | import io.github.plastix.forage.util.ActivityUtils; 18 | import io.github.plastix.forage.util.LocationUtils; 19 | 20 | public class NavigateActivity extends PresenterActivity implements NavigateView { 21 | 22 | @BindView(R.id.navigate_toolbar) 23 | Toolbar toolbar; 24 | 25 | @BindView(R.id.navigate_latitude) 26 | EditText latitude; 27 | 28 | @BindView(R.id.navigate_longitude) 29 | EditText longitude; 30 | 31 | @BindView(R.id.navigate_latitude_text_input_layout) 32 | TextInputLayout latitudeInputLayout; 33 | 34 | @BindView(R.id.navigate_longitude_text_input_layout) 35 | TextInputLayout longitudeInputLayout; 36 | 37 | /** 38 | * Returns a new intent that opens the NavigateActivity. 39 | * 40 | * @param context Current context. 41 | * @return New intent object. 42 | */ 43 | public static Intent newIntent(Context context) { 44 | return new Intent(context, NavigateActivity.class); 45 | } 46 | 47 | @Override 48 | protected void onCreate(Bundle savedInstanceState) { 49 | super.onCreate(savedInstanceState); 50 | setContentView(R.layout.activity_navigate); 51 | setSupportActionBar(toolbar); 52 | ActivityUtils.setSupportActionBarBack(getDelegate()); 53 | } 54 | 55 | @Override 56 | protected void injectDependencies(ApplicationComponent component) { 57 | component.plus(new NavigateModule(this)) 58 | .injectTo(this); 59 | } 60 | 61 | @OnClick(R.id.navigate_button) 62 | public void onNavigate() { 63 | String lat = latitude.getText().toString(); 64 | String lon = longitude.getText().toString(); 65 | presenter.navigate(lat, lon); 66 | } 67 | 68 | @Override 69 | public void errorParsingLatitude() { 70 | latitudeInputLayout.setError(getString(R.string.navigate_invalid_latitude)); 71 | latitudeInputLayout.setErrorEnabled(true); 72 | } 73 | 74 | @Override 75 | public void errorParsingLongitude() { 76 | longitudeInputLayout.setError(getString(R.string.navigate_invalid_longitude)); 77 | longitudeInputLayout.setErrorEnabled(true); 78 | } 79 | 80 | @Override 81 | public void openCompassScreen(double lat, double lon) { 82 | latitudeInputLayout.setErrorEnabled(false); 83 | longitudeInputLayout.setErrorEnabled(false); 84 | Location location = LocationUtils.buildLocation(lat, lon); 85 | startActivity(CompassActivity.newIntent(this, location)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/plastix/forage/util/ActivityUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.plastix.forage.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.graphics.Color; 7 | import android.net.Uri; 8 | import android.os.Build; 9 | import android.support.annotation.NonNull; 10 | import android.support.annotation.StringRes; 11 | import android.support.v7.app.ActionBar; 12 | import android.support.v7.app.AppCompatActivity; 13 | import android.support.v7.app.AppCompatDelegate; 14 | import android.view.View; 15 | import android.view.Window; 16 | 17 | public class ActivityUtils { 18 | 19 | private ActivityUtils() { 20 | throw new UnsupportedOperationException("No Instantiation!"); 21 | } 22 | 23 | /** 24 | * Returns an intent for launching the application settings for the app with the specified activity. 25 | * 26 | * @param context Context instance 27 | * @return Intent object. 28 | */ 29 | public static Intent getApplicationSettingsIntent(@NonNull Context context) { 30 | Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 31 | intent.setData(Uri.parse("package:" + context.getPackageName())); 32 | 33 | return intent; 34 | } 35 | 36 | /** 37 | * Overloaded version of {@link #setSupportActionBarTitle(Activity, String)}. 38 | * 39 | * @param activity Activity to set action bar on. 40 | * @param titleID String ID of title. 41 | */ 42 | public static void setSupportActionBarTitle(@NonNull Activity activity, @StringRes int titleID) { 43 | setSupportActionBarTitle(activity, activity.getString(titleID)); 44 | } 45 | 46 | /** 47 | * Sets the support action bar title for the specified activity. 48 | * 49 | * @param activity Activity to set action bar on. Must be a {@link AppCompatActivity}. 50 | * @param title String title to set. 51 | */ 52 | public static void setSupportActionBarTitle(@NonNull Activity activity, String title) { 53 | ActionBar actionBar = ((AppCompatActivity) activity).getSupportActionBar(); 54 | if (actionBar != null) { 55 | actionBar.setTitle(title); 56 | } 57 | } 58 | 59 | /** 60 | * Sets the support action bar back button. The activity still has to manage the back button 61 | * event correctly! 62 | * 63 | * @param delegate AppCompatDelegate for the AppCompatActivity you want to modify. 64 | */ 65 | public static void setSupportActionBarBack(@NonNull AppCompatDelegate delegate) { 66 | ActionBar bar = delegate.getSupportActionBar(); 67 | if (bar != null) { 68 | delegate.getSupportActionBar().setDisplayShowHomeEnabled(true); 69 | delegate.getSupportActionBar().setDisplayHomeAsUpEnabled(true); 70 | } 71 | } 72 | 73 | public static void setStatusBarTranslucent(AppCompatActivity activity) { 74 | if (Build.VERSION.SDK_INT >= 21) { 75 | Window window = activity.getWindow(); 76 | window.addFlags(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 77 | window.setStatusBarColor(Color.TRANSPARENT); 78 | 79 | } 80 | 81 | } 82 | 83 | } 84 | --------------------------------------------------------------------------------