├── .gitignore ├── .idea ├── assetWizardSettings.xml ├── caches │ └── build_file_checksums.ser ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── manveerbasra │ │ └── ontime │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── manveerbasra │ │ │ └── ontime │ │ │ ├── alarmmanager │ │ │ ├── AlarmHandler.java │ │ │ ├── AlarmSoundControl.java │ │ │ └── receiver │ │ │ │ ├── AlarmReceiver.java │ │ │ │ ├── AlarmSnoozeReceiver.java │ │ │ │ ├── AlarmStopReceiver.java │ │ │ │ └── TimeShiftReceiver.java │ │ │ ├── db │ │ │ ├── Alarm.java │ │ │ ├── AlarmDao.java │ │ │ ├── AlarmDatabase.java │ │ │ └── converter │ │ │ │ ├── BooleanArrayConverter.java │ │ │ │ ├── DateConverter.java │ │ │ │ └── LatLngConverter.java │ │ │ ├── timehandlers │ │ │ ├── TimeShiftHandler.java │ │ │ ├── TrafficTimeHandler.java │ │ │ └── WeatherTimeHandler.java │ │ │ ├── ui │ │ │ ├── AlarmListAdapter.java │ │ │ ├── AppCompatPreferenceActivity.java │ │ │ ├── SetRepeatDaysDialogFragment.java │ │ │ └── activity │ │ │ │ ├── AddAlarmActivity.java │ │ │ │ ├── MainActivity.java │ │ │ │ ├── MapsActivity.java │ │ │ │ └── SettingsActivity.java │ │ │ ├── util │ │ │ ├── AlarmRepository.java │ │ │ └── JSONParser.java │ │ │ └── viewmodel │ │ │ └── AlarmViewModel.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_add_white_24dp.xml │ │ ├── ic_check_24dp.xml │ │ ├── ic_check_white_24dp.xml │ │ ├── ic_delete_white_24dp.xml │ │ ├── ic_directions_bike_white_24dp.xml │ │ ├── ic_directions_car_white_24dp.xml │ │ ├── ic_directions_transit_white_24dp.xml │ │ ├── ic_directions_walk_white_24dp.xml │ │ ├── ic_edit_24dp.xml │ │ ├── ic_info_24dp.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_location_24dp.xml │ │ ├── ic_notifications_24dp.xml │ │ ├── ic_repeat_24dp.xml │ │ ├── ic_set_time_24dp.xml │ │ ├── ic_start_location_24dp.xml │ │ ├── ic_sync_24dp.xml │ │ ├── rounded_rectangle.xml │ │ ├── solid_circle_blue.xml │ │ ├── solid_circle_grey.xml │ │ └── solid_circle_light_blue.xml │ │ ├── layout │ │ ├── activity_add_alarm.xml │ │ ├── activity_main.xml │ │ ├── activity_maps.xml │ │ ├── content_main.xml │ │ └── item_alarm.xml │ │ ├── menu │ │ ├── menu_add_alarm.xml │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── fonts.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── pref_general.xml │ │ ├── pref_headers.xml │ │ └── pref_notification.xml │ ├── release │ └── res │ │ └── values │ │ └── google_maps_api.xml │ └── test │ └── java │ └── com │ └── manveerbasra │ └── ontime │ └── ExampleUnitTest.java ├── build.gradle ├── demo_images └── ui_demo.gif ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | /app/src/debug/res/values 12 | -------------------------------------------------------------------------------- /.idea/assetWizardSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 51 | 52 | -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manveerxyz/OnTime/962b67761bf38a205d5e9166dc445b4454ec14ac/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OnTime 2 | (In Development) An Android App to let user set an alarm, the app then adjusts the alarm time according to traffic and weather data, ensuring the user arrives OnTime. 3 | 4 | ## UI Demo 5 | A preliminary demo of how a user can interact with the UI to create an alarm.
6 | 7 | Creating An Alarm | 8 | :--------------------------------------:| 9 | | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.manveerbasra.ontime" 7 | minSdkVersion 26 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | productFlavors { 20 | } 21 | } 22 | 23 | dependencies { 24 | implementation fileTree(include: ['*.jar'], dir: 'libs') 25 | implementation 'com.android.support:appcompat-v7:28.0.0' 26 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 27 | implementation 'com.android.support:design:28.0.0' 28 | implementation 'com.android.support:support-v4:28.0.0' 29 | testImplementation 'junit:junit:4.12' 30 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 31 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 32 | 33 | def room_version = "1.1.1" 34 | implementation "android.arch.persistence.room:runtime:$room_version" 35 | annotationProcessor "android.arch.persistence.room:compiler:$room_version" 36 | implementation "android.arch.lifecycle:extensions:$room_version" 37 | annotationProcessor "android.arch.lifecycle:compiler:$room_version" 38 | 39 | implementation 'com.google.android.gms:play-services-maps:16.0.0' 40 | implementation 'com.google.android.gms:play-services-places:16.0.0' 41 | implementation 'com.google.android.gms:play-services-location:16.0.0' 42 | } 43 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/manveerbasra/ontime/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime; 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.*; 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() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.manveerbasra.ontime", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 21 | 22 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | 44 | 45 | 49 | 52 | 53 | 54 | 57 | 60 | 63 | 64 | 65 | 66 | 67 | 70 | 71 | 72 | 73 | 74 | 75 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/alarmmanager/AlarmHandler.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.alarmmanager; 2 | 3 | import android.app.AlarmManager; 4 | import android.app.PendingIntent; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.support.design.widget.Snackbar; 8 | import android.util.Log; 9 | import android.view.View; 10 | 11 | import com.manveerbasra.ontime.R; 12 | import com.manveerbasra.ontime.alarmmanager.receiver.AlarmReceiver; 13 | import com.manveerbasra.ontime.alarmmanager.receiver.TimeShiftReceiver; 14 | import com.manveerbasra.ontime.db.Alarm; 15 | 16 | import java.util.Calendar; 17 | import java.util.List; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | 21 | /** 22 | * Class to control alarm scheduling/cancelling 23 | */ 24 | public class AlarmHandler { 25 | 26 | public static final String EXTRA_ID = "extra_id"; 27 | 28 | private final String TAG = "AlarmHandler"; 29 | 30 | private Context mContext; 31 | private View mSnackBarAnchor; 32 | 33 | public AlarmHandler(Context context, View snackBarAnchor) { 34 | this.mContext = context; 35 | this.mSnackBarAnchor = snackBarAnchor; 36 | } 37 | 38 | /** 39 | * Schedule alarm TimeShiftReceiver an hour before alarm's time using AlarmManager 40 | * 41 | * @param alarm Alarm to schedule 42 | */ 43 | public void scheduleAlarm(Alarm alarm) { 44 | if (!alarm.isActive()) { 45 | return; 46 | } 47 | 48 | // Get PendingIntent to TimeShiftReceiver Broadcast channel 49 | Intent intent = new Intent(mContext, AlarmReceiver.class); 50 | intent.putExtra(EXTRA_ID, alarm.id); 51 | PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, alarm.id, intent, PendingIntent.FLAG_UPDATE_CURRENT); 52 | 53 | AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 54 | 55 | long nextAlarmRing = 0; // used in Snackbar 56 | 57 | if (alarmManager == null) { 58 | alarm.setActive(false); 59 | Snackbar.make(mSnackBarAnchor, 60 | mContext.getString(R.string.alarm_set_error), 61 | Snackbar.LENGTH_SHORT).show(); 62 | return; 63 | } 64 | 65 | if (alarm.isRepeating()) { 66 | // get list of time to ring in milliseconds for each active day, and repeat weekly 67 | List timeToWeeklyRings = alarm.getTimeToWeeklyRings(); 68 | Calendar calendar = Calendar.getInstance(); 69 | for (long millis : timeToWeeklyRings) { 70 | calendar.setTimeInMillis(millis); 71 | Log.i(TAG, "Setting weekly repeat at " + calendar.getTime().toString()); 72 | alarmManager.setRepeating( 73 | AlarmManager.RTC_WAKEUP, 74 | millis - TimeUnit.HOURS.toMillis(1), // need to call TimeShift an hour early 75 | AlarmManager.INTERVAL_DAY * 7, 76 | pendingIntent); 77 | 78 | if (millis < nextAlarmRing || nextAlarmRing == 0) nextAlarmRing = millis; 79 | } 80 | } else { 81 | nextAlarmRing = alarm.getTimeToNextRing(); // get time until next alarm ring 82 | 83 | Log.i(TAG, "setting alarm " + alarm.id + " to AlarmManager"); 84 | alarmManager.set( 85 | AlarmManager.RTC_WAKEUP, 86 | nextAlarmRing - TimeUnit.HOURS.toMillis(1), // need to call TimeShift an hour early 87 | pendingIntent); 88 | } 89 | 90 | String timeUntilNextRing = getStringOfTimeUntilNextRing(nextAlarmRing - System.currentTimeMillis()); 91 | // Show snackbar to notify user 92 | Snackbar.make(mSnackBarAnchor, 93 | String.format(mContext.getString(R.string.alarm_set), timeUntilNextRing), 94 | Snackbar.LENGTH_SHORT).show(); 95 | } 96 | 97 | /** 98 | * Converts milliseconds to # day(s), # hour(s), # minute(s) 99 | * 100 | * @param millisToRing milliseconds to next alarm ring 101 | * @return a user readable String of time until next alarm ring 102 | */ 103 | private String getStringOfTimeUntilNextRing(long millisToRing) { 104 | long seconds = millisToRing / 1000; 105 | long minutes = seconds / 60; 106 | long hours = minutes / 60; 107 | long days = hours / 24; 108 | 109 | minutes -= hours * 60; 110 | hours -= days * 24; 111 | 112 | if (days == 0 && hours == 0 && minutes == 0) { 113 | return "less than a minute"; 114 | } 115 | 116 | StringBuilder sbTime = new StringBuilder(); 117 | if (days >= 1) { 118 | sbTime.append(days); 119 | sbTime.append(days > 1 ? " days" : " day"); 120 | if (hours >= 1 || minutes >= 1) { 121 | sbTime.append(", "); 122 | } 123 | } 124 | if (hours >= 1) { 125 | sbTime.append(hours); 126 | sbTime.append(hours > 1 ? " hours" : " hour"); 127 | if (minutes >= 1) { 128 | sbTime.append(", "); 129 | } 130 | } 131 | if (minutes >= 1) { 132 | sbTime.append(minutes); 133 | sbTime.append(minutes > 1 ? " minutes" : " minute"); 134 | } 135 | 136 | return sbTime.toString(); 137 | } 138 | 139 | /** 140 | * Schedule alarm notification based on time until next alarm 141 | * 142 | * @param timeToRing time to next alarm in milliseconds 143 | * @param alarmID ID of alarm to ring 144 | */ 145 | public void scheduleAlarmWithTime(int timeToRing, int alarmID) { 146 | // Calculate time until alarm from millis since epoch 147 | Calendar calendar = Calendar.getInstance(); 148 | calendar.add(Calendar.MILLISECOND, timeToRing); 149 | long alarmTimeInMillis = calendar.getTimeInMillis(); 150 | 151 | // Get PendingIntent to AlarmReceiver Broadcast channel 152 | Intent intent = new Intent(mContext, AlarmReceiver.class); 153 | intent.putExtra(EXTRA_ID, alarmID); 154 | PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, alarmID, intent, PendingIntent.FLAG_UPDATE_CURRENT); 155 | 156 | Log.i(TAG, "setting timed alarm " + alarmID + " to AlarmManager for " + alarmTimeInMillis + " milliseconds"); 157 | AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 158 | if (alarmManager != null) { 159 | alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTimeInMillis, pendingIntent); 160 | } 161 | } 162 | 163 | /** 164 | * Cancel alarm notification and TimeShiftIntent using AlarmManager 165 | * 166 | * @param alarm Alarm to cancel 167 | */ 168 | public void cancelAlarm(Alarm alarm) { 169 | if (alarm.isActive()) { 170 | return; 171 | } 172 | 173 | // Get PendingIntent to AlarmReceiver Broadcast channel 174 | Intent intent = new Intent(mContext, AlarmReceiver.class); 175 | Intent shiftIntent = new Intent(mContext, TimeShiftReceiver.class); 176 | PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, alarm.id, intent, PendingIntent.FLAG_NO_CREATE); 177 | PendingIntent shiftPendingIntent = PendingIntent.getBroadcast(mContext, alarm.id, shiftIntent, PendingIntent.FLAG_NO_CREATE); 178 | 179 | AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 180 | Log.i(TAG, "cancelling alarm " + alarm.id); 181 | 182 | if (alarmManager != null) { 183 | // PendingIntent may be null if the alarm hasn't been set 184 | if (pendingIntent != null) alarmManager.cancel(pendingIntent); 185 | if (shiftPendingIntent != null) alarmManager.cancel(shiftPendingIntent); 186 | } 187 | 188 | // Show snackbar to notify user 189 | Snackbar.make(mSnackBarAnchor, mContext.getString(R.string.alarm_cancelled), Snackbar.LENGTH_SHORT).show(); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/alarmmanager/AlarmSoundControl.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.alarmmanager; 2 | 3 | import android.content.Context; 4 | import android.media.AudioManager; 5 | import android.media.MediaPlayer; 6 | import android.media.RingtoneManager; 7 | import android.net.Uri; 8 | import android.util.Log; 9 | 10 | import java.io.IOException; 11 | 12 | /** 13 | * Singleton class to control Alarm Ringing Sound 14 | */ 15 | public class AlarmSoundControl { 16 | 17 | private final String TAG = "AlarmSoundControl"; 18 | 19 | private static AlarmSoundControl mINSTANCE; 20 | private MediaPlayer mMediaPlayer; 21 | 22 | private AlarmSoundControl() { 23 | } 24 | 25 | public static AlarmSoundControl getInstance() { 26 | if (mINSTANCE == null) { 27 | mINSTANCE = new AlarmSoundControl(); 28 | } 29 | return mINSTANCE; 30 | } 31 | 32 | /** 33 | * Play Alarm Sound 34 | */ 35 | public void playAlarmSound(Context context) { 36 | Log.i(TAG, "Playing alarm ringing sound"); 37 | mMediaPlayer = new MediaPlayer(); 38 | try { 39 | mMediaPlayer.setDataSource(context, getAlarmUri()); 40 | final AudioManager audioManager = (AudioManager) context 41 | .getSystemService(Context.AUDIO_SERVICE); 42 | if (audioManager != null && 43 | audioManager.getStreamVolume(AudioManager.STREAM_ALARM) != 0) { 44 | mMediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); 45 | mMediaPlayer.prepare(); 46 | mMediaPlayer.start(); 47 | } 48 | } catch (IOException e) { 49 | System.out.println("Can't read Alarm uri: " + getAlarmUri()); 50 | } 51 | } 52 | 53 | /** 54 | * Stop Alarm Sound currently playing 55 | */ 56 | public void stopAlarmSound() { 57 | if (mMediaPlayer != null) { 58 | mMediaPlayer.stop(); 59 | } 60 | } 61 | 62 | /** 63 | * Get alarm sound, try to get default, then notification, then ringtone 64 | * 65 | * @return URI for alarm sound 66 | */ 67 | private Uri getAlarmUri() { 68 | Uri alert = RingtoneManager 69 | .getDefaultUri(RingtoneManager.TYPE_ALARM); 70 | if (alert == null) { 71 | alert = RingtoneManager 72 | .getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); 73 | if (alert == null) { 74 | alert = RingtoneManager 75 | .getDefaultUri(RingtoneManager.TYPE_RINGTONE); 76 | } 77 | } 78 | return alert; 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/alarmmanager/receiver/AlarmReceiver.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.alarmmanager.receiver; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationChannel; 5 | import android.app.NotificationManager; 6 | import android.app.PendingIntent; 7 | import android.content.BroadcastReceiver; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.content.SharedPreferences; 11 | import android.preference.PreferenceManager; 12 | import android.support.v4.app.NotificationCompat; 13 | import android.util.Log; 14 | 15 | import com.manveerbasra.ontime.R; 16 | import com.manveerbasra.ontime.alarmmanager.AlarmHandler; 17 | import com.manveerbasra.ontime.alarmmanager.AlarmSoundControl; 18 | 19 | /** 20 | * BroadcastReceiver to setup and display alarm notification 21 | */ 22 | public class AlarmReceiver extends BroadcastReceiver { 23 | 24 | private final String TAG = "AlarmReceiver"; 25 | private final String CHANNEL_ID = "AlarmReceiverChannel"; 26 | private SharedPreferences mPreferences; 27 | 28 | @Override 29 | public void onReceive(Context context, Intent intent) { 30 | Log.i(TAG, "received alarm intent"); 31 | 32 | // Initialize preferences 33 | mPreferences = PreferenceManager.getDefaultSharedPreferences(context); 34 | 35 | int alarmID = intent.getIntExtra(AlarmHandler.EXTRA_ID, 0); 36 | 37 | NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 38 | 39 | // Create and add notification channel 40 | NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, TAG, NotificationManager.IMPORTANCE_HIGH); 41 | if (notificationManager != null) notificationManager.createNotificationChannel(mChannel); 42 | 43 | Notification notification = buildNotification( 44 | context, 45 | getStopAlarmIntent(context, alarmID), 46 | getSnoozeAlarmIntent(context, alarmID) 47 | ); 48 | 49 | // Play alarm ringing sound 50 | AlarmSoundControl alarmSoundControl = AlarmSoundControl.getInstance(); 51 | alarmSoundControl.playAlarmSound(context.getApplicationContext()); 52 | 53 | Log.i(TAG, "displaying notification for alarm " + alarmID); 54 | if (notificationManager != null) notificationManager.notify(alarmID, notification); 55 | } 56 | 57 | /** 58 | * Get PendingIntent to Snooze Alarm 59 | * 60 | * @param context current App context 61 | * @param alarmID ID of alarm to handle 62 | * @return PendingIntent to AlarmSnooze(Broadcast)Receiver 63 | */ 64 | private PendingIntent getSnoozeAlarmIntent(Context context, int alarmID) { 65 | Intent snoozeAlarmIntent = new Intent(context, AlarmSnoozeReceiver.class); 66 | snoozeAlarmIntent.putExtra(AlarmHandler.EXTRA_ID, alarmID); 67 | snoozeAlarmIntent.setAction("Snooze Alarm"); 68 | return PendingIntent.getBroadcast(context, 0, snoozeAlarmIntent, 0); 69 | } 70 | 71 | /** 72 | * Get PendingIntent to Stop Alarm 73 | * 74 | * @param context current App context 75 | * @param alarmID ID of alarm to handle 76 | * @return PendingIntent to AlarmStop(Broadcast)Receiver 77 | */ 78 | private PendingIntent getStopAlarmIntent(Context context, int alarmID) { 79 | Intent stopAlarmIntent = new Intent(context, AlarmStopReceiver.class); 80 | stopAlarmIntent.putExtra(AlarmHandler.EXTRA_ID, alarmID); 81 | stopAlarmIntent.setAction("Stop Alarm"); 82 | return PendingIntent.getBroadcast(context, 0, stopAlarmIntent, 0); 83 | } 84 | 85 | /** 86 | * Build the alarm notification 87 | * 88 | * @param context current App context 89 | * @param stopAlarmPendingIntent PendingIntent object to stop the alarm ringing 90 | * @param snoozeAlarmPendingIntent Pending Intent object to snooze the alarm ringing 91 | * @return a Notification object of the built alarm notification 92 | */ 93 | private Notification buildNotification(Context context, PendingIntent stopAlarmPendingIntent, PendingIntent snoozeAlarmPendingIntent) { 94 | NotificationCompat.Builder notification = new NotificationCompat.Builder(context, CHANNEL_ID) 95 | .setDefaults(Notification.DEFAULT_ALL) 96 | .setPriority(NotificationCompat.PRIORITY_MAX) 97 | .setWhen(System.currentTimeMillis()) 98 | .setSmallIcon(R.drawable.ic_launcher_background) 99 | .setContentTitle(context.getString(R.string.app_name)) 100 | .setContentText(context.getString(R.string.alarm_notification_title)) 101 | .addAction(R.drawable.ic_launcher_background, context.getString(R.string.stop), 102 | stopAlarmPendingIntent) 103 | .addAction(R.drawable.ic_launcher_background, context.getString(R.string.snooze), 104 | snoozeAlarmPendingIntent); 105 | 106 | // Set vibrate based on preferences, default is vibrate 107 | if (!mPreferences.getBoolean("alarm_ring_vibrate", true)) { 108 | Log.i(TAG, "notification vibrate set to false"); 109 | notification.setVibrate(null); 110 | } 111 | 112 | return notification.build(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/alarmmanager/receiver/AlarmSnoozeReceiver.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.alarmmanager.receiver; 2 | 3 | import android.app.NotificationManager; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.SharedPreferences; 8 | import android.preference.PreferenceManager; 9 | import android.util.Log; 10 | 11 | import com.manveerbasra.ontime.alarmmanager.AlarmHandler; 12 | import com.manveerbasra.ontime.alarmmanager.AlarmSoundControl; 13 | 14 | /** 15 | * Broadcast Receiver to snooze alarm 16 | */ 17 | public class AlarmSnoozeReceiver extends BroadcastReceiver { 18 | 19 | private final String TAG = "AlarmSnoozeReceiver"; 20 | private SharedPreferences mPreferences; 21 | 22 | @Override 23 | public void onReceive(Context context, Intent intent) { 24 | mPreferences = PreferenceManager.getDefaultSharedPreferences(context); 25 | 26 | int alarmID = intent.getIntExtra(AlarmHandler.EXTRA_ID, 0); 27 | 28 | Log.i(TAG, "Stopping ringing and dismissing alarm " + alarmID); 29 | AlarmSoundControl alarmSoundControl = AlarmSoundControl.getInstance(); 30 | alarmSoundControl.stopAlarmSound(); 31 | 32 | // Dismiss notification 33 | NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 34 | if (notificationManager != null) notificationManager.cancelAll(); 35 | 36 | // Get snooze length from shared preferences 37 | int snoozeInSecs = Integer.parseInt(mPreferences.getString("alarm_snooze_length_list", "300")); 38 | 39 | // Schedule next ring 40 | Log.i(TAG, "Snoozing alarm " + alarmID + " for " + snoozeInSecs + " seconds"); 41 | AlarmHandler alarmHandler = new AlarmHandler(context, null); 42 | alarmHandler.scheduleAlarmWithTime(snoozeInSecs * 1000, alarmID); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/alarmmanager/receiver/AlarmStopReceiver.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.alarmmanager.receiver; 2 | 3 | import android.app.NotificationManager; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.util.Log; 8 | 9 | import com.manveerbasra.ontime.alarmmanager.AlarmHandler; 10 | import com.manveerbasra.ontime.alarmmanager.AlarmSoundControl; 11 | 12 | /** 13 | * BroadcastReceiver to stop alarm ringing 14 | */ 15 | public class AlarmStopReceiver extends BroadcastReceiver { 16 | 17 | private final String TAG = "AlarmStopReceiver"; 18 | 19 | @Override 20 | public void onReceive(Context context, Intent intent) { 21 | int alarmID = intent.getIntExtra(AlarmHandler.EXTRA_ID, 0); 22 | 23 | Log.i(TAG, "Stopping ringing and cancelling alarm " + alarmID); 24 | AlarmSoundControl alarmSoundControl = AlarmSoundControl.getInstance(); 25 | alarmSoundControl.stopAlarmSound(); 26 | 27 | // Dismiss notification 28 | NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 29 | if (notificationManager != null) notificationManager.cancelAll(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/alarmmanager/receiver/TimeShiftReceiver.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.alarmmanager.receiver; 2 | 3 | import android.app.Application; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.util.Log; 8 | 9 | import com.manveerbasra.ontime.R; 10 | import com.manveerbasra.ontime.alarmmanager.AlarmHandler; 11 | import com.manveerbasra.ontime.db.Alarm; 12 | import com.manveerbasra.ontime.timehandlers.TimeShiftHandler; 13 | import com.manveerbasra.ontime.util.AlarmRepository; 14 | 15 | import java.util.concurrent.ExecutionException; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | /** 19 | * BroadcastReceiver to process time shift required to alarm time 20 | * and set respective alarm. 21 | */ 22 | public class TimeShiftReceiver extends BroadcastReceiver { 23 | 24 | private final String TAG = "TimeShiftReceiver"; 25 | 26 | @Override 27 | public void onReceive(Context context, Intent intent) { 28 | Log.i(TAG, "received alarm time shift intent"); 29 | 30 | int alarmID = intent.getIntExtra(AlarmHandler.EXTRA_ID, 0); 31 | 32 | // Get alarm from repository 33 | AlarmRepository repository = new AlarmRepository((Application) context.getApplicationContext()); 34 | Alarm alarm; 35 | try { 36 | alarm = repository.getAlarmById(alarmID); 37 | } catch (InterruptedException | ExecutionException e) { 38 | Log.e(TAG, "Error when retrieving alarm by id: " + alarmID); 39 | e.printStackTrace(); 40 | alarm = null; 41 | } 42 | 43 | AlarmHandler alarmHandler = new AlarmHandler(context, null); 44 | 45 | if (alarm != null) { 46 | String mapsApiKey = context.getString(R.string.google_maps_ip_key); 47 | String weatherApiKey = context.getString(R.string.weather_map_key); 48 | TimeShiftHandler timeShiftHandler = new TimeShiftHandler(mapsApiKey, weatherApiKey); 49 | 50 | long timeToNextRing = alarm.getTimeToNextRing(); 51 | int departureTimeInSecs = (int) (timeToNextRing / 1000) + 30 * 60; 52 | long timeShiftInMillis = timeShiftHandler.getTimeShiftInMillis(alarm.startPoint, alarm.endPoint, departureTimeInSecs, alarm.transMode); 53 | Log.i(TAG, "setting alarm's time shift to " + timeShiftInMillis); 54 | 55 | // Set alarm to 1 hour from now minus time shift 56 | long alarmTime = TimeUnit.HOURS.toMillis(1) - timeShiftInMillis; 57 | alarmHandler.scheduleAlarmWithTime((int) alarmTime, alarmID); 58 | 59 | } else { // couldn't find alarm in repository, schedule alarm for 1 hour from now 60 | alarmHandler.scheduleAlarmWithTime((int) TimeUnit.HOURS.toMillis(1), alarmID); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/db/Alarm.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.db; 2 | 3 | import android.arch.persistence.room.ColumnInfo; 4 | import android.arch.persistence.room.Entity; 5 | import android.arch.persistence.room.Ignore; 6 | import android.arch.persistence.room.PrimaryKey; 7 | import android.arch.persistence.room.TypeConverters; 8 | import android.os.Bundle; 9 | import android.os.Parcel; 10 | import android.os.Parcelable; 11 | import android.support.annotation.NonNull; 12 | import android.util.Log; 13 | 14 | import com.google.android.gms.maps.model.LatLng; 15 | import com.manveerbasra.ontime.db.converter.BooleanArrayConverter; 16 | import com.manveerbasra.ontime.db.converter.DateConverter; 17 | import com.manveerbasra.ontime.db.converter.LatLngConverter; 18 | 19 | import java.text.DateFormat; 20 | import java.text.ParseException; 21 | import java.text.SimpleDateFormat; 22 | import java.util.ArrayList; 23 | import java.util.Calendar; 24 | import java.util.Date; 25 | import java.util.GregorianCalendar; 26 | import java.util.List; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | @Entity(tableName = "alarms") 30 | @TypeConverters({DateConverter.class, BooleanArrayConverter.class, LatLngConverter.class}) 31 | public class Alarm implements Parcelable { 32 | 33 | // Class members 34 | 35 | @PrimaryKey(autoGenerate = true) 36 | @ColumnInfo(name = "alarm_id") 37 | public int id; 38 | 39 | @ColumnInfo(name = "alarm_time") 40 | public Date time; 41 | 42 | @ColumnInfo(name = "alarm_active") 43 | public boolean active; 44 | 45 | // Must have length 7 46 | // i'th item being true means alarm is active on i'th day 47 | @ColumnInfo(name = "alarm_active_days") 48 | public boolean[] activeDays; 49 | 50 | @ColumnInfo(name = "alarm_start_point") 51 | public LatLng startPoint; 52 | @ColumnInfo(name = "alarm_end_point") 53 | public LatLng endPoint; 54 | @ColumnInfo(name = "alarm_start_place") 55 | public String startPlace; 56 | @ColumnInfo(name = "alarm_end_place") 57 | public String endPlace; 58 | @ColumnInfo(name = "alarm_transportation_mode") 59 | public String transMode; 60 | 61 | public Alarm() { 62 | } 63 | 64 | // Getters/Setters 65 | 66 | public void setTime(String stringTime) { 67 | DateFormat formatter = new SimpleDateFormat("hh:mm aa"); 68 | try { 69 | time = formatter.parse(stringTime); 70 | } catch (ParseException e) { 71 | e.printStackTrace(); 72 | } 73 | } 74 | 75 | public boolean isActive() { 76 | return active; 77 | } 78 | 79 | public void setActive(boolean active) { 80 | this.active = active; 81 | } 82 | 83 | public boolean isRepeating() { 84 | // Iterate through active days to see if one is true 85 | boolean isRepeat = false; 86 | for (boolean bool : activeDays) { 87 | if (bool) isRepeat = true; 88 | } 89 | return isRepeat; 90 | } 91 | 92 | 93 | // Ignored Members 94 | 95 | // Used to get user-readable String representation of activeDays 96 | @Ignore 97 | private static final String[] daysOfWeek = 98 | {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; 99 | 100 | @Ignore 101 | public Alarm(Date time, boolean active, boolean[] activeDays, LatLng startPoint, LatLng endPoint, 102 | String startPlace, String endPlace, String transMode) { 103 | this.time = time; 104 | this.active = active; 105 | this.activeDays = activeDays; 106 | this.startPoint = startPoint; 107 | this.endPoint = endPoint; 108 | this.startPlace = startPlace; 109 | this.endPlace = endPlace; 110 | this.transMode = transMode; 111 | } 112 | 113 | /** 114 | * Get String of alarm ring time in 12 hour format 115 | */ 116 | @Ignore 117 | public String getStringTime() { 118 | SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm aa"); 119 | String stringTime = dateFormatter.format(this.time); 120 | if (stringTime.startsWith("0")) { 121 | return stringTime.substring(1); 122 | } 123 | return stringTime; 124 | } 125 | 126 | @Ignore 127 | public String getStringOfActiveDays() { 128 | return getStringOfActiveDays(activeDays); 129 | } 130 | 131 | /** 132 | * Get a simple user-readable representation of activeDays 133 | * 134 | * @param activeDays boolean array of days alarm is active 135 | * @return a String of alarm's active days 136 | */ 137 | @Ignore 138 | public static String getStringOfActiveDays(boolean[] activeDays) { 139 | // Build string based on which indices are true in activeDays 140 | StringBuilder builder = new StringBuilder(); 141 | int activeCount = 0; 142 | for (int i = 0; i < 7; i++) { 143 | if (activeDays[i]) { 144 | String formattedDay = daysOfWeek[i].substring(0, 3) + ", "; 145 | builder.append(formattedDay); 146 | activeCount++; 147 | } 148 | } 149 | 150 | if (activeCount == 7) { 151 | return "everyday"; 152 | } else if (activeCount == 0) { 153 | return "never"; 154 | } 155 | 156 | boolean satInArray = activeDays[6]; // "Saturday" in activeDays 157 | boolean sunInArray = activeDays[0]; // "Sunday" in activeDays 158 | 159 | if (satInArray && sunInArray && activeCount == 2) { 160 | return "weekends"; 161 | } else if (!satInArray && !sunInArray && activeCount == 5) { 162 | return "weekdays"; 163 | } 164 | 165 | if (builder.length() > 1) { 166 | builder.setLength(builder.length() - 2); 167 | } 168 | 169 | return builder.toString(); 170 | } 171 | 172 | /** 173 | * Get time until next alarm ring in milliseconds since epoch 174 | */ 175 | @Ignore 176 | public long getTimeToNextRing() { 177 | Calendar calendar = getAlarmTimeAsCalendar(); 178 | 179 | if (calendar.getTimeInMillis() < System.currentTimeMillis()) { // alarm time has passed for today 180 | calendar.add(Calendar.DAY_OF_MONTH, 1); // set alarm to ring tomorrow 181 | } 182 | 183 | Log.i("Alarm.java", "Alarm ring time is " + calendar.getTime().toString()); 184 | return calendar.getTimeInMillis(); 185 | } 186 | 187 | /** 188 | * Get Alarm time as a calendar object set to current date\ 189 | */ 190 | @Ignore 191 | @NonNull 192 | private Calendar getAlarmTimeAsCalendar() { 193 | String[] timeString = new SimpleDateFormat("HH:mm").format(this.time).split(":"); 194 | int hour = Integer.parseInt(timeString[0]); 195 | int minute = Integer.parseInt(timeString[1]); 196 | 197 | Calendar calendar = new GregorianCalendar(); 198 | calendar.set(Calendar.HOUR_OF_DAY, hour); 199 | calendar.set(Calendar.MINUTE, minute); 200 | calendar.set(Calendar.SECOND, 0); 201 | 202 | return calendar; 203 | } 204 | 205 | /** 206 | * Get the time to ring in milliseconds since epoch for each day alarm is active 207 | * 208 | * @return a List of milliseconds to the next alarm ring for each active day 209 | */ 210 | @Ignore 211 | public List getTimeToWeeklyRings() { 212 | Calendar calendar = getAlarmTimeAsCalendar(); 213 | long currAlarmTime = calendar.getTimeInMillis(); 214 | 215 | List weekRingTimes = new ArrayList<>(); 216 | 217 | for (int i = 0; i < 7; i++) { 218 | if (activeDays[i]) { // if alarm is active on that day 219 | weekRingTimes.add(getCorrectRingDay(currAlarmTime, i)); 220 | } 221 | } 222 | 223 | return weekRingTimes; 224 | } 225 | 226 | /** 227 | * Correctly get the next alarm ring day based on the day it's active and current day of week 228 | * 229 | * @param alarmTime long of milliseconds since epoch of alarm time **today** 230 | * @param activeDay int of day alarm is active on 231 | * @return long of milliseconds since epoch of next alarm ring time on active day 232 | */ 233 | @Ignore 234 | private long getCorrectRingDay(long alarmTime, int activeDay) { 235 | Calendar currentCalendar = Calendar.getInstance(); 236 | int currDay = currentCalendar.get(Calendar.DAY_OF_WEEK); 237 | currDay--; // index using 0 = Sunday 238 | 239 | if ((alarmTime < System.currentTimeMillis() && currDay == activeDay) // alarm time has passed for today 240 | || currDay != activeDay) { // current day is not an active day 241 | 242 | if (activeDay > currDay) { // Alarm is active a later day of the week 243 | alarmTime += TimeUnit.MILLISECONDS.convert(activeDay - currDay, TimeUnit.DAYS); 244 | } else { // Have to move the alarm time to next week's active day 245 | alarmTime += TimeUnit.MILLISECONDS.convert(7 - currDay, TimeUnit.DAYS); 246 | alarmTime += TimeUnit.MILLISECONDS.convert(activeDay, TimeUnit.DAYS); 247 | } 248 | } 249 | return alarmTime; 250 | } 251 | 252 | // Parcelable implementation 253 | 254 | @Ignore 255 | public int describeContents() { 256 | return 0; 257 | } 258 | 259 | /** 260 | * Write all alarm contents to Parcel out 261 | */ 262 | @Ignore 263 | @Override 264 | public void writeToParcel(Parcel out, int flags) { 265 | out.writeInt(id); 266 | out.writeLong(DateConverter.toTimestamp(time)); 267 | out.writeBooleanArray(activeDays); 268 | 269 | Bundle bundle = new Bundle(); 270 | bundle.putParcelable("START_POINT", startPoint); 271 | bundle.putParcelable("END_POINT", endPoint); 272 | bundle.putString("START_PLACE", startPlace); 273 | bundle.putString("END_PLACE", endPlace); 274 | bundle.putString("TRANS_MODE", transMode); 275 | 276 | out.writeBundle(bundle); 277 | } 278 | 279 | @Ignore 280 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 281 | public Alarm createFromParcel(Parcel in) { 282 | return new Alarm(in); 283 | } 284 | 285 | public Alarm[] newArray(int size) { 286 | return new Alarm[size]; 287 | } 288 | }; 289 | 290 | /** 291 | * Construct alarm from Parcel of data written to using writeToParcel (above) 292 | */ 293 | @Ignore 294 | private Alarm(Parcel in) { 295 | id = in.readInt(); 296 | 297 | Long timestamp = in.readLong(); 298 | time = DateConverter.toDate(timestamp); 299 | 300 | activeDays = new boolean[7]; 301 | in.readBooleanArray(activeDays); 302 | active = false; 303 | 304 | Bundle args = in.readBundle(getClass().getClassLoader()); 305 | startPoint = args.getParcelable("START_POINT"); 306 | endPoint = args.getParcelable("END_POINT"); 307 | startPlace = args.getString("START_PLACE"); 308 | endPlace = args.getString("END_PLACE"); 309 | transMode = args.getString("TRANS_MODE"); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/db/AlarmDao.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.db; 2 | 3 | import android.arch.lifecycle.LiveData; 4 | import android.arch.persistence.room.Dao; 5 | import android.arch.persistence.room.Delete; 6 | import android.arch.persistence.room.Insert; 7 | import android.arch.persistence.room.Query; 8 | import android.arch.persistence.room.Update; 9 | 10 | import java.util.List; 11 | 12 | import static android.arch.persistence.room.OnConflictStrategy.IGNORE; 13 | import static android.arch.persistence.room.OnConflictStrategy.REPLACE; 14 | 15 | 16 | /** 17 | * Dao to interact with database at the lowest level 18 | */ 19 | @Dao 20 | public interface AlarmDao { 21 | @Query("select * from alarms") 22 | LiveData> getAllAlarms(); 23 | 24 | @Query("SELECT * FROM alarms WHERE alarm_id=:id") 25 | Alarm getById(int id); 26 | 27 | @Insert(onConflict = IGNORE) 28 | void insert(Alarm alarm); 29 | 30 | @Update 31 | void update(Alarm alarm); 32 | 33 | @Delete 34 | void delete(Alarm alarm); 35 | 36 | @Query("UPDATE alarms set alarm_active=:active where alarm_id=:id") 37 | void updateActive(int id, boolean active); 38 | 39 | @Insert(onConflict = REPLACE) 40 | void insertOrReplaceAlarm(Alarm alarm); 41 | 42 | @Query("DELETE FROM alarms") 43 | void deleteAll(); 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/db/AlarmDatabase.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.db; 2 | 3 | import android.arch.persistence.db.SupportSQLiteDatabase; 4 | import android.arch.persistence.room.Database; 5 | import android.arch.persistence.room.Room; 6 | import android.arch.persistence.room.RoomDatabase; 7 | import android.arch.persistence.room.TypeConverters; 8 | import android.content.Context; 9 | import android.os.AsyncTask; 10 | import android.support.annotation.NonNull; 11 | import android.support.annotation.VisibleForTesting; 12 | 13 | import com.google.android.gms.maps.model.LatLng; 14 | import com.manveerbasra.ontime.db.converter.BooleanArrayConverter; 15 | import com.manveerbasra.ontime.db.converter.DateConverter; 16 | import com.manveerbasra.ontime.db.converter.LatLngConverter; 17 | 18 | import java.text.DateFormat; 19 | import java.text.SimpleDateFormat; 20 | import java.util.Date; 21 | 22 | /** 23 | * Backend Database 24 | */ 25 | @Database(entities = {Alarm.class}, version = 1, exportSchema = false) 26 | @TypeConverters({DateConverter.class, BooleanArrayConverter.class, LatLngConverter.class}) 27 | public abstract class AlarmDatabase extends RoomDatabase { 28 | 29 | private static AlarmDatabase mINSTANCE; 30 | 31 | @VisibleForTesting 32 | public static final String DATABASE_NAME = "alarm-db"; 33 | 34 | public abstract AlarmDao alarmModel(); 35 | 36 | public static AlarmDatabase getInstance(final Context context) { 37 | if (mINSTANCE == null) { 38 | synchronized (AlarmDatabase.class) { 39 | if (mINSTANCE == null) { 40 | mINSTANCE = Room.databaseBuilder(context.getApplicationContext(), 41 | AlarmDatabase.class, DATABASE_NAME) 42 | .fallbackToDestructiveMigration() // TODO Add Proper Migration 43 | .addCallback(roomDatabaseCallback) 44 | .build(); 45 | } 46 | } 47 | } 48 | return mINSTANCE; 49 | } 50 | 51 | /** 52 | * Override the onCreate method to populate the database. 53 | */ 54 | private static RoomDatabase.Callback roomDatabaseCallback = new RoomDatabase.Callback() { 55 | 56 | @Override 57 | public void onCreate(@NonNull SupportSQLiteDatabase db) { 58 | super.onCreate(db); 59 | new PopulateDbAsync(mINSTANCE).execute(); 60 | } 61 | 62 | @Override 63 | public void onOpen(@NonNull SupportSQLiteDatabase db) { 64 | super.onOpen(db); 65 | } 66 | }; 67 | 68 | /** 69 | * Populate the database in the background when app is first created. 70 | */ 71 | private static class PopulateDbAsync extends AsyncTask { 72 | 73 | private final AlarmDao alarmModel; 74 | 75 | PopulateDbAsync(AlarmDatabase db) { 76 | alarmModel = db.alarmModel(); 77 | } 78 | 79 | @Override 80 | protected Void doInBackground(final Void... params) { 81 | String str = "08:00 am"; 82 | DateFormat formatter = new SimpleDateFormat("hh:mm aa"); 83 | Date time = null; 84 | try { 85 | time = formatter.parse(str); 86 | } catch (java.text.ParseException e) { 87 | e.printStackTrace(); 88 | } 89 | boolean[] activeDays = {false, true, true, true, true, true, false}; 90 | 91 | LatLng start = new LatLng(43.6426, 79.3871); 92 | String startPlace = "CN Tower"; 93 | LatLng end = new LatLng(43.6777, 79.6248); 94 | String endPlace = "Toronto Pearson International Airport"; 95 | 96 | Alarm alarm = new Alarm( 97 | time, false, activeDays, 98 | start, end, 99 | startPlace, endPlace, 100 | "drive" 101 | ); 102 | alarmModel.insert(alarm); 103 | return null; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/db/converter/BooleanArrayConverter.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.db.converter; 2 | 3 | import android.arch.persistence.room.TypeConverter; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | /** 10 | * Class to handle conversions between boolean[] and String 11 | *

12 | * Used in Alarm.java as a @TypeConverter 13 | */ 14 | public class BooleanArrayConverter { 15 | @TypeConverter 16 | public boolean[] fromString(String value) { 17 | boolean[] arr = new boolean[7]; 18 | if (value.equals("")) { 19 | return arr; 20 | } 21 | 22 | List list = new ArrayList<>(Arrays.asList(value.split(","))); 23 | 24 | int i = 0; 25 | for (String item : list) { 26 | arr[i] = Boolean.parseBoolean(item); 27 | i++; 28 | } 29 | return arr; 30 | } 31 | 32 | @TypeConverter 33 | public String arrayToString(boolean[] arr) { 34 | StringBuilder builder = new StringBuilder(); 35 | if (arr == null) { 36 | return ""; 37 | } 38 | for (boolean item : arr) { 39 | builder.append(item + ","); 40 | } 41 | if (builder.length() > 0) { // cut off trailing comma 42 | builder.setLength(builder.length() - 1); 43 | } 44 | return builder.toString(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/db/converter/DateConverter.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.db.converter; 2 | 3 | import android.arch.persistence.room.TypeConverter; 4 | 5 | import java.util.Date; 6 | 7 | public class DateConverter { 8 | @TypeConverter 9 | public static Date toDate(Long timestamp) { 10 | return timestamp == null ? null : new Date(timestamp); 11 | } 12 | 13 | @TypeConverter 14 | public static Long toTimestamp(Date date) { 15 | return date == null ? null : date.getTime(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/db/converter/LatLngConverter.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.db.converter; 2 | 3 | import android.arch.persistence.room.TypeConverter; 4 | 5 | import com.google.android.gms.maps.model.LatLng; 6 | 7 | /** 8 | * Class to handle conversions between LatLng and String 9 | *

10 | * Used in Alarm.java as a @TypeConverter 11 | */ 12 | public class LatLngConverter { 13 | @TypeConverter 14 | public LatLng fromString(String value) { 15 | String[] splitValue = value.split(":"); 16 | return new LatLng( 17 | Double.parseDouble(splitValue[0]), 18 | Double.parseDouble(splitValue[1]) 19 | ); 20 | } 21 | 22 | @TypeConverter 23 | public String latLngToString(LatLng latLng) { 24 | return latLng.latitude + ":" + latLng.longitude; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/timehandlers/TimeShiftHandler.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.timehandlers; 2 | 3 | import android.util.Log; 4 | 5 | import com.google.android.gms.maps.model.LatLng; 6 | 7 | /** 8 | * Class to get and handle time shift required for user to be alerted OnTime 9 | */ 10 | public class TimeShiftHandler { 11 | 12 | private final String TAG = "TimeShiftHandler"; 13 | 14 | private TrafficTimeHandler mTrafficTimeHandler; 15 | private WeatherTimeHandler mWeatherTimeHandler; 16 | 17 | public TimeShiftHandler(String mapsApiKey, String weatherApiKey) { 18 | mTrafficTimeHandler = new TrafficTimeHandler(mapsApiKey); 19 | mWeatherTimeHandler = new WeatherTimeHandler(weatherApiKey); 20 | } 21 | 22 | /** 23 | * Get the alarm's time shift derived from current traffic and weather conditions 24 | * 25 | * @param start starting LatLng point 26 | * @param end destination LatLng point 27 | * @param departureTimeInSecs time in seconds since epoch of expected departure time 28 | * @param transMode String of transportation mode 29 | * @return long alarm time shift in milliseconds 30 | */ 31 | public long getTimeShiftInMillis(LatLng start, LatLng end, int departureTimeInSecs, String transMode) { 32 | long trafficShift = mTrafficTimeHandler.getTimeShiftInMillis(start, end, departureTimeInSecs, transMode); 33 | long weatherShift = mWeatherTimeHandler.getTimeShiftInMillis(start, end); 34 | 35 | long totalShift = (long) (trafficShift + weatherShift); 36 | 37 | Log.i(TAG, "Total Time shift: " + totalShift + 38 | " = (" + trafficShift + " + " + weatherShift + ")"); 39 | return totalShift; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/timehandlers/TrafficTimeHandler.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.timehandlers; 2 | 3 | import android.os.AsyncTask; 4 | import android.util.Log; 5 | 6 | import com.google.android.gms.maps.model.LatLng; 7 | import com.manveerbasra.ontime.util.JSONParser; 8 | 9 | import org.json.JSONException; 10 | import org.json.JSONObject; 11 | 12 | import java.io.BufferedReader; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.io.InputStreamReader; 16 | import java.net.HttpURLConnection; 17 | import java.net.URL; 18 | import java.util.HashMap; 19 | import java.util.concurrent.ExecutionException; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | /** 23 | * Class to get and handle time shift for alarm's origin -> destination commute 24 | */ 25 | public class TrafficTimeHandler { 26 | 27 | private final String TAG = "TrafficTimeHandler"; 28 | 29 | // keys for HashMap of parsed JSON data 30 | public static final String DURATION = "duration"; 31 | public static final String DURATION_TRAFFIC = "duration_in_traffic"; 32 | 33 | private String mApiKey; 34 | 35 | TrafficTimeHandler(String apiKey) { 36 | this.mApiKey = apiKey; 37 | } 38 | 39 | /** 40 | * Get the difference between the usual and today's commute duration 41 | * 42 | * @param start starting LatLng point 43 | * @param end destination LatLng point 44 | * @param departureTimeInSecs time in seconds since epoch of expected departure time 45 | * @param transMode String of transportation mode 46 | * @return long time difference between usual and today's commute in milliseconds 47 | */ 48 | long getTimeShiftInMillis(LatLng start, LatLng end, int departureTimeInSecs, String transMode) { 49 | 50 | String jsonData; 51 | HashMap data; 52 | 53 | // Download data 54 | DownloadAsyncTask downloadAsyncTask = new DownloadAsyncTask(); 55 | downloadAsyncTask.execute(getHTTPSRequestUrl(start, end, departureTimeInSecs, transMode)); 56 | try { 57 | jsonData = downloadAsyncTask.get(); 58 | } catch (ExecutionException | InterruptedException e) { 59 | jsonData = ""; 60 | } 61 | 62 | // Parse JSON data 63 | ParserAsyncTask parserAsyncTask = new ParserAsyncTask(); 64 | parserAsyncTask.execute(jsonData); 65 | try { 66 | data = parserAsyncTask.get(); 67 | } catch (ExecutionException | InterruptedException e) { 68 | data = null; 69 | } 70 | 71 | if (data == null) return 0; 72 | else { 73 | long duration = data.get(DURATION); 74 | long durationTraffic = data.get(DURATION_TRAFFIC); 75 | Log.i(TAG, "Normal trip length: " + duration + " secs and length in traffic: " + durationTraffic + " secs"); 76 | long shiftInSecs = durationTraffic - duration; 77 | return TimeUnit.SECONDS.toMillis(shiftInSecs); 78 | } 79 | } 80 | 81 | /** 82 | * Build and return the HTTPS request url to get directions from the Google Maps API 83 | * 84 | * @param start starting LatLng point 85 | * @param end destination LatLng point 86 | * @param departureTimeInSecs time in seconds since epoch of expected departure time 87 | * @param transMode String of transportation mode 88 | * @return String url of HTTPS request 89 | */ 90 | private String getHTTPSRequestUrl(LatLng start, LatLng end, int departureTimeInSecs, String transMode) { 91 | 92 | // Build parameters 93 | String origin = "origin=" + start.latitude + "," + start.longitude; 94 | String dest = "destination=" + end.latitude + "," + end.longitude; 95 | String departureTime = "departure_time=" + departureTimeInSecs; 96 | String mode = "mode=" + transMode; 97 | String key = "key=" + mApiKey; 98 | 99 | String parameters = origin + "&" + dest + "&" + departureTime + "&" + mode + "&" + key; 100 | 101 | return "https://maps.googleapis.com/maps/api/directions/json?" + parameters; 102 | } 103 | 104 | /** 105 | * Download data from http url connection 106 | * 107 | * @param strURL url to connect and download from 108 | * @return String of read data 109 | * @throws IOException when handling InputStreams 110 | */ 111 | private static String downloadUrl(String strURL) throws IOException { 112 | StringBuilder stringBuilder = new StringBuilder(); 113 | InputStream inputStream = null; 114 | HttpURLConnection urlConnection = null; 115 | 116 | try { 117 | URL url = new URL(strURL); 118 | urlConnection = (HttpURLConnection) url.openConnection(); 119 | urlConnection.connect(); 120 | 121 | inputStream = urlConnection.getInputStream(); 122 | BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); 123 | 124 | String line; 125 | while ((line = br.readLine()) != null) { 126 | stringBuilder.append(line); 127 | } 128 | 129 | br.close(); 130 | 131 | } catch (IOException e) { 132 | Log.d("Exception while downloading url", e.toString()); 133 | } finally { 134 | if (inputStream != null) inputStream.close(); 135 | if (urlConnection != null) urlConnection.disconnect(); 136 | } 137 | return stringBuilder.toString(); 138 | } 139 | 140 | /** 141 | * Asynchronously download data from URL 142 | */ 143 | private static class DownloadAsyncTask extends AsyncTask { 144 | 145 | @Override 146 | protected String doInBackground(String... url) { 147 | String data = ""; 148 | 149 | try { 150 | data = downloadUrl(url[0]); 151 | } catch (IOException e) { 152 | e.printStackTrace(); 153 | } 154 | return data; 155 | } 156 | } 157 | 158 | /** 159 | * Asynchronously parse the Google Places in JSON format 160 | */ 161 | private static class ParserAsyncTask extends AsyncTask> { 162 | 163 | @Override 164 | protected HashMap doInBackground(String... jsonData) { 165 | 166 | JSONObject jObject; 167 | HashMap parsedData = null; 168 | 169 | try { 170 | jObject = new JSONObject(jsonData[0]); 171 | JSONParser parser = new JSONParser(); 172 | 173 | parsedData = parser.parseFromMaps(jObject); 174 | } catch (JSONException e) { 175 | e.printStackTrace(); 176 | } 177 | return parsedData; 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/timehandlers/WeatherTimeHandler.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.timehandlers; 2 | 3 | import android.os.AsyncTask; 4 | import android.util.Log; 5 | 6 | import com.google.android.gms.maps.model.LatLng; 7 | import com.manveerbasra.ontime.util.JSONParser; 8 | 9 | import org.json.JSONException; 10 | import org.json.JSONObject; 11 | 12 | import java.io.BufferedReader; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.io.InputStreamReader; 16 | import java.net.HttpURLConnection; 17 | import java.net.URL; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | import java.util.concurrent.ExecutionException; 21 | import java.util.concurrent.TimeUnit; 22 | 23 | /** 24 | * Class to get and handle time shift for alarm's location's weather 25 | */ 26 | public class WeatherTimeHandler { 27 | 28 | private static final String TAG = "WeatherTimeHandler"; 29 | 30 | // keys for HashMap of parsed JSON data 31 | public static final String CONDITIONS = "conditions"; // short summary of conditions 32 | public static final String CONDITIONS_DESC = "conditions_description"; 33 | public static final String TEMPERATURE = "temperature"; 34 | public static final String WIND = "wind"; // wind speed 35 | public static final String RAIN = "rain"; 36 | public static final String SNOW = "snow"; 37 | 38 | private static Map mConditionsToRatio = new HashMap<>(); 39 | 40 | static { // initialize mConditionsToRatio Map 41 | mConditionsToRatio.put("Thunderstorm", 0.4); 42 | mConditionsToRatio.put("Drizzle", 0.1); 43 | mConditionsToRatio.put("Rain", 0.2); 44 | mConditionsToRatio.put("Snow", 0.2); 45 | mConditionsToRatio.put("Atmosphere", 0.2); 46 | mConditionsToRatio.put("Clear", 0.0); 47 | mConditionsToRatio.put("Mist", 0.0); 48 | mConditionsToRatio.put("Clouds", 0.0); 49 | } 50 | 51 | private String mApiKey; 52 | 53 | WeatherTimeHandler(String apiKey) { 54 | this.mApiKey = apiKey; 55 | } 56 | 57 | /** 58 | * Get the time shift derived from today's weather conditions 59 | * 60 | * @param start starting LatLng point 61 | * @param end destination LatLng point 62 | * @return long time user should leave early in milliseconds 63 | */ 64 | long getTimeShiftInMillis(LatLng start, LatLng end) { 65 | 66 | String jsonData; 67 | HashMap data; 68 | 69 | // Download data 70 | DownloadAsyncTask downloadAsyncTask = new DownloadAsyncTask(); 71 | downloadAsyncTask.execute(getHTTPSRequestUrl(start)); 72 | try { 73 | jsonData = downloadAsyncTask.get(); 74 | } catch (ExecutionException | InterruptedException e) { 75 | jsonData = ""; 76 | } 77 | 78 | // Parse JSON data 79 | ParserAsyncTask parserAsyncTask = new ParserAsyncTask(); 80 | parserAsyncTask.execute(jsonData); 81 | try { 82 | data = parserAsyncTask.get(); 83 | } catch (ExecutionException | InterruptedException e) { 84 | data = null; 85 | } 86 | 87 | if (data == null) return 0; 88 | else { 89 | return calculateTimeShiftFromData(data); 90 | } 91 | } 92 | 93 | /** 94 | * Calculate time shift for alarm time based on parsed JSON weather data 95 | * 96 | * @param data Map of parsed weather data 97 | * @return long time in milliseconds of alarm shift 98 | */ 99 | private long calculateTimeShiftFromData(HashMap data) { 100 | String condition = (String) data.get(CONDITIONS); 101 | String desc = (String) data.get(CONDITIONS_DESC); 102 | 103 | double ratio = getShiftRatioFromWeatherConditions(condition, desc); 104 | 105 | if (( // Check if conditions might form ice 106 | condition.equals("Rain") 107 | || condition.equals("Snow") 108 | || (Integer) data.get(RAIN) > 2 109 | || (Integer) data.get(SNOW) > 20) 110 | && (Double) data.get(TEMPERATURE) < 0.0) { 111 | ratio += 0.1; 112 | } 113 | 114 | double shiftInMinutes = 60 * ratio; 115 | Log.i(TAG, "Shift from weather conditions: " + shiftInMinutes + " minutes"); 116 | return TimeUnit.MINUTES.toMillis((long) shiftInMinutes); 117 | } 118 | 119 | /** 120 | * Get a double from [0 - 1) based on weather conditions tables in 121 | * weather-conditions-table 122 | * 123 | * @param condition short one word summary of weather: one of 7 conditions from above url 124 | * @param desc short description of specific weather condition 125 | * @return double from [0-1) representing ratio of an hour to shift alarm forward 126 | */ 127 | private double getShiftRatioFromWeatherConditions(String condition, String desc) { 128 | double ratio = mConditionsToRatio.get(condition); 129 | if (desc.contains("light")) ratio -= 0.1; 130 | else if (desc.contains("heavy") && !condition.equals("Drizzle")) ratio += 0.1; 131 | else if (desc.equals("tornado")) ratio = 0.5; 132 | return ratio; 133 | } 134 | 135 | /** 136 | * Build and return the HTTPS request url to get weather data from the OpenWeatherMap API 137 | * 138 | * @param point LatLng point to get weather for 139 | * @return String url of HTTPS request 140 | */ 141 | private String getHTTPSRequestUrl(LatLng point) { 142 | 143 | // Build parameters 144 | String lat = "lat=" + point.latitude; 145 | String lon = "lon=" + point.longitude; 146 | String key = "appid=" + mApiKey; 147 | 148 | String parameters = lat + "&" + lon + "&" + key; 149 | 150 | return "https://api.openweathermap.org/data/2.5/weather?" + parameters; 151 | } 152 | 153 | /** 154 | * Download data from http url connection 155 | * 156 | * @param strURL url to connect and download from 157 | * @return String of read data 158 | * @throws IOException when handling InputStreams 159 | */ 160 | private static String downloadUrl(String strURL) throws IOException { 161 | StringBuilder stringBuilder = new StringBuilder(); 162 | InputStream inputStream = null; 163 | HttpURLConnection urlConnection = null; 164 | 165 | try { 166 | URL url = new URL(strURL); 167 | urlConnection = (HttpURLConnection) url.openConnection(); 168 | urlConnection.connect(); 169 | 170 | inputStream = urlConnection.getInputStream(); 171 | BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); 172 | 173 | String line; 174 | while ((line = br.readLine()) != null) { 175 | stringBuilder.append(line); 176 | } 177 | 178 | br.close(); 179 | 180 | } catch (IOException e) { 181 | Log.d("Exception while downloading url", e.toString()); 182 | } finally { 183 | if (inputStream != null) inputStream.close(); 184 | if (urlConnection != null) urlConnection.disconnect(); 185 | } 186 | return stringBuilder.toString(); 187 | } 188 | 189 | /** 190 | * Asynchronously download data from URL 191 | */ 192 | private static class DownloadAsyncTask extends AsyncTask { 193 | 194 | @Override 195 | protected String doInBackground(String... url) { 196 | String data = ""; 197 | 198 | try { 199 | data = downloadUrl(url[0]); 200 | } catch (IOException e) { 201 | e.printStackTrace(); 202 | } 203 | return data; 204 | } 205 | } 206 | 207 | /** 208 | * Asynchronously parse the Google Places in JSON format 209 | */ 210 | private static class ParserAsyncTask extends AsyncTask> { 211 | 212 | @Override 213 | protected HashMap doInBackground(String... jsonData) { 214 | 215 | JSONObject jObject; 216 | HashMap parsedData = null; 217 | 218 | try { 219 | jObject = new JSONObject(jsonData[0]); 220 | JSONParser parser = new JSONParser(); 221 | 222 | parsedData = parser.parseFromWeather(jObject); 223 | } catch (JSONException e) { 224 | e.printStackTrace(); 225 | } 226 | return parsedData; 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/ui/AlarmListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.ui; 2 | 3 | import android.arch.lifecycle.ViewModelProviders; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.res.Resources; 7 | import android.os.Bundle; 8 | import android.support.annotation.NonNull; 9 | import android.support.v7.widget.RecyclerView; 10 | import android.util.Log; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.widget.CompoundButton; 15 | import android.widget.ImageButton; 16 | import android.widget.Switch; 17 | import android.widget.TextView; 18 | 19 | import com.manveerbasra.ontime.R; 20 | import com.manveerbasra.ontime.alarmmanager.AlarmHandler; 21 | import com.manveerbasra.ontime.db.Alarm; 22 | import com.manveerbasra.ontime.ui.activity.AddAlarmActivity; 23 | import com.manveerbasra.ontime.ui.activity.MainActivity; 24 | import com.manveerbasra.ontime.viewmodel.AlarmViewModel; 25 | 26 | import java.util.Collections; 27 | import java.util.List; 28 | 29 | /** 30 | * ArrayAdapter used to populate MainActivity Alarms ListView 31 | */ 32 | public class AlarmListAdapter extends RecyclerView.Adapter { 33 | 34 | private final String TAG = "AlarmListAdapter"; 35 | 36 | /** 37 | * View for each alarm 38 | */ 39 | class AlarmViewHolder extends RecyclerView.ViewHolder { 40 | private final TextView timeTextView; 41 | private final TextView repetitionTextView; 42 | private final Switch activeSwitch; 43 | private final ImageButton editButton; 44 | 45 | private AlarmViewHolder(View itemView) { 46 | super(itemView); 47 | timeTextView = itemView.findViewById(R.id.alarm_time_text); 48 | repetitionTextView = itemView.findViewById(R.id.alarm_repetition_text); 49 | activeSwitch = itemView.findViewById(R.id.alarm_active_switch); 50 | editButton = itemView.findViewById(R.id.alarm_edit_button); 51 | } 52 | } 53 | 54 | // Layout members 55 | private final LayoutInflater mInflater; 56 | private final TextView mEmptyTextView; 57 | // Data list (cached copy of alarms) 58 | private List mAlarms = Collections.emptyList(); 59 | // To handle interactions with database 60 | private AlarmViewModel mAlarmViewModel; 61 | // To schedule alarms 62 | private AlarmHandler mAlarmHandler; 63 | 64 | public AlarmListAdapter(Context context) { 65 | mInflater = LayoutInflater.from(context); 66 | mEmptyTextView = ((MainActivity) context).findViewById(R.id.no_alarms_text); 67 | 68 | mAlarmViewModel = ViewModelProviders.of((MainActivity) context).get(AlarmViewModel.class); 69 | mAlarmHandler = new AlarmHandler(context, ((MainActivity) context).findViewById(R.id.fab)); 70 | 71 | setHasStableIds(true); // so Switch interaction has smooth animations 72 | } 73 | 74 | @NonNull 75 | @Override 76 | public AlarmViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 77 | View itemView = mInflater.inflate(R.layout.item_alarm, parent, false); 78 | return new AlarmViewHolder(itemView); 79 | } 80 | 81 | @Override 82 | public void onBindViewHolder(@NonNull AlarmViewHolder viewHolder, int position) { 83 | Resources resources = viewHolder.itemView.getContext().getResources(); 84 | Alarm alarm = mAlarms.get(position); 85 | 86 | viewHolder.timeTextView.setText(alarm.getStringTime()); // set alarm time 87 | 88 | // Set repeatTextView text 89 | if (alarm.isRepeating()) { 90 | String repetitionText = alarm.getStringOfActiveDays(); 91 | viewHolder.repetitionTextView.setText(repetitionText); 92 | } else { 93 | viewHolder.repetitionTextView.setText(resources.getString(R.string.no_repeat)); 94 | } 95 | 96 | // Set TextView colors based on alarm's active state 97 | if (alarm.isActive()) { 98 | viewHolder.activeSwitch.setChecked(true); 99 | viewHolder.timeTextView.setTextColor(resources.getColor(R.color.colorAccent)); 100 | viewHolder.repetitionTextView.setTextColor(resources.getColor(R.color.colorDarkText)); 101 | } else { 102 | viewHolder.activeSwitch.setChecked(false); 103 | viewHolder.timeTextView.setTextColor(resources.getColor(R.color.colorGrey500)); 104 | viewHolder.repetitionTextView.setTextColor(resources.getColor(R.color.colorGrey500)); 105 | } 106 | 107 | // Add Button click listeners 108 | addSwitchListener(alarm, viewHolder, resources); 109 | addEditButtonListener(alarm, viewHolder); 110 | } 111 | 112 | /** 113 | * Update current list of alarms and update UI 114 | * 115 | * @param alarms List of updated alarms 116 | */ 117 | public void setAlarms(List alarms) { 118 | Log.i(TAG, "updating alarms data-set"); 119 | this.mAlarms = alarms; 120 | notifyDataSetChanged(); 121 | } 122 | 123 | @Override 124 | public int getItemCount() { 125 | mEmptyTextView.setVisibility(mAlarms.size() > 0 ? View.GONE : View.VISIBLE); 126 | return mAlarms.size(); 127 | } 128 | 129 | @Override 130 | public long getItemId(int position) { 131 | return mAlarms.get(position).id; 132 | } 133 | 134 | /** 135 | * Add OnCheckedChange Listener to alarm's "active" switch 136 | * 137 | * @param alarm Alarm object 138 | * @param viewHolder Alarm's ViewHolder object, containing Switch 139 | * @param resources Resources file to get color values from 140 | */ 141 | private void addSwitchListener(final Alarm alarm, final AlarmViewHolder viewHolder, final Resources resources) { 142 | viewHolder.activeSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 143 | @Override 144 | public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { 145 | if (checked) { 146 | alarm.setActive(true); 147 | viewHolder.timeTextView.setTextColor(resources.getColor(R.color.colorAccent)); 148 | viewHolder.repetitionTextView.setTextColor(resources.getColor(R.color.colorDarkText)); 149 | // schedule alarm 150 | Log.i(TAG, "scheduling alarm: " + alarm.id); 151 | mAlarmHandler.scheduleAlarm(alarm); 152 | } else { 153 | alarm.setActive(false); 154 | viewHolder.timeTextView.setTextColor(resources.getColor(R.color.colorGrey500)); 155 | viewHolder.repetitionTextView.setTextColor(resources.getColor(R.color.colorGrey500)); 156 | mAlarmHandler.cancelAlarm(alarm); 157 | } 158 | 159 | // Update database and schedule alarm 160 | Log.i(TAG, "updating database with alarm: " + alarm.id); 161 | mAlarmViewModel.updateActive(alarm); 162 | } 163 | }); 164 | } 165 | 166 | /** 167 | * Add OnClickListener to alarm's edit button to open EditAlarmActivity (AddAlarmActivity.java) 168 | * 169 | * @param alarm Alarm object 170 | * @param viewHolder Alarm's ViewHolder object, containing edit button 171 | */ 172 | private void addEditButtonListener(final Alarm alarm, final AlarmViewHolder viewHolder) { 173 | viewHolder.editButton.setOnClickListener(new View.OnClickListener() { 174 | @Override 175 | public void onClick(View view) { 176 | Context context = view.getContext(); 177 | Intent intent = new Intent(context, AddAlarmActivity.class); 178 | 179 | Bundle args = new Bundle(); 180 | args.putParcelable(AddAlarmActivity.EXTRA_ALARM, alarm); 181 | intent.putExtra(AddAlarmActivity.EXTRA_BUNDLE, args); 182 | 183 | ((MainActivity) context).startActivityForResult(intent, MainActivity.EDIT_ALARM_ACTIVITY_REQUEST_CODE); 184 | } 185 | }); 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/ui/AppCompatPreferenceActivity.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.ui; 2 | 3 | import android.content.res.Configuration; 4 | import android.os.Bundle; 5 | import android.preference.PreferenceActivity; 6 | import android.support.annotation.LayoutRes; 7 | import android.support.annotation.Nullable; 8 | import android.support.v7.app.ActionBar; 9 | import android.support.v7.app.AppCompatDelegate; 10 | import android.support.v7.widget.Toolbar; 11 | import android.view.MenuInflater; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | 15 | /** 16 | * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls 17 | * to be used with AppCompat. 18 | */ 19 | public abstract class AppCompatPreferenceActivity extends PreferenceActivity { 20 | 21 | private AppCompatDelegate mDelegate; 22 | 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | getDelegate().installViewFactory(); 26 | getDelegate().onCreate(savedInstanceState); 27 | super.onCreate(savedInstanceState); 28 | } 29 | 30 | @Override 31 | protected void onPostCreate(Bundle savedInstanceState) { 32 | super.onPostCreate(savedInstanceState); 33 | getDelegate().onPostCreate(savedInstanceState); 34 | } 35 | 36 | public ActionBar getSupportActionBar() { 37 | return getDelegate().getSupportActionBar(); 38 | } 39 | 40 | public void setSupportActionBar(@Nullable Toolbar toolbar) { 41 | getDelegate().setSupportActionBar(toolbar); 42 | } 43 | 44 | @Override 45 | public MenuInflater getMenuInflater() { 46 | return getDelegate().getMenuInflater(); 47 | } 48 | 49 | @Override 50 | public void setContentView(@LayoutRes int layoutResID) { 51 | getDelegate().setContentView(layoutResID); 52 | } 53 | 54 | @Override 55 | public void setContentView(View view) { 56 | getDelegate().setContentView(view); 57 | } 58 | 59 | @Override 60 | public void setContentView(View view, ViewGroup.LayoutParams params) { 61 | getDelegate().setContentView(view, params); 62 | } 63 | 64 | @Override 65 | public void addContentView(View view, ViewGroup.LayoutParams params) { 66 | getDelegate().addContentView(view, params); 67 | } 68 | 69 | @Override 70 | protected void onPostResume() { 71 | super.onPostResume(); 72 | getDelegate().onPostResume(); 73 | } 74 | 75 | @Override 76 | protected void onTitleChanged(CharSequence title, int color) { 77 | super.onTitleChanged(title, color); 78 | getDelegate().setTitle(title); 79 | } 80 | 81 | @Override 82 | public void onConfigurationChanged(Configuration newConfig) { 83 | super.onConfigurationChanged(newConfig); 84 | getDelegate().onConfigurationChanged(newConfig); 85 | } 86 | 87 | @Override 88 | protected void onStop() { 89 | super.onStop(); 90 | getDelegate().onStop(); 91 | } 92 | 93 | @Override 94 | protected void onDestroy() { 95 | super.onDestroy(); 96 | getDelegate().onDestroy(); 97 | } 98 | 99 | public void invalidateOptionsMenu() { 100 | getDelegate().invalidateOptionsMenu(); 101 | } 102 | 103 | private AppCompatDelegate getDelegate() { 104 | if (mDelegate == null) { 105 | mDelegate = AppCompatDelegate.create(this, null); 106 | } 107 | return mDelegate; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/ui/SetRepeatDaysDialogFragment.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.ui; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.app.Dialog; 6 | import android.content.DialogInterface; 7 | import android.os.Bundle; 8 | import android.support.annotation.NonNull; 9 | import android.support.annotation.Nullable; 10 | import android.support.v4.app.DialogFragment; 11 | 12 | import com.manveerbasra.ontime.R; 13 | 14 | import java.lang.reflect.Array; 15 | import java.util.ArrayList; 16 | 17 | /** 18 | * Fragment used to display Alarm active days to user 19 | */ 20 | public class SetRepeatDaysDialogFragment extends DialogFragment { 21 | 22 | /** 23 | * Listener for Dialog Completion 24 | *

25 | * Implemented in AddAlarmActivity 26 | */ 27 | public interface OnDialogCompleteListener { 28 | void onDialogComplete(boolean[] selectedDays); 29 | } 30 | 31 | private OnDialogCompleteListener completeListener; 32 | 33 | @Override 34 | public void onAttach(Activity activity) { 35 | super.onAttach(activity); 36 | try { 37 | this.completeListener = (OnDialogCompleteListener) activity; 38 | } catch (final ClassCastException e) { 39 | throw new ClassCastException(activity.toString() + " must implement OnCompleteListener"); 40 | } 41 | } 42 | 43 | // End of DialogCompleteListener methods 44 | 45 | private boolean[] activeDays; 46 | 47 | @Override 48 | public void onCreate(Bundle savedInstanceState) { 49 | super.onCreate(savedInstanceState); 50 | activeDays = getArguments().getBooleanArray("activeDays"); 51 | } 52 | 53 | @NonNull 54 | @Override 55 | public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { 56 | AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); 57 | // Set the dialog title 58 | builder.setTitle(R.string.set_repeat); 59 | // Specify the list array, the items to be selected by default (null for none), 60 | // and the listener through which to receive callbacks when items are selected 61 | builder.setMultiChoiceItems(R.array.days_of_week, activeDays, 62 | new DialogInterface.OnMultiChoiceClickListener() { 63 | @Override 64 | public void onClick(DialogInterface dialog, int selectedDay, 65 | boolean isChecked) { 66 | activeDays[selectedDay] = isChecked; 67 | } 68 | }); 69 | // Set the action buttons 70 | builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { 71 | @Override 72 | public void onClick(DialogInterface dialog, int id) { 73 | // Return selectedDays to activity 74 | saveSelectedDays(activeDays); 75 | } 76 | }); 77 | builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { 78 | @Override 79 | public void onClick(DialogInterface dialog, int id) { 80 | } 81 | }); 82 | 83 | return builder.create(); 84 | } 85 | 86 | /** 87 | * Saved selectedDays by calling completeListener's complete function 88 | * 89 | * @param selectedDays ArrayList of selected days ints 90 | */ 91 | private void saveSelectedDays(boolean[] selectedDays) { 92 | this.completeListener.onDialogComplete(selectedDays); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/ui/activity/AddAlarmActivity.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.ui.activity; 2 | 3 | import android.app.TimePickerDialog; 4 | import android.arch.lifecycle.ViewModelProviders; 5 | import android.content.Intent; 6 | import android.support.annotation.NonNull; 7 | import android.support.annotation.Nullable; 8 | import android.support.design.widget.FloatingActionButton; 9 | import android.support.design.widget.Snackbar; 10 | import android.support.v7.app.AppCompatActivity; 11 | import android.os.Bundle; 12 | import android.view.Menu; 13 | import android.view.MenuItem; 14 | import android.view.View; 15 | import android.widget.ImageButton; 16 | import android.widget.RelativeLayout; 17 | import android.widget.TextView; 18 | import android.widget.TimePicker; 19 | 20 | import com.manveerbasra.ontime.R; 21 | import com.manveerbasra.ontime.db.Alarm; 22 | import com.manveerbasra.ontime.ui.SetRepeatDaysDialogFragment; 23 | import com.manveerbasra.ontime.viewmodel.AlarmViewModel; 24 | 25 | import java.util.Calendar; 26 | import java.util.GregorianCalendar; 27 | 28 | /** 29 | * Used to create and edit alarms, depending on REQUEST_CODE 30 | */ 31 | public class AddAlarmActivity extends AppCompatActivity implements SetRepeatDaysDialogFragment.OnDialogCompleteListener { 32 | 33 | private final String TAG = "AddAlarmActivity"; 34 | 35 | private static final int SET_START_LOCATION_ACTIVITY_REQUEST_CODE = 1; 36 | private static final int SET_END_LOCATION_ACTIVITY_REQUEST_CODE = 2; 37 | 38 | // Key values for returning intent. 39 | public static final String EXTRA_BUNDLE = "com.manveerbasra.ontime.AddAlarmActivity.BUNDLE"; 40 | public static final String EXTRA_ALARM = "com.manveerbasra.ontime.AddAlarmActivity.ALARM"; 41 | public static final String EXTRA_DELETE = "com.manveerbasra.ontime.AddAlarmActivity.DELETE"; 42 | 43 | private AlarmViewModel alarmViewModel; 44 | private int mCurrRequestCode; // current request code - static values in MainActivity 45 | 46 | private Alarm mAlarm; 47 | // Data objects 48 | private Calendar calendar; 49 | // View objects 50 | private TextView mTimeTextView; 51 | private TextView mRepeatTextView; 52 | private TextView mStartLocTextView; 53 | private TextView mEndLocTextView; 54 | private FloatingActionButton mDeleteButton; 55 | 56 | 57 | @Override 58 | protected void onCreate(Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | setContentView(R.layout.activity_add_alarm); 61 | 62 | calendar = Calendar.getInstance(); 63 | mDeleteButton = findViewById(R.id.fab_add_alarm_delete); 64 | 65 | mTimeTextView = findViewById(R.id.add_alarm_time_text); 66 | mRepeatTextView = findViewById(R.id.add_alarm_repeat_text); 67 | mStartLocTextView = findViewById(R.id.add_alarm_start_loc_text); 68 | mEndLocTextView = findViewById(R.id.add_alarm_end_loc_text); 69 | 70 | // Get a new or existing ViewModel from the ViewModelProvider. 71 | alarmViewModel = ViewModelProviders.of(this).get(AlarmViewModel.class); 72 | 73 | Intent intent = getIntent(); 74 | if (intent.hasExtra(EXTRA_BUNDLE)) { // Activity called to edit an alarm. 75 | Bundle args = intent.getBundleExtra(EXTRA_BUNDLE); 76 | mAlarm = args.getParcelable(EXTRA_ALARM); 77 | mCurrRequestCode = MainActivity.EDIT_ALARM_ACTIVITY_REQUEST_CODE; 78 | } else { 79 | mCurrRequestCode = MainActivity.NEW_ALARM_ACTIVITY_REQUEST_CODE; 80 | } 81 | 82 | if (mAlarm != null) { 83 | mStartLocTextView.setText(mAlarm.startPlace); 84 | mEndLocTextView.setText(mAlarm.endPlace); 85 | mTimeTextView.setText(mAlarm.getStringTime()); 86 | mRepeatTextView.setText(mAlarm.getStringOfActiveDays()); 87 | setTitle(R.string.edit_alarm); 88 | addModeButtonListeners(mAlarm.transMode); 89 | 90 | addDeleteButtonListener(); 91 | } else { 92 | mAlarm = new Alarm(); 93 | mAlarm.activeDays = new boolean[7]; 94 | mAlarm.transMode = "driving"; 95 | mDeleteButton.hide(); 96 | setInitialAlarmTime(); 97 | addModeButtonListeners("driving"); 98 | mRepeatTextView.setText(R.string.never); 99 | } 100 | 101 | addSetTimeLayoutListener(); 102 | addSetRepeatLayoutListener(); 103 | addSetStartLocationListener(); 104 | addEndStartLocationListener(); 105 | } 106 | 107 | 108 | /** 109 | * Initialize TimeTextView and Alarm's time with current time 110 | */ 111 | private void setInitialAlarmTime() { 112 | // Get time and set it to alarm time TextView 113 | int hour = calendar.get(Calendar.HOUR_OF_DAY); 114 | int minute = calendar.get(Calendar.MINUTE); 115 | 116 | String currentTime = getFormattedTime(hour, minute); 117 | mTimeTextView.setText(currentTime); 118 | mAlarm.setTime(currentTime); 119 | } 120 | 121 | private void addDeleteButtonListener() { 122 | mDeleteButton.setOnClickListener(new View.OnClickListener() { 123 | @Override 124 | public void onClick(View view) { 125 | Intent replyIntent = new Intent(); 126 | 127 | alarmViewModel.delete(mAlarm); 128 | replyIntent.putExtra(EXTRA_DELETE, true); 129 | 130 | setResult(RESULT_OK, replyIntent); 131 | finish(); 132 | } 133 | }); 134 | } 135 | 136 | /** 137 | * When timeChangeLayout is selected, open TimePickerDialog 138 | */ 139 | private void addSetTimeLayoutListener() { 140 | // Get layout view 141 | RelativeLayout setTimeButton = findViewById(R.id.add_alarm_time_layout); 142 | 143 | setTimeButton.setOnClickListener(new View.OnClickListener() { 144 | @Override 145 | public void onClick(View view) { 146 | 147 | // Get initial hour and minute values to display in dialog; 148 | int hour, minute; 149 | if (mAlarm.time == null) { 150 | hour = calendar.get(Calendar.HOUR_OF_DAY); 151 | minute = calendar.get(Calendar.MINUTE); 152 | } else { 153 | Calendar calendar = GregorianCalendar.getInstance(); 154 | calendar.setTime(mAlarm.time); 155 | hour = calendar.get(Calendar.HOUR_OF_DAY); 156 | minute = calendar.get(Calendar.MINUTE); 157 | } 158 | 159 | TimePickerDialog timePicker; 160 | timePicker = new TimePickerDialog(AddAlarmActivity.this, new TimePickerDialog.OnTimeSetListener() { 161 | @Override 162 | public void onTimeSet(TimePicker timePicker, int selectedHour, int selectedMinute) { 163 | String formattedTime = getFormattedTime(selectedHour, selectedMinute); 164 | mAlarm.setTime(formattedTime); 165 | mTimeTextView.setText(formattedTime); 166 | } 167 | }, hour, minute, false); 168 | timePicker.setTitle("Select Time"); 169 | timePicker.show(); 170 | } 171 | }); 172 | } 173 | 174 | /** 175 | * When repeatChangeLayout is selected, open SetRepeatDaysDialogFragment 176 | */ 177 | private void addSetRepeatLayoutListener() { 178 | // Get layout view 179 | RelativeLayout setRepeatButton = findViewById(R.id.add_alarm_repeat_layout); 180 | 181 | setRepeatButton.setOnClickListener(new View.OnClickListener() { 182 | @Override 183 | public void onClick(View view) { 184 | SetRepeatDaysDialogFragment setRepeatDaysDialogFragment = new SetRepeatDaysDialogFragment(); 185 | 186 | Bundle args = getBundle(); 187 | setRepeatDaysDialogFragment.setArguments(args); 188 | // Display dialog 189 | setRepeatDaysDialogFragment.show(getSupportFragmentManager(), "A"); 190 | } 191 | }); 192 | } 193 | 194 | /** 195 | * When startLocation Layout is selected, open maps activity 196 | */ 197 | private void addSetStartLocationListener() { 198 | RelativeLayout setStartLocButton = findViewById(R.id.add_alarm_start_loc_layout); 199 | 200 | setStartLocButton.setOnClickListener(new View.OnClickListener() { 201 | @Override 202 | public void onClick(View view) { 203 | Intent intent = new Intent(AddAlarmActivity.this, MapsActivity.class); 204 | 205 | Bundle args = new Bundle(); 206 | args.putParcelable(MapsActivity.EXTRA_LATLNG, mAlarm.startPoint); 207 | intent.putExtra(MapsActivity.BUNDLE_POINT, args); 208 | intent.putExtra(MapsActivity.EXTRA_PLACE, mAlarm.startPlace); 209 | 210 | startActivityForResult(intent, SET_START_LOCATION_ACTIVITY_REQUEST_CODE); 211 | } 212 | }); 213 | } 214 | 215 | /** 216 | * When endLocation Layout is selected, open maps activity 217 | */ 218 | private void addEndStartLocationListener() { 219 | RelativeLayout setEndLocButton = findViewById(R.id.add_alarm_end_loc_layout); 220 | 221 | setEndLocButton.setOnClickListener(new View.OnClickListener() { 222 | @Override 223 | public void onClick(View view) { 224 | Intent intent = new Intent(AddAlarmActivity.this, MapsActivity.class); 225 | 226 | Bundle args = new Bundle(); 227 | args.putParcelable(MapsActivity.EXTRA_LATLNG, mAlarm.endPoint); 228 | intent.putExtra(MapsActivity.BUNDLE_POINT, args); 229 | intent.putExtra(MapsActivity.EXTRA_PLACE, mAlarm.endPlace); 230 | 231 | startActivityForResult(intent, SET_END_LOCATION_ACTIVITY_REQUEST_CODE); 232 | } 233 | }); 234 | } 235 | 236 | private void addModeButtonListeners(String currMode) { 237 | final ImageButton walkButton = findViewById(R.id.mode_walk_button); 238 | final ImageButton bikeButton = findViewById(R.id.mode_bike_button); 239 | final ImageButton transitButton = findViewById(R.id.mode_transit_button); 240 | final ImageButton driveButton = findViewById(R.id.mode_drive_button); 241 | 242 | if (currMode == null) { 243 | currMode = "driving"; 244 | } 245 | 246 | switch (currMode) { // Set initial backgrounds based on currMode parameter 247 | case "driving": 248 | updateBackgrounds(driveButton, walkButton, bikeButton, transitButton); 249 | break; 250 | case "transit": 251 | updateBackgrounds(transitButton, walkButton, bikeButton, driveButton); 252 | break; 253 | case "bicycling": 254 | updateBackgrounds(bikeButton, walkButton, transitButton, driveButton); 255 | break; 256 | case "walking": 257 | updateBackgrounds(walkButton, bikeButton, transitButton, driveButton); 258 | break; 259 | } 260 | 261 | walkButton.setOnClickListener(new View.OnClickListener() { 262 | @Override 263 | public void onClick(View view) { 264 | updateBackgrounds(walkButton, bikeButton, transitButton, driveButton); 265 | mAlarm.transMode = "walking"; 266 | } 267 | }); 268 | 269 | bikeButton.setOnClickListener(new View.OnClickListener() { 270 | @Override 271 | public void onClick(View view) { 272 | updateBackgrounds(bikeButton, walkButton, transitButton, driveButton); 273 | mAlarm.transMode = "bicycling"; 274 | } 275 | }); 276 | 277 | transitButton.setOnClickListener(new View.OnClickListener() { 278 | @Override 279 | public void onClick(View view) { 280 | updateBackgrounds(transitButton, walkButton, bikeButton, driveButton); 281 | mAlarm.transMode = "transit"; 282 | } 283 | }); 284 | 285 | driveButton.setOnClickListener(new View.OnClickListener() { 286 | @Override 287 | public void onClick(View view) { 288 | updateBackgrounds(driveButton, walkButton, bikeButton, transitButton); 289 | mAlarm.transMode = "driving"; 290 | } 291 | }); 292 | } 293 | 294 | /** 295 | * Update backgrounds of different ImageButtons based on whether they should be active or not. 296 | * First parameter button is set to active 297 | */ 298 | private void updateBackgrounds(ImageButton active, ImageButton inactive1, ImageButton inactive2, ImageButton inactive3) { 299 | active.setBackground(getDrawable(R.drawable.solid_circle_blue)); 300 | inactive1.setBackground(getDrawable(R.drawable.solid_circle_grey)); 301 | inactive2.setBackground(getDrawable(R.drawable.solid_circle_grey)); 302 | inactive3.setBackground(getDrawable(R.drawable.solid_circle_grey)); 303 | } 304 | 305 | /** 306 | * Get Bundle of arguments for SetRepeatDaysDialogFragment, arguments include alarm's active days. 307 | */ 308 | @NonNull 309 | private Bundle getBundle() { 310 | Bundle args = new Bundle(); 311 | 312 | args.putBooleanArray("activeDays", mAlarm.activeDays); 313 | return args; 314 | } 315 | 316 | /** 317 | * This method is called when SetRepeatDaysDialogFragment completes, we get the selectedDays 318 | * and apply that to alarm 319 | * 320 | * @param selectedDaysBools boolean array of selected days of the week 321 | */ 322 | public void onDialogComplete(boolean[] selectedDaysBools) { 323 | mAlarm.activeDays = selectedDaysBools; 324 | String formattedActiveDays = Alarm.getStringOfActiveDays(mAlarm.activeDays); 325 | mRepeatTextView.setText(formattedActiveDays); 326 | } 327 | 328 | @Override 329 | public boolean onCreateOptionsMenu(Menu menu) { 330 | // Inflate the menu; this adds items to the action bar if it is present. 331 | getMenuInflater().inflate(R.menu.menu_add_alarm, menu); 332 | return true; 333 | } 334 | 335 | @Override 336 | public boolean onOptionsItemSelected(MenuItem item) { 337 | int id = item.getItemId(); 338 | 339 | //noinspection SimplifiableIfStatement 340 | if (id == R.id.action_alarm_save) { 341 | if (mStartLocTextView.getText().equals(getString(R.string.set_start_location)) 342 | || mEndLocTextView.getText().equals(getString(R.string.set_end_location))) { 343 | Snackbar.make(findViewById(R.id.fab_add_alarm_delete), getString(R.string.locs_not_selected), Snackbar.LENGTH_SHORT).show(); 344 | } else { 345 | Intent replyIntent = new Intent(); 346 | 347 | if (mCurrRequestCode == MainActivity.EDIT_ALARM_ACTIVITY_REQUEST_CODE) { 348 | alarmViewModel.update(mAlarm); 349 | } else { 350 | alarmViewModel.insert(mAlarm); 351 | } 352 | 353 | setResult(RESULT_OK, replyIntent); 354 | finish(); 355 | } 356 | } 357 | 358 | return super.onOptionsItemSelected(item); 359 | } 360 | 361 | /** 362 | * Updates UI with data received from MapActivity 363 | * 364 | * @param requestCode request code varies on whether start or end location set 365 | * @param resultCode whether activity successfully completed 366 | * @param data reply Intent, contains extras that vary based on request code 367 | */ 368 | @Override 369 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 370 | super.onActivityResult(requestCode, resultCode, data); 371 | 372 | if (data == null) { 373 | return; 374 | } 375 | 376 | if (requestCode == SET_START_LOCATION_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK) { 377 | // Get extras 378 | mAlarm.startPlace = data.getStringExtra(MapsActivity.EXTRA_PLACE); 379 | Bundle args = data.getBundleExtra(MapsActivity.BUNDLE_POINT); 380 | mAlarm.startPoint = args.getParcelable(MapsActivity.EXTRA_LATLNG); 381 | 382 | // Set place to start location textView 383 | mStartLocTextView.setText(mAlarm.startPlace); 384 | 385 | } else if (requestCode == SET_END_LOCATION_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK) { 386 | // Get extra 387 | mAlarm.endPlace = data.getStringExtra(MapsActivity.EXTRA_PLACE); 388 | Bundle args = data.getBundleExtra(MapsActivity.BUNDLE_POINT); 389 | mAlarm.endPoint = args.getParcelable(MapsActivity.EXTRA_LATLNG); 390 | // Set place to start location textView 391 | mEndLocTextView.setText(mAlarm.endPlace); 392 | } 393 | } 394 | 395 | /** 396 | * Return a string of the form hh:mm aa from given hour and minute 397 | * 398 | * @param hour integer of hour in 24h format 399 | * @param minute integer of minute 400 | * @return a String of formatted time 401 | */ 402 | private String getFormattedTime(int hour, int minute) { 403 | String meridian = "AM"; 404 | if (hour >= 12) { 405 | meridian = "PM"; 406 | } 407 | 408 | if (hour > 12) { 409 | hour -= 12; 410 | } else if (hour == 0) { 411 | hour = 12; 412 | } 413 | 414 | String formattedTime; 415 | if (minute < 10) { 416 | formattedTime = hour + ":0" + minute + " " + meridian; 417 | } else { 418 | formattedTime = hour + ":" + minute + " " + meridian; 419 | } 420 | 421 | return formattedTime; 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/ui/activity/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.ui.activity; 2 | 3 | import android.arch.lifecycle.Observer; 4 | import android.arch.lifecycle.ViewModelProviders; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.support.annotation.Nullable; 8 | import android.support.design.widget.FloatingActionButton; 9 | import android.support.design.widget.Snackbar; 10 | import android.support.v7.app.AppCompatActivity; 11 | import android.support.v7.widget.LinearLayoutManager; 12 | import android.support.v7.widget.RecyclerView; 13 | import android.support.v7.widget.Toolbar; 14 | import android.view.View; 15 | import android.view.Menu; 16 | import android.view.MenuItem; 17 | 18 | import com.google.android.gms.maps.model.LatLng; 19 | import com.manveerbasra.ontime.R; 20 | import com.manveerbasra.ontime.db.Alarm; 21 | import com.manveerbasra.ontime.ui.AlarmListAdapter; 22 | import com.manveerbasra.ontime.viewmodel.AlarmViewModel; 23 | 24 | import java.text.DateFormat; 25 | import java.text.SimpleDateFormat; 26 | import java.util.Date; 27 | import java.util.List; 28 | 29 | public class MainActivity extends AppCompatActivity { 30 | 31 | private final String TAG = "MainActivity"; 32 | 33 | public static final int NEW_ALARM_ACTIVITY_REQUEST_CODE = 1; 34 | public static final int EDIT_ALARM_ACTIVITY_REQUEST_CODE = 2; 35 | 36 | /** 37 | * Used to access AlarmDatabase 38 | */ 39 | private AlarmViewModel alarmViewModel; 40 | private View snackbarAnchor; 41 | 42 | @Override 43 | protected void onCreate(Bundle savedInstanceState) { 44 | super.onCreate(savedInstanceState); 45 | setContentView(R.layout.activity_main); 46 | Toolbar toolbar = findViewById(R.id.toolbar_main); 47 | setSupportActionBar(toolbar); 48 | snackbarAnchor = findViewById(R.id.fab); 49 | 50 | // Setup adapter to display alarms. 51 | RecyclerView recyclerView = findViewById(R.id.alarm_list); 52 | final AlarmListAdapter adapter = new AlarmListAdapter(this); 53 | recyclerView.setAdapter(adapter); 54 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 55 | 56 | // Get a new or existing ViewModel from the ViewModelProvider. 57 | alarmViewModel = ViewModelProviders.of(this).get(AlarmViewModel.class); 58 | 59 | // Add an observer on the LiveData returned by getAllAlarms. 60 | alarmViewModel.getAllAlarms().observe(this, new Observer>() { 61 | @Override 62 | public void onChanged(@Nullable final List alarms) { 63 | // Update the cached copy of the words in the adapter. 64 | adapter.setAlarms(alarms); 65 | } 66 | }); 67 | 68 | setFABListener(); 69 | } 70 | 71 | /** 72 | * Setup FloatingActionButton listener 73 | */ 74 | private void setFABListener() { 75 | FloatingActionButton fab = findViewById(R.id.fab); 76 | fab.setOnClickListener(new View.OnClickListener() { 77 | @Override 78 | public void onClick(View view) { 79 | Intent intent = new Intent(MainActivity.this, AddAlarmActivity.class); 80 | startActivityForResult(intent, NEW_ALARM_ACTIVITY_REQUEST_CODE); 81 | } 82 | }); 83 | } 84 | 85 | /** 86 | * Notifies user with data received from AddAlarmActivity. 87 | *

88 | * Handles both new Alarms and edited Alarms. 89 | * 90 | * @param requestCode request code varies on whether alarm is added or edited 91 | * @param resultCode whether activity successfully completed 92 | * @param data reply Intent, contains extras 93 | */ 94 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 95 | super.onActivityResult(requestCode, resultCode, data); 96 | 97 | if (resultCode == RESULT_OK) { 98 | if (data.hasExtra(AddAlarmActivity.EXTRA_DELETE)) { 99 | Snackbar.make(snackbarAnchor, R.string.alarm_deleted, Snackbar.LENGTH_SHORT).show(); 100 | } else { 101 | Snackbar.make(snackbarAnchor, R.string.alarm_saved, Snackbar.LENGTH_SHORT).show(); 102 | } 103 | 104 | } else { 105 | Snackbar.make(snackbarAnchor, R.string.alarm_not_saved, Snackbar.LENGTH_SHORT).show(); 106 | } 107 | } 108 | 109 | @Override 110 | public boolean onCreateOptionsMenu(Menu menu) { 111 | // Inflate the menu; this adds items to the action bar if it is present. 112 | getMenuInflater().inflate(R.menu.menu_main, menu); 113 | return true; 114 | } 115 | 116 | @Override 117 | public boolean onOptionsItemSelected(MenuItem item) { 118 | int id = item.getItemId(); 119 | 120 | //noinspection SimplifiableIfStatement 121 | if (id == R.id.action_settings) { 122 | Intent intent = new Intent(MainActivity.this, SettingsActivity.class); 123 | startActivity(intent); 124 | return true; 125 | } 126 | 127 | return super.onOptionsItemSelected(item); 128 | } 129 | 130 | 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/ui/activity/MapsActivity.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.ui.activity; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.design.widget.FloatingActionButton; 6 | import android.support.design.widget.Snackbar; 7 | import android.support.v4.app.FragmentActivity; 8 | import android.util.Log; 9 | import android.view.View; 10 | 11 | import com.google.android.gms.common.api.Status; 12 | import com.google.android.gms.location.places.Place; 13 | import com.google.android.gms.location.places.ui.PlaceAutocompleteFragment; 14 | import com.google.android.gms.location.places.ui.PlaceSelectionListener; 15 | import com.google.android.gms.maps.CameraUpdateFactory; 16 | import com.google.android.gms.maps.GoogleMap; 17 | import com.google.android.gms.maps.OnMapReadyCallback; 18 | import com.google.android.gms.maps.SupportMapFragment; 19 | import com.google.android.gms.maps.model.CameraPosition; 20 | import com.google.android.gms.maps.model.LatLng; 21 | import com.google.android.gms.maps.model.Marker; 22 | import com.google.android.gms.maps.model.MarkerOptions; 23 | import com.manveerbasra.ontime.R; 24 | 25 | /** 26 | * Activity to allow user to choose a location with map aid 27 | */ 28 | public class MapsActivity extends FragmentActivity implements OnMapReadyCallback { 29 | 30 | private final String TAG = "MapsActivity"; 31 | 32 | public static final String EXTRA_PLACE = "com.manveerbasra.ontime.MapsActivity.PLACE"; 33 | public static final String BUNDLE_POINT = "com.manveerbasra.ontime.MapsActivity.BUNDLE.POINT"; 34 | public static final String EXTRA_LATLNG = "com.manveerbasra.ontime.MapsActivity.LATLNG"; 35 | 36 | private GoogleMap mMap; 37 | private Marker mMarker; 38 | 39 | private LatLng mPoint; 40 | private String mPlace; 41 | 42 | @Override 43 | protected void onCreate(Bundle savedInstanceState) { 44 | super.onCreate(savedInstanceState); 45 | setContentView(R.layout.activity_maps); 46 | 47 | Intent intent = getIntent(); 48 | Bundle args = intent.getBundleExtra(BUNDLE_POINT); 49 | mPoint = args.getParcelable(EXTRA_LATLNG); 50 | mPlace = intent.getStringExtra(EXTRA_PLACE); 51 | 52 | setPlaceSearchBarListener(); 53 | setFABClickListener(); 54 | 55 | // Obtain the SupportMapFragment and get notified when the map is ready to be used. 56 | SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager() 57 | .findFragmentById(R.id.map); 58 | if (mapFragment != null) mapFragment.getMapAsync(this); 59 | } 60 | 61 | /** 62 | * Setup onPlaceSelected Listener for the Place Search Bar 63 | */ 64 | private void setPlaceSearchBarListener() { 65 | PlaceAutocompleteFragment placeAutoComplete = (PlaceAutocompleteFragment) getFragmentManager().findFragmentById(R.id.place_autocomplete); 66 | placeAutoComplete.setOnPlaceSelectedListener(new PlaceSelectionListener() { 67 | @Override 68 | public void onPlaceSelected(Place place) { 69 | if (mMarker != null) mMarker.remove(); // remove old marker 70 | 71 | // Add marker in selected place 72 | LatLng latLng = place.getLatLng(); 73 | mMarker = mMap.addMarker(new MarkerOptions().position(latLng).title(place.getName().toString())); 74 | 75 | // Animate camera's movement to selected place 76 | CameraPosition cameraPosition = new CameraPosition.Builder() 77 | .target(latLng) // Sets the center of the map to place's coordinates 78 | .zoom(14) // Sets the zoom 79 | .build(); // Creates a CameraPosition from the builder 80 | mMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); 81 | 82 | Log.d(TAG, "Place selected: " + place.getName()); 83 | } 84 | 85 | @Override 86 | public void onError(Status status) { 87 | Log.d(TAG, "An error occurred: " + status); 88 | } 89 | }); 90 | } 91 | 92 | /** 93 | * Setup onClick Listener for FAB save button 94 | */ 95 | public void setFABClickListener() { 96 | FloatingActionButton fab = findViewById(R.id.fab_loc_save); 97 | fab.setOnClickListener(new View.OnClickListener() { 98 | @Override 99 | public void onClick(View view) { 100 | if (mMarker != null) { 101 | Intent replyIntent = new Intent(); 102 | 103 | Bundle args = new Bundle(); 104 | args.putParcelable(EXTRA_LATLNG, mMarker.getPosition()); 105 | 106 | // Add extras 107 | replyIntent.putExtra(BUNDLE_POINT, args); 108 | replyIntent.putExtra(EXTRA_PLACE, mMarker.getTitle()); 109 | setResult(RESULT_OK, replyIntent); 110 | finish(); 111 | } else { 112 | Snackbar.make(findViewById(R.id.fab_loc_save), getString(R.string.loc_not_selected), Snackbar.LENGTH_SHORT).show(); 113 | } 114 | } 115 | }); 116 | } 117 | 118 | 119 | /** 120 | * Manipulates the map once available. 121 | * This callback is triggered when the map is ready to be used. 122 | * If Google Play services is not installed on the device, the user will be prompted to install 123 | * it inside the SupportMapFragment. This method will only be triggered once the user has 124 | * installed Google Play services and returned to the app. 125 | */ 126 | @Override 127 | public void onMapReady(GoogleMap googleMap) { 128 | mMap = googleMap; 129 | if (mPoint != null) { // add starting marker and move camera 130 | mMarker = mMap.addMarker(new MarkerOptions().position(mPoint).title(mPlace)); 131 | mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(mPoint, 14)); 132 | } 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/ui/activity/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.ui.activity; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.res.Configuration; 7 | import android.media.Ringtone; 8 | import android.media.RingtoneManager; 9 | import android.net.Uri; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import android.preference.ListPreference; 13 | import android.preference.Preference; 14 | import android.preference.PreferenceActivity; 15 | import android.support.v7.app.ActionBar; 16 | import android.preference.PreferenceFragment; 17 | import android.preference.PreferenceManager; 18 | import android.preference.RingtonePreference; 19 | import android.text.TextUtils; 20 | import android.view.MenuItem; 21 | import android.support.v4.app.NavUtils; 22 | 23 | import com.manveerbasra.ontime.R; 24 | import com.manveerbasra.ontime.ui.AppCompatPreferenceActivity; 25 | 26 | import java.util.List; 27 | 28 | /** 29 | * A {@link PreferenceActivity} that presents a set of application settings. On 30 | * handset devices, settings are presented as a single list. On tablets, 31 | * settings are split by category, with category headers shown to the left of 32 | * the list of settings. 33 | *

34 | * See 35 | * Android Design: Settings for design guidelines and the Settings 37 | * API Guide for more information on developing a Settings UI. 38 | */ 39 | public class SettingsActivity extends AppCompatPreferenceActivity { 40 | 41 | /** 42 | * A preference value change listener that updates the preference's summary 43 | * to reflect its new value. 44 | */ 45 | private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { 46 | @Override 47 | public boolean onPreferenceChange(Preference preference, Object value) { 48 | String stringValue = value.toString(); 49 | 50 | if (preference instanceof ListPreference) { 51 | // For list preferences, look up the correct display value in 52 | // the preference's 'entries' list. 53 | ListPreference listPreference = (ListPreference) preference; 54 | int index = listPreference.findIndexOfValue(stringValue); 55 | 56 | // Set the summary to reflect the new value. 57 | preference.setSummary( 58 | index >= 0 59 | ? listPreference.getEntries()[index] 60 | : null); 61 | 62 | } else if (preference instanceof RingtonePreference) { 63 | // For ringtone preferences, look up the correct display value 64 | // using RingtoneManager. 65 | if (TextUtils.isEmpty(stringValue)) { 66 | // Empty values correspond to 'silent' (no ringtone). 67 | preference.setSummary(R.string.pref_ringtone_silent); 68 | 69 | } else { 70 | Ringtone ringtone = RingtoneManager.getRingtone( 71 | preference.getContext(), Uri.parse(stringValue)); 72 | 73 | if (ringtone == null) { 74 | // Clear the summary if there was a lookup error. 75 | preference.setSummary(null); 76 | } else { 77 | // Set the summary to reflect the new ringtone display 78 | // name. 79 | String name = ringtone.getTitle(preference.getContext()); 80 | preference.setSummary(name); 81 | } 82 | } 83 | 84 | } else { 85 | // For all other preferences, set the summary to the value's 86 | // simple string representation. 87 | preference.setSummary(stringValue); 88 | } 89 | return true; 90 | } 91 | }; 92 | 93 | /** 94 | * Helper method to determine if the device has an extra-large screen. For 95 | * example, 10" tablets are extra-large. 96 | */ 97 | private static boolean isXLargeTablet(Context context) { 98 | return (context.getResources().getConfiguration().screenLayout 99 | & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE; 100 | } 101 | 102 | /** 103 | * Binds a preference's summary to its value. More specifically, when the 104 | * preference's value is changed, its summary (line of text below the 105 | * preference title) is updated to reflect the value. The summary is also 106 | * immediately updated upon calling this method. The exact display format is 107 | * dependent on the type of preference. 108 | * 109 | * @see #sBindPreferenceSummaryToValueListener 110 | */ 111 | private static void bindPreferenceSummaryToValue(Preference preference) { 112 | // Set the listener to watch for value changes. 113 | preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); 114 | 115 | // Trigger the listener immediately with the preference's 116 | // current value. 117 | sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, 118 | PreferenceManager 119 | .getDefaultSharedPreferences(preference.getContext()) 120 | .getString(preference.getKey(), "")); 121 | } 122 | 123 | @Override 124 | protected void onCreate(Bundle savedInstanceState) { 125 | super.onCreate(savedInstanceState); 126 | setupActionBar(); 127 | } 128 | 129 | /** 130 | * Set up the {@link android.app.ActionBar}, if the API is available. 131 | */ 132 | private void setupActionBar() { 133 | ActionBar actionBar = getSupportActionBar(); 134 | if (actionBar != null) { 135 | // Show the Up button in the action bar. 136 | actionBar.setDisplayHomeAsUpEnabled(true); 137 | } 138 | } 139 | 140 | @Override 141 | public boolean onMenuItemSelected(int featureId, MenuItem item) { 142 | int id = item.getItemId(); 143 | if (id == android.R.id.home) { 144 | if (!super.onMenuItemSelected(featureId, item)) { 145 | NavUtils.navigateUpFromSameTask(this); 146 | } 147 | return true; 148 | } 149 | return super.onMenuItemSelected(featureId, item); 150 | } 151 | 152 | /** 153 | * {@inheritDoc} 154 | */ 155 | @Override 156 | public boolean onIsMultiPane() { 157 | return isXLargeTablet(this); 158 | } 159 | 160 | /** 161 | * {@inheritDoc} 162 | */ 163 | @Override 164 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 165 | public void onBuildHeaders(List

target) { 166 | loadHeadersFromResource(R.xml.pref_headers, target); 167 | } 168 | 169 | /** 170 | * This method stops fragment injection in malicious applications. 171 | * Make sure to deny any unknown fragments here. 172 | */ 173 | protected boolean isValidFragment(String fragmentName) { 174 | return PreferenceFragment.class.getName().equals(fragmentName) 175 | || GeneralPreferenceFragment.class.getName().equals(fragmentName) 176 | || NotificationPreferenceFragment.class.getName().equals(fragmentName); 177 | } 178 | 179 | /** 180 | * This fragment shows general preferences only. It is used when the 181 | * activity is showing a two-pane settings UI. 182 | */ 183 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 184 | public static class GeneralPreferenceFragment extends PreferenceFragment { 185 | @Override 186 | public void onCreate(Bundle savedInstanceState) { 187 | super.onCreate(savedInstanceState); 188 | addPreferencesFromResource(R.xml.pref_general); 189 | setHasOptionsMenu(true); 190 | 191 | // Bind the summaries of EditText/List/Dialog/Ringtone preferences 192 | // to their values. When their values change, their summaries are 193 | // updated to reflect the new value, per the Android Design 194 | // guidelines. 195 | bindPreferenceSummaryToValue(findPreference("home_location")); 196 | bindPreferenceSummaryToValue(findPreference("work_location")); 197 | } 198 | 199 | @Override 200 | public boolean onOptionsItemSelected(MenuItem item) { 201 | int id = item.getItemId(); 202 | if (id == android.R.id.home) { 203 | startActivity(new Intent(getActivity(), SettingsActivity.class)); 204 | return true; 205 | } 206 | return super.onOptionsItemSelected(item); 207 | } 208 | } 209 | 210 | /** 211 | * This fragment shows notification preferences only. It is used when the 212 | * activity is showing a two-pane settings UI. 213 | */ 214 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 215 | public static class NotificationPreferenceFragment extends PreferenceFragment { 216 | @Override 217 | public void onCreate(Bundle savedInstanceState) { 218 | super.onCreate(savedInstanceState); 219 | addPreferencesFromResource(R.xml.pref_notification); 220 | setHasOptionsMenu(true); 221 | 222 | // Bind the summaries of EditText/List/Dialog/Ringtone preferences 223 | // to their values. When their values change, their summaries are 224 | // updated to reflect the new value, per the Android Design 225 | // guidelines. 226 | bindPreferenceSummaryToValue(findPreference("alarm_snooze_length_list")); 227 | } 228 | 229 | @Override 230 | public boolean onOptionsItemSelected(MenuItem item) { 231 | int id = item.getItemId(); 232 | if (id == android.R.id.home) { 233 | startActivity(new Intent(getActivity(), SettingsActivity.class)); 234 | return true; 235 | } 236 | return super.onOptionsItemSelected(item); 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/util/AlarmRepository.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.util; 2 | 3 | import android.app.Application; 4 | import android.arch.lifecycle.LiveData; 5 | import android.os.AsyncTask; 6 | 7 | import com.manveerbasra.ontime.db.Alarm; 8 | import com.manveerbasra.ontime.db.AlarmDao; 9 | import com.manveerbasra.ontime.db.AlarmDatabase; 10 | 11 | import java.util.List; 12 | import java.util.concurrent.ExecutionException; 13 | 14 | /** 15 | * Abstracted Repository to handle interactions between ViewModels and Database 16 | */ 17 | public class AlarmRepository { 18 | 19 | private AlarmDao mAlarmModel; 20 | private LiveData> mAllAlarms; 21 | 22 | public AlarmRepository(Application application) { 23 | // Application is used instead of Context in order to prevent memory leaks 24 | // between Activity switches 25 | AlarmDatabase db = AlarmDatabase.getInstance(application); 26 | mAlarmModel = db.alarmModel(); 27 | mAllAlarms = mAlarmModel.getAllAlarms(); 28 | } 29 | 30 | // Observed LiveData will notify the observer when data has changed 31 | public LiveData> getAllAlarms() { 32 | return mAllAlarms; 33 | } 34 | 35 | public void insert(Alarm alarm) { 36 | new insertAsyncTask(mAlarmModel).execute(alarm); 37 | } 38 | 39 | public void replace(Alarm alarm) { 40 | new replaceAsyncTask(mAlarmModel).execute(alarm); 41 | } 42 | 43 | public void update(Alarm alarm) { 44 | new updateAsyncTask(mAlarmModel).execute(alarm); 45 | } 46 | 47 | public void updateActive(Alarm alarm) { 48 | new updateActiveAsyncTask(mAlarmModel).execute(alarm); 49 | } 50 | 51 | public void delete(Alarm alarm) { 52 | new deleteAsyncTask(mAlarmModel).execute(alarm); 53 | } 54 | 55 | public Alarm getAlarmById(int id) throws ExecutionException, InterruptedException { 56 | return new getByIdAsyncTask(mAlarmModel).execute(id).get(); 57 | } 58 | 59 | 60 | /* 61 | * Asynchronous Tasks 62 | * 63 | * One for each interaction with database. 64 | * All classes are static to prevent memory leaks. 65 | */ 66 | 67 | private static class insertAsyncTask extends AsyncTask { 68 | 69 | private AlarmDao alarmModel; 70 | 71 | insertAsyncTask(AlarmDao alarmModel) { 72 | this.alarmModel = alarmModel; 73 | } 74 | 75 | @Override 76 | protected Void doInBackground(final Alarm... params) { 77 | alarmModel.insert(params[0]); 78 | return null; 79 | } 80 | } 81 | 82 | private static class replaceAsyncTask extends AsyncTask { 83 | 84 | private AlarmDao alarmModel; 85 | 86 | replaceAsyncTask(AlarmDao alarmModel) { 87 | this.alarmModel = alarmModel; 88 | } 89 | 90 | @Override 91 | protected Void doInBackground(final Alarm... params) { 92 | alarmModel.insertOrReplaceAlarm(params[0]); 93 | return null; 94 | } 95 | } 96 | 97 | private static class updateAsyncTask extends AsyncTask { 98 | 99 | private AlarmDao alarmModel; 100 | 101 | updateAsyncTask(AlarmDao alarmModel) { 102 | this.alarmModel = alarmModel; 103 | } 104 | 105 | @Override 106 | protected Void doInBackground(final Alarm... params) { 107 | alarmModel.update(params[0]); 108 | return null; 109 | } 110 | } 111 | 112 | private static class updateActiveAsyncTask extends AsyncTask { 113 | 114 | private AlarmDao alarmModel; 115 | 116 | updateActiveAsyncTask(AlarmDao alarmModel) { 117 | this.alarmModel = alarmModel; 118 | } 119 | 120 | @Override 121 | protected Void doInBackground(final Alarm... params) { 122 | alarmModel.updateActive(params[0].id, params[0].isActive()); 123 | return null; 124 | } 125 | } 126 | 127 | private static class deleteAsyncTask extends AsyncTask { 128 | 129 | private AlarmDao alarmModel; 130 | 131 | deleteAsyncTask(AlarmDao alarmModel) { 132 | this.alarmModel = alarmModel; 133 | } 134 | 135 | @Override 136 | protected Void doInBackground(final Alarm... params) { 137 | alarmModel.delete(params[0]); 138 | return null; 139 | } 140 | } 141 | 142 | private static class getByIdAsyncTask extends android.os.AsyncTask { 143 | 144 | private AlarmDao alarmModel; 145 | 146 | getByIdAsyncTask(AlarmDao alarmModel) { 147 | this.alarmModel = alarmModel; 148 | } 149 | 150 | @Override 151 | protected Alarm doInBackground(final Integer... params) { 152 | return alarmModel.getById(params[0]); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/util/JSONParser.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.util; 2 | 3 | import android.util.Log; 4 | 5 | import com.manveerbasra.ontime.timehandlers.TrafficTimeHandler; 6 | import com.manveerbasra.ontime.timehandlers.WeatherTimeHandler; 7 | 8 | import org.json.JSONArray; 9 | import org.json.JSONException; 10 | import org.json.JSONObject; 11 | 12 | import java.util.HashMap; 13 | 14 | /** 15 | * Class used to parse JSONObject received from a HTTPS request 16 | */ 17 | public class JSONParser { 18 | 19 | private final String TAG = "JSONParser"; 20 | 21 | /** 22 | * Receives a JSONObject from a GoogleMaps API Request 23 | * and returns a HashMap of keys to values containing only the route durations 24 | * See docs 25 | * 26 | * @param jObject JSONObject received from API request 27 | * @return a HashMap of keys (duration) to their respective int durations in seconds 28 | */ 29 | public HashMap parseFromMaps(JSONObject jObject){ 30 | 31 | HashMap routes = new HashMap<>(); 32 | 33 | try { 34 | String statusCode = jObject.getString("status"); 35 | if (!statusCode.equals("OK")) { 36 | Log.e(TAG, "Status code from GoogleMaps API: " + statusCode); 37 | return null; 38 | } 39 | JSONArray jRoutes = jObject.getJSONArray("routes"); 40 | JSONArray jLegs = (jRoutes.getJSONObject(0)).getJSONArray("legs"); 41 | 42 | JSONObject jTime = (jLegs.getJSONObject(0)).getJSONObject("duration"); 43 | JSONObject jTimeInTraffic = (jLegs.getJSONObject(0)).getJSONObject("duration_in_traffic"); 44 | 45 | routes.put(TrafficTimeHandler.DURATION, jTime.getInt("value")); 46 | routes.put(TrafficTimeHandler.DURATION_TRAFFIC, jTimeInTraffic.getInt("value")); 47 | 48 | } catch (JSONException e) { 49 | e.printStackTrace(); 50 | } 51 | return routes; 52 | } 53 | 54 | /** 55 | * Receives a JSONObject from a OpenWeatherMap API Request 56 | * and returns a HashMap of keys to values containing only the route durations 57 | * See docs 58 | * 59 | * @param jObject JSONObject received from API request 60 | * @return a HashMap of keys (duration) to their respective int durations in seconds 61 | */ 62 | public HashMap parseFromWeather(JSONObject jObject) { 63 | 64 | HashMap conditions = new HashMap<>(); 65 | 66 | try { 67 | JSONArray jWeather = jObject.getJSONArray("weather"); 68 | JSONObject jMain = jObject.getJSONObject("main"); 69 | 70 | conditions.put(WeatherTimeHandler.CONDITIONS, jWeather.getJSONObject(0).getString("main")); 71 | conditions.put(WeatherTimeHandler.CONDITIONS_DESC, jWeather.getJSONObject(0).getString("description")); 72 | conditions.put(WeatherTimeHandler.TEMPERATURE, jMain.get("temp")); 73 | 74 | if (jObject.has("wind")) 75 | conditions.put(WeatherTimeHandler.WIND, jObject.getJSONObject("wind").get("speed")); 76 | 77 | int rainAmount = 0; 78 | if (jObject.has("rain")) { 79 | JSONObject jRain = jObject.getJSONObject("rain"); 80 | if (jRain.has("1h")) { 81 | rainAmount = jRain.getInt("1h"); 82 | } else if (jRain.has("3h")) { 83 | rainAmount = jRain.getInt("3h"); 84 | } 85 | } 86 | 87 | int snowAmount = 0; 88 | if (jObject.has("snow")) { 89 | JSONObject jSnow = jObject.getJSONObject("snow"); 90 | if (jSnow.has("1h")) { 91 | snowAmount = jSnow.getInt("1h"); 92 | } else if (jSnow.has("3h")) { 93 | snowAmount = jSnow.getInt("3h"); 94 | } 95 | } 96 | 97 | conditions.put(WeatherTimeHandler.RAIN, rainAmount); 98 | conditions.put(WeatherTimeHandler.SNOW, snowAmount); 99 | 100 | } catch (JSONException e) { 101 | e.printStackTrace(); 102 | } 103 | return conditions; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/com/manveerbasra/ontime/viewmodel/AlarmViewModel.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime.viewmodel; 2 | 3 | import android.app.Application; 4 | import android.arch.lifecycle.AndroidViewModel; 5 | import android.arch.lifecycle.LiveData; 6 | import android.util.Log; 7 | 8 | import com.manveerbasra.ontime.util.AlarmRepository; 9 | import com.manveerbasra.ontime.db.Alarm; 10 | 11 | import java.util.List; 12 | import java.util.concurrent.ExecutionException; 13 | 14 | /** 15 | * View Model to keep a reference to the alarm repository and 16 | * an up-to-date list of all alarm. 17 | * Completely separates UI from Repository 18 | */ 19 | public class AlarmViewModel extends AndroidViewModel { 20 | 21 | private final String TAG = "AlarmViewModel"; 22 | 23 | private AlarmRepository mRepository; 24 | private LiveData> mAllAlarms; 25 | 26 | public AlarmViewModel(Application application) { 27 | super(application); 28 | mRepository = new AlarmRepository(application); 29 | mAllAlarms = mRepository.getAllAlarms(); 30 | } 31 | 32 | // List is wrapped in LiveData in order to be observed and updated efficiently 33 | public LiveData> getAllAlarms() { 34 | return mAllAlarms; 35 | } 36 | 37 | public void insert(Alarm alarm) { 38 | mRepository.insert(alarm); 39 | } 40 | 41 | public void replace(Alarm alarm) { 42 | mRepository.replace(alarm); 43 | } 44 | 45 | public void update(Alarm alarm) { 46 | mRepository.update(alarm); 47 | } 48 | 49 | public void updateActive(Alarm alarm) { 50 | mRepository.updateActive(alarm); 51 | } 52 | 53 | public void delete(Alarm alarm) { 54 | mRepository.delete(alarm); 55 | } 56 | 57 | public Alarm getAlarmById(int id) { 58 | try { 59 | return mRepository.getAlarmById(id); 60 | } catch (InterruptedException | ExecutionException e) { 61 | Log.e(TAG, "Error when retrieving alarm by id: " + id); 62 | e.printStackTrace(); 63 | } 64 | return new Alarm(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_directions_bike_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_directions_car_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_directions_transit_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_directions_walk_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_location_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notifications_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_repeat_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_set_time_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_start_location_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sync_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_rectangle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/solid_circle_blue.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/solid_circle_grey.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/solid_circle_light_blue.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_add_alarm.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | 31 | 32 | 38 | 39 | 45 | 46 | 54 | 55 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 78 | 79 | 85 | 86 | 92 | 93 | 101 | 102 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 121 | 122 | 135 | 136 | 137 | 149 | 150 | 159 | 160 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 185 | 186 | 193 | 194 | 201 | 202 | 209 | 210 | 222 | 223 | 232 | 233 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 263 | 264 | 269 | 270 | 274 | 275 | 280 | 281 | 285 | 286 | 291 | 292 | 296 | 297 | 302 | 303 | 304 | 305 | 306 | 310 | 311 | 319 | 320 | 321 | 322 | 323 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_maps.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 20 | 21 | 27 | 28 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_alarm.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 24 | 25 | 33 | 34 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_add_alarm.xml: -------------------------------------------------------------------------------- 1 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manveerxyz/OnTime/962b67761bf38a205d5e9166dc445b4454ec14ac/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manveerxyz/OnTime/962b67761bf38a205d5e9166dc445b4454ec14ac/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manveerxyz/OnTime/962b67761bf38a205d5e9166dc445b4454ec14ac/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manveerxyz/OnTime/962b67761bf38a205d5e9166dc445b4454ec14ac/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manveerxyz/OnTime/962b67761bf38a205d5e9166dc445b4454ec14ac/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manveerxyz/OnTime/962b67761bf38a205d5e9166dc445b4454ec14ac/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manveerxyz/OnTime/962b67761bf38a205d5e9166dc445b4454ec14ac/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manveerxyz/OnTime/962b67761bf38a205d5e9166dc445b4454ec14ac/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manveerxyz/OnTime/962b67761bf38a205d5e9166dc445b4454ec14ac/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manveerxyz/OnTime/962b67761bf38a205d5e9166dc445b4454ec14ac/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #2196F3 4 | #1565C0 5 | #64B5F6 6 | 7 | #FAFAFA 8 | #E0E0E0 9 | #BDBDBD 10 | #9E9E9E 11 | #616161 12 | 13 | #ef5350 14 | #FFFFFF 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 20dp 5 | 6 | 7 | 36sp 8 | 14sp 9 | 16sp 10 | 18sp 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/fonts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | sans-serif-thin 4 | sans-serif-light 5 | sans-serif-medium 6 | sans-serif 7 | sans-serif-condensed 8 | sans-serif-black 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | OnTime 3 | Settings 4 | 5 | 6 | Sunday 7 | Monday 8 | Tuesday 9 | Wednesday 10 | Thursday 11 | Friday 12 | Saturday 13 | 14 | 15 | No alarms yet. \nClick \'+\' to add an alarm. 16 | 17 | 18 | Alarm 19 | Add New Alarm 20 | Edit Alarm 21 | Time 22 | Repeat 23 | Set Repeat 24 | no repeat 25 | never 26 | Save Alarm 27 | Commute 28 | Choose Starting Point 29 | Destination 30 | Not Set 31 | Save Location 32 | More Options 33 | Departure Time 34 | 35 | Choose a location before saving. 36 | Set starting & ending points before saving. 37 | 38 | 39 | Alarm not saved. 40 | Alarm saved. 41 | Alarm deleted. 42 | Alarm set for %s from now. 43 | Alarm cancelled. 44 | Error setting alarm. 45 | 46 | 47 | Alarm going off! 48 | Stop 49 | Snooze 50 | 51 | OK 52 | Cancel 53 | 54 | Delete Alarm 55 | Settings 56 | 57 | 58 | 59 | 60 | General 61 | 62 | Home 63 | Toronto, ON 64 | Work 65 | CN Tower 66 | 67 | 68 | Alarm 69 | 70 | Ringtone 71 | Silent 72 | 73 | Vibrate 74 | 75 | Snooze Length 76 | 77 | 1 minute 78 | 2 minutes 79 | 5 minutes 80 | 10 minutes 81 | 82 | 83 | 60 84 | 120 85 | 300 86 | 600 87 | 88 | Map 89 | 90 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 14 | 15 | 24 | 25 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/xml/pref_general.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/xml/pref_headers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
9 | 10 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/xml/pref_notification.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/release/res/values/google_maps_api.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | YOUR_KEY_HERE 20 | 21 | -------------------------------------------------------------------------------- /app/src/test/java/com/manveerbasra/ontime/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.manveerbasra.ontime; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.2.1' 11 | 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /demo_images/ui_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manveerxyz/OnTime/962b67761bf38a205d5e9166dc445b4454ec14ac/demo_images/ui_demo.gif -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manveerxyz/OnTime/962b67761bf38a205d5e9166dc445b4454ec14ac/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Nov 09 14:41:12 EST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------