├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hitherejoe │ │ └── watchtower │ │ ├── AddAttachmentActivityTest.java │ │ ├── AttachmentsActivityTest.java │ │ ├── DetailActivityTest.java │ │ ├── MainActivityTest.java │ │ ├── PropertiesActivityTest.java │ │ ├── injection │ │ ├── TestComponentRule.java │ │ ├── component │ │ │ ├── DataManagerTestComponent.java │ │ │ └── TestComponent.java │ │ └── module │ │ │ ├── ApplicationTestModule.java │ │ │ └── DataManagerTestModule.java │ │ └── util │ │ └── TestDataManager.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── hitherejoe │ │ │ └── watchtower │ │ │ ├── WatchTowerApplication.java │ │ │ ├── data │ │ │ ├── BusEvent.java │ │ │ ├── DataManager.java │ │ │ ├── local │ │ │ │ └── PreferencesHelper.java │ │ │ ├── model │ │ │ │ ├── AdvertisedId.java │ │ │ │ ├── Attachment.java │ │ │ │ ├── Beacon.java │ │ │ │ ├── Diagnostics.java │ │ │ │ ├── ErrorResponse.java │ │ │ │ ├── LatLng.java │ │ │ │ └── Namespace.java │ │ │ └── remote │ │ │ │ ├── RetrofitHelper.java │ │ │ │ └── WatchTowerService.java │ │ │ ├── injection │ │ │ ├── component │ │ │ │ ├── ApplicationComponent.java │ │ │ │ └── DataManagerComponent.java │ │ │ ├── module │ │ │ │ ├── ApplicationModule.java │ │ │ │ └── DataManagerModule.java │ │ │ └── scope │ │ │ │ └── PerDataManager.java │ │ │ ├── ui │ │ │ ├── activity │ │ │ │ ├── AddAttachmentActivity.java │ │ │ │ ├── AttachmentsActivity.java │ │ │ │ ├── AuthActivity.java │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── DetailActivity.java │ │ │ │ ├── LauncherActivity.java │ │ │ │ ├── MainActivity.java │ │ │ │ └── PropertiesActivity.java │ │ │ ├── adapter │ │ │ │ ├── AlertHolder.java │ │ │ │ ├── AttachmentHolder.java │ │ │ │ └── BeaconHolder.java │ │ │ └── fragment │ │ │ │ ├── AlertsFragment.java │ │ │ │ └── PropertiesFragment.java │ │ │ └── util │ │ │ ├── AccountUtils.java │ │ │ ├── DataUtils.java │ │ │ ├── DialogFactory.java │ │ │ └── MockModelsUtil.java │ └── res │ │ ├── drawable-hdpi │ │ ├── ic_active.png │ │ ├── ic_add.png │ │ ├── ic_decommissioned.png │ │ ├── ic_done.png │ │ ├── ic_inactive.png │ │ └── ic_unspecified.png │ │ ├── drawable-mdpi │ │ ├── ic_active.png │ │ ├── ic_add.png │ │ ├── ic_decommissioned.png │ │ ├── ic_done.png │ │ ├── ic_inactive.png │ │ └── ic_unspecified.png │ │ ├── drawable-v21 │ │ └── touchable_background_white.xml │ │ ├── drawable-xhdpi │ │ ├── ic_active.png │ │ ├── ic_add.png │ │ ├── ic_decommissioned.png │ │ ├── ic_done.png │ │ ├── ic_inactive.png │ │ └── ic_unspecified.png │ │ ├── drawable-xxhdpi │ │ ├── ic_active.png │ │ ├── ic_add.png │ │ ├── ic_decommissioned.png │ │ ├── ic_done.png │ │ ├── ic_inactive.png │ │ └── ic_unspecified.png │ │ ├── drawable-xxxhdpi │ │ ├── ic_active.png │ │ ├── ic_add.png │ │ ├── ic_decommissioned.png │ │ ├── ic_done.png │ │ ├── ic_inactive.png │ │ └── ic_unspecified.png │ │ ├── drawable │ │ └── touchable_background_white.xml │ │ ├── layout │ │ ├── activity_add_attachment.xml │ │ ├── activity_attachments.xml │ │ ├── activity_detail.xml │ │ ├── activity_main.xml │ │ ├── activity_update.xml │ │ ├── fragment_alerts.xml │ │ ├── fragment_properties.xml │ │ ├── item_alert.xml │ │ ├── item_attachment.xml │ │ └── item_beacon.xml │ │ ├── menu │ │ ├── add_attachment.xml │ │ ├── attachments.xml │ │ ├── detail.xml │ │ ├── main.xml │ │ └── register.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── hitherejoe │ └── watchtower │ ├── DataManagerTest.java │ └── util │ ├── DefaultConfig.java │ └── RxAssertions.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── device_screenshot.png └── ic_launcher_web.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | .DS_Store 5 | /build 6 | .idea/ 7 | *iml 8 | *.iml 9 | */build -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WatchTower 2 | =================== 3 | 4 |

5 | Web Launcher 6 |

