├── .idea └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── debug │ ├── app-debug.apk │ └── output.json ├── proguard-rules.pro ├── release │ ├── app-release.apk │ └── output.json └── src │ ├── androidTest │ └── java │ │ └── bg │ │ └── devlabs │ │ └── walkdetector │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ │ └── bg │ │ │ └── devlabs │ │ │ └── walkdetector │ │ │ ├── MainActivity.java │ │ │ ├── WalkDetectService.java │ │ │ └── util │ │ │ ├── DateTimeHelper.java │ │ │ ├── NotificationHelper.java │ │ │ ├── PermissionsHelper.java │ │ │ ├── SettingsManager.java │ │ │ └── SharedPreferencesHelper.java │ └── res │ │ ├── animator │ │ ├── play_to_stop.xml │ │ └── stop_to_play.xml │ │ ├── drawable │ │ ├── avd_play_to_stop.xml │ │ ├── avd_stop_to_play.xml │ │ ├── ic_directions_walk_black_24dp.xml │ │ ├── ic_play_arrow_white_24dp.xml │ │ └── ic_stop_white_24dp.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── all_colors.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── integers.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── bg │ └── devlabs │ └── walkdetector │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # walk-detector 2 | ## An Android app that notifies you when you start walking by sending you a notification and playing a sound. 3 | 4 | I made this app because I always forget to turn on the [Charity Miles](http://www.charitymiles.org/) app when I go for a walk. 5 | And also I wanted to try the play-services-fitness API. 6 | The minimum Android version required is API level 21 5.0 (LOLLIPOP) 7 | 8 | ## In the project I use: 9 | - [Fitness History API](https://developers.google.com/fit/android/history) to query for step_count.delta in the checked period of time 10 | - [ReactiveX/RxAndroid](https://github.com/ReactiveX/RxAndroid) to describe my interval based logic in more readable way 11 | - [Service](https://developer.android.com/guide/components/services.html) which is defined as indestructible and handles the detection 12 | - [Alarm manager](https://developer.android.com/training/scheduling/alarms.html) to send auto start and stop Intents to the Service 13 | - [Lambda expressions](https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html) 14 | - [Butterknife](http://jakewharton.github.io/butterknife/) field and method binding for Android views 15 | - [Adaptive icons](https://developer.android.com/preview/features/adaptive-icons.html) just to test them 16 | - [AnimatedVectorDrawable](https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable.html) to animate the FAB icon on the Home screen 17 | 18 | ## How to run the project: 19 | 20 | - [Get an OAuth 2.0 Client ID](https://developers.google.com/fit/android/get-api-key) this is needed for developer authentication for the Fit App. The link describes how to generate it. Once generated it is not used anywhere. The servers know that the build is made on your machine because of the Certificate fingerprints you give them through the generation process. 21 | - Open the project in Android Studio 22 | - You will need [Java 8](https://developer.android.com/studio/write/java8-support.html). Update the Android plugin to 3.0.0-alpha1 (or higher) or add [Retrolambda](https://github.com/evant/gradle-retrolambda) to the gradle. 23 | - In your gradle.properties file you need to add: 24 | ``` 25 | RELEASE_STORE_FILE= 26 | RELEASE_STORE_PASSWORD= 27 | RELEASE_KEY_ALIAS= 28 | RELEASE_KEY_PASSWORD= 29 | ``` 30 | - In order for the certificate to work you may need to Build signed APK from Build > Generate Signed APK > Create new / Choose existing 31 | 32 | 33 | 34 | Please feel free to contact me if you have any questions. :) 35 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | signingConfigs { 5 | release { 6 | keyAlias "${RELEASE_KEY_ALIAS}" 7 | keyPassword "${RELEASE_KEY_PASSWORD}" 8 | storeFile file("${RELEASE_STORE_FILE}") 9 | storePassword "${RELEASE_KEY_PASSWORD}" 10 | } 11 | } 12 | compileSdkVersion 26 13 | buildToolsVersion "25.0.3" 14 | defaultConfig { 15 | applicationId "bg.devlabs.walkdetector" 16 | minSdkVersion 21 17 | targetSdkVersion 26 18 | versionCode 2 19 | versionName "1.0" 20 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 21 | signingConfig signingConfigs.release 22 | } 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | debuggable true 27 | signingConfig signingConfigs.release 28 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | productFlavors { 32 | } 33 | packagingOptions { 34 | exclude 'META-INF/rxjava.properties' 35 | } 36 | compileOptions { 37 | targetCompatibility 1.8 38 | sourceCompatibility 1.8 39 | } 40 | } 41 | 42 | ext { 43 | google_play_services_library = '11.0.0' 44 | google_support_library = '26.0.0' 45 | butterknife_version = '8.8.0' 46 | } 47 | 48 | dependencies { 49 | // Note, specific version numbers are managed in gradle.properties. This is preferred since it 50 | // easily keeps sub-projects on the same versions. 51 | compile "com.google.android.gms:play-services-fitness:$google_play_services_library" 52 | compile "com.android.support:appcompat-v7:$google_support_library" 53 | compile "com.android.support:design:$google_support_library" 54 | compile fileTree(include: ['*.jar'], dir: 'libs') 55 | androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.0', { 56 | exclude group: 'com.android.support', module: 'support-annotations' 57 | }) 58 | 59 | // Required for local unit tests (JUnit 4 framework) 60 | testCompile 'junit:junit:4.12' 61 | // Required for instrumented tests 62 | androidTestCompile "com.android.support:support-annotations:$google_support_library" 63 | androidTestCompile 'com.android.support.test:runner:1.0.0' 64 | 65 | // RxAndroid and RxJava 66 | compile 'io.reactivex.rxjava2:rxandroid:2.0.1' 67 | // Because RxAndroid releases are few and far between, it is recommended you also 68 | // explicitly depend on RxJava's latest version for bug fixes and new features. 69 | compile 'io.reactivex.rxjava2:rxjava:2.1.0' 70 | 71 | // Butterknife 72 | compile "com.jakewharton:butterknife:$butterknife_version" 73 | annotationProcessor "com.jakewharton:butterknife-compiler:$butterknife_version" 74 | } -------------------------------------------------------------------------------- /app/debug/app-debug.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-labs-bg/walk-detector/dfd0c0b722f70fc179ea15c655e6bebb8d120d78/app/debug/app-debug.apk -------------------------------------------------------------------------------- /app/debug/output.json: -------------------------------------------------------------------------------- 1 | [{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":1},"outputFile":{"path":"/home/simona/Documents/projects/WalkDetector/app/debug/app-debug.apk"},"properties":{"packageId":"bg.devlabs.walkdetector","split":""}}] -------------------------------------------------------------------------------- /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 /home/simona/Programs/Android/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 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-labs-bg/walk-detector/dfd0c0b722f70fc179ea15c655e6bebb8d120d78/app/release/app-release.apk -------------------------------------------------------------------------------- /app/release/output.json: -------------------------------------------------------------------------------- 1 | [{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":1},"outputFile":{"path":"/home/simona/Documents/projects/WalkDetector/app/release/app-release.apk"},"properties":{"packageId":"bg.devlabs.walkdetector","split":""}}] -------------------------------------------------------------------------------- /app/src/androidTest/java/bg/devlabs/walkdetector/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package bg.devlabs.walkdetector; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("bg.devlabs.walkdetector", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-labs-bg/walk-detector/dfd0c0b722f70fc179ea15c655e6bebb8d120d78/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/bg/devlabs/walkdetector/MainActivity.java: -------------------------------------------------------------------------------- 1 | package bg.devlabs.walkdetector; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.AlarmManager; 5 | import android.app.PendingIntent; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.graphics.drawable.AnimatedVectorDrawable; 9 | import android.net.Uri; 10 | import android.os.Bundle; 11 | import android.provider.Settings; 12 | import android.support.annotation.NonNull; 13 | import android.support.design.widget.FloatingActionButton; 14 | import android.support.design.widget.Snackbar; 15 | import android.support.v7.app.AppCompatActivity; 16 | import android.view.View; 17 | import android.view.inputmethod.InputMethodManager; 18 | import android.widget.Button; 19 | import android.widget.CheckBox; 20 | import android.widget.EditText; 21 | import android.widget.LinearLayout; 22 | import android.widget.Toast; 23 | 24 | import bg.devlabs.walkdetector.util.DateTimeHelper; 25 | import bg.devlabs.walkdetector.util.PermissionsHelper; 26 | import bg.devlabs.walkdetector.util.SettingsManager; 27 | import bg.devlabs.walkdetector.util.SharedPreferencesHelper; 28 | import butterknife.BindView; 29 | import butterknife.ButterKnife; 30 | import butterknife.OnCheckedChanged; 31 | import butterknife.OnClick; 32 | 33 | import static bg.devlabs.walkdetector.util.DateTimeHelper.isTimeValid; 34 | 35 | /** 36 | * Created by Simona Stoyanova on 8/8/17. 37 | * simona@devlabs.bg 38 | *

