diff --git a/.travis.yml b/.travis.yml index 65ed4acca..ababc3604 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,12 +3,12 @@ sudo: false jdk: oraclejdk7 env: matrix: - - ANDROID_SDKS=android-22,sysimg-19 ANDROID_TARGET=android-19 ANDROID_ABI=armeabi-v7a + - ANDROID_SDKS=android-23,sysimg-19 ANDROID_TARGET=android-19 ANDROID_ABI=armeabi-v7a android: components: - - android-22 + - android-23 - platform-tools-23.0.1 - - build-tools-22.0.1 + - build-tools-23.0.2 - extra-android-m2repository - extra-google-m2repository licenses: diff --git a/build.gradle b/build.gradle index cbebdce03..a4c5d11b4 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,6 @@ task wrapper(type: Wrapper) { buildscript { repositories { - mavenCentral() jcenter() } @@ -16,7 +15,7 @@ buildscript { } repositories { - mavenCentral() + jcenter() } android { @@ -24,14 +23,14 @@ android { lintConfig file("lint.xml") } - compileSdkVersion 22 - buildToolsVersion "22.0.1" + compileSdkVersion 23 + buildToolsVersion "23.0.2" defaultConfig { versionCode 379 versionName "4.7.22" minSdkVersion 9 - targetSdkVersion 22 + targetSdkVersion 23 } signingConfigs { @@ -94,7 +93,7 @@ dependencies { compile group: 'com.jakewharton', name: 'butterknife', version: '7.0.1' - compile group: 'com.android.support', name: 'design', version: '22.2.1' + compile group: 'com.android.support', name: 'design', version: '23.1.1' compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.7' @@ -113,6 +112,7 @@ dependencies { googleplayCompile(group: 'com.google.api-client', name: 'google-api-client-android', version: '1.21.0') { exclude group: 'org.apache.httpcomponents', module: 'httpclient' } + compile group: 'com.google.guava', name: 'guava-jdk5', version: '13.0' compile group: 'com.github.tony19', name: 'logback-android-classic', version: '1.1.1-2' diff --git a/src/androidTest/java/com/todoroo/astrid/reminders/NotifyAtDeadlineTest.java b/src/androidTest/java/com/todoroo/astrid/reminders/NotifyAtDeadlineTest.java index f4c1463ce..e3db01d4d 100644 --- a/src/androidTest/java/com/todoroo/astrid/reminders/NotifyAtDeadlineTest.java +++ b/src/androidTest/java/com/todoroo/astrid/reminders/NotifyAtDeadlineTest.java @@ -34,7 +34,7 @@ public class NotifyAtDeadlineTest extends AndroidTestCase { @Override public void setUp() { - preferences = new Preferences(getContext(), null); + preferences = new Preferences(getContext(), null, null); reminderService = new ReminderService(getContext(), preferences, mock(AlarmManager.class)); freezeAt(new DateTime(2014, 1, 24, 17, 23, 37)); } diff --git a/src/androidTest/java/org/tasks/NotifierTests.java b/src/androidTest/java/org/tasks/NotifierTests.java index e8b1398ab..cb2c8a1c1 100644 --- a/src/androidTest/java/org/tasks/NotifierTests.java +++ b/src/androidTest/java/org/tasks/NotifierTests.java @@ -23,7 +23,7 @@ public class NotifierTests extends AndroidTestCase { @Override public void setUp() { - preferences = new Preferences(getContext(), null); + preferences = new Preferences(getContext(), null, null); preferences.clear(); preferences.setBoolean(R.string.p_rmd_enable_quiet, true); freezeAt(now); diff --git a/src/androidTestGoogleplay/java/com/todoroo/astrid/gtasks/GtasksMetadataServiceTest.java b/src/androidTestGoogleplay/java/com/todoroo/astrid/gtasks/GtasksMetadataServiceTest.java index 7696133eb..d48d34fe7 100644 --- a/src/androidTestGoogleplay/java/com/todoroo/astrid/gtasks/GtasksMetadataServiceTest.java +++ b/src/androidTestGoogleplay/java/com/todoroo/astrid/gtasks/GtasksMetadataServiceTest.java @@ -29,7 +29,7 @@ public class GtasksMetadataServiceTest extends DatabaseTestCase { private final GtasksTestPreferenceService service; public GtasksMetadataServiceTestModule(Context context) { - service = new GtasksTestPreferenceService(context, new Preferences(context, null)); + service = new GtasksTestPreferenceService(context, new Preferences(context, null, null)); } @Provides diff --git a/src/googleplay/java/com/todoroo/astrid/gtasks/GtasksPreferences.java b/src/googleplay/java/com/todoroo/astrid/gtasks/GtasksPreferences.java index 78fd65817..4a011c11b 100644 --- a/src/googleplay/java/com/todoroo/astrid/gtasks/GtasksPreferences.java +++ b/src/googleplay/java/com/todoroo/astrid/gtasks/GtasksPreferences.java @@ -6,6 +6,7 @@ package com.todoroo.astrid.gtasks; import android.content.Intent; +import android.content.pm.PackageManager; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.Preference; @@ -16,6 +17,7 @@ import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity; import org.tasks.R; import org.tasks.activities.ClearGtaskDataActivity; import org.tasks.injection.InjectingPreferenceActivity; +import org.tasks.preferences.PermissionRequestor; import org.tasks.scheduling.BackgroundScheduler; import javax.inject.Inject; @@ -27,6 +29,7 @@ public class GtasksPreferences extends InjectingPreferenceActivity { @Inject GtasksPreferenceService gtasksPreferenceService; @Inject BackgroundScheduler backgroundScheduler; + @Inject PermissionRequestor permissionRequestor; @Override public void onCreate(Bundle savedInstanceState) { @@ -39,12 +42,18 @@ public class GtasksPreferences extends InjectingPreferenceActivity { gtaskPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - if ((boolean) newValue && !gtasksPreferenceService.isLoggedIn()) { - startActivityForResult(new Intent(GtasksPreferences.this, GtasksLoginActivity.class), REQUEST_LOGIN); + if ((boolean) newValue) { + if (gtasksPreferenceService.isLoggedIn()) { + return true; + } + if (permissionRequestor.requestAccountPermissions()) { + requestLogin(); + } + return false; } else { gtasksPreferenceService.stopOngoing(); + return true; } - return true; } }); if (gtasksPreferenceService.getLastSyncDate() > 0) { @@ -61,6 +70,10 @@ public class GtasksPreferences extends InjectingPreferenceActivity { }); } + private void requestLogin() { + startActivityForResult(new Intent(GtasksPreferences.this, GtasksLoginActivity.class), REQUEST_LOGIN); + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_LOGIN) { @@ -74,6 +87,17 @@ public class GtasksPreferences extends InjectingPreferenceActivity { } } + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == PermissionRequestor.REQUEST_ACCOUNTS) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + requestLogin(); + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + @Override protected void onPause() { super.onPause(); diff --git a/src/googleplay/java/org/tasks/dialogs/LocationPickerDialog.java b/src/googleplay/java/org/tasks/dialogs/LocationPickerDialog.java index 21093baa8..5e76f2e5e 100644 --- a/src/googleplay/java/org/tasks/dialogs/LocationPickerDialog.java +++ b/src/googleplay/java/org/tasks/dialogs/LocationPickerDialog.java @@ -49,6 +49,7 @@ public class LocationPickerDialog extends InjectingDialogFragment implements Goo private OnLocationPickedHandler onLocationPickedHandler; private DialogInterface.OnCancelListener onCancelListener; + private boolean resolvingError; public void setOnLocationPickedHandler(OnLocationPickedHandler onLocationPickedHandler) { this.onLocationPickedHandler = onLocationPickedHandler; @@ -137,13 +138,18 @@ public class LocationPickerDialog extends InjectingDialogFragment implements Goo @Override public void onConnectionFailed(ConnectionResult connectionResult) { - if (connectionResult.hasResolution()) { + if (resolvingError) { + log.info("Ignoring {}, already resolving error", connectionResult); + } else if (connectionResult.hasResolution()) { try { + resolvingError = true; connectionResult.startResolutionForResult(fragmentActivity, RC_RESOLVE_GPS_ISSUE); } catch (IntentSender.SendIntentException e) { log.error(e.getMessage(), e); + googleApi.connect(this); } } else { + resolvingError = true; GooglePlayServicesUtil .getErrorDialog(connectionResult.getErrorCode(), fragmentActivity, RC_RESOLVE_GPS_ISSUE) .show(); diff --git a/src/googleplay/java/org/tasks/location/GeofenceApi.java b/src/googleplay/java/org/tasks/location/GeofenceApi.java index f89d5935c..8528e762f 100644 --- a/src/googleplay/java/org/tasks/location/GeofenceApi.java +++ b/src/googleplay/java/org/tasks/location/GeofenceApi.java @@ -16,6 +16,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tasks.R; import org.tasks.injection.ForApplication; +import org.tasks.preferences.PermissionChecker; import org.tasks.preferences.Preferences; import java.util.List; @@ -35,15 +36,17 @@ public class GeofenceApi { private Context context; private Preferences preferences; + private PermissionChecker permissionChecker; @Inject - public GeofenceApi(@ForApplication Context context, Preferences preferences) { + public GeofenceApi(@ForApplication Context context, Preferences preferences, PermissionChecker permissionChecker) { this.context = context; this.preferences = preferences; + this.permissionChecker = permissionChecker; } public void register(final List geofences) { - if (geofences.isEmpty() || !preferences.geofencesEnabled()) { + if (geofences.isEmpty() || !preferences.geofencesEnabled() || !permissionChecker.canAccessLocation()) { return; } @@ -75,7 +78,7 @@ public class GeofenceApi { } public void cancel(final List geofences) { - if (geofences.isEmpty()) { + if (geofences.isEmpty() || !permissionChecker.canAccessLocation()) { return; } diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 05c230403..a24223161 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -379,17 +379,6 @@ - - - - - - - - - - - @@ -515,6 +504,9 @@ + calendars = calendarHelper.getCalendars(); - for (AndroidCalendar calendar : calendars) { - if (calendar.getId().equals(calendarId)) { - defaultCalendarPref.setSummary(calendar.getName()); - return; + if (permissionChecker.canAccessCalendars()) { + List calendars = calendarHelper.getCalendars(); + for (AndroidCalendar calendar : calendars) { + if (calendar.getId().equals(calendarId)) { + defaultCalendarPref.setSummary(calendar.getName()); + return; + } } } defaultCalendarPref.setSummary(getString(R.string.none)); } + private void startCalendarSelectionActivity() { + startActivityForResult(new Intent(DefaultsPreferences.this, CalendarSelectionActivity.class), REQUEST_CALENDAR_SELECTION); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == PermissionRequestor.REQUEST_CALENDAR) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + startCalendarSelectionActivity(); + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CALENDAR_SELECTION && resultCode == RESULT_OK) { diff --git a/src/main/java/com/todoroo/astrid/files/AACRecordingActivity.java b/src/main/java/com/todoroo/astrid/files/AACRecordingActivity.java index 77f994647..a53e1956b 100644 --- a/src/main/java/com/todoroo/astrid/files/AACRecordingActivity.java +++ b/src/main/java/com/todoroo/astrid/files/AACRecordingActivity.java @@ -6,9 +6,8 @@ package com.todoroo.astrid.files; import android.content.Intent; +import android.content.pm.PackageManager; import android.os.Bundle; -import android.view.View; -import android.view.View.OnClickListener; import android.widget.Chronometer; import com.todoroo.astrid.voice.AACRecorder; @@ -16,67 +15,66 @@ import com.todoroo.astrid.voice.AACRecorder.AACRecorderCallbacks; import org.tasks.R; import org.tasks.injection.InjectingAppCompatActivity; +import org.tasks.preferences.PermissionRequestor; import org.tasks.preferences.Preferences; import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; +import butterknife.Bind; +import butterknife.ButterKnife; +import butterknife.OnClick; + public class AACRecordingActivity extends InjectingAppCompatActivity implements AACRecorderCallbacks { public static final String RESULT_OUTFILE = "outfile"; //$NON-NLS-1$ public static final String RESULT_FILENAME = "filename"; //$NON-NLS-1$ + private final AtomicReference nameRef = new AtomicReference<>(); private AACRecorder recorder; - private Chronometer timer; - private AtomicReference nameRef; private String tempFile; @Inject Preferences preferences; + @Inject PermissionRequestor permissionRequestor; + + @Bind(R.id.timer) Chronometer timer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.aac_record_activity); + if (permissionRequestor.requestMic()) { + startRecording(); + } + } - setupUi(); + private void startRecording() { + setContentView(R.layout.aac_record_activity); + ButterKnife.bind(this); - nameRef = new AtomicReference<>(); tempFile = preferences.getNewAudioAttachmentPath(nameRef); - recorder = new AACRecorder(); recorder.setListener(this); recorder.startRecording(tempFile); timer.start(); } - private void setupUi() { - View stopRecording = findViewById(R.id.stop_recording); - - stopRecording.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - stopRecording(); - } - }); - - View dismiss = findViewById(R.id.dismiss); - dismiss.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - recorder.setListener(null); - recorder.stopRecording(); - finish(); - } - }); - - timer = (Chronometer) findViewById(R.id.timer); + @OnClick(R.id.stop_recording) + void stopRecording() { + if (recorder != null) { + recorder.stopRecording(); + timer.stop(); + } } - private void stopRecording() { - recorder.stopRecording(); - timer.stop(); + @OnClick(R.id.dismiss) + void dismiss() { + if (recorder != null) { + recorder.setListener(null); + recorder.stopRecording(); + } + finish(); } @Override @@ -94,4 +92,17 @@ public class AACRecordingActivity extends InjectingAppCompatActivity implements setResult(RESULT_OK, result); finish(); } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == PermissionRequestor.REQUEST_MIC) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + startRecording(); + } else { + finish(); + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } } diff --git a/src/main/java/com/todoroo/astrid/files/FileExplore.java b/src/main/java/com/todoroo/astrid/files/FileExplore.java index 8b7a5e4bc..ecce51e20 100644 --- a/src/main/java/com/todoroo/astrid/files/FileExplore.java +++ b/src/main/java/com/todoroo/astrid/files/FileExplore.java @@ -9,6 +9,7 @@ import android.app.Dialog; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Environment; @@ -27,6 +28,7 @@ import org.tasks.R; import org.tasks.dialogs.DialogBuilder; import org.tasks.injection.InjectingAppCompatActivity; import org.tasks.preferences.ActivityPreferences; +import org.tasks.preferences.PermissionRequestor; import java.io.File; import java.io.FilenameFilter; @@ -54,6 +56,7 @@ public class FileExplore extends InjectingAppCompatActivity { @Inject DialogBuilder dialogBuilder; @Inject ActivityPreferences activityPreferences; + @Inject PermissionRequestor permissionRequestor; private Item[] fileList; private File path; @@ -66,13 +69,19 @@ public class FileExplore extends InjectingAppCompatActivity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + if (permissionRequestor.requestFileWritePermission()) { + showDialog(); + } + } + + private void showDialog() { activityPreferences.applyDialogTheme(); if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { - path = new File(Environment.getExternalStorageDirectory().toString()); - } else { - path = Environment.getRootDirectory(); - } + path = new File(Environment.getExternalStorageDirectory().toString()); + } else { + path = Environment.getRootDirectory(); + } loadFileList(); @@ -83,6 +92,19 @@ public class FileExplore extends InjectingAppCompatActivity { log.debug(path.getAbsolutePath()); } + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == PermissionRequestor.REQUEST_FILE_WRITE) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + showDialog(); + } else { + finish(); + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + private void loadFileList() { try { path.mkdirs(); diff --git a/src/main/java/com/todoroo/astrid/files/FilesControlSet.java b/src/main/java/com/todoroo/astrid/files/FilesControlSet.java index 8f1913c78..7c6a75711 100644 --- a/src/main/java/com/todoroo/astrid/files/FilesControlSet.java +++ b/src/main/java/com/todoroo/astrid/files/FilesControlSet.java @@ -7,6 +7,7 @@ package com.todoroo.astrid.files; import android.app.Activity; import android.content.ActivityNotFoundException; +import android.content.ClipData; import android.content.DialogInterface; import android.content.Intent; import android.media.MediaPlayer; @@ -40,6 +41,8 @@ import org.tasks.preferences.ActivityPreferences; import java.io.File; import java.util.ArrayList; +import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; + public class FilesControlSet extends TaskEditControlSetBase { private static final Logger log = LoggerFactory.getLogger(FilesControlSet.class); @@ -201,9 +204,15 @@ public class FilesControlSet extends TaskEditControlSetBase { }); } else if (fileType.startsWith(TaskAttachment.FILE_TYPE_IMAGE)) { try { - activity.startActivity(new Intent(Intent.ACTION_VIEW) {{ - setDataAndType(Uri.fromFile(new File(filePath)), fileType); - }}); + Intent intent = new Intent(Intent.ACTION_VIEW); + Uri uri = Uri.fromFile(new File(filePath)); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + intent.setDataAndType(uri, TaskAttachment.FILE_TYPE_IMAGE + "*"); + if (atLeastLollipop()) { + intent.setClipData(ClipData.newRawUri(null, uri)); + } + activity.startActivity(intent); } catch(ActivityNotFoundException e) { log.error(e.getMessage(), e); Toast.makeText(activity, R.string.no_application_found, Toast.LENGTH_SHORT).show(); @@ -232,6 +241,7 @@ public class FilesControlSet extends TaskEditControlSetBase { try { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(Uri.fromFile(new File(file)), type); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); activity.startActivity(intent); } catch (ActivityNotFoundException e) { log.error(e.getMessage(), e); diff --git a/src/main/java/com/todoroo/astrid/gcal/CalendarAlarmReceiver.java b/src/main/java/com/todoroo/astrid/gcal/CalendarAlarmReceiver.java index 69739857c..5cd39961f 100644 --- a/src/main/java/com/todoroo/astrid/gcal/CalendarAlarmReceiver.java +++ b/src/main/java/com/todoroo/astrid/gcal/CalendarAlarmReceiver.java @@ -16,7 +16,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tasks.R; import org.tasks.injection.InjectingBroadcastReceiver; +import org.tasks.preferences.PermissionChecker; import org.tasks.preferences.Preferences; +import org.tasks.scheduling.CalendarNotificationIntentService; import java.util.ArrayList; import java.util.HashSet; @@ -43,6 +45,7 @@ public class CalendarAlarmReceiver extends InjectingBroadcastReceiver { }; @Inject Preferences preferences; + @Inject PermissionChecker permissionChecker; @Override public void onReceive(Context context, Intent intent) { @@ -51,6 +54,11 @@ public class CalendarAlarmReceiver extends InjectingBroadcastReceiver { if (!preferences.getBoolean(R.string.p_calendar_reminders, true)) { return; } + + if (!permissionChecker.canAccessCalendars()) { + return; + } + try { Uri data = intent.getData(); if (data == null) { @@ -65,7 +73,7 @@ public class CalendarAlarmReceiver extends InjectingBroadcastReceiver { return; } long eventId = Long.parseLong(uriString.substring(pathIndex)); - boolean fromPostpone = CalendarAlarmScheduler.URI_PREFIX_POSTPONE.equals(data.getScheme()); + boolean fromPostpone = CalendarNotificationIntentService.URI_PREFIX_POSTPONE.equals(data.getScheme()); if (eventId > 0) { showCalReminder(context, eventId, fromPostpone); } diff --git a/src/main/java/com/todoroo/astrid/gcal/CalendarReminderActivity.java b/src/main/java/com/todoroo/astrid/gcal/CalendarReminderActivity.java index 373b3b3d5..f36cdf016 100644 --- a/src/main/java/com/todoroo/astrid/gcal/CalendarReminderActivity.java +++ b/src/main/java/com/todoroo/astrid/gcal/CalendarReminderActivity.java @@ -26,6 +26,7 @@ import org.tasks.preferences.ActivityPreferences; import org.tasks.preferences.BasicPreferences; import org.tasks.preferences.ResourceResolver; import org.tasks.scheduling.AlarmManager; +import org.tasks.scheduling.CalendarNotificationIntentService; import javax.inject.Inject; @@ -225,7 +226,7 @@ public class CalendarReminderActivity extends InjectingAppCompatActivity { private void postpone() { Intent eventAlarm = new Intent(this, CalendarAlarmReceiver.class); eventAlarm.setAction(CalendarAlarmReceiver.BROADCAST_CALENDAR_REMINDER); - eventAlarm.setData(Uri.parse(CalendarAlarmScheduler.URI_PREFIX_POSTPONE + "://" + eventId)); + eventAlarm.setData(Uri.parse(CalendarNotificationIntentService.URI_PREFIX_POSTPONE + "://" + eventId)); PendingIntent pendingIntent = PendingIntent.getBroadcast(this, CalendarAlarmReceiver.REQUEST_CODE_CAL_REMINDER, eventAlarm, 0); diff --git a/src/main/java/com/todoroo/astrid/gcal/CalendarStartupReceiver.java b/src/main/java/com/todoroo/astrid/gcal/CalendarStartupReceiver.java deleted file mode 100644 index a0de0de3a..000000000 --- a/src/main/java/com/todoroo/astrid/gcal/CalendarStartupReceiver.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.todoroo.astrid.gcal; - -import android.content.Context; -import android.content.Intent; - -import org.tasks.BuildConfig; -import org.tasks.injection.InjectingBroadcastReceiver; - -import javax.inject.Inject; - -public class CalendarStartupReceiver extends InjectingBroadcastReceiver { - - public static final String BROADCAST_RESCHEDULE_CAL_ALARMS = BuildConfig.APPLICATION_ID + ".SCHEDULE_CAL_REMINDERS"; //$NON-NLS-1$ - - @Inject CalendarAlarmScheduler calendarAlarmScheduler; - - @Override - public void onReceive(Context context, Intent intent) { - super.onReceive(context, intent); - - calendarAlarmScheduler.scheduleCalendarAlarms(context, false); - } -} diff --git a/src/main/java/com/todoroo/astrid/gcal/GCalControlSet.java b/src/main/java/com/todoroo/astrid/gcal/GCalControlSet.java index 0118a4b3c..92ec0b5bb 100644 --- a/src/main/java/com/todoroo/astrid/gcal/GCalControlSet.java +++ b/src/main/java/com/todoroo/astrid/gcal/GCalControlSet.java @@ -26,6 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tasks.R; import org.tasks.activities.CalendarSelectionDialog; +import org.tasks.preferences.PermissionRequestor; import org.tasks.preferences.Preferences; import org.tasks.reminders.SnoozeDialog; @@ -45,6 +46,7 @@ public class GCalControlSet extends TaskEditControlSetBase implements CalendarSe private final GCalHelper gcal; private Preferences preferences; private final TaskEditFragment taskEditFragment; + private PermissionRequestor permissionRequestor; private Uri calendarUri = null; @@ -54,11 +56,13 @@ public class GCalControlSet extends TaskEditControlSetBase implements CalendarSe private String calendarId; private String calendarName; - public GCalControlSet(GCalHelper gcal, Preferences preferences, TaskEditFragment taskEditFragment) { + public GCalControlSet(GCalHelper gcal, Preferences preferences, + TaskEditFragment taskEditFragment, PermissionRequestor permissionRequestor) { super(taskEditFragment.getActivity(), R.layout.control_set_gcal_display); this.gcal = gcal; this.preferences = preferences; this.taskEditFragment = taskEditFragment; + this.permissionRequestor = permissionRequestor; } @Override @@ -72,13 +76,12 @@ public class GCalControlSet extends TaskEditControlSetBase implements CalendarSe if (hasEvent) { viewCalendarEvent(); } else { - FragmentManager fragmentManager = taskEditFragment.getFragmentManager(); - CalendarSelectionDialog fragmentByTag = (CalendarSelectionDialog) fragmentManager.findFragmentByTag(FRAG_TAG_CALENDAR_SELECTION); - if (fragmentByTag == null) { - fragmentByTag = new CalendarSelectionDialog(); - fragmentByTag.show(fragmentManager, FRAG_TAG_CALENDAR_SELECTION); + // TODO: show calendar selection if permission has just been granted + // can't do this now because the app saves state when TEA is paused, + // which triggers calendar creation if there is a default add to calendar. + if (permissionRequestor.requestCalendarPermissions()) { + showCalendarSelectionDialog(); } - fragmentByTag.setCalendarSelectionHandler(GCalControlSet.this); } } }); @@ -91,6 +94,16 @@ public class GCalControlSet extends TaskEditControlSetBase implements CalendarSe }); } + public void showCalendarSelectionDialog() { + FragmentManager fragmentManager = taskEditFragment.getFragmentManager(); + CalendarSelectionDialog fragmentByTag = (CalendarSelectionDialog) fragmentManager.findFragmentByTag(FRAG_TAG_CALENDAR_SELECTION); + if (fragmentByTag == null) { + fragmentByTag = new CalendarSelectionDialog(); + fragmentByTag.show(fragmentManager, FRAG_TAG_CALENDAR_SELECTION); + } + fragmentByTag.setCalendarSelectionHandler(GCalControlSet.this); + } + @Override protected void readFromTaskOnInitialize() { String uri = gcal.getTaskEventUri(model); diff --git a/src/main/java/com/todoroo/astrid/reminders/ReminderPreferences.java b/src/main/java/com/todoroo/astrid/reminders/ReminderPreferences.java index 94bbcf468..c25aecd9d 100644 --- a/src/main/java/com/todoroo/astrid/reminders/ReminderPreferences.java +++ b/src/main/java/com/todoroo/astrid/reminders/ReminderPreferences.java @@ -6,14 +6,18 @@ package com.todoroo.astrid.reminders; import android.content.Intent; +import android.content.pm.PackageManager; import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; import android.os.Bundle; +import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; +import org.tasks.preferences.PermissionChecker; +import org.tasks.preferences.PermissionRequestor; import org.tasks.time.DateTime; import org.tasks.R; import org.tasks.activities.TimePickerActivity; @@ -37,6 +41,10 @@ public class ReminderPreferences extends InjectingPreferenceActivity { private Bundle result; @Inject DeviceInfo deviceInfo; + @Inject PermissionRequestor permissionRequestor; + @Inject PermissionChecker permissionChecker; + + private CheckBoxPreference fieldMissedCalls; @Override public void onCreate(Bundle savedInstanceState) { @@ -60,12 +68,35 @@ public class ReminderPreferences extends InjectingPreferenceActivity { preferenceScreen.removePreference(findPreference(getString(R.string.geolocation_reminders))); } + fieldMissedCalls = (CheckBoxPreference) findPreference(getString(R.string.p_field_missed_calls)); + fieldMissedCalls.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + return newValue != null && (!(boolean) newValue || permissionRequestor.requestMissedCallPermissions()); + } + }); + fieldMissedCalls.setChecked(fieldMissedCalls.isChecked() && permissionChecker.canAccessMissedCallPermissions()); + initializeRingtonePreference(); initializeTimePreference(getDefaultRemindTimePreference(), REQUEST_DEFAULT_REMIND); initializeTimePreference(getQuietStartPreference(), REQUEST_QUIET_START); initializeTimePreference(getQuietEndPreference(), REQUEST_QUIET_END); } + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == PermissionRequestor.REQUEST_CONTACTS) { + for (int grantResult : grantResults) { + if (grantResult != PackageManager.PERMISSION_GRANTED) { + return; + } + } + fieldMissedCalls.setChecked(true); + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); diff --git a/src/main/java/com/todoroo/astrid/service/StartupService.java b/src/main/java/com/todoroo/astrid/service/StartupService.java index aeb789537..05d74a063 100644 --- a/src/main/java/com/todoroo/astrid/service/StartupService.java +++ b/src/main/java/com/todoroo/astrid/service/StartupService.java @@ -8,8 +8,6 @@ package com.todoroo.astrid.service; import android.app.Activity; import android.content.ContentValues; import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; import android.database.sqlite.SQLiteException; import android.preference.PreferenceManager; @@ -24,17 +22,16 @@ import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; import com.todoroo.astrid.dao.TagDataDao; import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.data.TagData; -import com.todoroo.astrid.gcal.CalendarAlarmScheduler; import com.todoroo.astrid.gtasks.GtasksPreferenceService; import com.todoroo.astrid.gtasks.sync.GtasksSyncService; import com.todoroo.astrid.provider.Astrid2TaskProvider; import com.todoroo.astrid.provider.Astrid3ContentProvider; import com.todoroo.astrid.tags.TaskToTagMetadata; -import com.todoroo.astrid.utility.Constants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tasks.Broadcaster; +import org.tasks.BuildConfig; import org.tasks.R; import org.tasks.dialogs.DialogBuilder; import org.tasks.preferences.Preferences; @@ -65,7 +62,6 @@ public class StartupService { private final MetadataDao metadataDao; private final Preferences preferences; private final TasksXmlImporter xmlImporter; - private final CalendarAlarmScheduler calendarAlarmScheduler; private final TaskDeleter taskDeleter; private Broadcaster broadcaster; private DialogBuilder dialogBuilder; @@ -75,8 +71,7 @@ public class StartupService { GtasksPreferenceService gtasksPreferenceService, GtasksSyncService gtasksSyncService, MetadataDao metadataDao, Preferences preferences, TasksXmlImporter xmlImporter, - CalendarAlarmScheduler calendarAlarmScheduler, TaskDeleter taskDeleter, - Broadcaster broadcaster, DialogBuilder dialogBuilder) { + TaskDeleter taskDeleter, Broadcaster broadcaster, DialogBuilder dialogBuilder) { this.upgradeService = upgradeService; this.tagDataDao = tagDataDao; this.database = database; @@ -85,7 +80,6 @@ public class StartupService { this.metadataDao = metadataDao; this.preferences = preferences; this.xmlImporter = xmlImporter; - this.calendarAlarmScheduler = calendarAlarmScheduler; this.taskDeleter = taskDeleter; this.broadcaster = broadcaster; this.dialogBuilder = dialogBuilder; @@ -128,19 +122,10 @@ public class StartupService { log.error(e.getMessage(), e); } - int version = 0; - String versionName = "0"; //$NON-NLS-1$ - try { - PackageManager pm = activity.getPackageManager(); - PackageInfo pi = pm.getPackageInfo(Constants.PACKAGE, PackageManager.GET_META_DATA); - version = pi.versionCode; - versionName = pi.versionName; - } catch (Exception e) { - log.error(e.getMessage(), e); - } + int version = BuildConfig.VERSION_CODE; + String versionName = BuildConfig.VERSION_NAME; - log.info("astrid", "Astrid Startup. " + latestSetVersion + //$NON-NLS-1$ //$NON-NLS-2$ - " => " + version); //$NON-NLS-1$ + log.info("Astrid Startup. {} => {}", latestSetVersion, version); databaseRestoreIfEmpty(activity); @@ -177,8 +162,6 @@ public class StartupService { broadcaster.firstLaunch(); } - calendarAlarmScheduler.scheduleCalendarAlarms(activity, false); // This needs to be after set preference defaults for the purposes of ab testing - hasStartedUp = true; } diff --git a/src/main/java/com/todoroo/astrid/service/UpgradeService.java b/src/main/java/com/todoroo/astrid/service/UpgradeService.java index 0535b5dbb..030d0f999 100644 --- a/src/main/java/com/todoroo/astrid/service/UpgradeService.java +++ b/src/main/java/com/todoroo/astrid/service/UpgradeService.java @@ -9,14 +9,20 @@ import android.app.Activity; import android.app.ProgressDialog; import android.content.Intent; import android.os.Bundle; +import android.os.Environment; import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.astrid.activity.AstridActivity; import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.data.TaskAttachment; +import org.tasks.BuildConfig; import org.tasks.R; import org.tasks.dialogs.DialogBuilder; import org.tasks.injection.InjectingAppCompatActivity; +import org.tasks.preferences.Preferences; + +import java.io.File; import javax.inject.Inject; import javax.inject.Singleton; @@ -24,7 +30,8 @@ import javax.inject.Singleton; @Singleton public final class UpgradeService { - public static final int V4_6_5 = 306; + public static final int CURRENT = BuildConfig.VERSION_CODE; + public static final int V4_8_0 = 376; public static final int V3_0_0 = 136; @Inject @@ -37,9 +44,7 @@ public final class UpgradeService { * show users a change log. */ public void performUpgrade(final Activity context, final int from) { - int maxWithUpgrade = V4_6_5; - - if(from < maxWithUpgrade) { + if(from < CURRENT) { Intent upgrade = new Intent(context, UpgradeActivity.class); upgrade.putExtra(UpgradeActivity.TOKEN_FROM_VERSION, from); context.startActivityForResult(upgrade, 0); @@ -54,6 +59,7 @@ public final class UpgradeService { private boolean finished = false; @Inject DialogBuilder dialogBuilder; + @Inject Preferences preferences; @Override protected void onCreate(Bundle savedInstanceState) { @@ -66,7 +72,9 @@ public final class UpgradeService { public void run() { //noinspection EmptyTryBlock try { - + if (from < V4_8_0) { + performMarshmallowMigration(); + } } finally { finished = true; DialogUtilities.dismissDialog(UpgradeActivity.this, dialog); @@ -89,5 +97,30 @@ public final class UpgradeService { super.onBackPressed(); } } + + private void performMarshmallowMigration() { + // preserve pre-marshmallow default attachment and backup locations + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + if (!preferences.isStringValueSet(R.string.p_backup_dir)) { + String directory = String.format("%s/astrid", + Environment.getExternalStorageDirectory()); + File file = new File(directory); + if (file.exists() && file.isDirectory()) { + preferences.setString(R.string.p_backup_dir, directory); + } + } + + if (!preferences.isStringValueSet(R.string.p_attachment_dir)) { + String directory = String.format("%s/Android/data/%s/files/%s", + Environment.getExternalStorageDirectory(), + BuildConfig.APPLICATION_ID, + TaskAttachment.FILES_DIRECTORY_DEFAULT); + File file = new File(directory); + if (file.exists() && file.isDirectory()) { + preferences.setString(R.string.p_attachment_dir, directory); + } + } + } + } } } diff --git a/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.java b/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.java index e390ffcb1..8428ebe90 100644 --- a/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.java +++ b/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.java @@ -33,6 +33,7 @@ import org.tasks.activities.DateAndTimePickerActivity; import org.tasks.activities.LocationPickerActivity; import org.tasks.location.Geofence; import org.tasks.location.GeofenceService; +import org.tasks.preferences.PermissionRequestor; import org.tasks.preferences.Preferences; import org.tasks.time.DateTime; @@ -68,17 +69,20 @@ public class ReminderControlSet extends TaskEditControlSetBase implements Adapte private GeofenceService geofenceService; private TaskEditFragment taskEditFragment; private Preferences preferences; + private PermissionRequestor permissionRequestor; private List spinnerOptions = new ArrayList<>(); private ArrayAdapter remindAdapter; public ReminderControlSet(AlarmService alarmService, GeofenceService geofenceService, - TaskEditFragment taskEditFragment, Preferences preferences) { + TaskEditFragment taskEditFragment, Preferences preferences, + PermissionRequestor permissionRequestor) { super(taskEditFragment.getActivity(), R.layout.control_set_reminders); this.alarmService = alarmService; this.geofenceService = geofenceService; this.taskEditFragment = taskEditFragment; this.preferences = preferences; + this.permissionRequestor = permissionRequestor; } public int getValue() { @@ -240,7 +244,7 @@ public class ReminderControlSet extends TaskEditControlSetBase implements Adapte @Override public void onItemSelected(AdapterView parent, View view, - int position, long id) { + int position, long id) { modeDisplay.setText(modeAdapter.getItem(position)); } @@ -379,13 +383,19 @@ public class ReminderControlSet extends TaskEditControlSetBase implements Adapte } else if (selected.equals(taskEditFragment.getString(R.string.pick_a_date_and_time))) { addNewAlarm(); } else if (selected.equals(taskEditFragment.getString(R.string.pick_a_location))) { - taskEditFragment.startActivityForResult(new Intent(taskEditFragment.getActivity(), LocationPickerActivity.class), REQUEST_LOCATION_REMINDER); + if (permissionRequestor.requestFineLocation()) { + pickLocation(); + } } if (position != 0) { updateSpinner(); } } + public void pickLocation() { + taskEditFragment.startActivityForResult(new Intent(taskEditFragment.getActivity(), LocationPickerActivity.class), REQUEST_LOCATION_REMINDER); + } + @Override public void onNothingSelected(AdapterView parent) { } diff --git a/src/main/java/org/tasks/activities/AddAttachmentActivity.java b/src/main/java/org/tasks/activities/AddAttachmentActivity.java index ff2d961ea..a18ca31e3 100644 --- a/src/main/java/org/tasks/activities/AddAttachmentActivity.java +++ b/src/main/java/org/tasks/activities/AddAttachmentActivity.java @@ -1,5 +1,6 @@ package org.tasks.activities; +import android.content.ClipData; import android.content.ContentResolver; import android.content.DialogInterface; import android.content.Intent; @@ -7,12 +8,10 @@ import android.net.Uri; import android.os.Bundle; import android.provider.MediaStore; import android.support.v4.app.FragmentManager; -import android.text.TextUtils; import android.webkit.MimeTypeMap; import android.widget.Toast; import com.todoroo.andlib.utility.AndroidUtilities; -import com.todoroo.astrid.data.TaskAttachment; import com.todoroo.astrid.files.FileExplore; import org.slf4j.Logger; @@ -30,6 +29,8 @@ import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; +import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; + public class AddAttachmentActivity extends InjectingAppCompatActivity implements DialogInterface.OnCancelListener, AddAttachmentDialog.AddAttachmentCallback { private static final Logger log = LoggerFactory.getLogger(AddAttachmentActivity.class); @@ -69,11 +70,15 @@ public class AddAttachmentActivity extends InjectingAppCompatActivity implements if (lastTempFile == null) { Toast.makeText(this, R.string.external_storage_unavailable, Toast.LENGTH_LONG).show(); } else { - startActivityForResult(new Intent(MediaStore.ACTION_IMAGE_CAPTURE) {{ - addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(lastTempFile)); - }}, REQUEST_CAMERA); + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + Uri uri = Uri.fromFile(lastTempFile); + intent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); + if (atLeastLollipop()) { + intent.setClipData(ClipData.newRawUri(null, uri)); + } + startActivityForResult(intent, REQUEST_CAMERA); } } diff --git a/src/main/java/org/tasks/injection/BroadcastModule.java b/src/main/java/org/tasks/injection/BroadcastModule.java index 46dc9e194..b65278ca8 100644 --- a/src/main/java/org/tasks/injection/BroadcastModule.java +++ b/src/main/java/org/tasks/injection/BroadcastModule.java @@ -3,7 +3,6 @@ package org.tasks.injection; import com.todoroo.astrid.alarms.AlarmTaskRepeatListener; import com.todoroo.astrid.calls.PhoneStateChangedReceiver; import com.todoroo.astrid.gcal.CalendarAlarmReceiver; -import com.todoroo.astrid.gcal.CalendarStartupReceiver; import com.todoroo.astrid.gcal.GCalTaskCompleteListener; import com.todoroo.astrid.repeats.RepeatTaskCompleteListener; import com.todoroo.astrid.timers.TimerTaskCompleteListener; @@ -32,7 +31,6 @@ import dagger.Module; AlarmTaskRepeatListener.class, PhoneStateChangedReceiver.class, CalendarAlarmReceiver.class, - CalendarStartupReceiver.class, BootCompletedReceiver.class, FirstLaunchReceiver.class, MyPackageReplacedReceiver.class, diff --git a/src/main/java/org/tasks/injection/IntentServiceModule.java b/src/main/java/org/tasks/injection/IntentServiceModule.java index 8c4ca06ec..9d9cd5cdf 100644 --- a/src/main/java/org/tasks/injection/IntentServiceModule.java +++ b/src/main/java/org/tasks/injection/IntentServiceModule.java @@ -13,7 +13,8 @@ import dagger.Module; MidnightRefreshService.class, RefreshSchedulerIntentService.class, ReminderSchedulerIntentService.class, - GeofenceTransitionsIntentService.class + GeofenceTransitionsIntentService.class, + CalendarNotificationIntentService.class }) public class IntentServiceModule { } diff --git a/src/main/java/org/tasks/preferences/ActivityPreferences.java b/src/main/java/org/tasks/preferences/ActivityPreferences.java index 3b55fc2c8..0bdc2c203 100644 --- a/src/main/java/org/tasks/preferences/ActivityPreferences.java +++ b/src/main/java/org/tasks/preferences/ActivityPreferences.java @@ -24,8 +24,8 @@ public class ActivityPreferences extends Preferences { private final Activity activity; @Inject - public ActivityPreferences(Activity activity, DeviceInfo deviceInfo) { - super(activity, deviceInfo); + public ActivityPreferences(Activity activity, DeviceInfo deviceInfo, PermissionChecker permissionChecker) { + super(activity, deviceInfo, permissionChecker); this.activity = activity; } diff --git a/src/main/java/org/tasks/preferences/HelpAndFeedbackActivity.java b/src/main/java/org/tasks/preferences/HelpAndFeedbackActivity.java index 99467d1e3..1c9f15cdc 100644 --- a/src/main/java/org/tasks/preferences/HelpAndFeedbackActivity.java +++ b/src/main/java/org/tasks/preferences/HelpAndFeedbackActivity.java @@ -1,8 +1,11 @@ package org.tasks.preferences; import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.Preference; import org.tasks.R; import org.tasks.injection.InjectingPreferenceActivity; @@ -12,6 +15,11 @@ import javax.inject.Inject; public class HelpAndFeedbackActivity extends InjectingPreferenceActivity { @Inject DeviceInfo deviceInfo; + @Inject Preferences preferences; + @Inject PermissionChecker permissionChecker; + @Inject PermissionRequestor permissionRequestor; + + private CheckBoxPreference debugLogging; @Override public void onCreate(Bundle savedInstanceState) { @@ -27,6 +35,38 @@ public class HelpAndFeedbackActivity extends InjectingPreferenceActivity { if (!deviceInfo.isPlayStoreAvailable()) { remove(R.string.rate_tasks); } + + debugLogging = (CheckBoxPreference) findPreference(getString(R.string.p_debug_logging)); + debugLogging.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (newValue != null && (boolean) newValue) { + if (permissionRequestor.requestFileWritePermission()) { + enableDebugLogging(true); + } + } else { + enableDebugLogging(false); + } + return true; + } + }); + enableDebugLogging( + preferences.getBoolean(R.string.p_debug_logging, false) && + permissionChecker.canWriteToExternalStorage()); + } + + private void enableDebugLogging(boolean enabled) { + debugLogging.setChecked(enabled); + preferences.setupLogger(enabled); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == PermissionRequestor.REQUEST_FILE_WRITE) { + enableDebugLogging(grantResults[0] == PackageManager.PERMISSION_GRANTED); + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } } private void remove(int resId) { diff --git a/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java b/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java index 7eff0a91a..5853f05f2 100644 --- a/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java +++ b/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java @@ -1,18 +1,20 @@ package org.tasks.preferences; import android.content.Intent; +import android.content.pm.PackageManager; import android.os.Bundle; +import android.preference.CheckBoxPreference; import android.preference.Preference; import android.speech.tts.TextToSpeech; import com.todoroo.astrid.files.FileExplore; -import com.todoroo.astrid.gcal.CalendarAlarmScheduler; import com.todoroo.astrid.voice.VoiceOutputAssistant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tasks.R; import org.tasks.injection.InjectingPreferenceActivity; +import org.tasks.scheduling.BackgroundScheduler; import java.io.File; @@ -25,8 +27,12 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity { private static final int REQUEST_CODE_TTS_CHECK = 2534; @Inject Preferences preferences; - @Inject CalendarAlarmScheduler calendarAlarmScheduler; @Inject VoiceOutputAssistant voiceOutputAssistant; + @Inject PermissionRequestor permissionRequestor; + @Inject PermissionChecker permissionChecker; + @Inject BackgroundScheduler backgroundScheduler; + + private CheckBoxPreference calendarReminderPreference; @Override public void onCreate(Bundle savedInstanceState) { @@ -34,13 +40,7 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity { addPreferencesFromResource(R.xml.preferences_misc); - findPreference(getString(R.string.p_debug_logging)).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - preferences.setupLogger((boolean) newValue); - return true; - } - }); + calendarReminderPreference = (CheckBoxPreference) findPreference(getString(R.string.p_calendar_reminders)); initializeAttachmentDirectoryPreference(); initializeCalendarReminderPreference(); @@ -103,16 +103,24 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity { } private void initializeCalendarReminderPreference() { - Preference calendarReminderPreference = findPreference(getString(R.string.p_calendar_reminders)); + CheckBoxPreference calendarReminderPreference = (CheckBoxPreference) findPreference(getString(R.string.p_calendar_reminders)); calendarReminderPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - if (newValue != null && ((Boolean) newValue)) { - calendarAlarmScheduler.scheduleCalendarAlarms(MiscellaneousPreferences.this, true); + if (newValue == null) { + return false; } - return true; + if (!(Boolean) newValue) { + return true; + } + if (permissionRequestor.requestCalendarPermissions()) { + backgroundScheduler.scheduleCalendarNotifications(); + return true; + } + return false; } }); + calendarReminderPreference.setChecked(calendarReminderPreference.isChecked() && permissionChecker.canAccessCalendars()); } private void initializeVoiceReminderPreference() { @@ -137,4 +145,15 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity { } }); } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == PermissionRequestor.REQUEST_CALENDAR) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + calendarReminderPreference.setChecked(true); + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } } diff --git a/src/main/java/org/tasks/preferences/PermissionChecker.java b/src/main/java/org/tasks/preferences/PermissionChecker.java new file mode 100644 index 000000000..b833de184 --- /dev/null +++ b/src/main/java/org/tasks/preferences/PermissionChecker.java @@ -0,0 +1,67 @@ +package org.tasks.preferences; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.support.v4.app.ActivityCompat; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tasks.injection.ForApplication; + +import java.util.List; + +import javax.inject.Inject; + +import static java.util.Arrays.asList; + +public class PermissionChecker { + + private static final Logger log = LoggerFactory.getLogger(PermissionChecker.class); + + private final Context context; + + @Inject + public PermissionChecker(@ForApplication Context context) { + this.context = context; + } + + public boolean canAccessCalendars() { + return checkPermissions(asList(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR)); + } + + public boolean canWriteToExternalStorage() { + return checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); + } + + public boolean canAccessAccounts() { + return checkPermission(Manifest.permission.GET_ACCOUNTS); + } + + public boolean canAccessLocation() { + return checkPermission(Manifest.permission.ACCESS_FINE_LOCATION); + } + + public boolean canAccessMic() { + return checkPermission(Manifest.permission.RECORD_AUDIO); + } + + public boolean canAccessMissedCallPermissions() { + return checkPermission(Manifest.permission.READ_CONTACTS) && + checkPermission(Manifest.permission.READ_PHONE_STATE); + } + + private boolean checkPermission(String permission) { + return checkPermissions(asList(permission)); + } + + private boolean checkPermissions(List permissions) { + for (String permission : permissions) { + if (ActivityCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) { + log.warn("Request for {} denied", permission); + return false; + } + } + return true; + } +} diff --git a/src/main/java/org/tasks/preferences/PermissionRequestor.java b/src/main/java/org/tasks/preferences/PermissionRequestor.java new file mode 100644 index 000000000..577dfcaa0 --- /dev/null +++ b/src/main/java/org/tasks/preferences/PermissionRequestor.java @@ -0,0 +1,84 @@ +package org.tasks.preferences; + +import android.Manifest; +import android.app.Activity; +import android.support.v4.app.ActivityCompat; + +import javax.inject.Inject; + +public class PermissionRequestor { + + public static final int REQUEST_FILE_WRITE = 50; + public static final int REQUEST_CALENDAR = 51; + public static final int REQUEST_MIC = 52; + public static final int REQUEST_ACCOUNTS = 53; + public static final int REQUEST_LOCATION = 54; + public static final int REQUEST_CONTACTS = 55; + + private final Activity activity; + private final PermissionChecker permissionChecker; + + @Inject + public PermissionRequestor(Activity activity, PermissionChecker permissionChecker) { + this.activity = activity; + this.permissionChecker = permissionChecker; + } + + public boolean requestMic() { + if (permissionChecker.canAccessMic()) { + return true; + } + requestPermission(Manifest.permission.RECORD_AUDIO, REQUEST_MIC); + return false; + } + + public boolean requestFileWritePermission() { + if (permissionChecker.canWriteToExternalStorage()) { + return true; + } + requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, REQUEST_FILE_WRITE); + return false; + } + + public boolean requestCalendarPermissions() { + if (permissionChecker.canAccessCalendars()) { + return true; + } + requestPermissions( + new String[]{Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR}, + REQUEST_CALENDAR); + return false; + } + + public boolean requestAccountPermissions() { + if (permissionChecker.canAccessAccounts()) { + return true; + } + requestPermission(Manifest.permission.GET_ACCOUNTS, REQUEST_ACCOUNTS); + return false; + } + + public boolean requestFineLocation() { + if (permissionChecker.canAccessLocation()) { + return true; + } + requestPermission(Manifest.permission.ACCESS_FINE_LOCATION, REQUEST_LOCATION); + return false; + } + + public boolean requestMissedCallPermissions() { + if (permissionChecker.canAccessMissedCallPermissions()) { + return true; + } + requestPermissions(new String[]{Manifest.permission.READ_CONTACTS, Manifest.permission.READ_PHONE_STATE}, REQUEST_CONTACTS); + return false; + } + + private void requestPermission(String permission, int rc) { + requestPermissions(new String[] {permission}, rc); + } + + private void requestPermissions(String[] permissions, int rc) { + ActivityCompat.requestPermissions(activity, permissions, rc); + } +} diff --git a/src/main/java/org/tasks/preferences/Preferences.java b/src/main/java/org/tasks/preferences/Preferences.java index 300497935..98fb1d0a1 100644 --- a/src/main/java/org/tasks/preferences/Preferences.java +++ b/src/main/java/org/tasks/preferences/Preferences.java @@ -15,11 +15,11 @@ import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.TaskAttachment; import com.todoroo.astrid.widget.WidgetConfigActivity; -import org.tasks.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tasks.R; import org.tasks.injection.ForApplication; +import org.tasks.time.DateTime; import java.io.File; import java.util.ArrayList; @@ -51,14 +51,16 @@ public class Preferences { private static final String FILE_APPENDER_NAME = "FILE"; protected final Context context; - private DeviceInfo deviceInfo; + private final DeviceInfo deviceInfo; + private final PermissionChecker permissionChecker; private final SharedPreferences prefs; private final SharedPreferences publicPrefs; @Inject - public Preferences(@ForApplication Context context, DeviceInfo deviceInfo) { + public Preferences(@ForApplication Context context, DeviceInfo deviceInfo, PermissionChecker permissionChecker) { this.context = context; this.deviceInfo = deviceInfo; + this.permissionChecker = permissionChecker; prefs = PreferenceManager.getDefaultSharedPreferences(context); publicPrefs = context.getSharedPreferences(AstridApiConstants.PUBLIC_PREFS, Context.MODE_WORLD_READABLE); } @@ -140,6 +142,10 @@ public class Preferences { return prefs.getString(context.getResources().getString(keyResource), null); } + public boolean isStringValueSet(int keyResource) { + return !TextUtils.isEmpty(getStringValue(keyResource)); + } + public int getDefaultReminders() { return getIntegerFromString(R.string.p_default_reminders_key, Task.NOTIFY_AT_DEADLINE | Task.NOTIFY_AFTER_DEADLINE); } @@ -193,7 +199,9 @@ public class Preferences { } public boolean fieldMissedPhoneCalls() { - return getBoolean(R.string.p_field_missed_calls, true) && notificationsEnabled(); + return getBoolean(R.string.p_field_missed_calls, true) && + notificationsEnabled() && + permissionChecker.canAccessMissedCallPermissions(); } public boolean getBoolean(int keyResources, boolean defValue) { @@ -278,7 +286,9 @@ public class Preferences { } public void setupLogger() { - setupLogger(getBoolean(R.string.p_debug_logging, false)); + if (permissionChecker.canWriteToExternalStorage()) { + setupLogger(getBoolean(R.string.p_debug_logging, false)); + } } public void setupLogger(boolean enableDebugLogging) { @@ -335,27 +345,27 @@ public class Preferences { public File getAttachmentsDirectory() { File directory = null; String customDir = getStringValue(R.string.p_attachment_dir); - if (!TextUtils.isEmpty(customDir)) { + if (permissionChecker.canWriteToExternalStorage() && !TextUtils.isEmpty(customDir)) { directory = new File(customDir); } if (directory == null || !directory.exists()) { - directory = getExternalFilesDir(TaskAttachment.FILES_DIRECTORY_DEFAULT); + directory = getDefaultFileLocation(TaskAttachment.FILES_DIRECTORY_DEFAULT); } return directory; } - private File getExternalFilesDir(String type) { - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - String directory = String.format("%s/Android/data/%s/files/%s", Environment.getExternalStorageDirectory(), context.getPackageName(), type); - File file = new File(directory); - if (file.isDirectory() || file.mkdirs()) { - return file; - } + private File getDefaultFileLocation(String type) { + File externalFilesDir = context.getExternalFilesDir(null); + if (externalFilesDir == null) { + return null; } - - return null; + String path = String.format("%s/%s", + externalFilesDir.getAbsolutePath(), + type); + File file = new File(path); + return file.isDirectory() || file.mkdirs() ? file : null; } public String getNewAudioAttachmentPath(AtomicReference nameReference) { @@ -389,12 +399,12 @@ public class Preferences { public File getBackupDirectory() { File directory = null; String customDir = getStringValue(R.string.p_backup_dir); - if (!TextUtils.isEmpty(customDir)) { + if (permissionChecker.canWriteToExternalStorage() && !TextUtils.isEmpty(customDir)) { directory = new File(customDir); } if (directory == null || !directory.exists()) { - directory = defaultExportDirectory(); + directory = getDefaultFileLocation("backups"); } return directory; diff --git a/src/main/java/org/tasks/scheduling/BackgroundScheduler.java b/src/main/java/org/tasks/scheduling/BackgroundScheduler.java index 9828264b4..892fb31d4 100644 --- a/src/main/java/org/tasks/scheduling/BackgroundScheduler.java +++ b/src/main/java/org/tasks/scheduling/BackgroundScheduler.java @@ -22,7 +22,7 @@ public class BackgroundScheduler { context.startService(new Intent(context, ReminderSchedulerIntentService.class)); scheduleBackupService(); scheduleMidnightRefresh(); - + scheduleCalendarNotifications(); if (context.getResources().getBoolean(R.bool.sync_enabled)) { scheduleGtaskSync(); } @@ -39,4 +39,8 @@ public class BackgroundScheduler { public void scheduleGtaskSync() { context.startService(new Intent(context, GtasksBackgroundService.class)); } + + public void scheduleCalendarNotifications() { + context.startService(new Intent(context, CalendarNotificationIntentService.class)); + } } diff --git a/src/main/java/com/todoroo/astrid/gcal/CalendarAlarmScheduler.java b/src/main/java/org/tasks/scheduling/CalendarNotificationIntentService.java similarity index 61% rename from src/main/java/com/todoroo/astrid/gcal/CalendarAlarmScheduler.java rename to src/main/java/org/tasks/scheduling/CalendarNotificationIntentService.java index b6625881e..890b00461 100644 --- a/src/main/java/com/todoroo/astrid/gcal/CalendarAlarmScheduler.java +++ b/src/main/java/org/tasks/scheduling/CalendarNotificationIntentService.java @@ -1,4 +1,4 @@ -package com.todoroo.astrid.gcal; +package org.tasks.scheduling; import android.app.PendingIntent; import android.content.ContentResolver; @@ -8,46 +8,34 @@ import android.database.Cursor; import android.net.Uri; import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.astrid.gcal.CalendarAlarmReceiver; +import com.todoroo.astrid.gcal.Calendars; import org.tasks.R; +import org.tasks.injection.ForApplication; +import org.tasks.preferences.PermissionChecker; import org.tasks.preferences.Preferences; -import org.tasks.scheduling.AlarmManager; + +import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import javax.inject.Singleton; -@Singleton -public class CalendarAlarmScheduler { +public class CalendarNotificationIntentService extends RecurringIntervalIntentService { public static final String URI_PREFIX = "cal-reminder"; public static final String URI_PREFIX_POSTPONE = "cal-postpone"; - private final Preferences preferences; - private AlarmManager alarmManager; - - @Inject - public CalendarAlarmScheduler(Preferences preferences, AlarmManager alarmManager) { - this.preferences = preferences; - this.alarmManager = alarmManager; - } + @Inject Preferences preferences; + @Inject PermissionChecker permissionChecker; + @Inject @ForApplication Context context; + @Inject AlarmManager alarmManager; - public void scheduleCalendarAlarms(final Context context, boolean force) { - if (!preferences.getBoolean(R.string.p_calendar_reminders, true) && !force) { - return; - } - new Thread(new Runnable() { - @Override - public void run() { - scheduleAllCalendarAlarms(context); - } - }).start(); + public CalendarNotificationIntentService() { + super(CalendarNotificationIntentService.class.getSimpleName()); } - private void scheduleAllCalendarAlarms(Context context) { - if (!preferences.getBoolean(R.string.p_calendar_reminders, true)) { - return; - } - + @Override + void run() { ContentResolver cr = context.getContentResolver(); long now = DateUtilities.now(); @@ -80,17 +68,22 @@ public class CalendarAlarmScheduler { alarmManager.wakeup(alarmTime, pendingIntent); } } - - // Schedule alarm to recheck and reschedule calendar alarms in 12 hours - Intent rescheduleAlarm = new Intent(CalendarStartupReceiver.BROADCAST_RESCHEDULE_CAL_ALARMS); - PendingIntent pendingReschedule = PendingIntent.getBroadcast(context, 0, - rescheduleAlarm, 0); - alarmManager.cancel(pendingReschedule); - alarmManager.noWakeup(DateUtilities.now() + DateUtilities.ONE_HOUR * 12, pendingReschedule); } finally { if (events != null) { events.close(); } } } + + @Override + long intervalMillis() { + return preferences.getBoolean(R.string.p_calendar_reminders, false) && permissionChecker.canAccessCalendars() + ? TimeUnit.HOURS.toMillis(12) + : 0; + } + + @Override + String getLastRunPreference() { + return null; + } } diff --git a/src/main/java/org/tasks/scheduling/RecurringIntervalIntentService.java b/src/main/java/org/tasks/scheduling/RecurringIntervalIntentService.java index 004ff72e1..adf309938 100644 --- a/src/main/java/org/tasks/scheduling/RecurringIntervalIntentService.java +++ b/src/main/java/org/tasks/scheduling/RecurringIntervalIntentService.java @@ -38,14 +38,17 @@ public abstract class RecurringIntervalIntentService extends InjectingIntentServ return; } - long lastRun = preferences.getLong(getLastRunPreference(), 0); + String lastRunPreference = getLastRunPreference(); + long lastRun = lastRunPreference != null ? preferences.getLong(lastRunPreference, 0) : 0; long now = currentTimeMillis(); long nextRun = lastRun + interval; - if (nextRun < now + PADDING) { + if (lastRunPreference == null || nextRun < now + PADDING) { nextRun = now + interval; log.debug("running now [nextRun={}]", printTimestamp(nextRun)); - preferences.setLong(getLastRunPreference(), now); + if (lastRunPreference != null) { + preferences.setLong(lastRunPreference, now); + } run(); } else { log.debug("will run at {} [lastRun={}]", printTimestamp(nextRun), printTimestamp(lastRun)); diff --git a/src/main/java/org/tasks/time/DateTime.java b/src/main/java/org/tasks/time/DateTime.java index 43d03bd05..2b6ac1178 100644 --- a/src/main/java/org/tasks/time/DateTime.java +++ b/src/main/java/org/tasks/time/DateTime.java @@ -152,9 +152,7 @@ public class DateTime { } public DateTime plusMonths(int interval) { - Calendar calendar = getCalendar(); - calendar.add(Calendar.MONTH, interval); - return new DateTime(calendar); + return add(Calendar.MONTH, interval); } public DateTime plusWeeks(int weeks) { diff --git a/src/main/res/xml/preferences_help.xml b/src/main/res/xml/preferences_help.xml index d734b5e4e..e537bfa13 100644 --- a/src/main/res/xml/preferences_help.xml +++ b/src/main/res/xml/preferences_help.xml @@ -31,4 +31,8 @@ android:key="@string/contact_developer" android:title="@string/contact_developer" /> + + \ No newline at end of file diff --git a/src/main/res/xml/preferences_misc.xml b/src/main/res/xml/preferences_misc.xml index c715b7942..72bf8dcb0 100644 --- a/src/main/res/xml/preferences_misc.xml +++ b/src/main/res/xml/preferences_misc.xml @@ -16,7 +16,7 @@ android:key="@string/p_calendar_reminders" android:summary="@string/CRA_calendar_reminders_pref_desc_enabled" android:title="@string/CRA_calendar_reminders_pref_title" - android:defaultValue="true"/> + android:defaultValue="false"/> - diff --git a/src/main/res/xml/preferences_reminders.xml b/src/main/res/xml/preferences_reminders.xml index 0fa966b45..ff48a9b9e 100644 --- a/src/main/res/xml/preferences_reminders.xml +++ b/src/main/res/xml/preferences_reminders.xml @@ -41,7 +41,7 @@ android:summaryOn="@string/rmd_EPr_persistent_desc_true" android:title="@string/rmd_EPr_persistent_title" />