7 | 8 | Note: In order to use this app, you'll need to use the Google API Console to register your SHA1 token along with the package name, this app won't work otherwise. 9 | 10 | WatchTower is a simple application which was created to test and explore the functionality of the new [Proximity Beacon API](https://developers.google.com/beacons/proximity/guides). The application can be used to try out: 11 | 12 | - Registering Beacons 13 | - Updating Beacons 14 | - Viewing Beacons 15 | - Viewing Beacon Diagnostics 16 | - Viewing Beacon Attachments 17 | - Adding Beacon Attachments 18 | - Deleting Single Beacon Attachments 19 | - Deleting Batch Beacon Attachments by Type 20 | 21 | Some features are not very practical within a mobile-app (for example, adding json data to attachments), so these have not been included. 22 | 23 |

24 | Web Launcher 25 |

26 | 27 | Note: This was built *quickly* to simply test the APIs functionality. If you come across any bugs please feel free to submit them as an issue, or open a pull request ;) 28 | 29 | For further information, please read the [supporting blog post](https://medium.com/ribot-labs/exploring-google-eddystone-with-the-proximity-beacon-api-bc9256c97e05). 30 | 31 | Requirements 32 | ------------ 33 | 34 | - [Android SDK](http://developer.android.com/sdk/index.html). 35 | - Android [5.1 (API 22) ](http://developer.android.com/tools/revisions/platforms.html#5.1). 36 | - Android SDK Tools 37 | - Android SDK Build tools 22.0.1 38 | - Android Support Repository 39 | - Android Support library 40 | - Enabled [Unit Test support] (http://tools.android.com/tech-docs/unit-testing-support) 41 | 42 | Building 43 | -------- 44 | 45 | To build, install and run a debug version, run this from the root of the project: 46 | 47 | ./gradlew installRunDebug 48 | 49 | Testing 50 | -------- 51 | 52 | For Android Studio to use syntax highlighting for Automated tests and Unit tests you **must** switch the Build Variant to the desired mode. 53 | 54 | To run **unit** tests on your machine using [Robolectric] (http://robolectric.org/): 55 | 56 | ./gradlew testDebug 57 | 58 | To run **automated** tests on connected devices: 59 | 60 | ./gradlew connectedAndroidTest 61 | 62 | Attributions 63 | ------------ 64 | 65 | Thanks to the following for icons off of Noun Project: 66 |
67 | [Stéphanie Rusch](https://thenounproject.com/BeezkOt) - Beacon Icon
68 | [Abraham](https://thenounproject.com/gorilladigital) - Cloud Icon
69 | [S.Shohei](https://thenounproject.com/shohei909) - Battery Icon
70 | [Juergen Bauer](https://thenounproject.com/Juergen) - Alert Icon
71 | [Pham Thi Dieu Linh](https://thenounproject.com/phdieuli) - Attachment Icon
72 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *iml 3 | *.iml 4 | .idea -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'com.neenbedankt.android-apt' 3 | 4 | android { 5 | compileSdkVersion 22 6 | buildToolsVersion "22.0.1" 7 | 8 | defaultConfig { 9 | applicationId "com.hitherejoe.watchtower" 10 | minSdkVersion 16 11 | targetSdkVersion 22 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | versionCode 1000 14 | // Major -> Millions, Minor -> Thousands, Bugfix -> Hundreds. E.g 1.3.72 == 1,003,072 15 | versionName "1.0" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | debuggable true 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | debug { 25 | versionNameSuffix " Debug" 26 | debuggable true 27 | } 28 | } 29 | 30 | packagingOptions { 31 | exclude 'META-INF/DEPENDENCIES' 32 | exclude 'LICENSE.txt' 33 | exclude 'META-INF/LICENSE' 34 | exclude 'META-INF/LICENSE.txt' 35 | exclude 'META-INF/NOTICE' 36 | } 37 | } 38 | dependencies { 39 | final PLAY_SERVICES_VERSION = '7.5.0' 40 | final SUPPORT_LIBRARY_VERSION = '22.2.1' 41 | final DAGGER_VERSION = '2.0.1' 42 | final DEXMAKER_VERSION = '1.2' 43 | final HAMCREST_VERSION = '1.3' 44 | final MOCKITO_VERSION = '1.10.19' 45 | final ESPRESSO_VERSION = '2.2' 46 | 47 | compile fileTree(dir: 'libs', include: ['*.jar']) 48 | 49 | compile "com.google.android.gms:play-services:$PLAY_SERVICES_VERSION" 50 | compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION" 51 | compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION" 52 | compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION" 53 | compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION" 54 | 55 | compile 'io.reactivex:rxandroid:1.0.0' 56 | compile 'com.squareup.retrofit:retrofit:1.9.0' 57 | compile 'com.jakewharton:butterknife:7.0.1' 58 | compile 'com.jakewharton.timber:timber:3.1.0' 59 | compile 'uk.co.ribot:easyadapter:1.4.0@aar' 60 | compile 'com.android.support:design:22.2.1' 61 | compile 'com.squareup:otto:1.3.8' 62 | 63 | compile "com.google.dagger:dagger:$DAGGER_VERSION" 64 | apt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" 65 | provided 'org.glassfish:javax.annotation:10.0-b28' 66 | 67 | // Espresso 68 | androidTestCompile 'junit:junit:4.12' 69 | androidTestCompile "com.android.support.test.espresso:espresso-core:$ESPRESSO_VERSION" 70 | androidTestCompile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION" 71 | androidTestCompile "org.mockito:mockito-core:$MOCKITO_VERSION" 72 | androidTestCompile 'com.android.support.test:runner:0.3' 73 | androidTestCompile 'com.android.support.test:rules:0.3' 74 | androidTestCompile "com.google.dexmaker:dexmaker:$DEXMAKER_VERSION" 75 | androidTestCompile "com.google.dexmaker:dexmaker-mockito:$DEXMAKER_VERSION" 76 | androidTestCompile("com.android.support.test.espresso:espresso-contrib:$ESPRESSO_VERSION") { 77 | exclude group: 'com.android.support', module: 'appcompat' 78 | exclude group: 'com.android.support', module: 'support-v4' 79 | exclude module: 'recyclerview-v7' 80 | } 81 | 82 | androidTestApt 'com.google.dagger:dagger-compiler:2.0.1' 83 | 84 | // Robolectric 85 | testCompile 'junit:junit:4.12' 86 | testCompile "org.hamcrest:hamcrest-core:$HAMCREST_VERSION" 87 | testCompile "org.hamcrest:hamcrest-library:$HAMCREST_VERSION" 88 | testCompile "org.hamcrest:hamcrest-integration:$HAMCREST_VERSION" 89 | testCompile "org.mockito:mockito-core:$MOCKITO_VERSION" 90 | testCompile('org.robolectric:robolectric:3.0') { 91 | exclude group: 'commons-logging', module: 'commons-logging' 92 | exclude group: 'org.apache.httpcomponents', module: 'httpclient' 93 | }\ 94 | } 95 | -------------------------------------------------------------------------------- /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 /Users/hitherejoe/Android Studio.app/sdk/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 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hitherejoe/watchtower/AddAttachmentActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower; 2 | 3 | 4 | import android.content.Intent; 5 | import android.support.test.InstrumentationRegistry; 6 | import android.support.test.rule.ActivityTestRule; 7 | import android.support.test.runner.AndroidJUnit4; 8 | 9 | import com.hitherejoe.watchtower.data.model.Beacon; 10 | import com.hitherejoe.watchtower.data.remote.WatchTowerService; 11 | import com.hitherejoe.watchtower.injection.TestComponentRule; 12 | import com.hitherejoe.watchtower.ui.activity.AddAttachmentActivity; 13 | import com.hitherejoe.watchtower.util.MockModelsUtil; 14 | 15 | import org.junit.Rule; 16 | import org.junit.Test; 17 | import org.junit.runner.RunWith; 18 | 19 | import rx.Observable; 20 | 21 | import static android.support.test.espresso.Espresso.onData; 22 | import static android.support.test.espresso.Espresso.onView; 23 | import static android.support.test.espresso.action.ViewActions.click; 24 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 25 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 26 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 27 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 28 | import static org.hamcrest.CoreMatchers.allOf; 29 | import static org.hamcrest.CoreMatchers.not; 30 | import static org.hamcrest.Matchers.instanceOf; 31 | import static org.hamcrest.core.Is.is; 32 | import static org.mockito.Mockito.when; 33 | 34 | @RunWith(AndroidJUnit4.class) 35 | public class AddAttachmentActivityTest { 36 | 37 | @Rule 38 | public final ActivityTestRule main = 39 | new ActivityTestRule<>(AddAttachmentActivity.class, false, false); 40 | 41 | @Rule 42 | public final TestComponentRule component = new TestComponentRule(); 43 | 44 | @Test 45 | public void testActivityDisplayed() { 46 | Beacon beacon = MockModelsUtil.createMockRegisteredBeacon(); 47 | WatchTowerService.NamespacesResponse namespacesResponse = new WatchTowerService.NamespacesResponse(); 48 | namespacesResponse.namespaces = MockModelsUtil.createMockListOfNamespaces(1); 49 | when(component.getMockWatchTowerService().getNamespaces()) 50 | .thenReturn(Observable.just(namespacesResponse)); 51 | 52 | Intent i = new Intent(AddAttachmentActivity.getStartIntent(InstrumentationRegistry.getTargetContext(), beacon)); 53 | main.launchActivity(i); 54 | 55 | onView(withId(R.id.text_namespace)) 56 | .check(matches(isDisplayed())); 57 | onView(withId(R.id.spinner_namespace)) 58 | .check(matches(isDisplayed())); 59 | onView(withId(R.id.text_data)) 60 | .check(matches(isDisplayed())); 61 | onView(withId(R.id.edit_text_data)) 62 | .check(matches(isDisplayed())); 63 | onView(withId(R.id.text_data_error_message)) 64 | .check(matches(not(isDisplayed()))); 65 | 66 | onData(allOf(is(instanceOf(String.class)), is(namespacesResponse.namespaces.get(0).namespaceName))) 67 | .check(matches(isDisplayed())); 68 | } 69 | 70 | @Test 71 | public void testAddAttachmentInvalidInput() { 72 | Beacon beacon = MockModelsUtil.createMockRegisteredBeacon(); 73 | WatchTowerService.NamespacesResponse namespacesResponse = new WatchTowerService.NamespacesResponse(); 74 | namespacesResponse.namespaces = MockModelsUtil.createMockListOfNamespaces(1); 75 | when(component.getMockWatchTowerService().getNamespaces()) 76 | .thenReturn(Observable.just(namespacesResponse)); 77 | 78 | Intent i = new Intent(AddAttachmentActivity.getStartIntent(InstrumentationRegistry.getTargetContext(), beacon)); 79 | main.launchActivity(i); 80 | 81 | onView(withId(R.id.text_data_error_message)) 82 | .check(matches(not(isDisplayed()))); 83 | onView(withId(R.id.action_done)) 84 | .perform(click()); 85 | onView(withText(R.string.dialog_error_blank_data)) 86 | .check(matches(isDisplayed())); 87 | onView(withText(R.string.dialog_action_ok)) 88 | .perform(click()); 89 | onView(withId(R.id.text_data_error_message)) 90 | .check(matches(isDisplayed())); 91 | } 92 | 93 | // Test for adding an attachment are found in AttachmentsActivityTest as the activity is finished, and we 94 | // need to test this 95 | 96 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hitherejoe/watchtower/AttachmentsActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower; 2 | 3 | 4 | import android.content.Intent; 5 | import android.support.test.InstrumentationRegistry; 6 | import android.support.test.espresso.contrib.RecyclerViewActions; 7 | import android.support.test.rule.ActivityTestRule; 8 | import android.support.test.runner.AndroidJUnit4; 9 | 10 | import com.hitherejoe.watchtower.data.model.Attachment; 11 | import com.hitherejoe.watchtower.data.model.Beacon; 12 | import com.hitherejoe.watchtower.data.model.Namespace; 13 | import com.hitherejoe.watchtower.data.remote.WatchTowerService; 14 | import com.hitherejoe.watchtower.injection.TestComponentRule; 15 | import com.hitherejoe.watchtower.ui.activity.AttachmentsActivity; 16 | import com.hitherejoe.watchtower.util.MockModelsUtil; 17 | 18 | import org.junit.Rule; 19 | import org.junit.Test; 20 | import org.junit.runner.RunWith; 21 | 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | import rx.Observable; 26 | 27 | import static android.support.test.espresso.Espresso.onData; 28 | import static android.support.test.espresso.Espresso.onView; 29 | import static android.support.test.espresso.Espresso.openContextualActionModeOverflowMenu; 30 | import static android.support.test.espresso.action.ViewActions.click; 31 | import static android.support.test.espresso.action.ViewActions.typeText; 32 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 33 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 34 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 35 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 36 | import static org.hamcrest.CoreMatchers.allOf; 37 | import static org.hamcrest.Matchers.instanceOf; 38 | import static org.hamcrest.core.Is.is; 39 | import static org.mockito.Matchers.any; 40 | import static org.mockito.Matchers.anyString; 41 | import static org.mockito.Mockito.when; 42 | 43 | @RunWith(AndroidJUnit4.class) 44 | public class AttachmentsActivityTest { 45 | 46 | @Rule 47 | public final ActivityTestRule main = 48 | new ActivityTestRule<>(AttachmentsActivity.class, false, false); 49 | 50 | @Rule 51 | public final TestComponentRule component = new TestComponentRule(); 52 | 53 | @Test 54 | public void testAttachmentsDisplayed() { 55 | Beacon beacon = MockModelsUtil.createMockRegisteredBeacon(); 56 | List attachments = MockModelsUtil.createMockListOfAttachments(beacon.beaconName, 10); 57 | stubMockAttachments(beacon.beaconName, attachments); 58 | 59 | Intent i = new Intent(AttachmentsActivity.getStartIntent(InstrumentationRegistry.getTargetContext(), beacon)); 60 | main.launchActivity(i); 61 | 62 | checkAttachmentsDisplayOnRecyclerView(attachments); 63 | } 64 | 65 | @Test 66 | public void testDeleteAttachment() { 67 | Beacon beacon = MockModelsUtil.createMockRegisteredBeacon(); 68 | List attachments = MockModelsUtil.createMockListOfAttachments(beacon.beaconName, 1); 69 | stubMockAttachments(beacon.beaconName, attachments); 70 | 71 | when(component.getMockWatchTowerService().deleteAttachment(attachments.get(0).attachmentName)) 72 | .thenReturn(Observable.empty()); 73 | 74 | Intent i = new Intent(AttachmentsActivity.getStartIntent(InstrumentationRegistry.getTargetContext(), beacon)); 75 | main.launchActivity(i); 76 | 77 | onView(withId(R.id.text_delete)) 78 | .perform(click()); 79 | onView(withText(R.string.text_no_attachments)) 80 | .check(matches(isDisplayed())); 81 | } 82 | 83 | @Test 84 | public void testEmptyAttachmentsFeed() { 85 | Beacon beacon = MockModelsUtil.createMockRegisteredBeacon(); 86 | stubMockAttachments(beacon.beaconName, new ArrayList()); 87 | Intent i = new Intent(AttachmentsActivity.getStartIntent(InstrumentationRegistry.getTargetContext(), beacon)); 88 | main.launchActivity(i); 89 | 90 | onView(withText(R.string.text_no_attachments)) 91 | .check(matches(isDisplayed())); 92 | } 93 | 94 | @Test 95 | public void testAddAttachmentActivityStarted() { 96 | Beacon beacon = MockModelsUtil.createMockRegisteredBeacon(); 97 | stubMockAttachments(beacon.beaconName, new ArrayList()); 98 | List namespaces = MockModelsUtil.createMockListOfNamespaces(1); 99 | WatchTowerService.NamespacesResponse namespacesResponse = new WatchTowerService.NamespacesResponse(); 100 | namespacesResponse.namespaces = namespaces; 101 | 102 | when(component.getMockWatchTowerService().getNamespaces()) 103 | .thenReturn(Observable.just(namespacesResponse)); 104 | 105 | Intent i = new Intent(AttachmentsActivity.getStartIntent(InstrumentationRegistry.getTargetContext(), beacon)); 106 | main.launchActivity(i); 107 | 108 | onView(withId(R.id.fab_add)) 109 | .perform(click()); 110 | onView(withId(R.id.spinner_namespace)) 111 | .check(matches(isDisplayed())); 112 | } 113 | 114 | @Test 115 | public void testDeleteAllAttachments() { 116 | Beacon beacon = MockModelsUtil.createMockRegisteredBeacon(); 117 | List attachments = MockModelsUtil.createMockListOfAttachments(beacon.beaconName, 10); 118 | stubMockAttachments(beacon.beaconName, attachments); 119 | 120 | WatchTowerService.AttachmentResponse attachmentResponse = new WatchTowerService.AttachmentResponse(); 121 | attachmentResponse.attachments = new ArrayList<>(); 122 | 123 | when(component.getMockWatchTowerService().deleteBatchAttachments(beacon.beaconName, null)) 124 | .thenReturn(Observable.empty()); 125 | 126 | Intent i = new Intent(AttachmentsActivity.getStartIntent(InstrumentationRegistry.getTargetContext(), beacon)); 127 | main.launchActivity(i); 128 | 129 | when(component.getMockWatchTowerService().getAttachments(beacon.beaconName, null)) 130 | .thenReturn(Observable.just(attachmentResponse)); 131 | 132 | openContextualActionModeOverflowMenu(); 133 | onView(withText(R.string.action_delete_all)) 134 | .perform(click()); 135 | } 136 | 137 | @Test 138 | public void testAddAttachmentValidInput() { 139 | Beacon beacon = MockModelsUtil.createMockRegisteredBeacon(); 140 | Attachment attachment = MockModelsUtil.createMockAttachment(); 141 | WatchTowerService.NamespacesResponse namespacesResponse = new WatchTowerService.NamespacesResponse(); 142 | namespacesResponse.namespaces = MockModelsUtil.createMockListOfNamespaces(1); 143 | 144 | when(component.getMockWatchTowerService().getNamespaces()) 145 | .thenReturn(Observable.just(namespacesResponse)); 146 | 147 | List attachments = MockModelsUtil.createMockListOfAttachments(beacon.beaconName, 10); 148 | stubMockAttachments(beacon.beaconName, attachments); 149 | 150 | WatchTowerService.AttachmentResponse attachmentResponse = new WatchTowerService.AttachmentResponse(); 151 | attachmentResponse.attachments = new ArrayList<>(); 152 | 153 | when(component.getMockWatchTowerService().deleteBatchAttachments(beacon.beaconName, null)) 154 | .thenReturn(Observable.empty()); 155 | 156 | when(component.getMockWatchTowerService().createAttachment(anyString(), any(Attachment.class))) 157 | .thenReturn(Observable.just(attachment)); 158 | 159 | Intent i = new Intent(AttachmentsActivity.getStartIntent(InstrumentationRegistry.getTargetContext(), beacon)); 160 | main.launchActivity(i); 161 | 162 | onView(withId(R.id.fab_add)).perform(click()); 163 | 164 | onData(allOf(is(instanceOf(String.class)), is(namespacesResponse.namespaces.get(0).namespaceName))) 165 | .check(matches(isDisplayed())); 166 | onView(withId(R.id.edit_text_data)) 167 | .perform(typeText("Data")); 168 | onView(withId(R.id.action_done)) 169 | .perform(click()); 170 | onView(withId(R.id.fab_add)).check(matches(isDisplayed())); 171 | } 172 | 173 | private void checkAttachmentsDisplayOnRecyclerView(List beaconsToCheck) { 174 | for (int i = 0; i < beaconsToCheck.size(); i++) { 175 | onView(withId(R.id.recycler_attachments)) 176 | .perform(RecyclerViewActions.scrollToPosition(i)); 177 | checkPostDisplays(beaconsToCheck.get(i)); 178 | } 179 | } 180 | 181 | private void checkPostDisplays(Attachment attachment) { 182 | onView(withText(attachment.attachmentName)) 183 | .check(matches(isDisplayed())); 184 | } 185 | 186 | private void stubMockAttachments(String beaconName, List mockAttachments) { 187 | WatchTowerService.AttachmentResponse attachmentResponse = new WatchTowerService.AttachmentResponse(); 188 | attachmentResponse.attachments = mockAttachments; 189 | when(component.getMockWatchTowerService().getAttachments(beaconName, null)) 190 | .thenReturn(Observable.just(attachmentResponse)); 191 | } 192 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hitherejoe/watchtower/MainActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower; 2 | 3 | 4 | import android.content.Intent; 5 | import android.support.test.InstrumentationRegistry; 6 | import android.support.test.espresso.contrib.RecyclerViewActions; 7 | import android.support.test.rule.ActivityTestRule; 8 | import android.support.test.runner.AndroidJUnit4; 9 | 10 | import com.hitherejoe.watchtower.data.model.Beacon; 11 | import com.hitherejoe.watchtower.data.model.Diagnostics; 12 | import com.hitherejoe.watchtower.data.remote.WatchTowerService; 13 | import com.hitherejoe.watchtower.injection.TestComponentRule; 14 | import com.hitherejoe.watchtower.ui.activity.MainActivity; 15 | import com.hitherejoe.watchtower.ui.activity.PropertiesActivity; 16 | import com.hitherejoe.watchtower.ui.fragment.PropertiesFragment; 17 | import com.hitherejoe.watchtower.util.MockModelsUtil; 18 | 19 | import org.junit.Rule; 20 | import org.junit.Test; 21 | import org.junit.runner.RunWith; 22 | import org.mockito.Matchers; 23 | 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | import rx.Observable; 28 | 29 | import static android.support.test.espresso.Espresso.onData; 30 | import static android.support.test.espresso.Espresso.onView; 31 | import static android.support.test.espresso.action.ViewActions.click; 32 | import static android.support.test.espresso.action.ViewActions.scrollTo; 33 | import static android.support.test.espresso.action.ViewActions.typeText; 34 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 35 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 36 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 37 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 38 | import static org.hamcrest.CoreMatchers.allOf; 39 | import static org.hamcrest.core.Is.is; 40 | import static org.hamcrest.core.IsInstanceOf.instanceOf; 41 | import static org.mockito.Mockito.when; 42 | 43 | @RunWith(AndroidJUnit4.class) 44 | public class MainActivityTest { 45 | 46 | @Rule 47 | public final ActivityTestRule main = 48 | new ActivityTestRule<>(MainActivity.class, false, false); 49 | 50 | @Rule 51 | public final TestComponentRule component = new TestComponentRule(); 52 | 53 | @Test 54 | public void testBeaconsShowAndAreScrollableInFeed() { 55 | List mockBeacons = MockModelsUtil.createMockListOfBeacons(20); 56 | stubMockPosts(mockBeacons); 57 | main.launchActivity(null); 58 | 59 | checkPostsDisplayOnRecyclerView(mockBeacons); 60 | } 61 | 62 | @Test 63 | public void testClickOnCardAndNavigateToBeaconDetails() { 64 | 65 | List mockBeacons = MockModelsUtil.createMockListOfBeacons(1); 66 | stubMockPosts(mockBeacons); 67 | 68 | String beaconName = mockBeacons.get(0).beaconName; 69 | Diagnostics diagnostics = MockModelsUtil.createMockDiagnostics(beaconName); 70 | when(component.getMockWatchTowerService().beaconDiagnostics(beaconName)) 71 | .thenReturn(Observable.just(diagnostics)); 72 | 73 | main.launchActivity(null); 74 | 75 | onView(withText(mockBeacons.get(0).beaconName)) 76 | .perform(click()); 77 | onView(withText(mockBeacons.get(0).beaconName)) 78 | .check(matches(isDisplayed())); 79 | } 80 | 81 | @Test 82 | public void testClickOnViewAndNavigateToBeaconDetails() { 83 | List mockBeacons = MockModelsUtil.createMockListOfBeacons(1); 84 | stubMockPosts(mockBeacons); 85 | 86 | String beaconName = mockBeacons.get(0).beaconName; 87 | Diagnostics diagnostics = MockModelsUtil.createMockDiagnostics(beaconName); 88 | when(component.getMockWatchTowerService().beaconDiagnostics(beaconName)) 89 | .thenReturn(Observable.just(diagnostics)); 90 | 91 | main.launchActivity(null); 92 | 93 | onView(withText(R.string.text_view)) 94 | .perform(click()); 95 | onView(withText(mockBeacons.get(0).beaconName)) 96 | .check(matches(isDisplayed())); 97 | } 98 | 99 | @Test 100 | public void testClickOnAttachmentsAndNavigateToBeaconAttachments() { 101 | List mockBeacons = MockModelsUtil.createMockListOfBeacons(1); 102 | stubMockPosts(mockBeacons); 103 | 104 | WatchTowerService.AttachmentResponse attachmentResponse = new WatchTowerService.AttachmentResponse(); 105 | attachmentResponse.attachments = MockModelsUtil.createMockListOfAttachments(mockBeacons.get(0).beaconName, 1); 106 | when(component.getMockWatchTowerService().getAttachments(mockBeacons.get(0).beaconName, null)) 107 | .thenReturn(Observable.just(attachmentResponse)); 108 | main.launchActivity(null); 109 | 110 | onView(withId(R.id.text_attachments)) 111 | .perform(click()); 112 | onView(withText(attachmentResponse.attachments.get(0).attachmentName)) 113 | .check(matches(isDisplayed())); 114 | } 115 | 116 | @Test 117 | public void testEmptyPostsFeed() { 118 | stubMockPosts(new ArrayList()); 119 | main.launchActivity(null); 120 | 121 | onView(withText(R.string.text_no_beacons)) 122 | .check(matches(isDisplayed())); 123 | } 124 | 125 | @Test 126 | public void testRegisterBeaconValidData() { 127 | Beacon mockBeacon = MockModelsUtil.createMockUnregisteredBeacon(); 128 | mockBeacon.status = Beacon.Status.ACTIVE; 129 | Beacon registeredBeacon = MockModelsUtil.createMockRegisteredBeacon(); 130 | 131 | when(component.getMockWatchTowerService().registerBeacon(Matchers.any(Beacon.class))) 132 | .thenReturn(Observable.just(registeredBeacon)); 133 | 134 | Intent i = new Intent(PropertiesActivity.getStartIntent(InstrumentationRegistry.getTargetContext(), PropertiesFragment.Mode.REGISTER)); 135 | stubMockPosts(new ArrayList()); 136 | main.launchActivity(i); 137 | 138 | onView(withId(R.id.fab_add)).perform(click()); 139 | 140 | onView(withId(R.id.edit_text_advertised_id)).perform(typeText(mockBeacon.advertisedId.id)); 141 | onView(withId(R.id.edit_text_description)).perform(typeText(mockBeacon.description)); 142 | onView(withId(R.id.spinner_type)).perform(scrollTo(), click()); 143 | onData(allOf(is(instanceOf(String.class)), is("Eddystone"))).check(matches(isDisplayed())).perform(click()); 144 | onView(withId(R.id.spinner_status)).perform(scrollTo(), click()); 145 | onData(allOf(is(instanceOf(String.class)), is("Active"))).check(matches(isDisplayed())).perform(click()); 146 | onView(withId(R.id.spinner_stability)).perform(scrollTo(), click()); 147 | onData(allOf(is(instanceOf(String.class)), is("Mobile"))).check(matches(isDisplayed())).perform(click()); 148 | onView(withId(R.id.edit_text_latitude)).perform(scrollTo(), typeText(String.valueOf(mockBeacon.latLng.latitude))); 149 | onView(withId(R.id.edit_text_longitude)).perform(scrollTo(), typeText(String.valueOf(mockBeacon.latLng.longitude))); 150 | onView(withId(R.id.edit_text_place_id)).perform(scrollTo(), typeText(mockBeacon.placeId)); 151 | onView(withId(R.id.action_done)).perform(click()); 152 | onView(withId(R.id.fab_add)).check(matches(isDisplayed())); 153 | } 154 | 155 | private void checkPostsDisplayOnRecyclerView(List beaconsToCheck) { 156 | for (int i = 0; i < beaconsToCheck.size(); i++) { 157 | onView(withId(R.id.recycler_beacons)) 158 | .perform(RecyclerViewActions.scrollToPosition(i)); 159 | checkPostDisplays(beaconsToCheck.get(i)); 160 | } 161 | } 162 | 163 | private void checkPostDisplays(Beacon beacon) { 164 | onView(withText(beacon.beaconName)) 165 | .check(matches(isDisplayed())); 166 | } 167 | 168 | private void stubMockPosts(List mockBeacons) { 169 | WatchTowerService.BeaconsResponse beaconsResponse = new WatchTowerService.BeaconsResponse(); 170 | beaconsResponse.beacons = mockBeacons; 171 | when(component.getMockWatchTowerService().getBeacons()) 172 | .thenReturn(Observable.just(beaconsResponse)); 173 | } 174 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hitherejoe/watchtower/PropertiesActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower; 2 | 3 | 4 | import android.content.Intent; 5 | import android.support.test.InstrumentationRegistry; 6 | import android.support.test.rule.ActivityTestRule; 7 | import android.support.test.runner.AndroidJUnit4; 8 | 9 | import com.hitherejoe.watchtower.data.model.Beacon; 10 | import com.hitherejoe.watchtower.ui.activity.PropertiesActivity; 11 | import com.hitherejoe.watchtower.ui.fragment.PropertiesFragment; 12 | import com.hitherejoe.watchtower.util.DataUtils; 13 | import com.hitherejoe.watchtower.util.MockModelsUtil; 14 | 15 | import org.junit.Rule; 16 | import org.junit.Test; 17 | import org.junit.runner.RunWith; 18 | 19 | import static android.support.test.espresso.Espresso.onData; 20 | import static android.support.test.espresso.Espresso.onView; 21 | import static android.support.test.espresso.action.ViewActions.click; 22 | import static android.support.test.espresso.action.ViewActions.scrollTo; 23 | import static android.support.test.espresso.action.ViewActions.typeText; 24 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 25 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 26 | import static android.support.test.espresso.matcher.ViewMatchers.isFocusable; 27 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 28 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 29 | import static org.hamcrest.CoreMatchers.allOf; 30 | import static org.hamcrest.CoreMatchers.not; 31 | import static org.hamcrest.core.Is.is; 32 | import static org.hamcrest.core.IsInstanceOf.instanceOf; 33 | 34 | @RunWith(AndroidJUnit4.class) 35 | public class PropertiesActivityTest { 36 | 37 | @Rule 38 | public final ActivityTestRule main = 39 | new ActivityTestRule<>(PropertiesActivity.class, false, false); 40 | 41 | @Test 42 | public void testRegisterBeaconForm() { 43 | Intent i = new Intent(PropertiesActivity.getStartIntent(InstrumentationRegistry.getTargetContext(), PropertiesFragment.Mode.REGISTER)); 44 | main.launchActivity(i); 45 | 46 | onView(withId(R.id.text_title_beacon_name)) 47 | .check(matches(not(isDisplayed()))); 48 | onView(withId(R.id.edit_text_beacon_name)) 49 | .check(matches(not(isDisplayed()))); 50 | 51 | onView(withId(R.id.text_title_advertised_id)) 52 | .check(matches(isDisplayed())); 53 | onView(withId(R.id.edit_text_advertised_id)) 54 | .check(matches(isDisplayed())); 55 | onView(withId(R.id.edit_text_advertised_id)) 56 | .check(matches(isFocusable())); 57 | onView(withId(R.id.text_advertised_id_error_message)) 58 | .check(matches(not(isDisplayed()))); 59 | 60 | onView(withId(R.id.text_title_description)) 61 | .perform(scrollTo()) 62 | .check(matches(isDisplayed())); 63 | onView(withId(R.id.edit_text_description)) 64 | .perform(scrollTo()) 65 | .check(matches(isDisplayed())); 66 | 67 | onView(withId(R.id.text_title_type)) 68 | .perform(scrollTo()) 69 | .check(matches(isDisplayed())); 70 | onView(withId(R.id.spinner_type)).perform(scrollTo(), click()); 71 | String[] beaconTypes = 72 | InstrumentationRegistry.getTargetContext().getResources().getStringArray(R.array.types); 73 | for (String beaconType : beaconTypes) { 74 | onData(allOf(is(instanceOf(String.class)), is(beaconType))).check(matches(isDisplayed())); 75 | if (beaconTypes[beaconTypes.length - 1].equals(beaconType)) { 76 | onData(allOf(is(instanceOf(String.class)), is(beaconType))).perform(click()); 77 | } 78 | } 79 | 80 | onView(withId(R.id.text_title_status)) 81 | .perform(scrollTo()) 82 | .check(matches(isDisplayed())); 83 | onView(withId(R.id.spinner_status)).perform(scrollTo(), click()); 84 | 85 | String[] beaconStatuses = 86 | InstrumentationRegistry.getTargetContext().getResources().getStringArray(R.array.statuses); 87 | for (String beaconStatus : beaconStatuses) { 88 | onData(allOf(is(instanceOf(String.class)), is(beaconStatus))).check(matches(isDisplayed())); 89 | if (beaconStatuses[beaconStatuses.length - 1].equals(beaconStatus)) { 90 | onData(allOf(is(instanceOf(String.class)), is(beaconStatus))).perform(click()); 91 | } 92 | } 93 | onView(withId(R.id.text_status_error_message)) 94 | .check(matches(not(isDisplayed()))); 95 | 96 | onView(withId(R.id.text_title_stability)) 97 | .perform(scrollTo()) 98 | .check(matches(isDisplayed())); 99 | onView(withId(R.id.spinner_stability)).perform(scrollTo(), click()); 100 | 101 | String[] beaconStabilities = 102 | InstrumentationRegistry.getTargetContext().getResources().getStringArray(R.array.stabilities); 103 | for (String beaconStability : beaconStabilities) { 104 | onData(allOf(is(instanceOf(String.class)), is(beaconStability))).check(matches(isDisplayed())); 105 | if (beaconStabilities[beaconStabilities.length - 1].equals(beaconStability)) { 106 | onData(allOf(is(instanceOf(String.class)), is(beaconStability))).perform(click()); 107 | } 108 | } 109 | onView(withId(R.id.text_title_location)) 110 | .perform(scrollTo()) 111 | .check(matches(isDisplayed())); 112 | onView(withId(R.id.edit_text_latitude)) 113 | .perform(scrollTo()) 114 | .check(matches(isDisplayed())); 115 | onView(withId(R.id.edit_text_longitude)) 116 | .perform(scrollTo()) 117 | .check(matches(isDisplayed())); 118 | onView(withId(R.id.text_title_place_id)) 119 | .perform(scrollTo()) 120 | .check(matches(isDisplayed())); 121 | onView(withId(R.id.edit_text_place_id)) 122 | .perform(scrollTo()) 123 | .check(matches(isDisplayed())); 124 | } 125 | 126 | 127 | public void testRegisterBeaconInvalidData() { 128 | Intent i = new Intent(PropertiesActivity.getStartIntent(InstrumentationRegistry.getTargetContext(), PropertiesFragment.Mode.REGISTER)); 129 | main.launchActivity(i); 130 | 131 | onView(withId(R.id.text_advertised_id_error_message)) 132 | .check(matches(not(isDisplayed()))); 133 | onView(withId(R.id.text_status_error_message)) 134 | .check(matches(not(isDisplayed()))); 135 | onView(withId(R.id.edit_text_latitude)) 136 | .perform(scrollTo(), typeText("f")); 137 | onView(withId(R.id.edit_text_longitude)) 138 | .perform(scrollTo(), typeText("f")); 139 | onView(withId(R.id.action_done)).perform(click()); 140 | onView(withId(R.id.text_advertised_id_error_message)) 141 | .check(matches(isDisplayed())); 142 | onView(withId(R.id.text_status_error_message)) 143 | .check(matches(isDisplayed())); 144 | onView(withId(R.id.text_latitude_error_message)) 145 | .check(matches(isDisplayed())); 146 | onView(withId(R.id.text_longitude_error_message)) 147 | .check(matches(isDisplayed())); 148 | } 149 | 150 | @Test 151 | public void testUpdateBeacon() { 152 | Beacon beacon = MockModelsUtil.createMockRegisteredBeacon(); 153 | Intent i = new Intent(PropertiesActivity.getStartIntent(InstrumentationRegistry.getTargetContext(), beacon, PropertiesFragment.Mode.UPDATE)); 154 | main.launchActivity(i); 155 | 156 | onView(withId(R.id.text_title_beacon_name)) 157 | .check(matches(isDisplayed())); 158 | onView(withText(beacon.beaconName)) 159 | .check(matches(isDisplayed())) 160 | .check(matches(not(isFocusable()))); 161 | 162 | onView(withId(R.id.text_title_advertised_id)) 163 | .check(matches(isDisplayed())); 164 | onView(withText(DataUtils.base64DecodeToString(beacon.advertisedId.id))) 165 | .check(matches(isDisplayed())) 166 | .check(matches(not(isFocusable()))); 167 | onView(withId(R.id.text_advertised_id_error_message)) 168 | .check(matches(not(isDisplayed()))); 169 | 170 | onView(withId(R.id.text_title_description)) 171 | .perform(scrollTo()) 172 | .check(matches(isDisplayed())); 173 | onView(withText(beacon.description)) 174 | .perform(scrollTo()) 175 | .check(matches(isDisplayed())); 176 | 177 | onView(withId(R.id.text_title_type)) 178 | .perform(scrollTo()) 179 | .check(matches(isDisplayed())); 180 | onView(withText(beacon.advertisedId.type.getString())).perform(scrollTo()).check(matches(isDisplayed())); 181 | 182 | onView(withId(R.id.text_title_status)) 183 | .perform(scrollTo()) 184 | .check(matches(isDisplayed())); 185 | onView(withText(beacon.status.getString())).perform(scrollTo()).check(matches(isDisplayed())); 186 | onView(withId(R.id.text_status_error_message)) 187 | .check(matches(not(isDisplayed()))); 188 | 189 | onView(withId(R.id.text_title_stability)) 190 | .perform(scrollTo()) 191 | .check(matches(isDisplayed())); 192 | onView(withText(beacon.expectedStability.getString())).perform(scrollTo()).check(matches(isDisplayed())); 193 | 194 | onView(withId(R.id.text_title_location)) 195 | .perform(scrollTo()) 196 | .check(matches(isDisplayed())); 197 | onView(withText(String.valueOf(beacon.latLng.latitude))) 198 | .perform(scrollTo()) 199 | .check(matches(isDisplayed())); 200 | onView(withText(String.valueOf(beacon.latLng.longitude))) 201 | .perform(scrollTo()) 202 | .check(matches(isDisplayed())); 203 | onView(withId(R.id.text_latitude_error_message)) 204 | .check(matches(not(isDisplayed()))); 205 | onView(withId(R.id.text_longitude_error_message)) 206 | .check(matches(not(isDisplayed()))); 207 | onView(withId(R.id.text_title_place_id)) 208 | .perform(scrollTo()) 209 | .check(matches(isDisplayed())); 210 | onView(withText(beacon.placeId)) 211 | .perform(scrollTo()) 212 | .check(matches(isDisplayed())); 213 | } 214 | 215 | // Tests for registering are found in MainActivityTest and updating beacon are found in DetailActivityTest, 216 | // this is because the activity gets closed upon success, so we need to test this functions correctly 217 | 218 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hitherejoe/watchtower/injection/TestComponentRule.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.injection; 2 | 3 | import android.support.test.InstrumentationRegistry; 4 | 5 | import com.hitherejoe.watchtower.WatchTowerApplication; 6 | import com.hitherejoe.watchtower.data.local.PreferencesHelper; 7 | import com.hitherejoe.watchtower.data.remote.WatchTowerService; 8 | import com.hitherejoe.watchtower.injection.component.DaggerTestComponent; 9 | import com.hitherejoe.watchtower.injection.component.TestComponent; 10 | import com.hitherejoe.watchtower.injection.module.ApplicationTestModule; 11 | import com.hitherejoe.watchtower.util.TestDataManager; 12 | 13 | import org.junit.rules.TestRule; 14 | import org.junit.runner.Description; 15 | import org.junit.runners.model.Statement; 16 | 17 | /** 18 | * Test rule that creates and sets a Dagger TestComponent into the application overriding the 19 | * existing application component. 20 | * Use this rule in your test case in order for the app to use mock dependencies. 21 | * It also exposes some of the dependencies so they can be easily accessed from the tests, e.g. to 22 | * stub mocks etc. 23 | */ 24 | public class TestComponentRule implements TestRule { 25 | 26 | private TestComponent mTestComponent; 27 | 28 | public TestComponent getTestComponent() { 29 | return mTestComponent; 30 | } 31 | 32 | public TestDataManager getDataManager() { 33 | return (TestDataManager) mTestComponent.dataManager(); 34 | } 35 | 36 | public WatchTowerService getMockWatchTowerService() { 37 | return getDataManager().getWatchTowerService(); 38 | } 39 | 40 | public PreferencesHelper getPreferencesHelper() { 41 | return getDataManager().getPreferencesHelper(); 42 | } 43 | 44 | private void setupDaggerTestComponentInApplication() { 45 | WatchTowerApplication application = WatchTowerApplication 46 | .get(InstrumentationRegistry.getTargetContext()); 47 | if (application.getComponent() instanceof TestComponent) { 48 | mTestComponent = (TestComponent) application.getComponent(); 49 | } else { 50 | mTestComponent = DaggerTestComponent.builder() 51 | .applicationTestModule(new ApplicationTestModule(application)) 52 | .build(); 53 | application.setComponent(mTestComponent); 54 | } 55 | } 56 | 57 | @Override 58 | public Statement apply(final Statement base, Description description) { 59 | return new Statement() { 60 | @Override 61 | public void evaluate() throws Throwable { 62 | try { 63 | setupDaggerTestComponentInApplication(); 64 | base.evaluate(); 65 | } finally { 66 | mTestComponent = null; 67 | } 68 | } 69 | }; 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hitherejoe/watchtower/injection/component/DataManagerTestComponent.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.injection.component; 2 | 3 | import com.hitherejoe.watchtower.injection.module.DataManagerTestModule; 4 | import com.hitherejoe.watchtower.injection.scope.PerDataManager; 5 | 6 | import dagger.Component; 7 | 8 | @PerDataManager 9 | @Component(dependencies = TestComponent.class, modules = DataManagerTestModule.class) 10 | public interface DataManagerTestComponent extends DataManagerComponent { 11 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hitherejoe/watchtower/injection/component/TestComponent.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.injection.component; 2 | 3 | import com.hitherejoe.watchtower.injection.module.ApplicationTestModule; 4 | 5 | import javax.inject.Singleton; 6 | 7 | import dagger.Component; 8 | 9 | @Singleton 10 | @Component(modules = ApplicationTestModule.class) 11 | public interface TestComponent extends ApplicationComponent { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hitherejoe/watchtower/injection/module/ApplicationTestModule.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.injection.module; 2 | 3 | import android.app.Application; 4 | 5 | import com.hitherejoe.watchtower.data.DataManager; 6 | import com.hitherejoe.watchtower.util.TestDataManager; 7 | import com.squareup.otto.Bus; 8 | 9 | import javax.inject.Singleton; 10 | 11 | import dagger.Module; 12 | import dagger.Provides; 13 | 14 | /** 15 | * Provides application-level dependencies for an app running on a testing environment 16 | * This allows injecting mocks if necessary. 17 | */ 18 | @Module 19 | public class ApplicationTestModule { 20 | private final Application mApplication; 21 | 22 | public ApplicationTestModule(Application application) { 23 | mApplication = application; 24 | } 25 | 26 | @Provides 27 | @Singleton 28 | Application provideApplication() { 29 | return mApplication; 30 | } 31 | 32 | @Provides 33 | @Singleton 34 | DataManager provideDataManager() { 35 | return new TestDataManager(mApplication); 36 | } 37 | 38 | @Provides 39 | @Singleton 40 | Bus provideEventBus() { 41 | return new Bus(); 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hitherejoe/watchtower/injection/module/DataManagerTestModule.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.injection.module; 2 | 3 | import android.content.Context; 4 | 5 | import com.hitherejoe.watchtower.data.local.PreferencesHelper; 6 | import com.hitherejoe.watchtower.data.remote.WatchTowerService; 7 | import com.hitherejoe.watchtower.injection.scope.PerDataManager; 8 | 9 | import dagger.Module; 10 | import dagger.Provides; 11 | import rx.Scheduler; 12 | import rx.schedulers.Schedulers; 13 | 14 | import static org.mockito.Mockito.mock; 15 | 16 | /** 17 | * Provides dependencies for an app running on a testing environment 18 | * This allows injecting mocks if necessary 19 | */ 20 | @Module 21 | public class DataManagerTestModule { 22 | 23 | private final Context mContext; 24 | 25 | public DataManagerTestModule(Context context) { 26 | mContext = context; 27 | } 28 | 29 | @Provides 30 | @PerDataManager 31 | PreferencesHelper providePreferencesHelper() { 32 | return new PreferencesHelper(mContext); 33 | } 34 | 35 | @Provides 36 | @PerDataManager 37 | WatchTowerService provideWatchTowerService() { 38 | return mock(WatchTowerService.class); 39 | } 40 | 41 | @Provides 42 | @PerDataManager 43 | Scheduler provideSubscribeScheduler() { 44 | return Schedulers.immediate(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hitherejoe/watchtower/util/TestDataManager.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.util; 2 | 3 | import android.content.Context; 4 | 5 | import com.hitherejoe.watchtower.WatchTowerApplication; 6 | import com.hitherejoe.watchtower.data.DataManager; 7 | import com.hitherejoe.watchtower.data.remote.WatchTowerService; 8 | import com.hitherejoe.watchtower.injection.component.DaggerDataManagerTestComponent; 9 | import com.hitherejoe.watchtower.injection.component.TestComponent; 10 | import com.hitherejoe.watchtower.injection.module.DataManagerTestModule; 11 | 12 | /** 13 | * Extension of DataManager to be used on a testing environment. 14 | * It uses DataManagerTestComponent to inject dependencies that are different to the 15 | * normal runtime ones. e.g. mock objects etc. 16 | * It also exposes some helpers like the DatabaseHelper or the Retrofit service that are helpful 17 | * during testing. 18 | */ 19 | public class TestDataManager extends DataManager { 20 | 21 | public TestDataManager(Context context) { 22 | super(context); 23 | } 24 | 25 | @Override 26 | protected void injectDependencies(Context context) { 27 | TestComponent testComponent = (TestComponent) 28 | WatchTowerApplication.get(context).getComponent(); 29 | DaggerDataManagerTestComponent.builder() 30 | .testComponent(testComponent) 31 | .dataManagerTestModule(new DataManagerTestModule(context)) 32 | .build() 33 | .inject(this); 34 | } 35 | 36 | public WatchTowerService getWatchTowerService() { 37 | return mWatchTowerService; 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 35 | 38 | 41 | 44 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/WatchTowerApplication.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.hitherejoe.watchtower.data.BusEvent; 7 | import com.hitherejoe.watchtower.injection.component.ApplicationComponent; 8 | import com.hitherejoe.watchtower.injection.component.DaggerApplicationComponent; 9 | import com.hitherejoe.watchtower.injection.module.ApplicationModule; 10 | import com.hitherejoe.watchtower.ui.activity.AuthActivity; 11 | import com.squareup.otto.Subscribe; 12 | 13 | import rx.Observer; 14 | import rx.android.schedulers.AndroidSchedulers; 15 | import rx.schedulers.Schedulers; 16 | import rx.subscriptions.CompositeSubscription; 17 | import timber.log.Timber; 18 | 19 | public class WatchTowerApplication extends Application { 20 | 21 | ApplicationComponent mApplicationComponent; 22 | private CompositeSubscription mSubscriptions; 23 | 24 | @Override 25 | public void onCreate() { 26 | super.onCreate(); 27 | if (BuildConfig.DEBUG) Timber.plant(new Timber.DebugTree()); 28 | 29 | mApplicationComponent = DaggerApplicationComponent.builder() 30 | .applicationModule(new ApplicationModule(this)) 31 | .build(); 32 | mSubscriptions = new CompositeSubscription(); 33 | getComponent().eventBus().register(this); 34 | } 35 | 36 | @Override 37 | public void onTerminate() { 38 | getComponent().eventBus().unregister(this); 39 | super.onTerminate(); 40 | } 41 | 42 | @Subscribe 43 | public void onAuthenticationError(BusEvent.AuthenticationError event) { 44 | mSubscriptions.add(getComponent().dataManager().clearUserCredentials() 45 | .observeOn(AndroidSchedulers.mainThread()) 46 | .subscribeOn(Schedulers.io()) 47 | .subscribe(new Observer() { 48 | @Override 49 | public void onCompleted() { 50 | startAuthActivityWithDialog(); 51 | } 52 | 53 | @Override 54 | public void onError(Throwable e) { 55 | Timber.e("There was an error clearing user credentials " + e); 56 | startAuthActivityWithDialog(); 57 | } 58 | 59 | @Override 60 | public void onNext(Void aVoid) { 61 | } 62 | })); 63 | } 64 | 65 | public static WatchTowerApplication get(Context context) { 66 | return (WatchTowerApplication) context.getApplicationContext(); 67 | } 68 | 69 | public ApplicationComponent getComponent() { 70 | return mApplicationComponent; 71 | } 72 | 73 | // Needed to replace the component with a test specific one 74 | public void setComponent(ApplicationComponent applicationComponent) { 75 | mApplicationComponent = applicationComponent; 76 | } 77 | 78 | private void startAuthActivityWithDialog() { 79 | startActivity(AuthActivity.getStartIntent(this, true)); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/data/BusEvent.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.data; 2 | 3 | import com.hitherejoe.watchtower.data.model.Beacon; 4 | 5 | public class BusEvent { 6 | public static class BeaconListAmended { } 7 | public static class BeaconUpdated { 8 | public Beacon beacon; 9 | 10 | public BeaconUpdated(Beacon beacon) { 11 | this.beacon = beacon; 12 | } 13 | 14 | } 15 | public static class AttachmentAdded { } 16 | public static class AuthenticationError { } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/data/DataManager.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.data; 2 | 3 | import android.content.Context; 4 | 5 | import com.hitherejoe.watchtower.WatchTowerApplication; 6 | import com.hitherejoe.watchtower.data.local.PreferencesHelper; 7 | import com.hitherejoe.watchtower.data.model.Attachment; 8 | import com.hitherejoe.watchtower.data.model.Beacon; 9 | import com.hitherejoe.watchtower.data.model.Diagnostics; 10 | import com.hitherejoe.watchtower.data.remote.WatchTowerService; 11 | import com.hitherejoe.watchtower.injection.component.DaggerDataManagerComponent; 12 | import com.hitherejoe.watchtower.injection.module.DataManagerModule; 13 | import com.squareup.otto.Bus; 14 | 15 | import javax.inject.Inject; 16 | 17 | import rx.Observable; 18 | import rx.Scheduler; 19 | import rx.Subscriber; 20 | import rx.functions.Func1; 21 | 22 | public class DataManager { 23 | 24 | @Inject protected WatchTowerService mWatchTowerService; 25 | @Inject protected PreferencesHelper mPreferencesHelper; 26 | @Inject protected Scheduler mSubscribeScheduler; 27 | @Inject protected Bus mEventBus; 28 | 29 | public DataManager(Context context) { 30 | injectDependencies(context); 31 | } 32 | 33 | /* This constructor is provided so we can set up a DataManager with mocks from unit test. 34 | * At the moment this is not possible to do with Dagger because the Gradle APT plugin doesn't 35 | * work for the unit test variant, plus Dagger 2 doesn't provide a nice way of overriding 36 | * modules */ 37 | public DataManager(WatchTowerService watchTowerService, 38 | Bus eventBus, 39 | PreferencesHelper preferencesHelper, 40 | Scheduler subscribeScheduler) { 41 | mWatchTowerService = watchTowerService; 42 | mEventBus = eventBus; 43 | mPreferencesHelper = preferencesHelper; 44 | mSubscribeScheduler = subscribeScheduler; 45 | } 46 | 47 | protected void injectDependencies(Context context) { 48 | DaggerDataManagerComponent.builder() 49 | .applicationComponent(WatchTowerApplication.get(context).getComponent()) 50 | .dataManagerModule(new DataManagerModule(context)) 51 | .build() 52 | .inject(this); 53 | } 54 | 55 | public PreferencesHelper getPreferencesHelper() { 56 | return mPreferencesHelper; 57 | } 58 | 59 | public Scheduler getScheduler() { 60 | return mSubscribeScheduler; 61 | } 62 | 63 | public Bus getBus() { 64 | return mEventBus; 65 | } 66 | 67 | public Observable clearUserCredentials() { 68 | return Observable.create(new Observable.OnSubscribe() { 69 | @Override 70 | public void call(Subscriber subscriber) { 71 | mPreferencesHelper.clear(); 72 | subscriber.onCompleted(); 73 | } 74 | }); 75 | } 76 | 77 | public Observable registerBeacon(Beacon beacon) { 78 | return mWatchTowerService.registerBeacon(beacon); 79 | } 80 | 81 | public Observable getBeacons() { 82 | return mWatchTowerService.getBeacons() 83 | .flatMapIterable(new Func1>() { 84 | @Override 85 | public Iterable call(WatchTowerService.BeaconsResponse beaconsResponse) { 86 | return beaconsResponse.beacons; 87 | } 88 | }).flatMap(new Func1>() { 89 | @Override 90 | public Observable call(Beacon beacon) { 91 | return Observable.just(beacon); 92 | } 93 | }); 94 | } 95 | 96 | public Observable updateBeacon(String beaconName, Beacon beacon, final boolean hasStatusChanges, final Beacon.Status status) { 97 | return mWatchTowerService.updateBeacon(beaconName, beacon).flatMap(new Func1>() { 98 | @Override 99 | public Observable call(Beacon beacon) { 100 | if (hasStatusChanges) return setBeaconStatus(beacon, status); 101 | return Observable.just(beacon); 102 | } 103 | }); 104 | } 105 | 106 | public Observable setBeaconStatus(Beacon beacon, Beacon.Status status) { 107 | String name = beacon.beaconName; 108 | Observable statusObservable = null; 109 | switch (status) { 110 | case ACTIVE: 111 | statusObservable = mWatchTowerService.activateBeacon(name); 112 | break; 113 | case INACTIVE: 114 | statusObservable = mWatchTowerService.deactivateBeacon(name); 115 | break; 116 | case DECOMMISSIONED: 117 | statusObservable = mWatchTowerService.decomissionBeacon(name); 118 | break; 119 | case STATUS_UNSPECIFIED: 120 | statusObservable = mWatchTowerService.deactivateBeacon(name); 121 | break; 122 | } 123 | if (statusObservable != null) { 124 | return statusObservable.flatMap(new Func1>() { 125 | @Override 126 | public Observable call(Beacon updateBeacon) { 127 | return mWatchTowerService.getBeacon(updateBeacon.beaconName); 128 | } 129 | }); 130 | } 131 | return Observable.just(beacon); 132 | } 133 | 134 | public Observable getDiagnostics(String beaconName) { 135 | return mWatchTowerService.beaconDiagnostics(beaconName); 136 | } 137 | 138 | public Observable createAttachment(String beaconName, Attachment attachment) { 139 | return mWatchTowerService.createAttachment(beaconName, attachment); 140 | } 141 | 142 | public Observable deleteAttachment(String attachmentName) { 143 | return mWatchTowerService.deleteAttachment(attachmentName); 144 | } 145 | 146 | public Observable getAttachments(String beaconName, String namespaceType) { 147 | return mWatchTowerService.getAttachments(beaconName, namespaceType); 148 | } 149 | 150 | public Observable getNamespaces() { 151 | return mWatchTowerService.getNamespaces(); 152 | } 153 | 154 | public Observable deleteBatchAttachments(final String beaconName, String type) { 155 | return mWatchTowerService.deleteBatchAttachments(beaconName, type).flatMap(new Func1>() { 156 | @Override 157 | public Observable call(Void aVoid) { 158 | return getAttachments(beaconName, null); 159 | } 160 | }); 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/data/local/PreferencesHelper.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.data.local; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | public class PreferencesHelper { 7 | 8 | private static SharedPreferences mPref; 9 | 10 | private static final String PREF_FILE_NAME = "watchtower_pref_file"; 11 | private static final String PREF_KEY_TOKEN = "key_access_token"; 12 | private static final String PREF_KEY_USER = "key_user"; 13 | 14 | 15 | public PreferencesHelper(Context context) { 16 | mPref = context.getSharedPreferences(PREF_FILE_NAME, Context.MODE_PRIVATE); 17 | } 18 | 19 | public void setUser(String user) { 20 | mPref.edit().putString(PREF_KEY_USER, user).apply(); 21 | } 22 | 23 | public void saveToken(String token) { 24 | mPref.edit().putString(PREF_KEY_TOKEN, token).apply(); 25 | } 26 | 27 | public String getUser() { 28 | return mPref.getString(PREF_KEY_USER, null); 29 | } 30 | public String getToken() { 31 | return mPref.getString(PREF_KEY_TOKEN, null); 32 | } 33 | 34 | public void clear() { 35 | mPref.edit().clear().apply(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/data/model/AdvertisedId.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.data.model; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | public class AdvertisedId implements Parcelable { 7 | public String id; 8 | public Type type; 9 | 10 | public enum Type { 11 | TYPE_UNSPECIFIED("Unspecified"), 12 | EDDYSTONE("Eddystone"), 13 | IBEACON("iBeacon"), 14 | ALTBEACON("AltBeacon"); 15 | 16 | private String string; 17 | 18 | Type(String string) { 19 | this.string = string; 20 | } 21 | 22 | public static Type fromString(String string) { 23 | if (string != null) { 24 | for (Type status : Type.values()) { 25 | if (string.equalsIgnoreCase(status.string)) { 26 | return status; 27 | } 28 | } 29 | } 30 | return null; 31 | } 32 | 33 | public String getString() { 34 | return string; 35 | } 36 | 37 | } 38 | 39 | public AdvertisedId(String id, Type type) { 40 | this.id = id; 41 | this.type = type; 42 | } 43 | 44 | @Override 45 | public int describeContents() { 46 | return 0; 47 | } 48 | 49 | @Override 50 | public void writeToParcel(Parcel dest, int flags) { 51 | dest.writeInt(this.type == null ? -1 : this.type.ordinal()); 52 | dest.writeString(this.id); 53 | } 54 | 55 | public AdvertisedId() { } 56 | 57 | protected AdvertisedId(Parcel in) { 58 | int tmpType = in.readInt(); 59 | this.type = tmpType == -1 ? null : Type.values()[tmpType]; 60 | this.id = in.readString(); 61 | } 62 | 63 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 64 | public AdvertisedId createFromParcel(Parcel source) { 65 | return new AdvertisedId(source); 66 | } 67 | 68 | public AdvertisedId[] newArray(int size) { 69 | return new AdvertisedId[size]; 70 | } 71 | }; 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/data/model/Attachment.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.data.model; 2 | 3 | public class Attachment { 4 | public String attachmentName; 5 | public String namespacedType; 6 | public String data; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/data/model/Beacon.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.data.model; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | public class Beacon implements Parcelable { 7 | 8 | public AdvertisedId advertisedId; 9 | public String beaconName; 10 | public Status status; 11 | public Stability expectedStability; 12 | public LatLng latLng; 13 | public String description; 14 | public String placeId; 15 | 16 | public enum Status { 17 | STATUS_UNSPECIFIED("Unspecified"), 18 | ACTIVE("Active"), 19 | DECOMMISSIONED("Decommissioned"), 20 | INACTIVE("Inactive"); 21 | 22 | private String string; 23 | 24 | Status(String string) { 25 | this.string = string; 26 | } 27 | 28 | public static Status fromString(String string) { 29 | if (string != null) { 30 | for (Status status : Status.values()) { 31 | if (string.equalsIgnoreCase(status.string)) { 32 | return status; 33 | } 34 | } 35 | } 36 | return null; 37 | } 38 | 39 | public String getString() { 40 | return string; 41 | } 42 | 43 | public String getDisplayName() { 44 | return string.replaceAll("_", " "); 45 | } 46 | } 47 | 48 | public enum Stability { 49 | STATUS_UNSPECIFIED("Unspecified"), 50 | STABLE("Stable"), 51 | PORTABLE("Portable"), 52 | MOBILE("Mobile"), 53 | ROVING("Roving"); 54 | 55 | private String string; 56 | 57 | Stability(String string) { 58 | this.string = string; 59 | } 60 | 61 | public static Stability fromString(String string) { 62 | if (string != null) { 63 | for (Stability status : Stability.values()) { 64 | if (string.equalsIgnoreCase(status.string)) { 65 | return status; 66 | } 67 | } 68 | } 69 | return null; 70 | } 71 | 72 | public String getString() { 73 | return string; 74 | } 75 | 76 | public String getDisplayName() { 77 | return string.replaceAll("_", " "); 78 | } 79 | } 80 | 81 | private Beacon (BeaconBuilder builder) { 82 | this.advertisedId = builder.advertisedId; 83 | this.beaconName = builder.beaconName; 84 | this.status = builder.status; 85 | this.expectedStability = builder.expectedStability; 86 | this.latLng = builder.latLng; 87 | this.description = builder.description; 88 | this.placeId = builder.placeId; 89 | } 90 | 91 | public static class BeaconBuilder { 92 | private AdvertisedId advertisedId; 93 | private String beaconName; 94 | private Status status; 95 | private Stability expectedStability; 96 | private LatLng latLng; 97 | private String description; 98 | private String placeId; 99 | 100 | public BeaconBuilder(AdvertisedId advertisedId) { 101 | this.advertisedId = advertisedId; 102 | } 103 | 104 | public BeaconBuilder beaconName(String beaconName) { 105 | this.beaconName = beaconName; 106 | return this; 107 | } 108 | 109 | public BeaconBuilder status(Status status) { 110 | this.status = status; 111 | return this; 112 | } 113 | 114 | public BeaconBuilder stability(Stability stability) { 115 | this.expectedStability = stability; 116 | return this; 117 | } 118 | 119 | public BeaconBuilder latLng(LatLng latLng) { 120 | this.latLng = latLng; 121 | return this; 122 | } 123 | 124 | public BeaconBuilder description(String description) { 125 | this.description = description; 126 | return this; 127 | } 128 | 129 | public BeaconBuilder placeId(String placeId) { 130 | this.placeId = placeId; 131 | return this; 132 | } 133 | 134 | public Beacon build() { 135 | return new Beacon(this); 136 | } 137 | 138 | } 139 | 140 | @Override 141 | public int describeContents() { 142 | return 0; 143 | } 144 | 145 | @Override 146 | public void writeToParcel(Parcel dest, int flags) { 147 | dest.writeParcelable(this.advertisedId, 0); 148 | dest.writeString(this.beaconName); 149 | dest.writeInt(this.status == null ? -1 : this.status.ordinal()); 150 | dest.writeInt(this.expectedStability == null ? -1 : this.expectedStability.ordinal()); 151 | dest.writeParcelable(this.latLng, 0); 152 | dest.writeString(this.description); 153 | dest.writeString(this.placeId); 154 | } 155 | 156 | public Beacon() { } 157 | 158 | protected Beacon(Parcel in) { 159 | this.advertisedId = in.readParcelable(AdvertisedId.class.getClassLoader()); 160 | this.beaconName = in.readString(); 161 | int tmpStatus = in.readInt(); 162 | this.status = tmpStatus == -1 ? null : Status.values()[tmpStatus]; 163 | int tmpStability = in.readInt(); 164 | this.expectedStability = tmpStability == -1 ? null : Stability.values()[tmpStability]; 165 | this.latLng = in.readParcelable(LatLng.class.getClassLoader()); 166 | this.description = in.readString(); 167 | this.placeId = in.readString(); 168 | } 169 | 170 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 171 | public Beacon createFromParcel(Parcel source) { 172 | return new Beacon(source); 173 | } 174 | 175 | public Beacon[] newArray(int size) { 176 | return new Beacon[size]; 177 | } 178 | }; 179 | } 180 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/data/model/Diagnostics.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.data.model; 2 | 3 | public class Diagnostics { 4 | 5 | public enum Alert {WRONG_LOCATION, LOW_BATTERY} 6 | 7 | public String beaconName; 8 | public BeaconDate estimatedLowBatteryDate; 9 | public Alert[] alerts; 10 | 11 | public static class BeaconDate { 12 | public int year; 13 | public int month; 14 | public int day; 15 | 16 | public String buildDate() { 17 | return String.format("%d/%d/%d", this.day, this.month, this.year); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/data/model/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.data.model; 2 | 3 | public class ErrorResponse { 4 | public Error error; 5 | public class Error { 6 | public int code; 7 | public String message; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/data/model/LatLng.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.data.model; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | public class LatLng implements Parcelable { 7 | public Double latitude; 8 | public Double longitude; 9 | 10 | public LatLng() { } 11 | 12 | public LatLng(Double latitude, Double longitude) { 13 | this.latitude = latitude; 14 | this.longitude = longitude; 15 | } 16 | 17 | @Override 18 | public int describeContents() { 19 | return 0; 20 | } 21 | 22 | @Override 23 | public void writeToParcel(Parcel dest, int flags) { 24 | dest.writeValue(this.latitude); 25 | dest.writeValue(this.longitude); 26 | } 27 | 28 | protected LatLng(Parcel in) { 29 | this.latitude = (Double) in.readValue(Double.class.getClassLoader()); 30 | this.longitude = (Double) in.readValue(Double.class.getClassLoader()); 31 | } 32 | 33 | public static final Creator CREATOR = new Creator() { 34 | public LatLng createFromParcel(Parcel source) { 35 | return new LatLng(source); 36 | } 37 | 38 | public LatLng[] newArray(int size) { 39 | return new LatLng[size]; 40 | } 41 | }; 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/data/model/Namespace.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.data.model; 2 | 3 | public class Namespace { 4 | public String namespaceName; 5 | public Visibility servingVisibility; 6 | 7 | public enum Visibility { 8 | VISIBILITY_UNSPECIFIED("Unspecified"), 9 | UNLISTED("Unlisted"), 10 | PUBLIC("Public"); 11 | 12 | private String string; 13 | 14 | Visibility(String string) { 15 | this.string = string; 16 | } 17 | 18 | public String getString() { 19 | return string; 20 | } 21 | 22 | } 23 | 24 | public Namespace() { } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/data/remote/RetrofitHelper.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.data.remote; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | 7 | import com.google.gson.GsonBuilder; 8 | import com.hitherejoe.watchtower.WatchTowerApplication; 9 | import com.hitherejoe.watchtower.data.BusEvent; 10 | 11 | import retrofit.ErrorHandler; 12 | import retrofit.RequestInterceptor; 13 | import retrofit.RestAdapter; 14 | import retrofit.RetrofitError; 15 | import retrofit.client.Response; 16 | import retrofit.converter.GsonConverter; 17 | 18 | public class RetrofitHelper { 19 | 20 | public WatchTowerService newWatchTowerService(final Context context) { 21 | RestAdapter restAdapter = new RestAdapter.Builder() 22 | .setEndpoint(WatchTowerService.ENDPOINT) 23 | .setErrorHandler(new ResponseErrorHandler(context)) 24 | .setLogLevel(RestAdapter.LogLevel.FULL) 25 | .setConverter(new GsonConverter(new GsonBuilder().create())) 26 | .setRequestInterceptor(new RequestInterceptor() { 27 | @Override 28 | public void intercept(RequestInterceptor.RequestFacade request) { 29 | String accessToken = WatchTowerApplication.get(context).getComponent().dataManager().getPreferencesHelper().getToken(); 30 | 31 | if (accessToken != null) { 32 | request.addHeader("Authorization", addBearerToken(accessToken)); 33 | } 34 | } 35 | }) 36 | .build(); 37 | return restAdapter.create(WatchTowerService.class); 38 | } 39 | 40 | public static String addBearerToken(String authToken) { 41 | return "Bearer " + authToken; 42 | } 43 | 44 | private class ResponseErrorHandler implements ErrorHandler { 45 | 46 | private Context mContext; 47 | 48 | public ResponseErrorHandler(Context context) { 49 | mContext = context; 50 | } 51 | 52 | @Override public Throwable handleError(RetrofitError cause) { 53 | Response response = cause.getResponse(); 54 | if (response != null && (response.getStatus() == 401 || response.getStatus() == 403)) { 55 | new Handler(Looper.getMainLooper()).post(new Runnable() { 56 | @Override 57 | public void run() { 58 | WatchTowerApplication.get(mContext).getComponent().eventBus().post(new BusEvent.AuthenticationError()); 59 | } 60 | }); 61 | } 62 | return cause; 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/data/remote/WatchTowerService.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.data.remote; 2 | 3 | import com.hitherejoe.watchtower.data.model.Attachment; 4 | import com.hitherejoe.watchtower.data.model.Beacon; 5 | import com.hitherejoe.watchtower.data.model.Diagnostics; 6 | import com.hitherejoe.watchtower.data.model.Namespace; 7 | 8 | import java.util.List; 9 | 10 | import retrofit.http.Body; 11 | import retrofit.http.DELETE; 12 | import retrofit.http.GET; 13 | import retrofit.http.POST; 14 | import retrofit.http.PUT; 15 | import retrofit.http.Path; 16 | import retrofit.http.Query; 17 | import rx.Observable; 18 | 19 | public interface WatchTowerService { 20 | 21 | String ENDPOINT = "https://proximitybeacon.googleapis.com/v1beta1"; 22 | 23 | /** 24 | * Return a list of beacons 25 | */ 26 | @GET("/beacons") 27 | Observable getBeacons(); 28 | 29 | /** 30 | * Return a single beacon 31 | */ 32 | @GET("/{beaconName}") 33 | Observable getBeacon(@Path(value="beaconName", encode=false) String beaconName); 34 | 35 | /** 36 | * Register a beacon 37 | */ 38 | @POST("/beacons:register") 39 | Observable registerBeacon(@Body Beacon beacon); 40 | 41 | /** 42 | * Update a beacon 43 | */ 44 | @PUT("/{beaconName}") 45 | Observable updateBeacon(@Path(value="beaconName", encode=false) String beaconName, @Body Beacon beacon); 46 | 47 | /** 48 | * Activate a beacon 49 | */ 50 | @POST("/{beaconName}:activate") 51 | Observable activateBeacon(@Path(value="beaconName", encode=false) String beaconName); 52 | 53 | /** 54 | * Deactivate a beacon 55 | */ 56 | @POST("/{beaconName}:deactivate") 57 | Observable deactivateBeacon(@Path(value="beaconName", encode=false) String beaconName); 58 | 59 | /** 60 | * Decomission a beacon 61 | */ 62 | @POST("/{beaconName}:decommission") 63 | Observable decomissionBeacon(@Path(value="beaconName", encode=false) String beaconName); 64 | 65 | /** 66 | * Retrieve diagnostics for a beacon 67 | */ 68 | @GET("/{beaconName}/diagnostics") 69 | Observable beaconDiagnostics(@Path(value="beaconName", encode=false) String beaconName); 70 | 71 | /** 72 | * Create an attachment 73 | */ 74 | @POST("/{beaconName}/attachments") 75 | Observable createAttachment(@Path(value="beaconName", encode=false) String beaconName, @Body Attachment attachment); 76 | 77 | /** 78 | * Delete an attachment 79 | */ 80 | @DELETE("/{attachmentName}") 81 | Observable deleteAttachment(@Path(value="attachmentName", encode=false) String attachmentName); 82 | 83 | /** 84 | * Delete a batch of attachments 85 | */ 86 | @POST("/{beaconName}/attachments:batchDelete") 87 | Observable deleteBatchAttachments(@Path(value="beaconName", encode=false) String beaconName, @Query("namespacedType") String namespacedType); 88 | 89 | /** 90 | * Retrieve attachments 91 | */ 92 | @GET("/{beaconName}/attachments") 93 | Observable getAttachments(@Path(value="beaconName", encode=false) String beaconName, @Query("namespacedType") String namespacedType); 94 | 95 | /** 96 | * Retrieve namespaces 97 | */ 98 | @GET("/namespaces") 99 | Observable getNamespaces(); 100 | 101 | class BeaconsResponse { 102 | public List beacons; 103 | } 104 | 105 | class NamespacesResponse { 106 | public List namespaces; 107 | } 108 | 109 | class AttachmentResponse { 110 | public List attachments; 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/injection/component/ApplicationComponent.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.injection.component; 2 | 3 | import android.app.Application; 4 | 5 | import com.hitherejoe.watchtower.data.DataManager; 6 | import com.hitherejoe.watchtower.injection.module.ApplicationModule; 7 | import com.hitherejoe.watchtower.ui.activity.MainActivity; 8 | import com.squareup.otto.Bus; 9 | 10 | import javax.inject.Singleton; 11 | 12 | import dagger.Component; 13 | 14 | @Singleton 15 | @Component(modules = ApplicationModule.class) 16 | public interface ApplicationComponent { 17 | 18 | void inject(MainActivity mainActivity); 19 | 20 | Application application(); 21 | DataManager dataManager(); 22 | Bus eventBus(); 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/injection/component/DataManagerComponent.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.injection.component; 2 | 3 | import com.hitherejoe.watchtower.data.DataManager; 4 | import com.hitherejoe.watchtower.injection.module.DataManagerModule; 5 | import com.hitherejoe.watchtower.injection.scope.PerDataManager; 6 | 7 | import dagger.Component; 8 | 9 | @PerDataManager 10 | @Component(dependencies = ApplicationComponent.class, modules = DataManagerModule.class) 11 | public interface DataManagerComponent { 12 | 13 | void inject(DataManager dataManager); 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/injection/module/ApplicationModule.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.injection.module; 2 | 3 | import android.app.Application; 4 | 5 | import com.hitherejoe.watchtower.data.DataManager; 6 | import com.squareup.otto.Bus; 7 | 8 | import javax.inject.Singleton; 9 | 10 | import dagger.Module; 11 | import dagger.Provides; 12 | 13 | /** 14 | * Provide application-level dependencies. Mainly singleton object that can be injected from 15 | * anywhere in the app. 16 | */ 17 | @Module 18 | public class ApplicationModule { 19 | protected final Application mApplication; 20 | 21 | public ApplicationModule(Application application) { 22 | mApplication = application; 23 | } 24 | 25 | @Provides 26 | @Singleton 27 | Application provideApplication() { 28 | return mApplication; 29 | } 30 | 31 | @Provides 32 | @Singleton 33 | DataManager provideDataManager() { 34 | return new DataManager(mApplication); 35 | } 36 | 37 | @Provides 38 | @Singleton 39 | Bus provideEventBus() { 40 | return new Bus(); 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/injection/module/DataManagerModule.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.injection.module; 2 | 3 | import android.content.Context; 4 | 5 | import com.hitherejoe.watchtower.data.local.PreferencesHelper; 6 | import com.hitherejoe.watchtower.data.remote.RetrofitHelper; 7 | import com.hitherejoe.watchtower.data.remote.WatchTowerService; 8 | import com.hitherejoe.watchtower.injection.scope.PerDataManager; 9 | 10 | import dagger.Module; 11 | import dagger.Provides; 12 | import rx.Scheduler; 13 | import rx.schedulers.Schedulers; 14 | 15 | /** 16 | * Provide dependencies to the DataManager, mainly Helper classes and Retrofit services. 17 | */ 18 | @Module 19 | public class DataManagerModule { 20 | 21 | private final Context mContext; 22 | 23 | public DataManagerModule(Context context) { 24 | mContext = context; 25 | } 26 | 27 | @Provides 28 | @PerDataManager 29 | PreferencesHelper providePreferencesHelper() { 30 | return new PreferencesHelper(mContext); 31 | } 32 | 33 | @Provides 34 | @PerDataManager 35 | WatchTowerService provideRibotsService() { 36 | return new RetrofitHelper().newWatchTowerService(mContext); 37 | } 38 | 39 | @Provides 40 | @PerDataManager 41 | Scheduler provideSubscribeScheduler() { 42 | return Schedulers.io(); 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/injection/scope/PerDataManager.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.injection.scope; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Scope; 7 | 8 | /** 9 | * A scoping annotation to permit objects whose lifetime should 10 | * conform to the life of the DataManager to be memorised in the 11 | * correct component. 12 | */ 13 | @Scope 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface PerDataManager { 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/ui/activity/AddAttachmentActivity.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.ui.activity; 2 | 3 | import android.app.Dialog; 4 | import android.app.ProgressDialog; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.content.Intent; 8 | import android.os.Bundle; 9 | import android.support.v7.app.ActionBar; 10 | import android.view.Menu; 11 | import android.view.MenuItem; 12 | import android.view.View; 13 | import android.widget.ArrayAdapter; 14 | import android.widget.EditText; 15 | import android.widget.Spinner; 16 | import android.widget.TextView; 17 | 18 | import com.hitherejoe.watchtower.R; 19 | import com.hitherejoe.watchtower.WatchTowerApplication; 20 | import com.hitherejoe.watchtower.data.BusEvent; 21 | import com.hitherejoe.watchtower.data.DataManager; 22 | import com.hitherejoe.watchtower.data.model.Attachment; 23 | import com.hitherejoe.watchtower.data.model.Beacon; 24 | import com.hitherejoe.watchtower.data.model.Namespace; 25 | import com.hitherejoe.watchtower.data.remote.WatchTowerService; 26 | import com.hitherejoe.watchtower.util.DataUtils; 27 | import com.hitherejoe.watchtower.util.DialogFactory; 28 | 29 | import butterknife.Bind; 30 | import butterknife.ButterKnife; 31 | import retrofit.RetrofitError; 32 | import rx.Subscriber; 33 | import rx.android.schedulers.AndroidSchedulers; 34 | import rx.subscriptions.CompositeSubscription; 35 | import timber.log.Timber; 36 | 37 | public class AddAttachmentActivity extends BaseActivity { 38 | 39 | @Bind(R.id.spinner_namespace) 40 | Spinner mNamespaceSpinner; 41 | 42 | @Bind(R.id.edit_text_data) 43 | EditText mAttachmentDataText; 44 | 45 | @Bind(R.id.text_data_error_message) 46 | TextView mDataErrorText; 47 | 48 | private DataManager mDataManager; 49 | private CompositeSubscription mSubscriptions; 50 | private static final String EXTRA_BEACON = 51 | "com.hitherejoe.watchtower.ui.activity.UpdateActivity.EXTRA_BEACON"; 52 | private Beacon mBeacon; 53 | private ProgressDialog mProgressDialog; 54 | ArrayAdapter mSpinnerAdapter; 55 | 56 | public static Intent getStartIntent(Context context, Beacon beacon) { 57 | Intent intent = new Intent(context, AddAttachmentActivity.class); 58 | intent.putExtra(EXTRA_BEACON, beacon); 59 | return intent; 60 | } 61 | 62 | @Override 63 | protected void onCreate(Bundle savedInstanceState) { 64 | super.onCreate(savedInstanceState); 65 | setContentView(R.layout.activity_add_attachment); 66 | ButterKnife.bind(this); 67 | mBeacon = getIntent().getParcelableExtra(EXTRA_BEACON); 68 | if (mBeacon == null) throw new IllegalArgumentException("Beacon is required!"); 69 | mSubscriptions = new CompositeSubscription(); 70 | mDataManager = WatchTowerApplication.get(this).getComponent().dataManager(); 71 | mSpinnerAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item); 72 | mNamespaceSpinner.setAdapter(mSpinnerAdapter); 73 | setupActionBar(); 74 | getNameSpacesIfNetworkAvailable(); 75 | } 76 | 77 | @Override 78 | protected void onDestroy() { 79 | super.onDestroy(); 80 | mSubscriptions.unsubscribe(); 81 | } 82 | 83 | @Override 84 | public boolean onCreateOptionsMenu(Menu menu) { 85 | getMenuInflater().inflate(R.menu.add_attachment, menu); 86 | return true; 87 | } 88 | 89 | @Override 90 | public boolean onOptionsItemSelected(MenuItem item) { 91 | switch (item.getItemId()) { 92 | case R.id.action_done: 93 | validateAttachmentData(); 94 | return true; 95 | default: 96 | return super.onOptionsItemSelected(item); 97 | } 98 | } 99 | 100 | private void setupActionBar() { 101 | ActionBar actionBar = getSupportActionBar(); 102 | if (actionBar != null) { 103 | actionBar.setDisplayHomeAsUpEnabled(true); 104 | } 105 | } 106 | 107 | private void getNameSpacesIfNetworkAvailable() { 108 | if (DataUtils.isNetworkAvailable(this)) { 109 | retrieveNamespaces(); 110 | } else { 111 | Dialog dialog = DialogFactory.createSimpleOkErrorDialog( 112 | this, 113 | getString(R.string.dialog_error_title), 114 | getString(R.string.dialog_error_no_connection) 115 | ); 116 | dialog.setCanceledOnTouchOutside(true); 117 | dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { 118 | @Override 119 | public void onDismiss(DialogInterface dialog) { 120 | finish(); 121 | } 122 | }); 123 | dialog.show(); 124 | } 125 | } 126 | 127 | private void validateAttachmentData() { 128 | String data = mAttachmentDataText.getText().toString(); 129 | mDataErrorText.setVisibility(data.length() == 0 ? View.VISIBLE : View.GONE); 130 | if (data.length() == 0) { 131 | DialogFactory.createSimpleOkErrorDialog( 132 | this, 133 | getString(R.string.dialog_error_title), 134 | getString(R.string.dialog_error_blank_data) 135 | ).show(); 136 | } else if (data.contains(" ")) { 137 | DialogFactory.createSimpleOkErrorDialog( 138 | this, 139 | getString(R.string.dialog_error_title), 140 | getString(R.string.dialog_error_invalid_data) 141 | ).show(); 142 | } else if (data.length() > 0) { 143 | //TODO: For example purposes, allow more data types than text to be used 144 | Attachment attachment = new Attachment(); 145 | attachment.data = DataUtils.base64Encode(data.getBytes()); 146 | attachment.namespacedType = mNamespaceSpinner.getSelectedItem() + "/text"; 147 | addAttachment(attachment); 148 | } 149 | } 150 | 151 | private void retrieveNamespaces() { 152 | mProgressDialog = DialogFactory.createProgressDialog(this, R.string.progress_dialog_retrieving_namespaces); 153 | mProgressDialog.show(); 154 | mSubscriptions.add(mDataManager.getNamespaces() 155 | .observeOn(AndroidSchedulers.mainThread()) 156 | .subscribeOn(mDataManager.getScheduler()) 157 | .subscribe(new Subscriber() { 158 | @Override 159 | public void onCompleted() { 160 | mProgressDialog.dismiss(); 161 | } 162 | 163 | @Override 164 | public void onError(Throwable error) { 165 | mProgressDialog.dismiss(); 166 | Timber.e("There was an error retrieving the namespaces " + error); 167 | if (error instanceof RetrofitError) { 168 | DialogFactory.createRetrofitErrorDialog( 169 | AddAttachmentActivity.this, (RetrofitError) error).show(); 170 | } else { 171 | DialogFactory.createSimpleErrorDialog( 172 | AddAttachmentActivity.this).show(); 173 | } 174 | } 175 | 176 | @Override 177 | public void onNext(WatchTowerService.NamespacesResponse namespacesResponse) { 178 | for (Namespace namespace : namespacesResponse.namespaces) { 179 | mSpinnerAdapter.add(namespace.namespaceName); 180 | } 181 | } 182 | })); 183 | } 184 | 185 | private void addAttachment(Attachment attachment) { 186 | if (DataUtils.isNetworkAvailable(this)) { 187 | showProgressDialog(R.string.progress_dialog_adding_attachment); 188 | mSubscriptions.add(mDataManager.createAttachment(mBeacon.beaconName, attachment) 189 | .observeOn(AndroidSchedulers.mainThread()) 190 | .subscribeOn(mDataManager.getScheduler()) 191 | .subscribe(new Subscriber() { 192 | @Override 193 | public void onCompleted() { 194 | mProgressDialog.dismiss(); 195 | WatchTowerApplication.get(AddAttachmentActivity.this) 196 | .getComponent().eventBus().post(new BusEvent.AttachmentAdded()); 197 | finish(); 198 | } 199 | 200 | @Override 201 | public void onError(Throwable error) { 202 | mProgressDialog.dismiss(); 203 | Timber.e("There was a problem adding the attachment " + error); 204 | if (error instanceof RetrofitError) { 205 | DialogFactory.createRetrofitErrorDialog( 206 | AddAttachmentActivity.this, (RetrofitError) error).show(); 207 | } else { 208 | DialogFactory.createSimpleErrorDialog( 209 | AddAttachmentActivity.this).show(); 210 | } 211 | } 212 | 213 | @Override 214 | public void onNext(Attachment attachment) { 215 | } 216 | })); 217 | } else { 218 | DialogFactory.createSimpleOkErrorDialog( 219 | this, 220 | getString(R.string.dialog_error_title), 221 | getString(R.string.dialog_error_no_connection) 222 | ).show(); 223 | } 224 | } 225 | 226 | private void showProgressDialog(int messageResourceId) { 227 | mProgressDialog = DialogFactory.createProgressDialog(this, messageResourceId); 228 | mProgressDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { 229 | @Override 230 | public void onDismiss(DialogInterface dialog) { 231 | mSubscriptions.unsubscribe(); 232 | } 233 | }); 234 | mProgressDialog.show(); 235 | } 236 | 237 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/ui/activity/AuthActivity.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.ui.activity; 2 | 3 | import android.accounts.Account; 4 | import android.accounts.AccountManager; 5 | import android.accounts.AccountManagerCallback; 6 | import android.accounts.AccountManagerFuture; 7 | import android.accounts.AuthenticatorException; 8 | import android.accounts.OperationCanceledException; 9 | import android.app.Dialog; 10 | import android.content.Context; 11 | import android.content.DialogInterface; 12 | import android.content.Intent; 13 | import android.os.Bundle; 14 | 15 | import com.google.android.gms.common.AccountPicker; 16 | import com.google.android.gms.common.ConnectionResult; 17 | import com.google.android.gms.common.GooglePlayServicesUtil; 18 | import com.hitherejoe.watchtower.R; 19 | import com.hitherejoe.watchtower.WatchTowerApplication; 20 | import com.hitherejoe.watchtower.data.DataManager; 21 | import com.hitherejoe.watchtower.util.AccountUtils; 22 | import com.hitherejoe.watchtower.util.DataUtils; 23 | import com.hitherejoe.watchtower.util.DialogFactory; 24 | 25 | import java.io.IOException; 26 | 27 | import rx.subscriptions.CompositeSubscription; 28 | import timber.log.Timber; 29 | 30 | public class AuthActivity extends BaseActivity { 31 | 32 | 33 | public static final String EXTRA_SHOULD_SHOW_AUTH_MESSAGE = 34 | "com.hitherejoe.ui.activity.AuthActivity.EXTRA_SHOULD_SHOW_AUTH_MESSAGE"; 35 | private static final String REQUEST_SCOPE = 36 | "oauth2:https://www.googleapis.com/auth/userlocation.beacon.registry"; 37 | private static final String ACCOUNT_TYPE = "com.google"; 38 | 39 | private static final int REQUEST_CODE_AUTHORIZATION = 1234; 40 | private static final int REQUEST_CODE_PLAY_SERVICES = 1235; 41 | private static final int REQUEST_CODE_PICK_ACCOUNT = 1236; 42 | 43 | 44 | private DataManager mDataManager; 45 | private CompositeSubscription mSubscriptions; 46 | private AccountManager mAccountManager; 47 | 48 | public static Intent getStartIntent(Context context, boolean shouldShowAuthMessage) { 49 | Intent intent = new Intent(context, AuthActivity.class); 50 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 51 | intent.putExtra(AuthActivity.EXTRA_SHOULD_SHOW_AUTH_MESSAGE, shouldShowAuthMessage); 52 | return intent; 53 | } 54 | 55 | @Override 56 | protected void onCreate(Bundle savedInstanceState) { 57 | super.onCreate(savedInstanceState); 58 | mAccountManager = AccountManager.get(this); 59 | mSubscriptions = new CompositeSubscription(); 60 | mDataManager = WatchTowerApplication.get(this).getComponent().dataManager(); 61 | 62 | if (checkPlayServices()) { 63 | if (getIntent().getBooleanExtra(EXTRA_SHOULD_SHOW_AUTH_MESSAGE, false)) { 64 | showErrorDialog(getString(R.string.dialog_error_unauthorised_response)); 65 | } else { 66 | chooseAccount(); 67 | } 68 | } 69 | } 70 | 71 | @Override 72 | protected void onDestroy() { 73 | super.onDestroy(); 74 | mSubscriptions.unsubscribe(); 75 | } 76 | 77 | @Override 78 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 79 | super.onActivityResult(requestCode, resultCode, data); 80 | if (resultCode == RESULT_OK) { 81 | if (requestCode == REQUEST_CODE_AUTHORIZATION) { 82 | requestToken(); 83 | } else if (requestCode == REQUEST_CODE_PICK_ACCOUNT) { 84 | String accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); 85 | mDataManager.getPreferencesHelper().setUser(accountName); 86 | AccountUtils.invalidateToken(this); 87 | requestToken(); 88 | } else { 89 | showErrorDialog(getString(R.string.dialog_error_account_chooser)); 90 | } 91 | } else { 92 | showErrorDialog(getString(R.string.dialog_error_account_chooser)); 93 | } 94 | } 95 | 96 | private void showErrorDialog(String message) { 97 | Dialog dialog = DialogFactory.createAuthErrorDialog(this, 98 | message, 99 | new DialogInterface.OnClickListener() { 100 | @Override 101 | public void onClick(DialogInterface dialog, int which) { 102 | chooseAccount(); 103 | } 104 | }, 105 | new DialogInterface.OnClickListener() { 106 | @Override 107 | public void onClick(DialogInterface dialog, int which) { 108 | finish(); 109 | } 110 | }); 111 | dialog.setCanceledOnTouchOutside(true); 112 | dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { 113 | @Override 114 | public void onCancel(DialogInterface dialog) { 115 | finish(); 116 | } 117 | }); 118 | dialog.show(); 119 | } 120 | 121 | private boolean checkPlayServices() { 122 | int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this); 123 | if (resultCode != ConnectionResult.SUCCESS) { 124 | if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) { 125 | GooglePlayServicesUtil.getErrorDialog( 126 | resultCode, this, REQUEST_CODE_PLAY_SERVICES).show(); 127 | } else { 128 | Dialog playServicesDialog = DialogFactory.createSimpleOkErrorDialog( 129 | this, 130 | getString(R.string.dialog_error_title), 131 | getString(R.string.error_message_play_services) 132 | ); 133 | playServicesDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { 134 | @Override 135 | public void onDismiss(DialogInterface dialog) { 136 | finish(); 137 | } 138 | }); 139 | playServicesDialog.show(); 140 | } 141 | return false; 142 | } 143 | return true; 144 | } 145 | 146 | private void chooseAccount() { 147 | if (DataUtils.isNetworkAvailable(this)) { 148 | Intent intent = AccountPicker.newChooseAccountIntent(null, null, 149 | new String[]{ACCOUNT_TYPE}, false, null, null, null, null); 150 | startActivityForResult(intent, REQUEST_CODE_PICK_ACCOUNT); 151 | } else { 152 | Dialog noConnectionDialog = DialogFactory.createSimpleOkErrorDialog( 153 | this, 154 | getString(R.string.dialog_error_title), 155 | getString(R.string.dialog_error_no_connection) 156 | ); 157 | noConnectionDialog.setCanceledOnTouchOutside(true); 158 | noConnectionDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { 159 | @Override 160 | public void onDismiss(DialogInterface dialog) { 161 | finish(); 162 | } 163 | }); 164 | noConnectionDialog.show(); 165 | } 166 | } 167 | 168 | private void requestToken() { 169 | Account userAccount = null; 170 | String user = mDataManager.getPreferencesHelper().getUser(); 171 | for (Account account : mAccountManager.getAccountsByType(ACCOUNT_TYPE)) { 172 | if (account.name.equals(user)) { 173 | userAccount = account; 174 | break; 175 | } 176 | } 177 | mAccountManager.getAuthToken(userAccount, REQUEST_SCOPE, null, this, 178 | new OnTokenReceivedCallback(), null); 179 | } 180 | 181 | private void startMainActivity() { 182 | startActivity(MainActivity.getStartIntent(this)); 183 | finish(); 184 | } 185 | 186 | private class OnTokenReceivedCallback implements AccountManagerCallback { 187 | 188 | @Override 189 | public void run(AccountManagerFuture result) { 190 | try { 191 | Bundle bundle = result.getResult(); 192 | 193 | Intent launch = (Intent) bundle.get(AccountManager.KEY_INTENT); 194 | if (launch != null) { 195 | startActivityForResult(launch, REQUEST_CODE_AUTHORIZATION); 196 | } else { 197 | String token = bundle.getString(AccountManager.KEY_AUTHTOKEN); 198 | mDataManager.getPreferencesHelper().saveToken(token); 199 | startMainActivity(); 200 | } 201 | } catch (AuthenticatorException e) { 202 | Timber.e("There was an Authenticator error: " + e); 203 | } catch (OperationCanceledException e) { 204 | Timber.e("There was an Operation error: " + e); 205 | } catch (IOException e) { 206 | Timber.e("There was an IO Exception: " + e); 207 | } 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/ui/activity/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.ui.activity; 2 | 3 | import android.app.FragmentManager; 4 | import android.os.Bundle; 5 | import android.support.v7.app.ActionBarActivity; 6 | import android.view.Menu; 7 | import android.view.MenuItem; 8 | 9 | public class BaseActivity extends ActionBarActivity { 10 | 11 | @Override 12 | protected void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | } 15 | 16 | @Override 17 | public boolean onCreateOptionsMenu(Menu menu) { 18 | return super.onCreateOptionsMenu(menu); 19 | } 20 | 21 | @Override 22 | public boolean onOptionsItemSelected(MenuItem item) { 23 | switch (item.getItemId()) { 24 | case android.R.id.home: 25 | FragmentManager fm = getFragmentManager(); 26 | if (fm.getBackStackEntryCount() > 0) { 27 | fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); 28 | } else { 29 | finish(); 30 | } 31 | return true; 32 | default: 33 | return super.onOptionsItemSelected(item); 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/ui/activity/DetailActivity.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.ui.activity; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.design.widget.TabLayout; 7 | import android.support.v4.app.Fragment; 8 | import android.support.v4.app.FragmentPagerAdapter; 9 | import android.support.v4.view.ViewPager; 10 | import android.support.v7.app.ActionBar; 11 | import android.support.v7.widget.Toolbar; 12 | import android.view.Menu; 13 | import android.view.MenuItem; 14 | 15 | import com.hitherejoe.watchtower.R; 16 | import com.hitherejoe.watchtower.WatchTowerApplication; 17 | import com.hitherejoe.watchtower.data.BusEvent; 18 | import com.hitherejoe.watchtower.data.model.Beacon; 19 | import com.hitherejoe.watchtower.ui.fragment.AlertsFragment; 20 | import com.hitherejoe.watchtower.ui.fragment.PropertiesFragment; 21 | import com.squareup.otto.Subscribe; 22 | 23 | import butterknife.Bind; 24 | import butterknife.ButterKnife; 25 | 26 | public class DetailActivity extends BaseActivity { 27 | 28 | @Bind(R.id.sliding_tabs) 29 | TabLayout mTabLayout; 30 | 31 | @Bind(R.id.toolbar) 32 | Toolbar mToolbar; 33 | 34 | @Bind(R.id.pager_beacon_detail) 35 | ViewPager mBeaconDetailViewPager; 36 | 37 | private static final String EXTRA_BEACON = 38 | "com.hitherejoe.watchtower.ui.activity.DetailActivity.EXTRA_BEACON"; 39 | private Beacon mBeacon; 40 | 41 | public static Intent getStartIntent(Context context, Beacon beacon) { 42 | Intent intent = new Intent(context, DetailActivity.class); 43 | intent.putExtra(EXTRA_BEACON, beacon); 44 | return intent; 45 | } 46 | 47 | @Override 48 | protected void onCreate(Bundle savedInstanceState) { 49 | super.onCreate(savedInstanceState); 50 | setContentView(R.layout.activity_detail); 51 | ButterKnife.bind(this); 52 | mBeacon = getIntent().getParcelableExtra(EXTRA_BEACON); 53 | if (mBeacon == null) { 54 | throw new IllegalArgumentException("DetailActivity requires a Beacon object!"); 55 | } 56 | setupToolbar(); 57 | setupViewPager(); 58 | WatchTowerApplication.get(this).getComponent().eventBus().register(this); 59 | } 60 | 61 | @Override 62 | protected void onDestroy() { 63 | super.onDestroy(); 64 | WatchTowerApplication.get(this).getComponent().eventBus().unregister(this); 65 | } 66 | 67 | @Override 68 | public boolean onCreateOptionsMenu(Menu menu) { 69 | getMenuInflater().inflate(R.menu.detail, menu); 70 | return true; 71 | } 72 | 73 | @Override 74 | public boolean onOptionsItemSelected(MenuItem item) { 75 | switch (item.getItemId()) { 76 | case R.id.action_edit: 77 | Intent intent = PropertiesActivity.getStartIntent( 78 | DetailActivity.this, mBeacon, PropertiesFragment.Mode.UPDATE); 79 | startActivity(intent); 80 | return true; 81 | default: 82 | return super.onOptionsItemSelected(item); 83 | } 84 | } 85 | 86 | @Subscribe 87 | public void onBeaconUpdated(BusEvent.BeaconUpdated event) { 88 | mBeacon = event.beacon; 89 | } 90 | 91 | private void setupToolbar() { 92 | setSupportActionBar(mToolbar); 93 | ActionBar actionBar = getSupportActionBar(); 94 | if (actionBar != null) { 95 | actionBar.setDisplayShowTitleEnabled(true); 96 | actionBar.setDisplayHomeAsUpEnabled(true); 97 | } 98 | } 99 | 100 | private void setupViewPager() { 101 | mBeaconDetailViewPager.setOffscreenPageLimit(2); 102 | mBeaconDetailViewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) { 103 | 104 | String[] titles = getResources().getStringArray(R.array.detail_fragment_titles); 105 | 106 | @Override 107 | public Fragment getItem(int position) { 108 | return position == 0 109 | ? PropertiesFragment.newInstance(mBeacon, PropertiesFragment.Mode.VIEW) 110 | : AlertsFragment.newInstance(mBeacon); 111 | } 112 | 113 | @Override 114 | public CharSequence getPageTitle(int position) { 115 | return titles[position]; 116 | } 117 | 118 | @Override 119 | public int getCount() { 120 | return titles.length; 121 | } 122 | }); 123 | mTabLayout.setupWithViewPager(mBeaconDetailViewPager); 124 | } 125 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/ui/activity/LauncherActivity.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.ui.activity; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | 6 | import com.hitherejoe.watchtower.util.AccountUtils; 7 | 8 | public class LauncherActivity extends Activity { 9 | 10 | public LauncherActivity() { } 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | startActivity(AccountUtils.isUserAuthenticated(this) 16 | ? MainActivity.getStartIntent(this) 17 | : AuthActivity.getStartIntent(this, false)); 18 | finish(); 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/ui/activity/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.ui.activity; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.os.Bundle; 7 | import android.support.v4.widget.SwipeRefreshLayout; 8 | import android.support.v7.widget.LinearLayoutManager; 9 | import android.support.v7.widget.RecyclerView; 10 | import android.view.Menu; 11 | import android.view.MenuItem; 12 | import android.view.View; 13 | import android.widget.ProgressBar; 14 | import android.widget.TextView; 15 | 16 | import com.hitherejoe.watchtower.R; 17 | import com.hitherejoe.watchtower.WatchTowerApplication; 18 | import com.hitherejoe.watchtower.data.BusEvent; 19 | import com.hitherejoe.watchtower.data.DataManager; 20 | import com.hitherejoe.watchtower.data.model.Beacon; 21 | import com.hitherejoe.watchtower.ui.adapter.BeaconHolder; 22 | import com.hitherejoe.watchtower.ui.fragment.PropertiesFragment; 23 | import com.hitherejoe.watchtower.util.DataUtils; 24 | import com.hitherejoe.watchtower.util.DialogFactory; 25 | import com.squareup.otto.Subscribe; 26 | 27 | import java.util.ArrayList; 28 | 29 | import butterknife.Bind; 30 | import butterknife.ButterKnife; 31 | import butterknife.OnClick; 32 | import retrofit.RetrofitError; 33 | import rx.Subscriber; 34 | import rx.android.schedulers.AndroidSchedulers; 35 | import rx.subscriptions.CompositeSubscription; 36 | import timber.log.Timber; 37 | import uk.co.ribot.easyadapter.EasyRecyclerAdapter; 38 | 39 | public class MainActivity extends BaseActivity { 40 | 41 | @Bind(R.id.progress_indicator) 42 | ProgressBar mProgressBar; 43 | 44 | @Bind(R.id.recycler_beacons) 45 | RecyclerView mBeaconsRecycler; 46 | 47 | @Bind(R.id.swipe_refresh) 48 | SwipeRefreshLayout mSwipeRefresh; 49 | 50 | @Bind(R.id.text_no_beacons) 51 | TextView mNoBeaconsText; 52 | 53 | private static final String URL_MEDIUM_ARTICLE = 54 | "https://medium.com/ribot-labs/exploring-google-eddystone-with-the-proximity-beacon-api-bc9256c97e05"; 55 | private static final String URL_GITHUB_REPOSITORY = 56 | "https://github.com/hitherejoe/WatchTower"; 57 | private static final int REQUEST_CODE_REGISTER_BEACON = 1237; 58 | 59 | private DataManager mDataManager; 60 | private CompositeSubscription mSubscriptions; 61 | private EasyRecyclerAdapter mEasyRecycleAdapter; 62 | 63 | public static Intent getStartIntent(Context context) { 64 | return new Intent(context, MainActivity.class); 65 | } 66 | 67 | @Override 68 | protected void onCreate(Bundle savedInstanceState) { 69 | super.onCreate(savedInstanceState); 70 | setContentView(R.layout.activity_main); 71 | ButterKnife.bind(this); 72 | mSubscriptions = new CompositeSubscription(); 73 | mDataManager = WatchTowerApplication.get(this).getComponent().dataManager(); 74 | mEasyRecycleAdapter = new EasyRecyclerAdapter<>(this, BeaconHolder.class, mBeaconListener); 75 | setupLayoutViews(); 76 | getBeacons(); 77 | WatchTowerApplication.get(this).getComponent().eventBus().register(this); 78 | } 79 | 80 | @Override 81 | protected void onDestroy() { 82 | super.onDestroy(); 83 | WatchTowerApplication.get(this).getComponent().eventBus().unregister(this); 84 | mSubscriptions.unsubscribe(); 85 | } 86 | 87 | @Override 88 | public boolean onCreateOptionsMenu(Menu menu) { 89 | getMenuInflater().inflate(R.menu.main, menu); 90 | return true; 91 | } 92 | 93 | @Override 94 | public boolean onOptionsItemSelected(MenuItem item) { 95 | switch (item.getItemId()) { 96 | case R.id.action_medium: 97 | startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(URL_MEDIUM_ARTICLE))); 98 | return true; 99 | case R.id.action_github: 100 | startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(URL_GITHUB_REPOSITORY))); 101 | return true; 102 | default: 103 | return super.onOptionsItemSelected(item); 104 | } 105 | } 106 | 107 | @Subscribe 108 | public void onBeaconListAmended(BusEvent.BeaconListAmended event) { 109 | getBeacons(); 110 | } 111 | 112 | @OnClick(R.id.fab_add) 113 | public void onFabAddClick() { 114 | Intent intent = PropertiesActivity.getStartIntent(this, PropertiesFragment.Mode.REGISTER); 115 | startActivityForResult(intent, REQUEST_CODE_REGISTER_BEACON); 116 | } 117 | 118 | private void setupLayoutViews() { 119 | mBeaconsRecycler.setLayoutManager(new LinearLayoutManager(this)); 120 | mBeaconsRecycler.setAdapter(mEasyRecycleAdapter); 121 | mSwipeRefresh.setColorSchemeResources(R.color.primary); 122 | mSwipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { 123 | @Override 124 | public void onRefresh() { 125 | getBeacons(); 126 | } 127 | }); 128 | } 129 | 130 | private void getBeacons() { 131 | if (DataUtils.isNetworkAvailable(this)) { 132 | mEasyRecycleAdapter.setItems(new ArrayList()); 133 | mSubscriptions.add(mDataManager.getBeacons() 134 | .observeOn(AndroidSchedulers.mainThread()) 135 | .subscribeOn(mDataManager.getScheduler()) 136 | .subscribe(new Subscriber() { 137 | @Override 138 | public void onCompleted() { 139 | mProgressBar.setVisibility(View.GONE); 140 | mSwipeRefresh.setRefreshing(false); 141 | if (mEasyRecycleAdapter.getItemCount() > 0) { 142 | mBeaconsRecycler.setVisibility(View.VISIBLE); 143 | mNoBeaconsText.setVisibility(View.GONE); 144 | } else { 145 | mBeaconsRecycler.setVisibility(View.GONE); 146 | mNoBeaconsText.setVisibility(View.VISIBLE); 147 | } 148 | } 149 | 150 | @Override 151 | public void onError(Throwable error) { 152 | Timber.e("There was an error retrieving the beacons " + error); 153 | mProgressBar.setVisibility(View.GONE); 154 | mSwipeRefresh.setRefreshing(false); 155 | if (error instanceof RetrofitError) { 156 | DialogFactory.createRetrofitErrorDialog(MainActivity.this, (RetrofitError) error).show(); 157 | } else { 158 | DialogFactory.createSimpleErrorDialog(MainActivity.this).show(); 159 | } 160 | } 161 | 162 | @Override 163 | public void onNext(Beacon beacon) { 164 | mEasyRecycleAdapter.addItem(beacon); 165 | } 166 | })); 167 | } else { 168 | mProgressBar.setVisibility(View.GONE); 169 | mSwipeRefresh.setRefreshing(false); 170 | DialogFactory.createSimpleOkErrorDialog( 171 | this, 172 | getString(R.string.dialog_error_title), 173 | getString(R.string.dialog_error_no_connection) 174 | ).show(); 175 | } 176 | } 177 | 178 | private BeaconHolder.BeaconListener mBeaconListener = new BeaconHolder.BeaconListener() { 179 | @Override 180 | public void onAttachmentsClicked(Beacon beacon) { 181 | startActivity(AttachmentsActivity.getStartIntent(MainActivity.this, beacon)); 182 | } 183 | 184 | @Override 185 | public void onViewClicked(Beacon beacon) { 186 | startActivity(DetailActivity.getStartIntent(MainActivity.this, beacon)); 187 | } 188 | }; 189 | 190 | } 191 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/ui/activity/PropertiesActivity.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.ui.activity; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v7.app.ActionBar; 7 | 8 | import com.hitherejoe.watchtower.R; 9 | import com.hitherejoe.watchtower.data.model.Beacon; 10 | import com.hitherejoe.watchtower.ui.fragment.PropertiesFragment; 11 | import com.hitherejoe.watchtower.ui.fragment.PropertiesFragment.Mode; 12 | 13 | import butterknife.ButterKnife; 14 | 15 | public class PropertiesActivity extends BaseActivity { 16 | 17 | private static final String TAG = "PropertiesActivity"; 18 | private static final String EXTRA_BEACON = 19 | "com.hitherejoe.watchtower.ui.activity.PropertiesActivity.EXTRA_BEACON"; 20 | private static final String EXTRA_MODE = 21 | "com.hitherejoe.watchtower.ui.activity.PropertiesActivity.EXTRA_MODE"; 22 | 23 | public static Intent getStartIntent(Context context, Beacon beacon, PropertiesFragment.Mode mode) { 24 | Intent intent = new Intent(context, PropertiesActivity.class); 25 | intent.putExtra(EXTRA_BEACON, beacon); 26 | intent.putExtra(EXTRA_MODE, mode); 27 | return intent; 28 | } 29 | 30 | public static Intent getStartIntent(Context context, PropertiesFragment.Mode mode) { 31 | Intent intent = new Intent(context, PropertiesActivity.class); 32 | intent.putExtra(EXTRA_MODE, mode); 33 | return intent; 34 | } 35 | 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_update); 40 | ButterKnife.bind(this); 41 | Mode mode = (PropertiesFragment.Mode) getIntent().getSerializableExtra(EXTRA_MODE); 42 | if (mode == null) throw new IllegalArgumentException(TAG + ": Beacon is required!"); 43 | Beacon beacon = getIntent().getParcelableExtra(EXTRA_BEACON); 44 | if (mode == PropertiesFragment.Mode.UPDATE && beacon == null) { 45 | throw new IllegalArgumentException(TAG + ": Beacon is required!"); 46 | } 47 | setupActionBar(mode); 48 | addFragment(beacon, mode); 49 | } 50 | 51 | private void setupActionBar(Mode mode) { 52 | ActionBar actionBar = getSupportActionBar(); 53 | if (actionBar != null) { 54 | actionBar.setDisplayHomeAsUpEnabled(true); 55 | actionBar.setTitle(mode == Mode.REGISTER 56 | ? getString(R.string.label_register) 57 | : getString(R.string.label_update)); 58 | } 59 | } 60 | 61 | private void addFragment(Beacon beacon, Mode mode) { 62 | getSupportFragmentManager() 63 | .beginTransaction() 64 | .add(R.id.container_fragment, PropertiesFragment.newInstance(beacon, mode)) 65 | .commit(); 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/ui/adapter/AlertHolder.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.ui.adapter; 2 | 3 | import android.view.View; 4 | import android.widget.TextView; 5 | 6 | import com.hitherejoe.watchtower.R; 7 | import com.hitherejoe.watchtower.data.model.Diagnostics; 8 | 9 | import uk.co.ribot.easyadapter.ItemViewHolder; 10 | import uk.co.ribot.easyadapter.PositionInfo; 11 | import uk.co.ribot.easyadapter.annotations.LayoutId; 12 | import uk.co.ribot.easyadapter.annotations.ViewId; 13 | 14 | @LayoutId(R.layout.item_alert) 15 | public class AlertHolder extends ItemViewHolder { 16 | 17 | @ViewId(R.id.text_alert) 18 | TextView mAlertText; 19 | 20 | public AlertHolder(View view) { 21 | super(view); 22 | } 23 | 24 | @Override 25 | public void onSetValues(Diagnostics.Alert alert, PositionInfo positionInfo) { 26 | mAlertText.setText(alert.toString()); 27 | } 28 | 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/ui/adapter/AttachmentHolder.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.ui.adapter; 2 | 3 | import android.view.View; 4 | import android.widget.TextView; 5 | 6 | import com.hitherejoe.watchtower.R; 7 | import com.hitherejoe.watchtower.data.model.Attachment; 8 | import com.hitherejoe.watchtower.util.DataUtils; 9 | 10 | import uk.co.ribot.easyadapter.ItemViewHolder; 11 | import uk.co.ribot.easyadapter.PositionInfo; 12 | import uk.co.ribot.easyadapter.annotations.LayoutId; 13 | import uk.co.ribot.easyadapter.annotations.ViewId; 14 | 15 | @LayoutId(R.layout.item_attachment) 16 | public class AttachmentHolder extends ItemViewHolder { 17 | 18 | @ViewId(R.id.text_name) 19 | TextView mAttachmentNameText; 20 | 21 | @ViewId(R.id.text_data) 22 | TextView mAttachmentDataText; 23 | 24 | @ViewId(R.id.text_delete) 25 | TextView mAttachmentDeleteText; 26 | 27 | public AttachmentHolder(View view) { 28 | super(view); 29 | } 30 | 31 | @Override 32 | public void onSetValues(Attachment attachment, PositionInfo positionInfo) { 33 | mAttachmentNameText.setText(attachment.attachmentName); 34 | String data = DataUtils.base64DecodeToString(attachment.data); 35 | if (data != null) mAttachmentDataText.setText(data); 36 | } 37 | 38 | @Override 39 | public void onSetListeners() { 40 | mAttachmentDeleteText.setOnClickListener(new View.OnClickListener() { 41 | @Override 42 | public void onClick(View v) { 43 | AttachmentListener attachmentListener = getListener(AttachmentListener.class); 44 | if (attachmentListener != null) attachmentListener.onDeleteClicked(getItem()); 45 | } 46 | }); 47 | } 48 | 49 | public interface AttachmentListener { 50 | void onDeleteClicked(Attachment attachment); 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/ui/adapter/BeaconHolder.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.ui.adapter; 2 | 3 | import android.view.View; 4 | import android.widget.ImageView; 5 | import android.widget.TextView; 6 | 7 | import com.hitherejoe.watchtower.R; 8 | import com.hitherejoe.watchtower.data.model.Beacon; 9 | 10 | import uk.co.ribot.easyadapter.ItemViewHolder; 11 | import uk.co.ribot.easyadapter.PositionInfo; 12 | import uk.co.ribot.easyadapter.annotations.LayoutId; 13 | import uk.co.ribot.easyadapter.annotations.ViewId; 14 | 15 | @LayoutId(R.layout.item_beacon) 16 | public class BeaconHolder extends ItemViewHolder { 17 | 18 | @ViewId(R.id.image_status) 19 | ImageView mStatusImage; 20 | 21 | @ViewId(R.id.text_identifier) 22 | TextView mIdentifierText; 23 | 24 | @ViewId(R.id.text_type) 25 | TextView mTypeText; 26 | 27 | @ViewId(R.id.text_attachments) 28 | TextView mAttachmentsText; 29 | 30 | @ViewId(R.id.text_view) 31 | TextView mViewText; 32 | 33 | public BeaconHolder(View view) { 34 | super(view); 35 | } 36 | 37 | @Override 38 | public void onSetValues(Beacon beacon, PositionInfo positionInfo) { 39 | setBeaconStatusResource(beacon); 40 | mTypeText.setText(beacon.advertisedId.type.getString()); 41 | mIdentifierText.setText(beacon.beaconName); 42 | } 43 | 44 | @Override 45 | public void onSetListeners() { 46 | getView().setOnClickListener(new View.OnClickListener() { 47 | @Override 48 | public void onClick(View v) { 49 | BeaconListener beaconListener = getListener(BeaconListener.class); 50 | if (beaconListener != null) beaconListener.onViewClicked(getItem()); 51 | } 52 | }); 53 | mAttachmentsText.setOnClickListener(new View.OnClickListener() { 54 | @Override 55 | public void onClick(View v) { 56 | BeaconListener beaconListener = getListener(BeaconListener.class); 57 | if (beaconListener != null) beaconListener.onAttachmentsClicked(getItem()); 58 | } 59 | }); 60 | mViewText.setOnClickListener(new View.OnClickListener() { 61 | @Override 62 | public void onClick(View v) { 63 | BeaconListener beaconListener = getListener(BeaconListener.class); 64 | if (beaconListener != null) beaconListener.onViewClicked(getItem()); 65 | } 66 | }); 67 | } 68 | 69 | private void setBeaconStatusResource(Beacon beacon) { 70 | Beacon.Status status = beacon.status; 71 | int resource; 72 | switch (status) { 73 | case STATUS_UNSPECIFIED: 74 | resource = R.drawable.ic_unspecified; 75 | break; 76 | case ACTIVE: 77 | resource = R.drawable.ic_active; 78 | break; 79 | case INACTIVE: 80 | resource = R.drawable.ic_inactive; 81 | break; 82 | case DECOMMISSIONED: 83 | resource = R.drawable.ic_decommissioned; 84 | break; 85 | default: 86 | resource = R.drawable.ic_unspecified; 87 | break; 88 | } 89 | mStatusImage.setBackgroundResource(resource); 90 | } 91 | 92 | public interface BeaconListener { 93 | void onAttachmentsClicked(Beacon beacon); 94 | void onViewClicked(Beacon beacon); 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/ui/fragment/AlertsFragment.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.ui.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.app.Fragment; 5 | import android.support.v7.widget.LinearLayoutManager; 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.ProgressBar; 11 | import android.widget.TextView; 12 | 13 | import com.hitherejoe.watchtower.R; 14 | import com.hitherejoe.watchtower.WatchTowerApplication; 15 | import com.hitherejoe.watchtower.data.DataManager; 16 | import com.hitherejoe.watchtower.data.model.Beacon; 17 | import com.hitherejoe.watchtower.data.model.Diagnostics; 18 | import com.hitherejoe.watchtower.data.model.Diagnostics.Alert; 19 | import com.hitherejoe.watchtower.ui.adapter.AlertHolder; 20 | import com.hitherejoe.watchtower.util.DataUtils; 21 | import com.hitherejoe.watchtower.util.DialogFactory; 22 | 23 | import java.util.Arrays; 24 | 25 | import butterknife.Bind; 26 | import butterknife.ButterKnife; 27 | import retrofit.RetrofitError; 28 | import rx.Subscriber; 29 | import rx.android.schedulers.AndroidSchedulers; 30 | import rx.subscriptions.CompositeSubscription; 31 | import timber.log.Timber; 32 | import uk.co.ribot.easyadapter.EasyRecyclerAdapter; 33 | 34 | public class AlertsFragment extends Fragment { 35 | 36 | @Bind(R.id.recycler_alerts) 37 | RecyclerView mAlertsRecycler; 38 | 39 | @Bind(R.id.text_no_alerts) 40 | TextView mNoAttachmentsText; 41 | 42 | @Bind(R.id.progress_indicator) 43 | ProgressBar mProgressBar; 44 | 45 | @Bind(R.id.text_battery_date) 46 | TextView mBatteryDateText; 47 | 48 | private static final String EXTRA_BEACON = "EXTRA_BEACON"; 49 | private Beacon mBeacon; 50 | private CompositeSubscription mSubscriptions; 51 | private DataManager mDataManager; 52 | private Diagnostics mDiagnostics; 53 | private EasyRecyclerAdapter mEasyRecycleAdapter; 54 | 55 | public static AlertsFragment newInstance(Beacon beacon) { 56 | AlertsFragment propertiesFragment = new AlertsFragment(); 57 | Bundle args = new Bundle(); 58 | args.putParcelable(EXTRA_BEACON, beacon); 59 | propertiesFragment.setArguments(args); 60 | return propertiesFragment; 61 | } 62 | 63 | @Override 64 | public void onCreate(Bundle savedInstanceState) { 65 | super.onCreate(savedInstanceState); 66 | mBeacon = getArguments().getParcelable(EXTRA_BEACON); 67 | if (mBeacon == null) { 68 | throw new IllegalArgumentException("Alerts fragment requires a beacon instance!"); 69 | } 70 | mSubscriptions = new CompositeSubscription(); 71 | mDataManager = WatchTowerApplication.get(getActivity()).getComponent().dataManager(); 72 | mEasyRecycleAdapter = new EasyRecyclerAdapter<>(getActivity(), AlertHolder.class); 73 | setHasOptionsMenu(true); 74 | } 75 | 76 | @Override 77 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 78 | View fragmentView = inflater.inflate(R.layout.fragment_alerts, container, false); 79 | ButterKnife.bind(this, fragmentView); 80 | setupRecyclerView(); 81 | getDiagnostics(); 82 | return fragmentView; 83 | } 84 | 85 | @Override 86 | public void onDestroyView() { 87 | super.onDestroyView(); 88 | mSubscriptions.unsubscribe(); 89 | } 90 | 91 | private void setupRecyclerView() { 92 | mAlertsRecycler.setLayoutManager(new LinearLayoutManager(getActivity())); 93 | mAlertsRecycler.setAdapter(mEasyRecycleAdapter); 94 | } 95 | 96 | private void getDiagnostics() { 97 | if (DataUtils.isNetworkAvailable(getActivity())) { 98 | mSubscriptions.add(mDataManager.getDiagnostics(mBeacon.beaconName) 99 | .observeOn(AndroidSchedulers.mainThread()) 100 | .subscribeOn(mDataManager.getScheduler()) 101 | .subscribe(new Subscriber() { 102 | @Override 103 | public void onCompleted() { 104 | mProgressBar.setVisibility(View.GONE); 105 | if (mDiagnostics != null) { 106 | if (mDiagnostics.estimatedLowBatteryDate != null) { 107 | mBatteryDateText.setText(mDiagnostics.estimatedLowBatteryDate.buildDate()); 108 | } else { 109 | mBatteryDateText.setText(getString(R.string.text_battery_unknown)); 110 | } 111 | if (mDiagnostics.alerts != null 112 | && mDiagnostics.alerts.length > 0) { 113 | mEasyRecycleAdapter.addItems(Arrays.asList(mDiagnostics.alerts)); 114 | mNoAttachmentsText.setVisibility(View.GONE); 115 | mAlertsRecycler.setVisibility(View.VISIBLE); 116 | } else { 117 | mNoAttachmentsText.setVisibility(View.VISIBLE); 118 | mAlertsRecycler.setVisibility(View.GONE); 119 | } 120 | } 121 | } 122 | 123 | @Override 124 | public void onError(Throwable error) { 125 | mProgressBar.setVisibility(View.GONE); 126 | Timber.e("There was an error retrieving beacon diagnostics " + error); 127 | if (error instanceof RetrofitError) { 128 | DialogFactory.createRetrofitErrorDialog(getActivity(), (RetrofitError) error).show(); 129 | } else { 130 | DialogFactory.createSimpleErrorDialog(getActivity()).show(); 131 | } 132 | } 133 | 134 | @Override 135 | public void onNext(Diagnostics diagnostics) { 136 | mDiagnostics = diagnostics; 137 | } 138 | })); 139 | } else { 140 | mProgressBar.setVisibility(View.GONE); 141 | DialogFactory.createSimpleOkErrorDialog( 142 | getActivity(), 143 | getString(R.string.dialog_error_title), 144 | getString(R.string.dialog_error_no_connection) 145 | ).show(); 146 | } 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/util/AccountUtils.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.util; 2 | 3 | import android.accounts.AccountManager; 4 | import android.content.Context; 5 | 6 | import com.hitherejoe.watchtower.WatchTowerApplication; 7 | import com.hitherejoe.watchtower.data.DataManager; 8 | import com.hitherejoe.watchtower.data.local.PreferencesHelper; 9 | 10 | public class AccountUtils { 11 | 12 | public static boolean isUserAuthenticated(Context context) { 13 | PreferencesHelper preferencesHelper = WatchTowerApplication.get(context).getComponent().dataManager().getPreferencesHelper(); 14 | return preferencesHelper.getUser() != null && preferencesHelper.getToken() != null; 15 | } 16 | 17 | public static void invalidateToken(Context context) { 18 | DataManager dataManager = WatchTowerApplication.get(context).getComponent().dataManager(); 19 | AccountManager accountManager = AccountManager.get(context); 20 | accountManager.invalidateAuthToken("com.google", dataManager.getPreferencesHelper().getToken()); 21 | dataManager.getPreferencesHelper().saveToken(null); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/util/DataUtils.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.util; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.util.Base64; 6 | 7 | import com.google.gson.Gson; 8 | import com.hitherejoe.watchtower.data.model.ErrorResponse; 9 | 10 | import java.io.UnsupportedEncodingException; 11 | import java.util.regex.Pattern; 12 | 13 | import retrofit.RetrofitError; 14 | import retrofit.mime.TypedByteArray; 15 | import timber.log.Timber; 16 | 17 | public class DataUtils { 18 | 19 | public static boolean isNetworkAvailable(Context context) { 20 | ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 21 | return connectivityManager.getActiveNetworkInfo() != null; 22 | } 23 | 24 | public static ErrorResponse parseRetrofitError(Throwable error) { 25 | String json = new String(((TypedByteArray) ((RetrofitError) error).getResponse().getBody()).getBytes()); 26 | return new Gson().fromJson(json, ErrorResponse.class); 27 | } 28 | 29 | public static String base64DecodeToString(String s) { 30 | try { 31 | return new String(Base64.decode(s, Base64.DEFAULT), "UTF-8"); 32 | } catch (UnsupportedEncodingException e) { 33 | Timber.e("There was an error decoding the String: " + e); 34 | } 35 | return null; 36 | } 37 | 38 | public static String base64Encode(byte[] b) { 39 | return Base64.encodeToString(b, Base64.DEFAULT); 40 | } 41 | 42 | 43 | public static boolean isStringDoubleValue(String string) { 44 | final String Digits = "(\\p{Digit}+)"; 45 | final String HexDigits = "(\\p{XDigit}+)"; 46 | 47 | // an exponent is 'e' or 'E' followed by an optionally 48 | // signed decimal integer. 49 | final String Exp = "[eE][+-]?"+Digits; 50 | final String fpRegex = 51 | ("[\\x00-\\x20]*"+ // Optional leading "whitespace" 52 | "[+-]?(" + // Optional sign character 53 | "NaN|" + // "NaN" string 54 | "Infinity|" + // "Infinity" string 55 | 56 | // A decimal floating-point string representing a finite positive 57 | // number without a leading sign has at most five basic pieces: 58 | // Digits . Digits ExponentPart FloatTypeSuffix 59 | // 60 | // Since this method allows integer-only strings as input 61 | // in addition to strings of floating-point literals, the 62 | // two sub-patterns below are simplifications of the grammar 63 | // productions from the Java Language Specification, 2nd 64 | // edition, section 3.10.2. 65 | 66 | // Digits ._opt Digits_opt ExponentPart_opt FloatTypeSuffix_opt 67 | "((("+Digits+"(\\.)?("+Digits+"?)("+Exp+")?)|"+ 68 | 69 | // . Digits ExponentPart_opt FloatTypeSuffix_opt 70 | "(\\.("+Digits+")("+Exp+")?)|"+ 71 | 72 | // Hexadecimal strings 73 | "((" + 74 | // 0[xX] HexDigits ._opt BinaryExponent FloatTypeSuffix_opt 75 | "(0[xX]" + HexDigits + "(\\.)?)|" + 76 | 77 | // 0[xX] HexDigits_opt . HexDigits BinaryExponent FloatTypeSuffix_opt 78 | "(0[xX]" + HexDigits + "?(\\.)" + HexDigits + ")" + 79 | 80 | ")[pP][+-]?" + Digits + "))" + 81 | "[fFdD]?))" + 82 | "[\\x00-\\x20]*");// Optional trailing "whitespace" 83 | 84 | return Pattern.matches(fpRegex, string); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/util/DialogFactory.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.util; 2 | 3 | import android.app.AlertDialog; 4 | import android.app.Dialog; 5 | import android.app.ProgressDialog; 6 | import android.content.Context; 7 | import android.content.DialogInterface; 8 | import android.support.annotation.StringRes; 9 | import android.text.InputType; 10 | import android.widget.EditText; 11 | 12 | import com.hitherejoe.watchtower.R; 13 | import com.hitherejoe.watchtower.data.model.ErrorResponse; 14 | 15 | import retrofit.RetrofitError; 16 | 17 | public class DialogFactory { 18 | 19 | public static Dialog createSimpleOkErrorDialog(Context context, String title, String message) { 20 | AlertDialog.Builder alertDialog = new AlertDialog.Builder(context) 21 | .setTitle(title) 22 | .setMessage(message) 23 | .setNeutralButton(R.string.dialog_action_ok, null); 24 | return alertDialog.create(); 25 | } 26 | 27 | public static Dialog createSimpleErrorDialog(Context context) { 28 | AlertDialog.Builder alertDialog = new AlertDialog.Builder(context) 29 | .setTitle(context.getString(R.string.dialog_error_title)) 30 | .setMessage(context.getString(R.string.dialog_general_error_Message)) 31 | .setNeutralButton(R.string.dialog_action_ok, null); 32 | return alertDialog.create(); 33 | } 34 | 35 | public static Dialog createRetrofitErrorDialog(Context context, RetrofitError error) { 36 | ErrorResponse errorResponse = DataUtils.parseRetrofitError(error); 37 | return createSimpleOkErrorDialog(context, "Error: " + errorResponse.error.code, errorResponse.error.message); 38 | } 39 | 40 | public static ProgressDialog createProgressDialog(Context context, String message) { 41 | ProgressDialog progressDialog = new ProgressDialog(context); 42 | progressDialog.setMessage(message); 43 | return progressDialog; 44 | } 45 | 46 | public static Dialog createInputDialog(Context context, String title, String message, final DialogInputCallback inputCallback) { 47 | final EditText input = new EditText(context); 48 | input.setInputType(InputType.TYPE_CLASS_TEXT); 49 | input.setHint(context.getString(R.string.dialog_hint_namespaced_type)); 50 | AlertDialog.Builder alertDialog = new AlertDialog.Builder(context) 51 | .setTitle(title) 52 | .setMessage(message) 53 | .setView(input) 54 | .setPositiveButton(context.getString(R.string.dialog_action_delete), new DialogInterface.OnClickListener() { 55 | @Override 56 | public void onClick(DialogInterface dialog, int which) { 57 | inputCallback.onInputSubmitted(input.getText().toString()); 58 | } 59 | }) 60 | .setNegativeButton(context.getString(R.string.dialog_action_cancel), null); 61 | return alertDialog.create(); 62 | } 63 | 64 | public static Dialog createAuthErrorDialog(Context context, String message, DialogInterface.OnClickListener onPositiveClick, DialogInterface.OnClickListener onNegativeClick) { 65 | AlertDialog.Builder alertDialog = new AlertDialog.Builder(context) 66 | .setTitle(context.getString(R.string.dialog_error_title)) 67 | .setMessage(message) 68 | .setPositiveButton(R.string.dialog_action_authorise, onPositiveClick) 69 | .setNegativeButton(R.string.dialog_action_exit, onNegativeClick); 70 | return alertDialog.create(); 71 | } 72 | 73 | public static ProgressDialog createProgressDialog(Context context, @StringRes int messageResoruce) { 74 | return createProgressDialog(context, context.getString(messageResoruce)); 75 | } 76 | 77 | public interface DialogInputCallback { 78 | String onInputSubmitted(String input); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/watchtower/util/MockModelsUtil.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.util; 2 | 3 | import com.hitherejoe.watchtower.data.model.AdvertisedId; 4 | import com.hitherejoe.watchtower.data.model.Attachment; 5 | import com.hitherejoe.watchtower.data.model.Beacon; 6 | import com.hitherejoe.watchtower.data.model.Diagnostics; 7 | import com.hitherejoe.watchtower.data.model.LatLng; 8 | import com.hitherejoe.watchtower.data.model.Namespace; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.Random; 13 | import java.util.UUID; 14 | 15 | public class MockModelsUtil { 16 | 17 | public static String generateRandomString() { 18 | return UUID.randomUUID().toString(); 19 | } 20 | 21 | public static Beacon createMockUnregisteredBeacon() { 22 | Beacon beacon = new Beacon(); 23 | beacon.placeId = "1234"; 24 | beacon.status = Beacon.Status.ACTIVE; 25 | beacon.expectedStability = Beacon.Stability.MOBILE; 26 | beacon.description = "This is a description"; 27 | beacon.latLng = new LatLng(); 28 | beacon.latLng.latitude = 54.331; 29 | beacon.latLng.longitude = -12.435; 30 | beacon.advertisedId = new AdvertisedId("IDIDIDIDID", AdvertisedId.Type.EDDYSTONE); 31 | return beacon; 32 | } 33 | 34 | public static Beacon createMockRegisteredBeacon() { 35 | Beacon beacon = createMockUnregisteredBeacon(); 36 | beacon.beaconName = "beaconName/namenamename"; 37 | return beacon; 38 | } 39 | 40 | public static Beacon createMockIncompleteBeacon() { 41 | Beacon beacon = new Beacon(); 42 | beacon.beaconName = "beaconName/namenamename"; 43 | beacon.advertisedId = new AdvertisedId(); 44 | beacon.status = Beacon.Status.ACTIVE; 45 | beacon.advertisedId.id = "IDIDIDIDID"; 46 | return beacon; 47 | } 48 | 49 | public static Attachment createMockAttachment() { 50 | Attachment attachment = new Attachment(); 51 | attachment.namespacedType = "proximity-api/text"; 52 | attachment.data = "attachmentData"; 53 | return attachment; 54 | } 55 | 56 | public static Namespace createMockNamespace() { 57 | Namespace namespace = new Namespace(); 58 | namespace.namespaceName = "proximity-api"; 59 | namespace.servingVisibility = Namespace.Visibility.PUBLIC; 60 | return namespace; 61 | } 62 | 63 | public static Diagnostics createMockDiagnostics(String beaconName) { 64 | Diagnostics diagnostics = new Diagnostics(); 65 | diagnostics.beaconName = beaconName; 66 | Diagnostics.BeaconDate beaconDate = new Diagnostics.BeaconDate(); 67 | Random random = new Random(); 68 | beaconDate.day = random.nextInt(30); 69 | beaconDate.month = random.nextInt(12); 70 | beaconDate.year = random.nextInt(9999); 71 | diagnostics.estimatedLowBatteryDate = beaconDate; 72 | diagnostics.alerts = new Diagnostics.Alert[2]; 73 | diagnostics.alerts[0] = Diagnostics.Alert.LOW_BATTERY; 74 | diagnostics.alerts[1] = Diagnostics.Alert.WRONG_LOCATION; 75 | return diagnostics; 76 | } 77 | 78 | public static Diagnostics createMockEmptyDiagnostics(String beaconName) { 79 | Diagnostics diagnostics = new Diagnostics(); 80 | diagnostics.beaconName = beaconName; 81 | return diagnostics; 82 | } 83 | 84 | public static List createMockListOfNamespaces(int num) { 85 | ArrayList result = new ArrayList<>(); 86 | for (int i = 0; i < num; i++) { 87 | Namespace namespace = new Namespace(); 88 | namespace.namespaceName = "proximity-api/" + i; 89 | namespace.servingVisibility = Namespace.Visibility.PUBLIC; 90 | result.add(namespace); 91 | } 92 | return result; 93 | } 94 | 95 | public static List createMockListOfBeacons(int num) { 96 | ArrayList result = new ArrayList<>(); 97 | for (int i = 0; i < num; i++) { 98 | Beacon beacon = new Beacon(); 99 | beacon.beaconName = generateRandomString(); 100 | AdvertisedId advertisedId = new AdvertisedId(); 101 | advertisedId.type = AdvertisedId.Type.EDDYSTONE; 102 | advertisedId.id = generateRandomString(); 103 | beacon.advertisedId = advertisedId; 104 | beacon.description = "Descroption " + i; 105 | beacon.status = Beacon.Status.ACTIVE; 106 | beacon.expectedStability = Beacon.Stability.MOBILE; 107 | beacon.placeId = generateRandomString(); 108 | LatLng latLng = new LatLng(); 109 | latLng.latitude = 1.00 + num; 110 | latLng.longitude = 1.00 - num; 111 | beacon.latLng = latLng; 112 | result.add(beacon); 113 | } 114 | return result; 115 | } 116 | 117 | public static List createMockListOfAttachments(String beaconName, int num) { 118 | ArrayList result = new ArrayList<>(); 119 | for (int i = 0; i < num; i++) { 120 | Attachment attachment = new Attachment(); 121 | attachment.attachmentName = beaconName + "attachments/" + i; 122 | attachment.data = "Data"; 123 | attachment.namespacedType = "proximity-api/text"; 124 | result.add(attachment); 125 | } 126 | return result; 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-hdpi/ic_active.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-hdpi/ic_add.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_decommissioned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-hdpi/ic_decommissioned.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-hdpi/ic_done.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-hdpi/ic_inactive.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_unspecified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-hdpi/ic_unspecified.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-mdpi/ic_active.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-mdpi/ic_add.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_decommissioned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-mdpi/ic_decommissioned.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-mdpi/ic_done.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-mdpi/ic_inactive.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_unspecified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-mdpi/ic_unspecified.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/touchable_background_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xhdpi/ic_active.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xhdpi/ic_add.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_decommissioned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xhdpi/ic_decommissioned.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xhdpi/ic_done.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xhdpi/ic_inactive.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_unspecified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xhdpi/ic_unspecified.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xxhdpi/ic_active.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xxhdpi/ic_add.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_decommissioned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xxhdpi/ic_decommissioned.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xxhdpi/ic_done.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xxhdpi/ic_inactive.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_unspecified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xxhdpi/ic_unspecified.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xxxhdpi/ic_active.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xxxhdpi/ic_add.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_decommissioned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xxxhdpi/ic_decommissioned.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xxxhdpi/ic_done.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xxxhdpi/ic_inactive.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_unspecified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/drawable-xxxhdpi/ic_unspecified.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/touchable_background_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_add_attachment.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 19 | 20 | 26 | 27 | 33 | 34 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_attachments.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 18 | 19 | 20 | 21 | 27 | 28 | 32 | 33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_detail.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 17 | 18 | 23 | 24 | 32 | 33 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 18 | 19 | 20 | 21 | 27 | 28 | 32 | 33 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_update.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_alerts.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 22 | 23 | 29 | 30 | 37 | 38 | 44 | 45 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_properties.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 26 | 27 | 33 | 34 | 40 | 41 | 47 | 48 | 52 | 53 | 59 | 60 | 67 | 68 | 74 | 75 | 81 | 82 | 88 | 89 | 95 | 96 | 100 | 101 | 107 | 108 | 114 | 115 | 121 | 122 | 128 | 129 | 133 | 134 | 140 | 141 | 145 | 146 | 152 | 153 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_alert.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_attachment.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | 17 | 23 | 24 | 32 | 33 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_beacon.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | 17 | 22 | 23 | 31 | 32 | 42 | 43 | 51 | 52 | 53 | 54 | 59 | 60 | 66 | 67 | 74 | 75 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /app/src/main/res/menu/add_attachment.xml: -------------------------------------------------------------------------------- 1 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/menu/attachments.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/menu/detail.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/menu/register.xml: -------------------------------------------------------------------------------- 1 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #607D8B 4 | #455A64 5 | #CFD8DC 6 | #009688 7 | #212121 8 | #727272 9 | #FFFFFF 10 | #FFFFFF 11 | #B6B6B6 12 | #D4D4D4 13 | #FFFFFF 14 | #FAFAFA 15 | #D7D7D7 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | 24sp 7 | 22sp 8 | 20sp 9 | 18sp 10 | 16sp 11 | 14sp 12 | 12sp 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | WatchTower 3 | 4 | Update 5 | Register 6 | Attachments 7 | Add attachment 8 | Beacon detail 9 | 10 | 11 | 12 | View Medium Article 13 | View on GitHub 14 | done 15 | edit 16 | Add Attachment 17 | Delete 18 | Delete all 19 | 20 | 21 | 22 | Properties 23 | Diagnostics 24 | 25 | 26 | 27 | View 28 | Update 29 | Attachments 30 | 31 | 32 | 33 | Delete 34 | 35 | 36 | 37 | Beacon name 38 | Advertised ID 39 | Description 40 | Type 41 | Status 42 | Stability 43 | Location 44 | Google Places ID 45 | 46 | Beacon name 47 | Advertised Id 48 | Description 49 | Type 50 | Status 51 | Latitude 52 | Longitude 53 | Google Places ID 54 | 55 | 56 | 57 | 58 | Est. Low Battery 59 | Alerts 60 | Unknown 61 | 62 | 63 | 64 | Data 65 | 66 | 67 | 68 | Sorry, no beacons to display 69 | Sorry, no attachments to display 70 | Sorry, no alerts to display 71 | 72 | Sorry, you need Google Play Services to use this app :( 73 | The Advertised ID cannot be blank 74 | The beacon status cannot be blank 75 | The attachment data cannot be blank 76 | Advertised Id is required 77 | Must be a valid Double 78 | 79 | 80 | 81 | OK 82 | Delete 83 | Cancel 84 | Exit 85 | Authorise 86 | 87 | Namespaced Type 88 | Delete 89 | Please enter a namespaced type 90 | 91 | Oops 92 | There was an error making the request 93 | There was an error authorising the request, please connect again 94 | Sorry, but you can\'t use this app without connecting an account! 95 | 96 | Text data cannot be blank! 97 | Text data cannot contain spaces! 98 | Sorry, there are no attachments to delete! 99 | Sorry, you need a connection to do that! 100 | 101 | Saving beacon… 102 | Retrieving namespaces… 103 | Adding attachment… 104 | Deleting attachment… 105 | Deleting all attachments… 106 | Deleting %s attachments… 107 | 108 | 109 | 110 | Namespace 111 | Data 112 | 113 | 114 | @string/fragment_title_properties 115 | @string/fragment_title_alerts 116 | 117 | 118 | 119 | 120 | 121 | Select status 122 | Active 123 | Inactive 124 | Decommissioned 125 | 126 | 127 | 128 | Select stability 129 | Stable 130 | Portable 131 | Mobile 132 | Roving 133 | 134 | 135 | 136 | Select type 137 | Eddystone 138 | iBeacon 139 | AltBeacon 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | 19 | 20 | 23 | 24 | 27 | 28 | 35 | 36 | 44 | 45 | 54 | 55 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/test/java/com/hitherejoe/watchtower/DataManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower; 2 | 3 | 4 | import com.hitherejoe.watchtower.data.DataManager; 5 | import com.hitherejoe.watchtower.data.local.PreferencesHelper; 6 | import com.hitherejoe.watchtower.data.model.Attachment; 7 | import com.hitherejoe.watchtower.data.model.Beacon; 8 | import com.hitherejoe.watchtower.data.model.Namespace; 9 | import com.hitherejoe.watchtower.data.remote.WatchTowerService; 10 | import com.hitherejoe.watchtower.util.DefaultConfig; 11 | import com.hitherejoe.watchtower.util.MockModelsUtil; 12 | import com.squareup.otto.Bus; 13 | 14 | import org.junit.Before; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | import org.robolectric.RobolectricGradleTestRunner; 18 | import org.robolectric.RuntimeEnvironment; 19 | import org.robolectric.annotation.Config; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | import rx.Observable; 25 | import rx.schedulers.Schedulers; 26 | 27 | import static com.hitherejoe.watchtower.util.RxAssertions.subscribeAssertingThat; 28 | import static org.mockito.Matchers.any; 29 | import static org.mockito.Matchers.anyString; 30 | import static org.mockito.Mockito.mock; 31 | import static org.mockito.Mockito.when; 32 | 33 | @RunWith(RobolectricGradleTestRunner.class) 34 | @Config(constants = BuildConfig.class, sdk = DefaultConfig.EMULATE_SDK) 35 | public class DataManagerTest { 36 | 37 | private DataManager mDataManager; 38 | private WatchTowerService mMockWatchTowerService; 39 | 40 | @Before 41 | public void setUp() { 42 | mMockWatchTowerService = mock(WatchTowerService.class); 43 | Bus mMockBus = mock(Bus.class); 44 | PreferencesHelper mPreferencesHelper = new PreferencesHelper(RuntimeEnvironment.application); 45 | mDataManager = new DataManager(mMockWatchTowerService, 46 | mMockBus, 47 | mPreferencesHelper, 48 | Schedulers.immediate()); 49 | } 50 | 51 | @Test 52 | public void shouldRegisterBeacon() { 53 | Beacon unregisteredBeacon = MockModelsUtil.createMockUnregisteredBeacon(); 54 | Beacon registeredBeacon = MockModelsUtil.createMockRegisteredBeacon(); 55 | when(mMockWatchTowerService.registerBeacon(unregisteredBeacon)).thenReturn(Observable.just(registeredBeacon)); 56 | subscribeAssertingThat(mDataManager.registerBeacon(unregisteredBeacon)) 57 | .emits(registeredBeacon); 58 | } 59 | 60 | @Test 61 | public void shouldGetBeacons() { 62 | Beacon registeredBeacon = MockModelsUtil.createMockRegisteredBeacon(); 63 | Beacon registeredBeaconTwo = MockModelsUtil.createMockRegisteredBeacon(); 64 | registeredBeaconTwo.beaconName = "BeaconName"; 65 | WatchTowerService.BeaconsResponse beaconsResponse = new WatchTowerService.BeaconsResponse(); 66 | beaconsResponse.beacons = new ArrayList<>(); 67 | beaconsResponse.beacons.add(registeredBeacon); 68 | beaconsResponse.beacons.add(registeredBeaconTwo); 69 | when(mMockWatchTowerService.getBeacons()).thenReturn(Observable.just(beaconsResponse)); 70 | 71 | subscribeAssertingThat(mDataManager.getBeacons()) 72 | .emits(beaconsResponse.beacons); 73 | } 74 | 75 | @Test 76 | public void shouldUpdateBeacon() { 77 | Beacon registeredBeacon = MockModelsUtil.createMockRegisteredBeacon(); 78 | Beacon updatedBeacon = MockModelsUtil.createMockRegisteredBeacon(); 79 | updatedBeacon.description = "Desc"; 80 | 81 | when(mMockWatchTowerService.updateBeacon(registeredBeacon.beaconName, updatedBeacon)) 82 | .thenReturn(Observable.just(updatedBeacon)); 83 | 84 | subscribeAssertingThat(mDataManager.updateBeacon(registeredBeacon.beaconName, updatedBeacon, false, Beacon.Status.ACTIVE)) 85 | .emits(updatedBeacon); 86 | 87 | updatedBeacon.status = Beacon.Status.INACTIVE; 88 | subscribeAssertingThat(mDataManager.updateBeacon(updatedBeacon.beaconName, updatedBeacon, true, Beacon.Status.INACTIVE)) 89 | .emits(updatedBeacon); 90 | } 91 | 92 | @Test 93 | public void shouldUpdateBeaconStatus() { 94 | Beacon decommissionedBeacon = MockModelsUtil.createMockRegisteredBeacon(); 95 | decommissionedBeacon.status = Beacon.Status.DECOMMISSIONED; 96 | 97 | when(mMockWatchTowerService.decomissionBeacon(decommissionedBeacon.beaconName)) 98 | .thenReturn(Observable.just(decommissionedBeacon)); 99 | when(mMockWatchTowerService.getBeacon(decommissionedBeacon.beaconName)) 100 | .thenReturn(Observable.just(decommissionedBeacon)); 101 | subscribeAssertingThat(mDataManager.setBeaconStatus(decommissionedBeacon, Beacon.Status.DECOMMISSIONED)) 102 | .emits(decommissionedBeacon); 103 | 104 | Beacon inactiveBeacon = MockModelsUtil.createMockRegisteredBeacon(); 105 | inactiveBeacon.status = Beacon.Status.INACTIVE; 106 | 107 | when(mMockWatchTowerService.deactivateBeacon(inactiveBeacon.beaconName)) 108 | .thenReturn(Observable.just(inactiveBeacon)); 109 | when(mMockWatchTowerService.getBeacon(inactiveBeacon.beaconName)) 110 | .thenReturn(Observable.just(inactiveBeacon)); 111 | subscribeAssertingThat(mDataManager.setBeaconStatus(inactiveBeacon, Beacon.Status.INACTIVE)) 112 | .emits(inactiveBeacon); 113 | 114 | Beacon activeBeacon = MockModelsUtil.createMockRegisteredBeacon(); 115 | activeBeacon.status = Beacon.Status.ACTIVE; 116 | 117 | when(mMockWatchTowerService.activateBeacon(activeBeacon.beaconName)) 118 | .thenReturn(Observable.just(activeBeacon)); 119 | when(mMockWatchTowerService.getBeacon(activeBeacon.beaconName)) 120 | .thenReturn(Observable.just(activeBeacon)); 121 | subscribeAssertingThat(mDataManager.setBeaconStatus(activeBeacon, Beacon.Status.ACTIVE)) 122 | .emits(activeBeacon); 123 | } 124 | 125 | @Test 126 | public void shouldCreateAttachment() { 127 | Beacon registeredBeacon = MockModelsUtil.createMockRegisteredBeacon(); 128 | Attachment unRegisteredAttachment = MockModelsUtil.createMockAttachment(); 129 | Attachment registeredAttachment = MockModelsUtil.createMockAttachment(); 130 | registeredAttachment.attachmentName = "attachmentName"; 131 | 132 | when(mMockWatchTowerService.createAttachment(registeredBeacon.beaconName, unRegisteredAttachment)) 133 | .thenReturn(Observable.just(registeredAttachment)); 134 | subscribeAssertingThat(mDataManager.createAttachment(registeredBeacon.beaconName, unRegisteredAttachment)) 135 | .emits(registeredAttachment); 136 | } 137 | 138 | @Test 139 | public void shouldDeleteAttachment() { 140 | Attachment registeredAttachment = MockModelsUtil.createMockAttachment(); 141 | registeredAttachment.attachmentName = "attachmentName"; 142 | 143 | when(mMockWatchTowerService.deleteAttachment(registeredAttachment.attachmentName)) 144 | .thenReturn(Observable.empty()); 145 | subscribeAssertingThat(mDataManager.deleteAttachment(registeredAttachment.attachmentName)) 146 | .completesSuccessfully(); 147 | } 148 | 149 | @Test 150 | public void shouldGetAttachments() { 151 | Beacon registeredBeacon = MockModelsUtil.createMockRegisteredBeacon(); 152 | Attachment registeredAttachmentOne = MockModelsUtil.createMockAttachment(); 153 | registeredAttachmentOne.attachmentName = "attachmentNameOne"; 154 | Attachment registeredAttachmentTwo = MockModelsUtil.createMockAttachment(); 155 | registeredAttachmentTwo.attachmentName = "attachmentNameTwo"; 156 | List attachments = new ArrayList<>(); 157 | attachments.add(registeredAttachmentOne); 158 | attachments.add(registeredAttachmentTwo); 159 | 160 | WatchTowerService.AttachmentResponse attachmentResponse = new WatchTowerService.AttachmentResponse(); 161 | attachmentResponse.attachments = attachments; 162 | 163 | when(mMockWatchTowerService.getAttachments(any(String.class), any(String.class))) 164 | .thenReturn(Observable.just(attachmentResponse)); 165 | 166 | subscribeAssertingThat(mDataManager.getAttachments(registeredBeacon.beaconName, null)) 167 | .emits(attachmentResponse); 168 | } 169 | 170 | @Test 171 | public void getNamespaces() { 172 | Namespace namespace = MockModelsUtil.createMockNamespace(); 173 | List namespaces = new ArrayList<>(); 174 | namespaces.add(namespace); 175 | 176 | WatchTowerService.NamespacesResponse namespacesResponse = new WatchTowerService.NamespacesResponse(); 177 | namespacesResponse.namespaces = namespaces; 178 | 179 | when(mMockWatchTowerService.getNamespaces()) 180 | .thenReturn(Observable.just(namespacesResponse)); 181 | 182 | subscribeAssertingThat(mDataManager.getNamespaces()) 183 | .emits(namespacesResponse); 184 | } 185 | 186 | @Test 187 | public void shouldDeleteBatchAttachments() { 188 | Beacon registeredBeacon = MockModelsUtil.createMockRegisteredBeacon(); 189 | 190 | when(mMockWatchTowerService.deleteBatchAttachments(anyString(), anyString())) 191 | .thenReturn(Observable.empty()); 192 | 193 | subscribeAssertingThat(mDataManager.deleteBatchAttachments(registeredBeacon.beaconName, null)) 194 | .completesSuccessfully(); 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /app/src/test/java/com/hitherejoe/watchtower/util/DefaultConfig.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.util; 2 | 3 | public class DefaultConfig { 4 | //The api level that Roboelectric will use to run the unit tests 5 | public static final int EMULATE_SDK = 21; 6 | } -------------------------------------------------------------------------------- /app/src/test/java/com/hitherejoe/watchtower/util/RxAssertions.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.watchtower.util; 2 | 3 | import junit.framework.AssertionFailedError; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.List; 8 | 9 | import rx.Observable; 10 | import rx.Observer; 11 | import rx.schedulers.Schedulers; 12 | 13 | public class RxAssertions { 14 | 15 | public static ObservableAssertions subscribeAssertingThat(Observable observable) { 16 | return new ObservableAssertions<>(observable); 17 | } 18 | 19 | public static class ObservableAssertions { 20 | 21 | private List mResult; 22 | private Throwable mError; 23 | private boolean mCompleted; 24 | 25 | public ObservableAssertions(Observable observable) { 26 | mCompleted = false; 27 | mResult = new ArrayList<>(); 28 | observable.subscribeOn(Schedulers.immediate()) 29 | .subscribe(new Observer() { 30 | @Override 31 | public void onCompleted() { 32 | mCompleted = true; 33 | } 34 | 35 | @Override 36 | public void onError(Throwable error) { 37 | mError = error; 38 | } 39 | 40 | @Override 41 | public void onNext(T item) { 42 | mResult.add(item); 43 | } 44 | }); 45 | } 46 | 47 | public ObservableAssertions completesSuccessfully() { 48 | if (!mCompleted || mError != null) { 49 | if (mError != null) mError.printStackTrace(); 50 | throw new AssertionFailedError("Observable has not completed successfully - cause: " 51 | + (mError != null ? mError : "onComplete not called")); 52 | } 53 | return this; 54 | } 55 | 56 | public ObservableAssertions fails() { 57 | if (mError == null) { 58 | throw new AssertionFailedError("Observable has not failed"); 59 | } 60 | return this; 61 | } 62 | 63 | public ObservableAssertions failsWithError(Throwable throwable) { 64 | fails(); 65 | if (!throwable.equals(mError)) { 66 | throw new AssertionFailedError("Observable has failed with a different error," + 67 | " expected is " + throwable + " but thrown was " + mError); 68 | } 69 | return this; 70 | } 71 | 72 | public ObservableAssertions hasSize(int numItemsExpected) { 73 | if (numItemsExpected != mResult.size()) { 74 | throw new AssertionFailedError("Observable has emitted " + mResult.size() 75 | + " items but expected was " + numItemsExpected); 76 | } 77 | return this; 78 | } 79 | 80 | @SafeVarargs 81 | public final ObservableAssertions emits(T... itemsExpected) { 82 | completesSuccessfully(); 83 | assertEmittedEquals(itemsExpected); 84 | return this; 85 | } 86 | 87 | @SuppressWarnings("unchecked") 88 | public ObservableAssertions emits(Collection itemsExpected) { 89 | completesSuccessfully(); 90 | assertEmittedEquals((T[]) itemsExpected.toArray()); 91 | return this; 92 | } 93 | 94 | public ObservableAssertions emitsNothing() { 95 | completesSuccessfully(); 96 | if (mResult.size() > 0) { 97 | throw new AssertionFailedError("Observable has emitted " + mResult.size() + " items"); 98 | } 99 | return this; 100 | } 101 | 102 | private void assertEmittedEquals(T[] itemsExpected) { 103 | hasSize(itemsExpected.length); 104 | for (int i = 0; i < itemsExpected.length; i++) { 105 | T expected = itemsExpected[i]; 106 | T actual = mResult.get(i); 107 | if (!expected.equals(actual)) { 108 | throw new AssertionFailedError("Emitted item in position " + i + " does not match," + 109 | " expected " + expected + " actual " + actual); 110 | } 111 | } 112 | } 113 | 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.3.0' 9 | classpath 'com.neenbedankt.gradle.plugins:android-apt:1.6' 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 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-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /images/device_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/images/device_screenshot.png -------------------------------------------------------------------------------- /images/ic_launcher_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/WatchTower/53b5d3aa70134e991dbb1689161b71aa4d0eb372/images/ic_launcher_web.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------