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
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 |