39 | * This is the UI from which a Walk Detector Service can be stopped or started 40 | *

41 | * Once selected, the state is saved to shared preferences. 42 | * The state can e changed from the three dot Menu 43 | */ 44 | public class MainActivity extends AppCompatActivity implements PermissionsHelper.PermissionResultListener { 45 | // tag used for logging purposes 46 | private static final String TAG = MainActivity.class.getSimpleName(); 47 | @BindView(R.id.checked_period_edit_text) 48 | EditText checkedPeriodEditText; 49 | @BindView(R.id.detector_fab) 50 | FloatingActionButton detectorFab; 51 | boolean shouldDetect; 52 | @BindView(R.id.start_time_edit_text) 53 | EditText startTimeEditText; 54 | @BindView(R.id.end_time_edit_text) 55 | EditText endTimeEditText; 56 | @BindView(R.id.set_alarm_button) 57 | Button setAlarmButton; 58 | @BindView(R.id.alarm_info_layout) 59 | LinearLayout alarmInfoLayout; 60 | @BindView(R.id.all_day_check_box) 61 | CheckBox allDayCheckBox; 62 | 63 | @Override 64 | protected void onCreate(Bundle savedInstanceState) { 65 | super.onCreate(savedInstanceState); 66 | setContentView(R.layout.activity_main); 67 | ButterKnife.bind(this); 68 | showCurrentState(); 69 | } 70 | 71 | /** 72 | * Shows the saved check period edit text 73 | * Shows the correct state of the FAB depending on if the detector is started or stopped 74 | * Checks if an alarm has been set and shows it or clicks all day`s check box 75 | */ 76 | private void showCurrentState() { 77 | checkedPeriodEditText.setText(String.valueOf(SettingsManager.getInstance(this).checkedPeriodSeconds)); 78 | updateButtonStates(); 79 | boolean isAllDay = SettingsManager.getInstance(this).isAllDay; 80 | allDayCheckBox.setChecked(isAllDay); 81 | alarmInfoLayout.setVisibility(isAllDay ? View.INVISIBLE : View.VISIBLE); 82 | if (!isAllDay) { 83 | startTimeEditText.setText(SettingsManager.getInstance(this).startTime); 84 | endTimeEditText.setText(SettingsManager.getInstance(this).endTime); 85 | } 86 | } 87 | 88 | /** 89 | * Updates button icon to stop 90 | * Saves the new status to Shared Preferences 91 | * Calls the WalkDetectService, which start detecting 92 | */ 93 | private void startDetection() { 94 | showStopIcon(); 95 | SharedPreferencesHelper.saveShouldDetectStatus(this, true); 96 | //starting the service with the startDetection command 97 | Intent intent = new Intent(this, WalkDetectService.class); 98 | startService(intent); 99 | } 100 | 101 | /** 102 | * Animates start icon to stop icon 103 | */ 104 | private void showStopIcon() { 105 | AnimatedVectorDrawable animatedVectorDrawable = 106 | (AnimatedVectorDrawable) getDrawable(R.drawable.avd_play_to_stop); 107 | if (animatedVectorDrawable != null) { 108 | detectorFab.setImageDrawable(animatedVectorDrawable); 109 | animatedVectorDrawable.start(); 110 | } 111 | } 112 | 113 | /** 114 | * Animates stop icon to start icon 115 | */ 116 | private void showPlayIcon() { 117 | AnimatedVectorDrawable animatedVectorDrawable = 118 | (AnimatedVectorDrawable) getDrawable(R.drawable.avd_stop_to_play); 119 | if (animatedVectorDrawable != null) { 120 | detectorFab.setImageDrawable(animatedVectorDrawable); 121 | animatedVectorDrawable.start(); 122 | } 123 | } 124 | 125 | /** 126 | * Updates button icon to start 127 | * Saves the new status to Shared Preferences 128 | * Calls the WalkDetectService, which start detecting 129 | */ 130 | private void stopDetection() { 131 | showPlayIcon(); 132 | SharedPreferencesHelper.saveShouldDetectStatus(this, false); 133 | //starting the service with the stopDetection command 134 | Intent intent = new Intent(this, WalkDetectService.class); 135 | startService(intent); 136 | } 137 | 138 | /** 139 | * Updates button states depending on the saved to Shared preferences should detect status 140 | */ 141 | private void updateButtonStates() { 142 | if (SharedPreferencesHelper.shouldDetectWalking(this)) { 143 | shouldDetect = true; 144 | detectorFab.setImageResource(R.drawable.ic_stop_white_24dp); 145 | } else { 146 | shouldDetect = false; 147 | detectorFab.setImageResource(R.drawable.ic_play_arrow_white_24dp); 148 | } 149 | } 150 | 151 | /** 152 | * Callback received when a permissions request has been completed. 153 | */ 154 | @Override 155 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, 156 | @NonNull int[] grantResults) { 157 | PermissionsHelper.onRequestPermissionsResult(requestCode, grantResults, this); 158 | } 159 | 160 | @Override 161 | public void onPermissionGranted() { 162 | startDetection(); 163 | } 164 | 165 | /** 166 | * Handled on permission denied by showing a Snack bar 167 | * If the Snack bar is clicked the user is sent to settings where the current app is selected 168 | * so that the user can easily grant the needed permissions 169 | */ 170 | @SuppressLint("WrongViewCast") 171 | public void onPermissionDenied() { 172 | // Permission denied. 173 | // In this Activity we've chosen to notify the user that they 174 | // have rejected a core permission for the app since it makes the Activity useless. 175 | // We're communicating this message in a Snack bar since this is a sample app, but 176 | // core permissions would typically be best requested during a welcome-screen flow. 177 | // Additionally, it is important to remember that a permission might have been 178 | // rejected without asking the user for permission (device policy or "Never ask 179 | // again" prompts). Therefore, a user interface affordance is typically implemented 180 | // when permissions are denied. Otherwise, your app could appear unresponsive to 181 | // touches or interactions which have required permissions. 182 | Snackbar.make( 183 | findViewById(R.id.main_activity_view), 184 | R.string.permission_denied_explanation, 185 | Snackbar.LENGTH_INDEFINITE) 186 | .setAction(R.string.settings, view -> { 187 | // Build intent that displays the App settings screen. 188 | Intent intent = new Intent(); 189 | intent.setAction( 190 | Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 191 | Uri uri = Uri.fromParts("package", 192 | BuildConfig.APPLICATION_ID, null); 193 | intent.setData(uri); 194 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 195 | startActivity(intent); 196 | }) 197 | .show(); 198 | } 199 | 200 | /** 201 | * Handling button clicks using Butterknife 202 | * 203 | * @param view the view on which the user has clicked 204 | */ 205 | @OnClick({R.id.update_check_period_button, R.id.detector_fab, R.id.set_alarm_button}) 206 | public void onViewClicked(View view) { 207 | switch (view.getId()) { 208 | case R.id.update_check_period_button: 209 | onUpdateButtonClicked(); 210 | break; 211 | case R.id.detector_fab: 212 | onDetectorStateFabClicked(); 213 | break; 214 | case R.id.set_alarm_button: 215 | onAlarmButtonClicked(); 216 | break; 217 | } 218 | } 219 | 220 | /** 221 | * Checks for permissions and request them if needed 222 | * Validates if the times are valid 223 | *

224 | * If valid: 225 | * Saves the new auto start and stop times 226 | * Sets the two alarms for them 227 | *

228 | * If invalid: 229 | * Shows a toast 230 | */ 231 | private void onAlarmButtonClicked() { 232 | // When permissions are revoked the app is restarted so onCreate is sufficient to check for 233 | // permissions core to the Activity's functionality. 234 | if (!PermissionsHelper.checkPermissions(this)) { 235 | PermissionsHelper.requestPermissions(this); 236 | return; 237 | } 238 | String startTime = startTimeEditText.getText().toString(); 239 | String endTime = endTimeEditText.getText().toString(); 240 | if (!isTimeValid(startTime) || !isTimeValid(endTime)) { 241 | Toast.makeText(this, R.string.error_entered_invalid_time, Toast.LENGTH_SHORT).show(); 242 | return; 243 | } 244 | SharedPreferencesHelper.saveStartTime(this, startTime); 245 | SharedPreferencesHelper.saveEndTime(this, endTime); 246 | setAlarms(startTime, endTime); 247 | } 248 | 249 | /** 250 | * Sets the two alarms - one for starting and one for stopping the detection service 251 | * The alarms are triggered once a day by sending an intent to the service 252 | * 253 | * @param startTime at this time an intent will be sent to start the walk activity detection 254 | * @param endTime at this time an intent will be sent to stop the walk activity detection 255 | */ 256 | private void setAlarms(String startTime, String endTime) { 257 | AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 258 | setAlarm(am, "start", 0, startTime); 259 | setAlarm(am, "stop", 1, endTime); 260 | Toast.makeText(this, R.string.set_auto_turn_on_and_off, Toast.LENGTH_SHORT).show(); 261 | } 262 | 263 | /** 264 | * @param am The system's Alarm Manager 265 | * @param value what value should be sent to the Service trough the Intent Extras - start or stop 266 | * @param requestCode each request should have different code 267 | * or else they will override each other 268 | * @param time at this time each day a intent will be sent to the Service 269 | */ 270 | public void setAlarm(AlarmManager am, String value, int requestCode, String time) { 271 | Intent startDetectionIntent = new Intent(this, WalkDetectService.class); 272 | startDetectionIntent.putExtra("Alarm", value); 273 | PendingIntent pi = PendingIntent.getService(this, requestCode, 274 | startDetectionIntent, PendingIntent.FLAG_UPDATE_CURRENT); 275 | am.setRepeating(AlarmManager.RTC_WAKEUP, DateTimeHelper.getCalendarTimeMillis(time), 276 | AlarmManager.INTERVAL_DAY, pi); 277 | 278 | } 279 | 280 | /** 281 | * Checks for permissions and request them if needed 282 | * Stops or starts detection depending on the current state 283 | */ 284 | private void onDetectorStateFabClicked() { 285 | // When permissions are revoked the app is restarted so onCreate is sufficient to check for 286 | // permissions core to the Activity's functionality. 287 | if (!PermissionsHelper.checkPermissions(this)) { 288 | PermissionsHelper.requestPermissions(this); 289 | return; 290 | } 291 | if (shouldDetect) { 292 | stopDetection(); 293 | } else { 294 | startDetection(); 295 | } 296 | shouldDetect = !shouldDetect; 297 | } 298 | 299 | /** 300 | * Clears focus of the button 301 | * Validates the entered check period - if it is too short or too long 302 | * a toast is shown to the user to warn him 303 | *

304 | * The new period is sent to the Setting Manager where all other configuration values are updated 305 | * and the new setting is saved to the storage 306 | *

307 | * If the user has entered an invalid number a toast is shown to inform him 308 | */ 309 | private void onUpdateButtonClicked() { 310 | checkedPeriodEditText.clearFocus(); 311 | hideKeyboard(); 312 | try { 313 | int checkPeriod = Integer.valueOf(checkedPeriodEditText.getText().toString()); 314 | if (checkPeriod < 60 || checkPeriod > 600) { 315 | Toast.makeText(this, R.string.error_check_period_not_recommended, Toast.LENGTH_SHORT).show(); 316 | } 317 | SettingsManager.getInstance(this).saveNewCheckPeriod(this, checkPeriod); 318 | Toast.makeText(this, R.string.check_period_updated, Toast.LENGTH_SHORT).show(); 319 | } catch (Exception e) { 320 | Toast.makeText(this, R.string.error_parsing_check_period, Toast.LENGTH_SHORT).show(); 321 | } 322 | } 323 | 324 | /** 325 | * Hides the software keyboard 326 | */ 327 | private void hideKeyboard() { 328 | // Check if no view has focus: 329 | View view = this.getCurrentFocus(); 330 | if (view != null) { 331 | InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); 332 | imm.hideSoftInputFromWindow(view.getWindowToken(), 0); 333 | } 334 | } 335 | 336 | /** 337 | * Handles check state changes on the all_day_check_box 338 | * Hides and clears the alarm info if the checkbox is checked 339 | *

340 | * If it is unchecked shows the alarmInfoLayout, in which the user can enter start and end times 341 | * for auto stop and start of the detector service 342 | * 343 | * @param checked - the new state 344 | */ 345 | @OnCheckedChanged(R.id.all_day_check_box) 346 | void onChecked(boolean checked) { 347 | if (checked) { 348 | SharedPreferencesHelper.saveStartTime(this, SharedPreferencesHelper.DEFAULT_TIME); 349 | SharedPreferencesHelper.saveEndTime(this, SharedPreferencesHelper.DEFAULT_TIME); 350 | startTimeEditText.setText(""); 351 | endTimeEditText.setText(""); 352 | } 353 | alarmInfoLayout.setVisibility(checked ? View.INVISIBLE : View.VISIBLE); 354 | } 355 | } -------------------------------------------------------------------------------- /app/src/main/java/bg/devlabs/walkdetector/WalkDetectService.java: -------------------------------------------------------------------------------- 1 | package bg.devlabs.walkdetector; 2 | 3 | import android.app.Service; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.os.IBinder; 7 | import android.support.annotation.Nullable; 8 | import android.util.Log; 9 | 10 | import com.google.android.gms.common.Scopes; 11 | import com.google.android.gms.common.api.GoogleApiClient; 12 | import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; 13 | import com.google.android.gms.common.api.Scope; 14 | import com.google.android.gms.fitness.Fitness; 15 | import com.google.android.gms.fitness.data.Bucket; 16 | import com.google.android.gms.fitness.data.DataPoint; 17 | import com.google.android.gms.fitness.data.DataSet; 18 | import com.google.android.gms.fitness.data.DataType; 19 | import com.google.android.gms.fitness.data.Field; 20 | import com.google.android.gms.fitness.request.DataReadRequest; 21 | import com.google.android.gms.fitness.result.DataReadResult; 22 | 23 | import java.text.DateFormat; 24 | import java.util.Calendar; 25 | import java.util.Date; 26 | import java.util.List; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | import bg.devlabs.walkdetector.util.NotificationHelper; 30 | import bg.devlabs.walkdetector.util.SettingsManager; 31 | import bg.devlabs.walkdetector.util.SharedPreferencesHelper; 32 | import io.reactivex.Observable; 33 | import io.reactivex.android.schedulers.AndroidSchedulers; 34 | import io.reactivex.disposables.Disposable; 35 | import io.reactivex.schedulers.Schedulers; 36 | 37 | /** 38 | * Created by Simona Stoyanova on 8/8/17. 39 | * simona@devlabs.bg 40 | *

41 | * This sample demonstrates how to use the Sensors API of the Google Fit platform to find 42 | * available data sources and to register/unregister listeners to those sources 43 | *

44 | * This service is always running and checking if there has been enough steps for walking activity 45 | * to be detected. If so, a notification is shown. 46 | *

47 | * The service reads the tracking state from shared preferences. 48 | * The state can e changed from the UI (Main Activity -> three dot Menu) 49 | */ 50 | 51 | public class WalkDetectService extends Service { 52 | // tag used for logging purposes 53 | private static final String TAG = WalkDetectService.class.getSimpleName(); 54 | 55 | // Used to access the Fitness.HistoryApi 56 | static private GoogleApiClient mClient = null; 57 | 58 | // Simple DateTimeFormat for logging and information showing purposes 59 | static private final java.text.DateFormat dateTimeInstance = DateFormat.getDateTimeInstance(); 60 | 61 | // Disposable from the interval Observable, which is disposed when the user no longer wants 62 | // his status to be checked 63 | static private Disposable disposable; 64 | 65 | /** 66 | * Creates an IntentService. Invoked by your subclass's constructor. 67 | * name Used to name the worker thread, important only for debugging. 68 | */ 69 | public WalkDetectService() { 70 | super(); 71 | } 72 | 73 | /** 74 | * Called by the system every time a client explicitly starts the service by calling startService(Intent) 75 | * Checks the state and: 76 | * - if true starts the detection by building the client and connecting to it 77 | * - if false stops the detection by disposing the disposable and disconnecting from the client 78 | * 79 | * @return START_STICKY in order for the service not to die when the app dies 80 | */ 81 | @Override 82 | public int onStartCommand(Intent intent, int flags, int startId) { 83 | super.onStartCommand(intent, flags, startId); 84 | Log.d(TAG, "onStartCommand: flags = " + flags); 85 | 86 | if (intent.hasExtra("Alarm")) { 87 | if (intent.getStringExtra("Alarm").equals("start")) { 88 | Log.d(TAG, "onStartCommand: staring service"); 89 | SharedPreferencesHelper.saveShouldDetectStatus(getApplicationContext(), true); 90 | buildFitnessClient(); 91 | return START_STICKY; 92 | } else if (intent.getStringExtra("Alarm").equals("stop")) { 93 | Log.d(TAG, "onStartCommand: stopping service"); 94 | SharedPreferencesHelper.saveShouldDetectStatus(getApplicationContext(), false); 95 | stopCheckingForWalking(); 96 | return START_STICKY; 97 | } 98 | } 99 | 100 | if (SharedPreferencesHelper.shouldDetectWalking(getApplicationContext())) { 101 | Log.d(TAG, "onStartCommand: staring service"); 102 | buildFitnessClient(); 103 | } else { 104 | Log.d(TAG, "onStartCommand: stopping service"); 105 | stopCheckingForWalking(); 106 | } 107 | //this service will run until we stop it 108 | return START_STICKY; 109 | } 110 | 111 | 112 | /** 113 | * Build a {@link GoogleApiClient} that will authenticate the user and allow the application 114 | * to connect to Fitness APIs. The scopes included should match the scopes your app needs 115 | * (see documentation for details). 116 | */ 117 | private void buildFitnessClient() { 118 | if (mClient != null) { 119 | return; 120 | } 121 | ConnectionCallbacks connectionCallbacks = getConnectionCallbacks(); 122 | mClient = new GoogleApiClient.Builder(this) 123 | .addApi(Fitness.HISTORY_API) 124 | .addScope(new Scope(Scopes.FITNESS_ACTIVITY_READ_WRITE)) 125 | .addConnectionCallbacks(connectionCallbacks) 126 | .build(); 127 | mClient.connect(); 128 | } 129 | 130 | /** 131 | * @return Connection callbacks for when the client is connected or the connection is suspended 132 | */ 133 | private ConnectionCallbacks getConnectionCallbacks() { 134 | return new GoogleApiClient.ConnectionCallbacks() { 135 | @Override 136 | public void onConnected(Bundle bundle) { 137 | startTimerObservable(); 138 | } 139 | 140 | @Override 141 | public void onConnectionSuspended(int i) { 142 | // If your connection to the sensor gets lost at some point, 143 | // you'll be able to determine the reason and react to it here. 144 | if (i == ConnectionCallbacks.CAUSE_NETWORK_LOST) { 145 | Log.d(TAG, "Connection lost. Cause: Network Lost."); 146 | } else if (i == ConnectionCallbacks.CAUSE_SERVICE_DISCONNECTED) { 147 | Log.d(TAG, 148 | "Connection lost. Reason: Service Disconnected"); 149 | } 150 | } 151 | }; 152 | } 153 | 154 | /** 155 | * Defines an interval based observable and subscribes to it. 156 | *

157 | * The subscription returns a disposable which later can be disposed 158 | * when we no longer want to detect walking activity 159 | *

160 | * Uses .startWith(0L) in order to trigger onNext immediately 161 | *

162 | * The first result from interval is ignored as the computations don`t depend on the current index, 163 | * but on the current moment 164 | *

165 | * On every "tick" of the interval the History API is queried 166 | * Then the result is computed in handleDataReadResult 167 | */ 168 | private void startTimerObservable() { 169 | disposable = Observable.interval(SettingsManager.getInstance(this).observablePeriodSeconds, TimeUnit.SECONDS) 170 | .startWith(0L) 171 | .map(ignored -> getReadRequest()) 172 | .map(this::callHistoryApi) 173 | .subscribeOn(Schedulers.io()) 174 | .observeOn(AndroidSchedulers.mainThread()) 175 | .subscribe(this::handleDataReadResult, 176 | (Throwable e) -> Log.d(TAG, "Throwable " + e.getLocalizedMessage()) 177 | ); 178 | } 179 | 180 | /** 181 | * Checks if the query result is containing any significant steps made in the last 182 | * {@link @SettingsManager#checkedPeriodSeconds} 183 | * 184 | * @param dataReadResult returned from the Fitness.HistoryApi 185 | */ 186 | private void handleDataReadResult(DataReadResult dataReadResult) { 187 | //Used for aggregated data 188 | if (dataReadResult.getBuckets().size() > 0) { 189 | for (Bucket bucket : dataReadResult.getBuckets()) { 190 | List dataSets = bucket.getDataSets(); 191 | for (DataSet dataSet : dataSets) { 192 | checkDataForWalkActivity(dataSet); 193 | } 194 | } 195 | } 196 | } 197 | 198 | /** 199 | * Invoke the History API to fetch the data with the query and await the result of the read request. 200 | * 201 | * @param dataReadRequest user for the readData request 202 | * @return DataReadResult returned fom the Fitness.HistoryApi 203 | */ 204 | private DataReadResult callHistoryApi(DataReadRequest dataReadRequest) { 205 | return Fitness.HistoryApi.readData(mClient, dataReadRequest) 206 | .await(SettingsManager.AWAIT_PERIOD_SECONDS, TimeUnit.SECONDS); 207 | 208 | } 209 | 210 | /** 211 | * Builds Read request depending on the current time 212 | * Queries step count delta between the current moment and checkedPeriodSeconds ago 213 | * 214 | * @return the DataReadRequest which can be used to Query the Fitness.HistoryApi 215 | */ 216 | private DataReadRequest getReadRequest() { 217 | Calendar cal = Calendar.getInstance(); 218 | Date now = new Date(); 219 | cal.setTime(now); 220 | long endTime = cal.getTimeInMillis(); 221 | cal.add(Calendar.SECOND, -SettingsManager.getInstance(this).checkedPeriodSeconds); 222 | long startTime = cal.getTimeInMillis(); 223 | 224 | //Check how many steps were walked and recorded in the last 7 days 225 | return new DataReadRequest.Builder() 226 | // The data request can specify multiple data types to return, effectively 227 | // combining multiple data queries into one call. 228 | // In this example, it's very unlikely that the request is for several hundred 229 | // data points each consisting of a few steps and a timestamp. The more likely 230 | // scenario is wanting to see how many steps were walked per day, for 7 days. 231 | .aggregate(DataType.TYPE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA) 232 | .bucketByTime(3, TimeUnit.DAYS) 233 | .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS) 234 | .build(); 235 | } 236 | 237 | /** 238 | * Checks for walking activity in the dataSet and if found shows a notification 239 | * 240 | * @param dataSet to be examined for walking activity 241 | */ 242 | private void checkDataForWalkActivity(DataSet dataSet) { 243 | for (DataPoint dp : dataSet.getDataPoints()) { 244 | if (!dp.getDataType().getName().equals("com.google.step_count.delta")) { 245 | break; 246 | } 247 | for (Field field : dp.getDataType().getFields()) { 248 | Log.d("History", "\tField: " + field.getName() + " Value: " + dp.getValue(field)); 249 | int count = dp.getValue(field).asInt(); 250 | if (field.getName().equals("steps") && count > SettingsManager.getInstance(this).neededStepsCountForWalking) { 251 | String message = "Steps = " + count 252 | + "\nFrom \t" + dateTimeInstance.format(dp.getStartTime(TimeUnit.MILLISECONDS)) 253 | + " to " + dateTimeInstance.format(dp.getEndTime(TimeUnit.MILLISECONDS)); 254 | NotificationHelper.sendNotification(message, getApplicationContext()); 255 | return; 256 | } 257 | } 258 | } 259 | } 260 | 261 | /** 262 | * Disposes the disposable, so that the interval is stopped 263 | * Disconnects the mClient 264 | */ 265 | private void stopCheckingForWalking() { 266 | Log.d(TAG, "stopCheckingForWalking: disposable.dispose();"); 267 | if (disposable != null) { 268 | disposable.dispose(); 269 | } 270 | if (mClient != null && mClient.isConnected()) { 271 | mClient.disconnect(); 272 | mClient = null; 273 | } 274 | } 275 | 276 | @Nullable 277 | @Override 278 | public IBinder onBind(Intent intent) { 279 | return null; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /app/src/main/java/bg/devlabs/walkdetector/util/DateTimeHelper.java: -------------------------------------------------------------------------------- 1 | package bg.devlabs.walkdetector.util; 2 | 3 | import java.util.Calendar; 4 | import java.util.regex.Matcher; 5 | import java.util.regex.Pattern; 6 | 7 | /** 8 | * Created by Simona Stoyanova on 8/9/17. 9 | * simona@devlabs.bg 10 | *

11 | * A helper class which validates the entered time values 12 | * and helps with Calendar calculations for the every day alarm times 13 | */ 14 | 15 | public class DateTimeHelper { 16 | // tag used for logging purposes 17 | private static final String TAG = DateTimeHelper.class.getSimpleName(); 18 | 19 | /** 20 | * Validates the user input 21 | * 22 | * @param startTime user input time 23 | * @return true if the format is correct and false otherwise 24 | */ 25 | public static boolean isTimeValid(String startTime) { 26 | // Pattern that matches the 24 hour minutes time format 27 | // For example 23:59 is a valid input 28 | Pattern p = Pattern.compile("^([0-1]\\d|2[0-3]):([0-5]\\d)$"); 29 | Matcher m = p.matcher(startTime); 30 | return m.matches(); 31 | } 32 | 33 | /** 34 | * Sets the hours and minutes to the calendar, so it can be used for everyday alarms 35 | * 36 | * @param startTime used to get the hour and minutes in the HH:mm format 37 | * @return time in millis of the calendar 38 | */ 39 | public static long getCalendarTimeMillis(String startTime) { 40 | Calendar calendar = Calendar.getInstance(); 41 | calendar.set(Calendar.HOUR_OF_DAY, Integer.valueOf(startTime.substring(0, 2))); // For 1 PM or 2 PM 42 | calendar.set(Calendar.MINUTE, Integer.valueOf(startTime.substring(3, 5))); 43 | calendar.set(Calendar.SECOND, 0); 44 | calendar.set(Calendar.MILLISECOND, 0); 45 | return calendar.getTimeInMillis(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/bg/devlabs/walkdetector/util/NotificationHelper.java: -------------------------------------------------------------------------------- 1 | package bg.devlabs.walkdetector.util; 2 | 3 | import android.app.NotificationManager; 4 | import android.app.PendingIntent; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.graphics.Color; 8 | import android.media.Ringtone; 9 | import android.media.RingtoneManager; 10 | import android.net.Uri; 11 | import android.support.v4.app.NotificationCompat; 12 | import android.support.v4.app.TaskStackBuilder; 13 | import android.util.Log; 14 | 15 | import bg.devlabs.walkdetector.MainActivity; 16 | import bg.devlabs.walkdetector.R; 17 | 18 | import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID; 19 | 20 | /** 21 | * Created by Simona Stoyanova on 8/9/17. 22 | * simona@devlabs.bg 23 | *

24 | * A helper class which simplifies Shared preference read/write operations 25 | */ 26 | 27 | public class NotificationHelper { 28 | // tag used for logging purposes 29 | private static final String TAG = NotificationHelper.class.getSimpleName(); 30 | private static final String APP_TO_OPEN_PACKAGE_NAME = "com.charitymilescm.android"; 31 | 32 | /** 33 | * Posts a notification in the notification bar when a transition is detected. 34 | * If the user clicks the notification, control goes to the MainActivity. 35 | */ 36 | public static void sendNotification(String message, Context context) { 37 | playNotificationSound(context); 38 | 39 | Log.d(TAG, "sendNotification " + message); 40 | // Create an explicit content Intent that starts the APP_TO_OPEN_PACKAGE_NAME. 41 | Intent notificationIntent = context.getPackageManager().getLaunchIntentForPackage(APP_TO_OPEN_PACKAGE_NAME); 42 | // If the app is not installed on the device the Home screen is opened 43 | if (notificationIntent == null) { 44 | notificationIntent = new Intent(context, MainActivity.class); 45 | } 46 | 47 | notificationIntent.putExtra("notificationDetails", message); 48 | // Construct a task stack. 49 | TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); 50 | // Push the content Intent onto the stack. 51 | stackBuilder.addNextIntent(notificationIntent); 52 | // Get a PendingIntent containing the entire back stack. 53 | PendingIntent notificationPendingIntent = stackBuilder.getPendingIntent(0, 54 | PendingIntent.FLAG_UPDATE_CURRENT); 55 | // Get a notification builder that's compatible with platform versions >= 4 56 | NotificationCompat.Builder builder = null; 57 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { 58 | builder = new NotificationCompat.Builder(context, DEFAULT_CHANNEL_ID); 59 | } else { 60 | // It is deprecated but the definition above is not supported on older versions 61 | builder = new NotificationCompat.Builder(context); 62 | } 63 | // Define the notification settings. 64 | builder.setSmallIcon(R.drawable.ic_directions_walk_black_24dp) 65 | .setColor(Color.RED) 66 | .setContentTitle(context.getString(R.string.walking_detected)) 67 | .setStyle(new NotificationCompat.BigTextStyle().bigText(message)) 68 | .setContentText(message) 69 | .setContentIntent(notificationPendingIntent); 70 | 71 | // Dismiss notification once the user touches it. 72 | builder.setAutoCancel(true); 73 | 74 | // Get an instance of the Notification manager 75 | NotificationManager mNotificationManager = (NotificationManager) context.getSystemService( 76 | Context.NOTIFICATION_SERVICE); 77 | 78 | // Issue the notification 79 | mNotificationManager.notify(0, builder.build()); 80 | } 81 | 82 | /** 83 | * Plays the default notification sound 84 | * 85 | * @param context needed to get the notification ringtone from RingtoneManager 86 | */ 87 | private static void playNotificationSound(Context context) { 88 | try { 89 | Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); 90 | Ringtone r = RingtoneManager.getRingtone(context, notification); 91 | r.play(); 92 | } catch (Exception e) { 93 | e.printStackTrace(); 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/bg/devlabs/walkdetector/util/PermissionsHelper.java: -------------------------------------------------------------------------------- 1 | package bg.devlabs.walkdetector.util; 2 | 3 | import android.Manifest; 4 | import android.annotation.SuppressLint; 5 | import android.app.Activity; 6 | import android.content.Context; 7 | import android.content.pm.PackageManager; 8 | import android.support.annotation.NonNull; 9 | import android.support.design.widget.Snackbar; 10 | import android.support.v4.app.ActivityCompat; 11 | import android.util.Log; 12 | 13 | import bg.devlabs.walkdetector.R; 14 | 15 | /** 16 | * Created by Simona Stoyanova on 8/9/17. 17 | * simona@devlabs.bg 18 | *

19 | * A helper class which simplifies Permission checking and requesting 20 | */ 21 | 22 | public class PermissionsHelper { 23 | // tag used for logging purposes 24 | private static final String TAG = PermissionsHelper.class.getSimpleName(); 25 | // request code for permissions 26 | private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34; 27 | 28 | /** 29 | * Return the current state of the permissions needed. 30 | */ 31 | public static boolean checkPermissions(Context context) { 32 | int permissionState = ActivityCompat.checkSelfPermission(context, 33 | Manifest.permission.ACCESS_FINE_LOCATION); 34 | return permissionState == PackageManager.PERMISSION_GRANTED; 35 | } 36 | 37 | @SuppressLint("WrongViewCast") 38 | public static void requestPermissions(Activity activity) { 39 | boolean shouldProvideRationale = 40 | ActivityCompat.shouldShowRequestPermissionRationale(activity, 41 | Manifest.permission.ACCESS_FINE_LOCATION); 42 | // Provide an additional rationale to the user. This would happen if the user denied the 43 | // request previously, but didn't check the "Don't ask again" checkbox. 44 | if (shouldProvideRationale) { 45 | Log.d(TAG, "Displaying permission rationale to provide additional context."); 46 | Snackbar.make( 47 | activity.findViewById(R.id.main_activity_view), 48 | R.string.permission_rationale, 49 | Snackbar.LENGTH_INDEFINITE) 50 | .setAction(R.string.ok, view -> { 51 | // Request permission 52 | ActivityCompat.requestPermissions(activity, 53 | new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 54 | REQUEST_PERMISSIONS_REQUEST_CODE); 55 | }) 56 | .show(); 57 | } else { 58 | Log.d(TAG, "Requesting permission"); 59 | // Request permission. It's possible this can be auto answered if device policy 60 | // sets the permission in a given state or the user denied the permission 61 | // previously and checked "Never ask again". 62 | ActivityCompat.requestPermissions(activity, 63 | new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 64 | REQUEST_PERMISSIONS_REQUEST_CODE); 65 | } 66 | } 67 | 68 | /** 69 | * Callback received when a permissions request has been completed. 70 | */ 71 | public static void onRequestPermissionsResult(int requestCode, 72 | @NonNull int[] grantResults, 73 | PermissionResultListener listener) { 74 | Log.d(TAG, "onRequestPermissionResult"); 75 | if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) { 76 | if (grantResults.length <= 0) { 77 | // If user interaction was interrupted, the permission request is cancelled and you 78 | // receive empty arrays. 79 | Log.d(TAG, "User interaction was cancelled."); 80 | } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 81 | // Permission was granted. 82 | listener.onPermissionGranted(); 83 | } else { 84 | listener.onPermissionDenied(); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * This methods are called in onRequestPermissionResult depending on the grantResults 91 | */ 92 | public interface PermissionResultListener { 93 | void onPermissionGranted(); 94 | 95 | void onPermissionDenied(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/bg/devlabs/walkdetector/util/SettingsManager.java: -------------------------------------------------------------------------------- 1 | package bg.devlabs.walkdetector.util; 2 | 3 | import android.content.Context; 4 | 5 | import static bg.devlabs.walkdetector.util.SharedPreferencesHelper.DEFAULT_TIME; 6 | 7 | /** 8 | * Created by Simona Stoyanova on 8/9/17. 9 | * simona@devlabs.bg 10 | *

11 | * Singleton class which handles check period read/ write and store operations 12 | * The moment the instance is created it reads the saved check period from storage 13 | */ 14 | 15 | public class SettingsManager { 16 | private static SettingsManager ourInstance; 17 | 18 | // How much will the app wait for response until a timeout exception is thrown 19 | public static final int AWAIT_PERIOD_SECONDS = 60; // 60 seconds = 1 min 20 | // Walking slow (2 mph) 67 steps per minute which is almost one step per second 21 | private static final int SLOW_WALKING_STEPS_PER_SECOND = 1; 22 | 23 | // How long will the checked for walking activity period be 24 | public int checkedPeriodSeconds = 180; //180 seconds = 3 minutes 25 | // How often will the app query the client for walking activity 26 | public int observablePeriodSeconds = checkedPeriodSeconds + AWAIT_PERIOD_SECONDS; 27 | // The calculated amount of steps if the user was walking during the checked period of time 28 | // For example 180 seconds * 1 step at a second = 180 steps 29 | // This value is used to determine if the user was walking trough the checked period of time 30 | public int neededStepsCountForWalking = checkedPeriodSeconds * SLOW_WALKING_STEPS_PER_SECOND; 31 | // should the detection run all day 32 | public boolean isAllDay; 33 | //if there is an alarm set the detection will work from startTime to endTime 34 | public String startTime, endTime; 35 | 36 | public static SettingsManager getInstance(Context context) { 37 | if (ourInstance == null) { 38 | ourInstance = new SettingsManager(context); 39 | } 40 | return ourInstance; 41 | } 42 | 43 | /** 44 | * Reads the saved value for checked period 45 | * 46 | * @param context needed for shared preferences read operations 47 | */ 48 | private SettingsManager(Context context) { 49 | checkedPeriodSeconds = SharedPreferencesHelper.readCheckPeriod(context); 50 | startTime = SharedPreferencesHelper.readStartTime(context); 51 | endTime = SharedPreferencesHelper.readEndTime(context); 52 | 53 | //check if hte is all day is active or the user has set an alarm 54 | isAllDay = startTime.equals(DEFAULT_TIME) && endTime.equals(DEFAULT_TIME); 55 | } 56 | 57 | public void saveNewCheckPeriod(Context context, int checkPeriod) { 58 | SharedPreferencesHelper.saveCheckPeriod(context, checkPeriod); 59 | checkedPeriodSeconds = checkPeriod; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/bg/devlabs/walkdetector/util/SharedPreferencesHelper.java: -------------------------------------------------------------------------------- 1 | package bg.devlabs.walkdetector.util; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.SharedPreferences; 6 | 7 | import bg.devlabs.walkdetector.R; 8 | 9 | /** 10 | * Created by Simona Stoyanova on 8/8/17. 11 | * simona@devlabs.bg 12 | *

13 | * A helper class which simplifies Shared preference read/write operations 14 | */ 15 | public class SharedPreferencesHelper { 16 | public static final String DEFAULT_TIME = "00:00"; 17 | 18 | /** 19 | * @param context needed in order to access the Shared preferences API and in order to read String keys 20 | * @return whether the app should detect walking 21 | */ 22 | public static boolean shouldDetectWalking(Context context) { 23 | SharedPreferences sharedPref = getSharedPreference(context); 24 | return sharedPref.getBoolean(context.getString(R.string.should_track_status), false); 25 | } 26 | 27 | /** 28 | * @param context needed in order to access the Shared preferences API and in order to read String keys 29 | * @param shouldTrack whether the app should detect walking 30 | */ 31 | @SuppressLint("ApplySharedPref") 32 | public static void saveShouldDetectStatus(Context context, boolean shouldTrack) { 33 | SharedPreferences sharedPref = getSharedPreference(context); 34 | SharedPreferences.Editor editor = sharedPref.edit(); 35 | editor.putBoolean(context.getString(R.string.should_track_status), shouldTrack); 36 | editor.commit(); 37 | } 38 | 39 | /** 40 | * @param context needed in order to access the Shared preferences API 41 | * @return SharedPreferences instance that can be user for read/write operations 42 | */ 43 | private static SharedPreferences getSharedPreference(Context context) { 44 | return context.getSharedPreferences( 45 | context.getString(R.string.preference_file_key), Context.MODE_PRIVATE); 46 | } 47 | 48 | /** 49 | * @param context needed in order to access the Shared preferences API and in order to read String keys 50 | * @param checkPeriod how often the app should check for steps count in order to detect walking activity 51 | */ 52 | @SuppressLint("ApplySharedPref") 53 | static void saveCheckPeriod(Context context, int checkPeriod) { 54 | SharedPreferences sharedPref = getSharedPreference(context); 55 | SharedPreferences.Editor editor = sharedPref.edit(); 56 | editor.putInt(context.getString(R.string.check_period_key), checkPeriod); 57 | editor.commit(); 58 | } 59 | 60 | /** 61 | * @param context needed in order to access the Shared preferences API and in order to read String keys 62 | */ 63 | static int readCheckPeriod(Context context) { 64 | SharedPreferences sharedPref = getSharedPreference(context); 65 | return sharedPref.getInt(context.getString(R.string.check_period_key), 180); 66 | } 67 | 68 | /** 69 | * @param context needed in order to access the Shared preferences API and in order to read String keys 70 | */ 71 | static String readStartTime(Context context) { 72 | SharedPreferences sharedPref = getSharedPreference(context); 73 | return sharedPref.getString(context.getString(R.string.alarm_start_time_key), DEFAULT_TIME); 74 | } 75 | 76 | /** 77 | * @param context needed in order to access the Shared preferences API and in order to read String keys 78 | * @param startTime when to auto start of the service 79 | */ 80 | @SuppressLint("ApplySharedPref") 81 | public static void saveStartTime(Context context, String startTime) { 82 | SharedPreferences sharedPref = getSharedPreference(context); 83 | SharedPreferences.Editor editor = sharedPref.edit(); 84 | editor.putString(context.getString(R.string.alarm_start_time_key), startTime); 85 | editor.commit(); 86 | } 87 | 88 | /** 89 | * @param context needed in order to access the Shared preferences API and in order to read String keys 90 | */ 91 | static String readEndTime(Context context) { 92 | SharedPreferences sharedPref = getSharedPreference(context); 93 | return sharedPref.getString(context.getString(R.string.alarm_end_time_key), DEFAULT_TIME); 94 | } 95 | 96 | /** 97 | * @param context needed in order to access the Shared preferences API and in order to read String keys 98 | * @param endTime when to auto stop of the service 99 | */ 100 | @SuppressLint("ApplySharedPref") 101 | public static void saveEndTime(Context context, String endTime) { 102 | SharedPreferences sharedPref = getSharedPreference(context); 103 | SharedPreferences.Editor editor = sharedPref.edit(); 104 | editor.putString(context.getString(R.string.alarm_end_time_key), endTime); 105 | editor.commit(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/res/animator/play_to_stop.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/animator/stop_to_play.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/avd_play_to_stop.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/avd_stop_to_play.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_directions_walk_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 16 | 25 | 26 | 33 | 34 | 43 | 44 |