From 72365624817aa5c72f04699a5cf533013450afd9 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 18 Jan 2018 08:54:03 +0100 Subject: [PATCH] Implement Android 6.0 runtime permissions Reorder and comment permissions Runtime permission WIP Show permission error message Simplify Calendar permissions Handle service notification request account manager permission Remove debug Default to light SDK 10 fixes Bump version --- CHANGES | 1 + app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 72 ++++++++++-- app/src/main/assets/about.html | 1 + .../java/com/zegoggles/smssync/Consts.java | 7 -- .../smssync/activity/AppPermission.java | 84 +++++++++++++ .../zegoggles/smssync/activity/Dialogs.java | 8 +- .../smssync/activity/MainActivity.java | 78 ++++++++++-- .../smssync/activity/StatusPreference.java | 45 +++++-- .../auth/AccountManagerAuthActivity.java | 40 ++++++- .../activity/auth/OAuth2WebAuthActivity.java | 9 +- .../activity/donation/DonationActivity.java | 1 - .../events/MissingPermissionsEvent.java | 13 ++ .../activity/fragments/AdvancedSettings.java | 85 +++++++++++--- .../com/zegoggles/smssync/mail/DataType.java | 19 ++- .../zegoggles/smssync/mail/PersonLookup.java | 5 +- .../smssync/preferences/Preferences.java | 24 ++-- .../smssync/service/AlarmManagerDriver.java | 4 +- .../smssync/service/BackupConfig.java | 5 - .../smssync/service/BackupItemsFetcher.java | 6 + .../zegoggles/smssync/service/BackupJobs.java | 3 - .../zegoggles/smssync/service/BackupTask.java | 18 ++- .../zegoggles/smssync/service/BackupType.java | 13 +- .../smssync/service/ServiceBase.java | 23 ++-- .../smssync/service/SmsBackupService.java | 111 ++++++++++++------ .../smssync/service/SmsJobService.java | 13 +- .../smssync/service/SmsRestoreService.java | 2 +- .../exception/MissingPermissionException.java | 12 ++ .../smssync/service/state/BackupState.java | 2 +- .../smssync/service/state/State.java | 26 +++- app/src/main/res/values/strings.xml | 14 +++ .../service/AlarmManagerDriverTest.java | 2 +- .../smssync/service/BackupConfigTest.java | 3 - .../smssync/service/BackupTaskTest.java | 4 +- .../smssync/service/SmsBackupServiceTest.java | 61 ++++------ 35 files changed, 602 insertions(+), 216 deletions(-) create mode 100644 app/src/main/java/com/zegoggles/smssync/activity/AppPermission.java create mode 100644 app/src/main/java/com/zegoggles/smssync/activity/events/MissingPermissionsEvent.java create mode 100644 app/src/main/java/com/zegoggles/smssync/service/exception/MissingPermissionException.java diff --git a/CHANGES b/CHANGES index 6ebb6937e..aba7d3163 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,6 @@ == 1.5.11 (1555) tbd + * Implemented runtime permissions for Android 6.0+ * Added dark/light theme switcher * Modernized UI (Material/AppCompat) * Simplify IMAP search logic to prevent timeouts diff --git a/app/build.gradle b/app/build.gradle index 0dcf43c1a..0e214b1a8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.zegoggles.smssync" minSdkVersion 9 targetSdkVersion 25 - versionCode 1563 - versionName "1.5.11-beta10" + versionCode 1564 + versionName "1.5.11-beta11" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a16fb1fbb..64074fb54 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,28 +4,74 @@ package="com.zegoggles.smssync" android:description="@string/app_description"> + + + - + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - @@ -35,7 +81,7 @@ diff --git a/app/src/main/assets/about.html b/app/src/main/assets/about.html index 6810ec711..5c84b260b 100644 --- a/app/src/main/assets/about.html +++ b/app/src/main/assets/about.html @@ -13,6 +13,7 @@

New in this release:

    +
  • Runtime permissions on Android 6.0+. It is recommended to run a manual backup after upgrade.
  • Improved backup schedule reliability (thanks Mads Andreasen for initial JobManager implementation)
  • Replaced WebView auth with browser
  • Improved restore experience (thanks Anton Keks/angryziber)
  • diff --git a/app/src/main/java/com/zegoggles/smssync/Consts.java b/app/src/main/java/com/zegoggles/smssync/Consts.java index 8d687b3d5..52338e8e2 100644 --- a/app/src/main/java/com/zegoggles/smssync/Consts.java +++ b/app/src/main/java/com/zegoggles/smssync/Consts.java @@ -25,13 +25,6 @@ public final class Consts { private Consts() {} - /** - * Key in the intent extras for indication whether all unsynced messages should - * be skipped or not. - */ - public static final String KEY_SKIP_MESSAGES = "com.zegoggles.smssync.SkipMessages"; - - /** {@link android.provider.Telephony.Mms#CONTENT_URI} */ public static final Uri MMS_PROVIDER = Uri.parse("content://mms"); public static final String MMS_PART = "part"; diff --git a/app/src/main/java/com/zegoggles/smssync/activity/AppPermission.java b/app/src/main/java/com/zegoggles/smssync/activity/AppPermission.java new file mode 100644 index 000000000..22145a034 --- /dev/null +++ b/app/src/main/java/com/zegoggles/smssync/activity/AppPermission.java @@ -0,0 +1,84 @@ +package com.zegoggles.smssync.activity; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import com.zegoggles.smssync.R; + +import java.util.ArrayList; +import java.util.List; + +import static android.content.pm.PackageManager.PERMISSION_DENIED; + +public enum AppPermission { + READ_SMS(Manifest.permission.READ_SMS, R.string.permission_read_sms), + READ_CALL_LOG(Manifest.permission.READ_CALL_LOG, R.string.permission_read_call_log), + READ_CONTACTS(Manifest.permission.READ_CONTACTS, R.string.permission_read_contacts), + UNKNOWN(null, R.string.permission_unknown); + + final int descriptionResource; + final @Nullable String androidPermission; + + AppPermission(String androidPermission, int descriptionResource) { + this.androidPermission = androidPermission; + this.descriptionResource = descriptionResource; + } + + @Override + public String toString() { + return "AppPermission{" + androidPermission + '}'; + } + + public static AppPermission from(@NonNull String androidPermission) { + for (AppPermission appPermission : AppPermission.values()) { + if (androidPermission.equals(appPermission.androidPermission)) { + return appPermission; + } + } + return UNKNOWN; + } + + public static List from(@NonNull String[] androidPermissions) { + List appPermissions = new ArrayList(); + for (String permission : androidPermissions) { + appPermissions.add(from(permission)); + } + return appPermissions; + } + + public static List from(@NonNull String[] androidPermissions, @NonNull int[] grantResults) { + List appPermissions = new ArrayList(); + for (int i=0; i appPermissions) { + List permissions = new ArrayList(); + for (AppPermission permission : appPermissions) { + permissions.add(resources.getString(permission.descriptionResource)); + } + return resources.getQuantityString(R.plurals.status_permission_problem_details, + permissions.size(), + TextUtils.join(", ", permissions)); + } +} diff --git a/app/src/main/java/com/zegoggles/smssync/activity/Dialogs.java b/app/src/main/java/com/zegoggles/smssync/activity/Dialogs.java index 97de0e054..d3213499a 100644 --- a/app/src/main/java/com/zegoggles/smssync/activity/Dialogs.java +++ b/app/src/main/java/com/zegoggles/smssync/activity/Dialogs.java @@ -16,7 +16,6 @@ package com.zegoggles.smssync.activity; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.app.Dialog; import android.app.ProgressDialog; import android.content.Context; @@ -24,7 +23,6 @@ import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -52,6 +50,8 @@ import static android.R.string.yes; import static android.app.ProgressDialog.STYLE_SPINNER; import static android.content.DialogInterface.BUTTON_NEGATIVE; +import static com.zegoggles.smssync.activity.MainActivity.REQUEST_CHANGE_DEFAULT_SMS_PACKAGE; +import static com.zegoggles.smssync.activity.MainActivity.REQUEST_WEB_AUTH; import static com.zegoggles.smssync.activity.events.PerformAction.Actions.Backup; import static com.zegoggles.smssync.activity.events.PerformAction.Actions.BackupSkip; @@ -204,6 +204,8 @@ public void onClick(DialogInterface dialog, int which) { public static class AccessTokenProgress extends BaseFragment { @Override @NonNull public Dialog onCreateDialog(Bundle savedInstanceState) { + // NB: progress dialog is not AppCompat-ready, and will not appear themed + // correctly on older devices ProgressDialog progress = new ProgressDialog(getContext()); progress.setTitle(null); progress.setProgressStyle(STYLE_SPINNER); @@ -259,7 +261,6 @@ public void onClick(DialogInterface dialog, int which) { } public static class WebConnect extends BaseFragment { - static final int REQUEST_WEB_AUTH = 3; static final String INTENT = "intent"; @Override @NonNull @@ -313,7 +314,6 @@ public void onClick(DialogInterface dialog, int which) { } public static class SmsDefaultPackage extends BaseFragment { - static final int REQUEST_CHANGE_DEFAULT_SMS_PACKAGE = 1; static final String INTENT = "intent"; @Override @NonNull diff --git a/app/src/main/java/com/zegoggles/smssync/activity/MainActivity.java b/app/src/main/java/com/zegoggles/smssync/activity/MainActivity.java index 031a06005..46b7e8534 100644 --- a/app/src/main/java/com/zegoggles/smssync/activity/MainActivity.java +++ b/app/src/main/java/com/zegoggles/smssync/activity/MainActivity.java @@ -23,6 +23,7 @@ import android.provider.Telephony.Sms; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager.BackStackEntry; @@ -40,7 +41,6 @@ import android.widget.Toast; import com.squareup.otto.Subscribe; import com.zegoggles.smssync.App; -import com.zegoggles.smssync.Consts; import com.zegoggles.smssync.R; import com.zegoggles.smssync.activity.Dialogs.SmsDefaultPackage; import com.zegoggles.smssync.activity.Dialogs.WebConnect; @@ -49,6 +49,7 @@ import com.zegoggles.smssync.activity.events.AccountAddedEvent; import com.zegoggles.smssync.activity.events.AccountConnectionChangedEvent; import com.zegoggles.smssync.activity.events.FallbackAuthEvent; +import com.zegoggles.smssync.activity.events.MissingPermissionsEvent; import com.zegoggles.smssync.activity.events.PerformAction; import com.zegoggles.smssync.activity.events.PerformAction.Actions; import com.zegoggles.smssync.activity.events.ThemeChangedEvent; @@ -59,10 +60,14 @@ import com.zegoggles.smssync.service.BackupType; import com.zegoggles.smssync.service.SmsBackupService; import com.zegoggles.smssync.service.SmsRestoreService; +import com.zegoggles.smssync.service.state.BackupState; import com.zegoggles.smssync.service.state.RestoreState; import com.zegoggles.smssync.tasks.OAuth2CallbackTask; import com.zegoggles.smssync.utils.BundleBuilder; +import java.util.Arrays; +import java.util.List; + import static android.os.Build.VERSION_CODES.HONEYCOMB; import static android.provider.Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT; import static android.provider.Telephony.Sms.Intents.EXTRA_PACKAGE_NAME; @@ -70,10 +75,11 @@ import static android.widget.Toast.LENGTH_LONG; import static com.zegoggles.smssync.App.LOCAL_LOGV; import static com.zegoggles.smssync.App.TAG; +import static com.zegoggles.smssync.App.post; +import static com.zegoggles.smssync.activity.AppPermission.allGranted; import static com.zegoggles.smssync.activity.Dialogs.ConfirmAction.ACTION; import static com.zegoggles.smssync.activity.Dialogs.FirstSync.MAX_ITEMS_PER_SYNC; import static com.zegoggles.smssync.activity.Dialogs.MissingCredentials.USE_XOAUTH; -import static com.zegoggles.smssync.activity.Dialogs.SmsDefaultPackage.REQUEST_CHANGE_DEFAULT_SMS_PACKAGE; import static com.zegoggles.smssync.activity.Dialogs.Type.ABOUT; import static com.zegoggles.smssync.activity.Dialogs.Type.ACCOUNT_MANAGER_TOKEN_ERROR; import static com.zegoggles.smssync.activity.Dialogs.Type.CONFIRM_ACTION; @@ -87,13 +93,13 @@ import static com.zegoggles.smssync.activity.Dialogs.Type.UPGRADE_FROM_SMSBACKUP; import static com.zegoggles.smssync.activity.Dialogs.Type.VIEW_LOG; import static com.zegoggles.smssync.activity.Dialogs.Type.WEB_CONNECT; -import static com.zegoggles.smssync.activity.Dialogs.WebConnect.REQUEST_WEB_AUTH; import static com.zegoggles.smssync.activity.auth.AccountManagerAuthActivity.ACTION_ADD_ACCOUNT; import static com.zegoggles.smssync.activity.auth.AccountManagerAuthActivity.ACTION_FALLBACK_AUTH; import static com.zegoggles.smssync.activity.auth.AccountManagerAuthActivity.EXTRA_ACCOUNT; import static com.zegoggles.smssync.activity.auth.AccountManagerAuthActivity.EXTRA_TOKEN; import static com.zegoggles.smssync.activity.events.PerformAction.Actions.Backup; -import static com.zegoggles.smssync.activity.events.PerformAction.Actions.BackupSkip; +import static com.zegoggles.smssync.service.BackupType.MANUAL; +import static com.zegoggles.smssync.service.BackupType.SKIP; /** * This is the main activity showing the status of the SMS Sync service and @@ -103,7 +109,14 @@ public class MainActivity extends ThemeActivity implements OnPreferenceStartFragmentCallback, OnPreferenceStartScreenCallback, FragmentManager.OnBackStackChangedListener { + static final int REQUEST_CHANGE_DEFAULT_SMS_PACKAGE = 1; private static final int REQUEST_PICK_ACCOUNT = 2; + static final int REQUEST_WEB_AUTH = 3; + private static final int REQUEST_PERMISSIONS_BACKUP_MANUAL = 4; + private static final int REQUEST_PERMISSIONS_BACKUP_MANUAL_SKIP = 5; + private static final int REQUEST_PERMISSIONS_BACKUP_SERVICE = 6; + + public static final String EXTRA_PERMISSIONS = "permissions"; private static final String SCREEN_TITLE = "title"; private Preferences preferences; @@ -136,6 +149,7 @@ public void onCreate(Bundle bundle) { showDialog(ABOUT); } checkDefaultSmsApp(); + requestPermissionsIfNeeded(); } @Override @@ -215,7 +229,7 @@ public boolean onOptionsItemSelected(MenuItem item) { handleFallbackAuth(new FallbackAuthEvent(true)); } } else if (LOCAL_LOGV) { - Log.v(TAG, "request canceled, result="+resultCode); + Log.v(TAG, "request canceled, result=" + resultCode); } break; } @@ -253,6 +267,15 @@ public boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, Preferen } } + @Subscribe public void backupStateChanged(final BackupState newState) { + if ((newState.backupType == MANUAL || newState.backupType == SKIP) && newState.isPermissionException()) { + ActivityCompat.requestPermissions(this, + newState.getMissingPermissions(), + newState.backupType == SKIP ? REQUEST_PERMISSIONS_BACKUP_MANUAL_SKIP : REQUEST_PERMISSIONS_BACKUP_MANUAL + ); + } + } + @Subscribe public void onOAuth2Callback(OAuth2CallbackTask.OAuth2CallbackEvent event) { if (event.valid()) { authPreferences.setOauth2Token(event.token.userName, event.token.accessToken, event.token.refreshToken); @@ -331,7 +354,7 @@ private void onAuthenticated() { switch (action) { case Backup: case BackupSkip: - startBackup(action == BackupSkip); + startBackup(action == Backup ? MANUAL : SKIP); break; case Restore: startRestore(); @@ -339,13 +362,8 @@ private void onAuthenticated() { } } - private void startBackup(boolean skip) { - final Intent intent = new Intent(this, SmsBackupService.class); - if (preferences.isFirstBackup()) { - intent.putExtra(Consts.KEY_SKIP_MESSAGES, skip); - } - intent.putExtra(BackupType.EXTRA, BackupType.MANUAL.name()); - startService(intent); + private void startBackup(BackupType backupType) { + startService(new Intent(this, SmsBackupService.class).setAction(backupType.name())); } @TargetApi(Build.VERSION_CODES.KITKAT) @@ -443,4 +461,38 @@ private void checkDefaultSmsApp() { restoreDefaultSmsProvider(preferences.getSmsDefaultPackage()); } } + + private void requestPermissionsIfNeeded() { + final Intent intent = getIntent(); + if (intent != null && intent.hasExtra(EXTRA_PERMISSIONS)) { + final String[] permissions = intent.getStringArrayExtra(EXTRA_PERMISSIONS); + Log.v(TAG, "requesting permissions "+ Arrays.toString(permissions)); + ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS_BACKUP_SERVICE); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + Log.v(TAG, "onRequestPermissionsResult("+requestCode+ ","+ Arrays.toString(permissions) +","+ Arrays.toString(grantResults)); + switch (requestCode) { + case REQUEST_PERMISSIONS_BACKUP_MANUAL: + case REQUEST_PERMISSIONS_BACKUP_MANUAL_SKIP: + if (allGranted(grantResults)) { + startBackup(requestCode == REQUEST_PERMISSIONS_BACKUP_MANUAL ? MANUAL : SKIP); + } else { + final List missing = AppPermission.from(permissions, grantResults); + Log.w(TAG, "not all permissions granted: "+missing); + post(new MissingPermissionsEvent(missing)); + } + break; + case REQUEST_PERMISSIONS_BACKUP_SERVICE: + if (allGranted(grantResults)) { + startBackup(MANUAL); + } else { + post(new MissingPermissionsEvent(AppPermission.from(permissions, grantResults))); + } + break; + } + } } diff --git a/app/src/main/java/com/zegoggles/smssync/activity/StatusPreference.java b/app/src/main/java/com/zegoggles/smssync/activity/StatusPreference.java index 1e5b3f127..2c7839f8f 100644 --- a/app/src/main/java/com/zegoggles/smssync/activity/StatusPreference.java +++ b/app/src/main/java/com/zegoggles/smssync/activity/StatusPreference.java @@ -16,6 +16,7 @@ import com.squareup.otto.Subscribe; import com.zegoggles.smssync.App; import com.zegoggles.smssync.R; +import com.zegoggles.smssync.activity.events.MissingPermissionsEvent; import com.zegoggles.smssync.activity.events.PerformAction; import com.zegoggles.smssync.preferences.AuthPreferences; import com.zegoggles.smssync.preferences.Preferences; @@ -30,6 +31,7 @@ import java.text.DateFormat; import java.util.Date; +import java.util.List; import static com.zegoggles.smssync.App.LOCAL_LOGV; import static com.zegoggles.smssync.App.TAG; @@ -139,7 +141,6 @@ public void onRestoreInstanceState(Parcelable state) { super.onRestoreInstanceState(state); } - @Subscribe public void restoreStateChanged(final RestoreState newState) { if (App.LOCAL_LOGV) Log.v(TAG, "restoreStateChanged:" + newState); if (backupButton == null) return; @@ -161,7 +162,7 @@ public void onRestoreInstanceState(Parcelable state) { case CANCELED_RESTORE: statusLabel.setText(R.string.status_canceled); - syncDetailsLabel.setText(getContext().getString(R.string.status_restore_canceled_details, + syncDetailsLabel.setText(getString(R.string.status_restore_canceled_details, newState.currentRestoredCount, newState.itemsToRestore)); break; @@ -194,13 +195,17 @@ public void onRestoreInstanceState(Parcelable state) { break; case CANCELED_BACKUP: statusLabel.setText(R.string.status_canceled); - syncDetailsLabel.setText(getContext().getString(R.string.status_canceled_details, + syncDetailsLabel.setText(getString(R.string.status_canceled_details, newState.currentSyncedItems, newState.itemsToSync)); break; } } + @Subscribe public void onMissingPermissions(MissingPermissionsEvent event) { + displayMissingPermissions(event.permissions); + } + private void onBackup() { if (!SmsBackupService.isServiceWorking()) { if (LOCAL_LOGV) Log.v(TAG, "user requested sync"); @@ -225,7 +230,7 @@ private void onRestore() { } } - private void authFailed() { + private void onAuthFailed() { statusLabel.setText(R.string.status_auth_failure); if (new AuthPreferences(getContext()).useXOAuth()) { @@ -235,6 +240,11 @@ private void authFailed() { } } + private void displayMissingPermissions(List appPermissions) { + statusLabel.setText(R.string.status_permission_problem); + syncDetailsLabel.setText(AppPermission.formatMissingPermissionDetails(getContext().getResources(), appPermissions)); + } + private void calc() { statusLabel.setText(R.string.status_working); syncDetailsLabel.setText(R.string.status_calc_details); @@ -245,12 +255,11 @@ private void finishedBackup(BackupState state) { int backedUpCount = state.currentSyncedItems; String text = null; if (backedUpCount == preferences.getMaxItemsPerSync()) { - text = getContext().getString(R.string.status_backup_done_details_max_per_sync, backedUpCount); + text = getString(R.string.status_backup_done_details_max_per_sync, backedUpCount); } else if (backedUpCount > 0) { - text = getContext().getResources().getQuantityString(R.plurals.status_backup_done_details, backedUpCount, - backedUpCount); + text = getQuantityString(R.plurals.status_backup_done_details, backedUpCount, backedUpCount); } else if (backedUpCount == 0) { - text = getContext().getString(R.string.status_backup_done_details_noitems); + text = getString(R.string.status_backup_done_details_noitems); } syncDetailsLabel.setText(text); statusLabel.setText(R.string.status_done); @@ -262,7 +271,7 @@ private void finishedRestore(RestoreState newState) { statusLabel.setTextColor(doneColor); statusLabel.setText(R.string.status_done); statusIcon.setImageDrawable(done); - syncDetailsLabel.setText(getContext().getResources().getQuantityString( + syncDetailsLabel.setText(getQuantityString( R.plurals.status_restore_done_details, newState.actualRestoredCount, newState.actualRestoredCount, @@ -277,8 +286,8 @@ private void idle() { } private String getLastSyncText(final long lastSync) { - return getContext().getString(R.string.status_idle_details, - lastSync < 0 ? getContext().getString(R.string.status_idle_details_never) : + return getString(R.string.status_idle_details, + lastSync < 0 ? getString(R.string.status_idle_details_never) : DateFormat.getDateTimeInstance().format(new Date(lastSync))); } @@ -298,11 +307,13 @@ private void stateChanged(State state) { break; case ERROR: if (state.isAuthException()) { - authFailed(); + onAuthFailed(); + } else if (state.isPermissionException()) { + displayMissingPermissions(AppPermission.from(state.getMissingPermissions())); } else { final String errorMessage = state.getErrorMessage(getContext().getResources()); statusLabel.setText(R.string.status_unknown_error); - syncDetailsLabel.setText(getContext().getString(R.string.status_unknown_error_details, + syncDetailsLabel.setText(getString(R.string.status_unknown_error_details, errorMessage == null ? "N/A" : errorMessage)); } break; @@ -341,4 +352,12 @@ private void setButtonsToDefault() { backupButton.setEnabled(true); backupButton.setText(R.string.ui_sync_button_label_idle); } + + private String getString(int resourceId, Object... formatArgs) { + return getContext().getResources().getString(resourceId, formatArgs); + } + + private String getQuantityString(int resourceId, int quantity, Object... formatArgs) { + return getContext().getResources().getQuantityString(resourceId, quantity, formatArgs); + } } diff --git a/app/src/main/java/com/zegoggles/smssync/activity/auth/AccountManagerAuthActivity.java b/app/src/main/java/com/zegoggles/smssync/activity/auth/AccountManagerAuthActivity.java index ae34a519c..d89acbd94 100644 --- a/app/src/main/java/com/zegoggles/smssync/activity/auth/AccountManagerAuthActivity.java +++ b/app/src/main/java/com/zegoggles/smssync/activity/auth/AccountManagerAuthActivity.java @@ -11,11 +11,11 @@ import android.content.res.ColorStateList; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; import android.util.Log; import com.zegoggles.smssync.R; import com.zegoggles.smssync.activity.Dialogs; @@ -24,8 +24,13 @@ import com.zegoggles.smssync.activity.ThemeActivity; import com.zegoggles.smssync.utils.BundleBuilder; +import java.util.Arrays; + +import static android.Manifest.permission.GET_ACCOUNTS; import static android.accounts.AccountManager.KEY_AUTHTOKEN; +import static android.content.pm.PackageManager.PERMISSION_DENIED; import static com.zegoggles.smssync.App.TAG; +import static com.zegoggles.smssync.activity.AppPermission.allGranted; import static com.zegoggles.smssync.activity.auth.AccountManagerAuthActivity.AccountDialogs.ACCOUNTS; import static com.zegoggles.smssync.utils.Drawables.getTinted; @@ -40,12 +45,45 @@ public class AccountManagerAuthActivity extends ThemeActivity { public static final String AUTH_TOKEN_TYPE = "oauth2:https://mail.google.com/"; public static final String GOOGLE_TYPE = "com.google"; + private static final int REQUEST_GET_ACCOUNTS = 0; private AccountManager accountManager; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); accountManager = AccountManager.get(this); + + if (needsGetAccountPermission()) { + requestGetAccountsPermission(); + } else { + checkAccounts(); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + Log.v(TAG, "onRequestPermissionsResult("+requestCode+ ","+ Arrays.toString(permissions) +","+ Arrays.toString(grantResults)); + if (requestCode == REQUEST_GET_ACCOUNTS) { + if (allGranted(grantResults)) { + checkAccounts(); + } else { + Log.w(TAG, "no permission to get accounts"); + setResult(RESULT_OK, new Intent(ACTION_FALLBACK_AUTH)); + finish(); + } + } + } + + private void requestGetAccountsPermission() { + ActivityCompat.requestPermissions(this, new String[] {GET_ACCOUNTS}, REQUEST_GET_ACCOUNTS); + } + + private boolean needsGetAccountPermission() { + return ContextCompat.checkSelfPermission(this, GET_ACCOUNTS) == PERMISSION_DENIED; + } + + private void checkAccounts() { Account[] accounts = accountManager.getAccountsByType(GOOGLE_TYPE); if (accounts == null || accounts.length == 0) { Log.d(TAG, "no google accounts found on this device, using standard auth"); diff --git a/app/src/main/java/com/zegoggles/smssync/activity/auth/OAuth2WebAuthActivity.java b/app/src/main/java/com/zegoggles/smssync/activity/auth/OAuth2WebAuthActivity.java index 63992910e..cb5652742 100644 --- a/app/src/main/java/com/zegoggles/smssync/activity/auth/OAuth2WebAuthActivity.java +++ b/app/src/main/java/com/zegoggles/smssync/activity/auth/OAuth2WebAuthActivity.java @@ -1,19 +1,16 @@ package com.zegoggles.smssync.activity.auth; -import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; -import android.util.Log; import com.squareup.otto.Subscribe; import com.zegoggles.smssync.App; +import com.zegoggles.smssync.activity.ThemeActivity; -import static com.zegoggles.smssync.App.TAG; - -public class OAuth2WebAuthActivity extends Activity { +public class OAuth2WebAuthActivity extends ThemeActivity { public static final String EXTRA_CODE = "code"; - public static final String EXTRA_ERROR = "error"; + private static final String EXTRA_ERROR = "error"; public void onCreate(Bundle bundle) { super.onCreate(bundle); diff --git a/app/src/main/java/com/zegoggles/smssync/activity/donation/DonationActivity.java b/app/src/main/java/com/zegoggles/smssync/activity/donation/DonationActivity.java index 63d363aa6..d644d7bf9 100644 --- a/app/src/main/java/com/zegoggles/smssync/activity/donation/DonationActivity.java +++ b/app/src/main/java/com/zegoggles/smssync/activity/donation/DonationActivity.java @@ -221,7 +221,6 @@ public void onBillingSetupFinished(@BillingResponse int resultCode) { try { helper.endConnection(); } catch (Exception ignored) { - Log.w(TAG, "ignoring error during endConnection()", ignored); } } } diff --git a/app/src/main/java/com/zegoggles/smssync/activity/events/MissingPermissionsEvent.java b/app/src/main/java/com/zegoggles/smssync/activity/events/MissingPermissionsEvent.java new file mode 100644 index 000000000..5582b0be4 --- /dev/null +++ b/app/src/main/java/com/zegoggles/smssync/activity/events/MissingPermissionsEvent.java @@ -0,0 +1,13 @@ +package com.zegoggles.smssync.activity.events; + +import com.zegoggles.smssync.activity.AppPermission; + +import java.util.List; + +public class MissingPermissionsEvent { + public final List permissions; + + public MissingPermissionsEvent(List permissions) { + this.permissions = permissions; + } +} diff --git a/app/src/main/java/com/zegoggles/smssync/activity/fragments/AdvancedSettings.java b/app/src/main/java/com/zegoggles/smssync/activity/fragments/AdvancedSettings.java index c4f5232b7..e9b3aa3c3 100644 --- a/app/src/main/java/com/zegoggles/smssync/activity/fragments/AdvancedSettings.java +++ b/app/src/main/java/com/zegoggles/smssync/activity/fragments/AdvancedSettings.java @@ -1,9 +1,16 @@ package com.zegoggles.smssync.activity.fragments; +import android.content.pm.PackageManager; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.support.v7.preference.CheckBoxPreference; import android.support.v7.preference.ListPreference; import android.support.v7.preference.Preference; import android.support.v7.preference.Preference.OnPreferenceChangeListener; +import android.util.Log; +import android.view.View; import com.zegoggles.smssync.R; import com.zegoggles.smssync.activity.events.ThemeChangedEvent; import com.zegoggles.smssync.calendar.CalendarAccessor; @@ -15,9 +22,13 @@ import com.zegoggles.smssync.utils.ListPreferenceHelper; import java.text.DateFormat; +import java.util.Arrays; import java.util.Date; import java.util.Locale; +import static android.Manifest.permission.WRITE_CALENDAR; +import static com.zegoggles.smssync.App.TAG; +import static com.zegoggles.smssync.activity.AppPermission.allGranted; import static com.zegoggles.smssync.activity.Dialogs.Type.INVALID_IMAP_FOLDER; import static com.zegoggles.smssync.mail.DataType.CALLLOG; import static com.zegoggles.smssync.mail.DataType.SMS; @@ -116,40 +127,82 @@ private void initGroups() { } public static class CallLog extends AdvancedSettings { + private static final int REQUEST_CALENDAR_ACCESS = 0; + private CheckBoxPreference enabledPreference; + private ListPreference calendarPreference; + private Preference folderPreference; + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + enabledPreference = (CheckBoxPreference) findPreference(CALLLOG_SYNC_CALENDAR_ENABLED.key); + calendarPreference = (ListPreference) findPreference(CALLLOG_SYNC_CALENDAR.key); + folderPreference = findPreference(CALLLOG.folderPreference); + } + @Override public void onResume() { super.onResume(); - updateCallLogCalendarLabelFromPref(); initCalendars(); + + updateCallLogCalendarLabelFromPref(); registerValidCallLogFolderCheck(); + registerCalendarSyncEnabledCallback(); addPreferenceListener(CALLLOG_BACKUP_AFTER_CALL.key); + + if (needCalendarPermission() && enabledPreference.isChecked()) { + // user revoked calendar permission manually + enabledPreference.setChecked(false); + } } - private void updateCallLogCalendarLabelFromPref() { - final ListPreference calendarPref = (ListPreference) - findPreference(CALLLOG_SYNC_CALENDAR.key); + private void registerCalendarSyncEnabledCallback() { + enabledPreference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (newValue == Boolean.TRUE && needCalendarPermission()) { + requestPermissions(new String[] {WRITE_CALENDAR}, REQUEST_CALENDAR_ACCESS); + return false; + } else { + return true; + } + } + }); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + Log.v(TAG, "onRequestPermissionsResult("+requestCode+ ","+ Arrays.toString(permissions) +","+ Arrays.toString(grantResults)); + + if (requestCode == REQUEST_CALENDAR_ACCESS) { + enabledPreference.setChecked(allGranted(grantResults)); + } + } - calendarPref.setTitle(calendarPref.getEntry() != null ? calendarPref.getEntry() : + private boolean needCalendarPermission() { + return ContextCompat.checkSelfPermission(getContext(), WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED; + } + + private void updateCallLogCalendarLabelFromPref() { + calendarPreference.setTitle(calendarPreference.getEntry() != null ? calendarPreference.getEntry() : getString(R.string.ui_backup_calllog_sync_calendar_label)); } private void initCalendars() { - final ListPreference calendarPref = (ListPreference) - findPreference(CALLLOG_SYNC_CALENDAR.key); - CalendarAccessor calendars = CalendarAccessor.Get.instance(getContext().getContentResolver()); - boolean enabled = ListPreferenceHelper.initListPreference(calendarPref, calendars.getCalendars(), false); + if (needCalendarPermission()) return; - findPreference(CALLLOG_SYNC_CALENDAR_ENABLED.key).setEnabled(enabled); + CalendarAccessor calendars = CalendarAccessor.Get.instance(getContext().getContentResolver()); + ListPreferenceHelper.initListPreference(calendarPreference, calendars.getCalendars(), false); } private void registerValidCallLogFolderCheck() { - findPreference(CALLLOG.folderPreference) - .setOnPreferenceChangeListener(new OnPreferenceChangeListener() { - public boolean onPreferenceChange(Preference preference, final Object newValue) { - return checkValidImapFolder(preference, newValue.toString()); - } - }); + folderPreference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, final Object newValue) { + return checkValidImapFolder(preference, newValue.toString()); + } + }); } } } diff --git a/app/src/main/java/com/zegoggles/smssync/mail/DataType.java b/app/src/main/java/com/zegoggles/smssync/mail/DataType.java index d6f6a8c4e..1eafbd4f6 100644 --- a/app/src/main/java/com/zegoggles/smssync/mail/DataType.java +++ b/app/src/main/java/com/zegoggles/smssync/mail/DataType.java @@ -1,11 +1,19 @@ package com.zegoggles.smssync.mail; +import android.os.Build; import com.zegoggles.smssync.R; +import static android.Manifest.permission.READ_CALL_LOG; +import static android.Manifest.permission.READ_CONTACTS; +import static android.Manifest.permission.READ_SMS; + public enum DataType { - SMS (R.string.sms, R.string.sms_with_field, PreferenceKeys.IMAP_FOLDER, Defaults.SMS_FOLDER, PreferenceKeys.BACKUP_SMS, Defaults.SMS_BACKUP_ENABLED, PreferenceKeys.RESTORE_SMS, Defaults.SMS_RESTORE_ENABLED, PreferenceKeys.MAX_SYNCED_DATE_SMS), - MMS (R.string.mms, R.string.mms_with_field, PreferenceKeys.IMAP_FOLDER, Defaults.SMS_FOLDER, PreferenceKeys.BACKUP_MMS, Defaults.MMS_BACKUP_ENABLED, null, Defaults.MMS_RESTORE_ENABLED, PreferenceKeys.MAX_SYNCED_DATE_MMS), - CALLLOG (R.string.calllog, R.string.call_with_field, PreferenceKeys.IMAP_FOLDER_CALLLOG, Defaults.CALLLOG_FOLDER, PreferenceKeys.BACKUP_CALLLOG, Defaults.CALLLOG_BACKUP_ENABLED, PreferenceKeys.RESTORE_CALLLOG, Defaults.CALLLOG_RESTORE_ENABLED, PreferenceKeys.MAX_SYNCED_DATE_CALLLOG); + SMS (R.string.sms, R.string.sms_with_field, PreferenceKeys.IMAP_FOLDER, Defaults.SMS_FOLDER, PreferenceKeys.BACKUP_SMS, Defaults.SMS_BACKUP_ENABLED, PreferenceKeys.RESTORE_SMS, Defaults.SMS_RESTORE_ENABLED, PreferenceKeys.MAX_SYNCED_DATE_SMS, new String[]{READ_SMS, READ_CONTACTS}), + MMS (R.string.mms, R.string.mms_with_field, PreferenceKeys.IMAP_FOLDER, Defaults.SMS_FOLDER, PreferenceKeys.BACKUP_MMS, Defaults.MMS_BACKUP_ENABLED, null, Defaults.MMS_RESTORE_ENABLED, PreferenceKeys.MAX_SYNCED_DATE_MMS, new String[]{READ_SMS, READ_CONTACTS}), + CALLLOG (R.string.calllog, R.string.call_with_field, PreferenceKeys.IMAP_FOLDER_CALLLOG, Defaults.CALLLOG_FOLDER, PreferenceKeys.BACKUP_CALLLOG, Defaults.CALLLOG_BACKUP_ENABLED, PreferenceKeys.RESTORE_CALLLOG, Defaults.CALLLOG_RESTORE_ENABLED, PreferenceKeys.MAX_SYNCED_DATE_CALLLOG, + // READ_CALL_LOG was introduced in API level 16 (Jelly Bean), previously part of READ_CONTACTS + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ? new String[]{ READ_CONTACTS, READ_CALL_LOG } : new String[]{ READ_CONTACTS } + ); public final int resId; public final int withField; @@ -16,6 +24,7 @@ public enum DataType { public final boolean backupEnabledByDefault; public final boolean restoreEnabledByDefault; public final String maxSyncedPreference; + public final String[] requiredPermissions; DataType(int resId, int withField, @@ -25,7 +34,8 @@ public enum DataType { boolean backupEnabledByDefault, String restoreEnabledPreference, boolean restoreEnabledByDefault, - String maxSyncedPreference) { + String maxSyncedPreference, + String[] requiredPermissions) { this.resId = resId; this.withField = withField; this.folderPreference = folderPreference; @@ -35,6 +45,7 @@ public enum DataType { this.restoreEnabledPreference = restoreEnabledPreference; this.restoreEnabledByDefault = restoreEnabledByDefault; this.maxSyncedPreference = maxSyncedPreference; + this.requiredPermissions = requiredPermissions; } public static class PreferenceKeys { diff --git a/app/src/main/java/com/zegoggles/smssync/mail/PersonLookup.java b/app/src/main/java/com/zegoggles/smssync/mail/PersonLookup.java index 1be0a7ded..23428e15d 100644 --- a/app/src/main/java/com/zegoggles/smssync/mail/PersonLookup.java +++ b/app/src/main/java/com/zegoggles/smssync/mail/PersonLookup.java @@ -38,7 +38,10 @@ public PersonLookup(ContentResolver resolver) { mResolver = resolver; } - /* Look up a person */ + /** + * Look up a person + * @throws SecurityException if the caller does not hold READ_CONTACTS + */ public @NonNull PersonRecord lookupPerson(final String address) { if (TextUtils.isEmpty(address)) { return new PersonRecord(0, null, null, "-1"); diff --git a/app/src/main/java/com/zegoggles/smssync/preferences/Preferences.java b/app/src/main/java/com/zegoggles/smssync/preferences/Preferences.java index c602582fe..707d7cdaa 100644 --- a/app/src/main/java/com/zegoggles/smssync/preferences/Preferences.java +++ b/app/src/main/java/com/zegoggles/smssync/preferences/Preferences.java @@ -58,6 +58,8 @@ import static com.zegoggles.smssync.preferences.Preferences.Keys.WIFI_ONLY; public class Preferences { + private static final int DEFAULT_APP_THEME = R.style.SMSBackupPlusTheme_Light; + private static final String ERROR = "error"; private final Context context; private final SharedPreferences preferences; @@ -112,9 +114,6 @@ public enum Keys { } private static final String CALLLOG_TYPES = "backup_calllog_types"; - public SharedPreferences getSharedPreferences() { - return preferences; - } public DataTypePreferences getDataTypePreferences() { return new DataTypePreferences(preferences); } public boolean isAppLogEnabled() { @@ -234,8 +233,8 @@ public boolean isFirstUse() { } } - public boolean setSmsDefaultPackage(String smsPackage) { - return preferences.edit().putString(SMS_DEFAULT_PACKAGE.key, smsPackage).commit(); + public void setSmsDefaultPackage(String smsPackage) { + preferences.edit().putString(SMS_DEFAULT_PACKAGE.key, smsPackage).commit(); } public String getSmsDefaultPackage() { @@ -246,8 +245,8 @@ public boolean hasSeenSmsDefaultPackageChangeDialog() { return preferences.contains(SMS_DEFAULT_PACKAGE_CHANGE_SEEN.key); } - public boolean setSeenSmsDefaultPackageChangeDialog() { - return preferences.edit().putBoolean(SMS_DEFAULT_PACKAGE_CHANGE_SEEN.key, true).commit(); + public void setSeenSmsDefaultPackageChangeDialog() { + preferences.edit().putBoolean(SMS_DEFAULT_PACKAGE_CHANGE_SEEN.key, true).commit(); } public void reset() { @@ -317,7 +316,8 @@ public boolean shouldShowAboutDialog() { final int lastSeenCode = preferences.getInt(LAST_VERSION_CODE.key, -1); if (lastSeenCode < code) { - return preferences.edit().putInt(LAST_VERSION_CODE.key, code).commit(); + preferences.edit().putInt(LAST_VERSION_CODE.key, code).commit(); + return true; } else { return false; } @@ -327,18 +327,20 @@ public boolean isUseOldScheduler() { return preferences.getBoolean(USE_OLD_SCHEDULER.key, false); } - public boolean setUseOldScheduler(boolean enabled) { - return preferences.edit().putBoolean(USE_OLD_SCHEDULER.key, enabled).commit(); + public void setUseOldScheduler(boolean enabled) { + preferences.edit().putBoolean(USE_OLD_SCHEDULER.key, enabled).commit(); } public int getAppTheme() { final String theme = preferences.getString(APP_THEME.key, null); if ("light".equals(theme)) { return R.style.SMSBackupPlusTheme_Light; + } else if ("dark".equals(theme)) { + return R.style.SMSBackupPlusTheme_Dark; } else if ("debug".equals(theme)) { return R.style.Debug; } else { - return R.style.SMSBackupPlusTheme_Dark; + return DEFAULT_APP_THEME; } } diff --git a/app/src/main/java/com/zegoggles/smssync/service/AlarmManagerDriver.java b/app/src/main/java/com/zegoggles/smssync/service/AlarmManagerDriver.java index 9b29973e6..29cdc637c 100644 --- a/app/src/main/java/com/zegoggles/smssync/service/AlarmManagerDriver.java +++ b/app/src/main/java/com/zegoggles/smssync/service/AlarmManagerDriver.java @@ -37,7 +37,6 @@ import static com.firebase.jobdispatcher.FirebaseJobDispatcher.SCHEDULE_RESULT_UNSUPPORTED_TRIGGER; import static com.zegoggles.smssync.App.LOCAL_LOGV; import static com.zegoggles.smssync.App.TAG; -import static com.zegoggles.smssync.service.BackupType.UNKNOWN; /** * Simple driver to emulate old AlarmManager backup scheduling behaviour. @@ -123,8 +122,7 @@ public List validate(RetryStrategy retryStrategy) { private static PendingIntent createPendingIntent(Context ctx, BackupType backupType) { final Intent intent = (new Intent(ctx, SmsBackupService.class)) - .setAction(backupType.name()) - .putExtra(BackupType.EXTRA, backupType.name()); + .setAction(backupType.name()); return PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT); } diff --git a/app/src/main/java/com/zegoggles/smssync/service/BackupConfig.java b/app/src/main/java/com/zegoggles/smssync/service/BackupConfig.java index 4a2dd053d..94342c252 100644 --- a/app/src/main/java/com/zegoggles/smssync/service/BackupConfig.java +++ b/app/src/main/java/com/zegoggles/smssync/service/BackupConfig.java @@ -9,7 +9,6 @@ public class BackupConfig { public final BackupImapStore imapStore; - public final boolean skip; public final int currentTry; public final int maxItemsPerSync; public final ContactGroup groupToBackup; @@ -19,7 +18,6 @@ public class BackupConfig { BackupConfig(@NonNull BackupImapStore imapStore, int currentTry, - boolean skip, int maxItemsPerSync, @NonNull ContactGroup groupToBackup, @NonNull BackupType backupType, @@ -30,7 +28,6 @@ public class BackupConfig { if (currentTry < 0) throw new IllegalArgumentException("currentTry < 0"); this.imapStore = imapStore; - this.skip = skip; this.currentTry = currentTry; this.maxItemsPerSync = maxItemsPerSync; this.groupToBackup = groupToBackup; @@ -41,7 +38,6 @@ public class BackupConfig { public BackupConfig retryWithStore(BackupImapStore store) { return new BackupConfig(store, currentTry + 1, - skip, maxItemsPerSync, groupToBackup, backupType, @@ -52,7 +48,6 @@ public BackupConfig retryWithStore(BackupImapStore store) { @Override public String toString() { return "BackupConfig{" + "imap=" + imapStore + - ", skip=" + skip + ", currentTry=" + currentTry + ", maxItemsPerSync=" + maxItemsPerSync + ", groupToBackup=" + groupToBackup + diff --git a/app/src/main/java/com/zegoggles/smssync/service/BackupItemsFetcher.java b/app/src/main/java/com/zegoggles/smssync/service/BackupItemsFetcher.java index 736daa2d5..1e13e6eed 100644 --- a/app/src/main/java/com/zegoggles/smssync/service/BackupItemsFetcher.java +++ b/app/src/main/java/com/zegoggles/smssync/service/BackupItemsFetcher.java @@ -32,6 +32,12 @@ public class BackupItemsFetcher { return performQuery(queryBuilder.buildQueryForDataType(dataType, group, max)); } + /** + * Gets the most recent timestamp for given datatype. + * @param dataType the data type + * @return timestamp + * @throws SecurityException if app does not hold necessary permissions + */ public long getMostRecentTimestamp(DataType dataType) { return getMostRecentTimestampForQuery(queryBuilder.buildMostRecentQueryForDataType(dataType)); } diff --git a/app/src/main/java/com/zegoggles/smssync/service/BackupJobs.java b/app/src/main/java/com/zegoggles/smssync/service/BackupJobs.java index cc46c12d7..bf815dbc5 100644 --- a/app/src/main/java/com/zegoggles/smssync/service/BackupJobs.java +++ b/app/src/main/java/com/zegoggles/smssync/service/BackupJobs.java @@ -183,12 +183,9 @@ private Job schedule(Job job) { } private @NonNull Job.Builder createBuilder(BackupType backupType) { - final Bundle extras = new Bundle(); - extras.putString(BackupType.EXTRA, backupType.name()); return firebaseJobDispatcher.newJobBuilder() .setReplaceCurrent(true) .setService(SmsJobService.class) - .setExtras(extras) .setTag(backupType.name()) .setRetryStrategy(defaultRetryStrategy()) .setConstraints(jobConstraints(backupType)); diff --git a/app/src/main/java/com/zegoggles/smssync/service/BackupTask.java b/app/src/main/java/com/zegoggles/smssync/service/BackupTask.java index d36da0de1..228a88ceb 100644 --- a/app/src/main/java/com/zegoggles/smssync/service/BackupTask.java +++ b/app/src/main/java/com/zegoggles/smssync/service/BackupTask.java @@ -37,6 +37,8 @@ import static com.zegoggles.smssync.mail.DataType.Defaults.MAX_SYNCED_DATE; import static com.zegoggles.smssync.mail.DataType.MMS; import static com.zegoggles.smssync.mail.DataType.SMS; +import static com.zegoggles.smssync.service.BackupType.MANUAL; +import static com.zegoggles.smssync.service.BackupType.SKIP; import static com.zegoggles.smssync.service.state.SmsSyncState.BACKUP; import static com.zegoggles.smssync.service.state.SmsSyncState.CALC; import static com.zegoggles.smssync.service.state.SmsSyncState.CANCELED_BACKUP; @@ -114,9 +116,11 @@ protected void onPreExecute() { } @Override protected BackupState doInBackground(BackupConfig... params) { - if (params == null || params.length == 0) throw new IllegalArgumentException("No config passed"); + if (params == null || params.length == 0) { + throw new IllegalArgumentException("No config passed"); + } final BackupConfig config = params[0]; - if (config.skip) { + if (config.backupType == SKIP) { return skip(config.typesToBackup); } else { return acquireLocksAndBackup(config); @@ -165,6 +169,8 @@ private BackupState fetchAndBackupItems(BackupConfig config) { return transition(ERROR, e); } catch (MessagingException e) { return transition(ERROR, e); + } catch (SecurityException e) { + return transition(ERROR, e); } finally { if (cursors != null) { cursors.close(); @@ -199,10 +205,14 @@ private BackupState handleAuthError(BackupConfig config, XOAuth2AuthenticationFa private BackupState skip(Iterable types) { appLog(R.string.app_log_skip_backup_skip_messages); for (DataType type : types) { - preferences.getDataTypePreferences().setMaxSyncedDate(type, fetcher.getMostRecentTimestamp(type)); + try { + preferences.getDataTypePreferences().setMaxSyncedDate(type, fetcher.getMostRecentTimestamp(type)); + } catch (SecurityException e ) { + return new BackupState(ERROR, 0, 0, MANUAL, type, e); + } } Log.i(TAG, "All messages skipped."); - return new BackupState(FINISHED_BACKUP, 0, 0, BackupType.MANUAL, null, null); + return new BackupState(FINISHED_BACKUP, 0, 0, MANUAL, null, null); } private void appLog(int id, Object... args) { diff --git a/app/src/main/java/com/zegoggles/smssync/service/BackupType.java b/app/src/main/java/com/zegoggles/smssync/service/BackupType.java index bf0bf941a..e8093617b 100644 --- a/app/src/main/java/com/zegoggles/smssync/service/BackupType.java +++ b/app/src/main/java/com/zegoggles/smssync/service/BackupType.java @@ -8,9 +8,8 @@ public enum BackupType { INCOMING(R.string.source_incoming), REGULAR(R.string.source_regular), UNKNOWN(R.string.source_unknown), - MANUAL(R.string.source_manual); - - public static final String EXTRA = "com.zegoggles.smssync.BackupTypeAsString"; + MANUAL(R.string.source_manual), + SKIP(R.string.source_manual); public final int resId; @@ -19,9 +18,8 @@ public enum BackupType { } public static BackupType fromIntent(Intent intent) { - if (intent.hasExtra(EXTRA)) { - final String name = intent.getStringExtra(EXTRA); - return fromName(name); + if (intent.getAction() != null) { + return fromName(intent.getAction()); } else { return UNKNOWN; } @@ -37,7 +35,8 @@ public static BackupType fromName(String name) { } public boolean isBackground() { - return this != MANUAL; + return this != MANUAL && this != SKIP; } + public boolean isRecurring() { return this == REGULAR; } } diff --git a/app/src/main/java/com/zegoggles/smssync/service/ServiceBase.java b/app/src/main/java/com/zegoggles/smssync/service/ServiceBase.java index 4e13c4222..b7e7cfd09 100644 --- a/app/src/main/java/com/zegoggles/smssync/service/ServiceBase.java +++ b/app/src/main/java/com/zegoggles/smssync/service/ServiceBase.java @@ -27,6 +27,7 @@ import android.net.NetworkInfo; import android.net.wifi.WifiManager; import android.os.Build; +import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; import android.support.annotation.NonNull; @@ -44,6 +45,7 @@ import com.zegoggles.smssync.service.state.State; import com.zegoggles.smssync.utils.AppLog; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static android.net.ConnectivityManager.TYPE_WIFI; import static com.zegoggles.smssync.App.LOCAL_LOGV; import static com.zegoggles.smssync.App.TAG; @@ -187,16 +189,21 @@ protected WifiManager getWifiManager() { @NonNull NotificationCompat.Builder createNotification(int resId) { return new NotificationCompat.Builder(this) - .setSmallIcon(R.drawable.ic_notification) - .setTicker(getString(resId)) - .setWhen(System.currentTimeMillis()) - .setOngoing(true); + .setSmallIcon(R.drawable.ic_notification) + .setTicker(getString(resId)) + .setWhen(System.currentTimeMillis()) + .setOngoing(true); } - PendingIntent getPendingIntent() { - return PendingIntent.getActivity(this, 0, - new Intent(this, MainActivity.class), - PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent getPendingIntent(@Nullable Bundle extras) { + final Intent intent = new Intent(getApplicationContext(), MainActivity.class); + if (extras != null) { + intent.putExtras(extras); + } + return PendingIntent.getActivity(getApplicationContext(), + 0, + intent, + FLAG_UPDATE_CURRENT); } boolean isConnectedViaWifi() { diff --git a/app/src/main/java/com/zegoggles/smssync/service/SmsBackupService.java b/app/src/main/java/com/zegoggles/smssync/service/SmsBackupService.java index ba9b64872..4ba48d5cd 100644 --- a/app/src/main/java/com/zegoggles/smssync/service/SmsBackupService.java +++ b/app/src/main/java/com/zegoggles/smssync/service/SmsBackupService.java @@ -16,12 +16,13 @@ package com.zegoggles.smssync.service; -import android.app.Notification; import android.content.Intent; import android.net.NetworkInfo; +import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; +import android.support.v4.content.ContextCompat; import android.text.format.DateFormat; import android.util.Log; import com.firebase.jobdispatcher.Job; @@ -30,12 +31,14 @@ import com.squareup.otto.Produce; import com.squareup.otto.Subscribe; import com.zegoggles.smssync.App; -import com.zegoggles.smssync.Consts; import com.zegoggles.smssync.R; +import com.zegoggles.smssync.activity.AppPermission; +import com.zegoggles.smssync.activity.MainActivity; import com.zegoggles.smssync.mail.BackupImapStore; import com.zegoggles.smssync.mail.DataType; import com.zegoggles.smssync.service.exception.BackupDisabledException; import com.zegoggles.smssync.service.exception.ConnectivityException; +import com.zegoggles.smssync.service.exception.MissingPermissionException; import com.zegoggles.smssync.service.exception.NoConnectionException; import com.zegoggles.smssync.service.exception.RequiresLoginException; import com.zegoggles.smssync.service.exception.RequiresWifiException; @@ -44,11 +47,17 @@ import java.util.Date; import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; +import static android.R.drawable.stat_sys_warning; +import static android.content.pm.PackageManager.PERMISSION_DENIED; import static com.zegoggles.smssync.App.LOCAL_LOGV; import static com.zegoggles.smssync.App.TAG; +import static com.zegoggles.smssync.activity.AppPermission.formatMissingPermissionDetails; import static com.zegoggles.smssync.service.BackupType.MANUAL; import static com.zegoggles.smssync.service.BackupType.REGULAR; +import static com.zegoggles.smssync.service.BackupType.SKIP; import static com.zegoggles.smssync.service.state.SmsSyncState.ERROR; import static com.zegoggles.smssync.service.state.SmsSyncState.FINISHED_BACKUP; import static com.zegoggles.smssync.service.state.SmsSyncState.INITIAL; @@ -83,33 +92,38 @@ public void onDestroy() { protected void handleIntent(final Intent intent) { if (intent == null) return; // NB: should not happen with START_NOT_STICKY final BackupType backupType = BackupType.fromIntent(intent); - if (LOCAL_LOGV) Log.v(TAG, "handleIntent(" + intent + - ", " + (intent.getExtras() == null ? "null" : intent.getExtras().keySet()) + - ", type="+backupType+")"); + if (LOCAL_LOGV) { + Log.v(TAG, "handleIntent(" + intent + + ", " + (intent.getExtras() == null ? "null" : intent.getExtras().keySet()) + + ", " + intent.getAction() + + ", type="+backupType+")"); + } appLog(R.string.app_log_backup_requested, getString(backupType.resId)); // Only start a backup if there's no other operation going on at this time. if (!isWorking() && !SmsRestoreService.isServiceWorking()) { - backup(backupType, intent.getBooleanExtra(Consts.KEY_SKIP_MESSAGES, false)); + backup(backupType); } else { appLog(R.string.app_log_skip_backup_already_running); } } - private void backup(BackupType backupType, boolean skip) { + private void backup(BackupType backupType) { getNotifier().cancel(NOTIFICATION_ID_WARNING); try { // set initial state mState = new BackupState(INITIAL, 0, 0, backupType, null, null); EnumSet enabledTypes = getEnabledBackupTypes(); - if (!skip) { + checkPermissions(enabledTypes); + if (backupType != SKIP) { checkCredentials(); - checkConnectivity(backupType); + if (getPreferences().isUseOldScheduler()) { + legacyCheckConnectivity(); + } } - appLog(R.string.app_log_start_backup, backupType); - getBackupTask().execute(getBackupConfig(backupType, enabledTypes, getBackupImapStore(), skip)); + getBackupTask().execute(getBackupConfig(backupType, enabledTypes, getBackupImapStore())); } catch (MessagingException e) { Log.w(TAG, e); moveToState(mState.transition(ERROR, e)); @@ -120,17 +134,31 @@ private void backup(BackupType backupType, boolean skip) { moveToState(mState.transition(ERROR, e)); } catch (BackupDisabledException e) { moveToState(mState.transition(FINISHED_BACKUP, e)); + } catch (MissingPermissionException e) { + moveToState(mState.transition(ERROR, e)); + } + } + + private void checkPermissions(EnumSet enabledTypes) throws MissingPermissionException { + Set missing = new HashSet(); + for (DataType dataType : enabledTypes) { + for (String permission : dataType.requiredPermissions) { + if (ContextCompat.checkSelfPermission(this, permission) == PERMISSION_DENIED) { + missing.add(permission); + } + } + } + if (!missing.isEmpty()) { + throw new MissingPermissionException(missing); } } private BackupConfig getBackupConfig(BackupType backupType, EnumSet enabledTypes, - BackupImapStore imapStore, - boolean skip) { + BackupImapStore imapStore) { return new BackupConfig( imapStore, 0, - skip, getPreferences().getMaxItemsPerSync(), getPreferences().getBackupContactGroup(), backupType, @@ -153,12 +181,7 @@ private void checkCredentials() throws RequiresLoginException { } } - private void checkConnectivity(BackupType backupType) throws ConnectivityException { - if (backupType != MANUAL && !getPreferences().isUseOldScheduler()) { - Log.d(TAG, "skipping connectivity check, running with new scheduler"); - return; - } - + private void legacyCheckConnectivity() throws ConnectivityException { NetworkInfo active = getConnectivityManager().getActiveNetworkInfo(); if (active == null || !active.isConnectedOrConnecting()) { throw new NoConnectionException(); @@ -214,21 +237,29 @@ private void handleErrorState(BackupState state) { appLog(R.string.app_log_backup_failed_authentication, state.getDetailedErrorMessage(getResources())); if (shouldNotifyUser(state)) { - notifyUser(android.R.drawable.stat_sys_warning, - NOTIFICATION_ID_WARNING, + notifyUser(NOTIFICATION_ID_WARNING, notificationBuilder(stat_sys_warning, getString(R.string.notification_auth_failure), - getString(getAuthPreferences().useXOAuth() ? R.string.status_auth_failure_details_xoauth : R.string.status_auth_failure_details_plain)); + getString(getAuthPreferences().useXOAuth() ? R.string.status_auth_failure_details_xoauth : R.string.status_auth_failure_details_plain))); } } else if (state.isConnectivityError()) { appLog(R.string.app_log_backup_failed_connectivity, state.getDetailedErrorMessage(getResources())); + } else if (state.isPermissionException()) { + if (state.backupType != MANUAL) { + Bundle extras = new Bundle(); + extras.putStringArray(MainActivity.EXTRA_PERMISSIONS, state.getMissingPermissions()); + + notifyUser(NOTIFICATION_ID_WARNING, notificationBuilder(R.drawable.ic_notification, + getString(R.string.notification_missing_permission), + formatMissingPermissionDetails(getResources(), state.getMissingPermissions())) + .setContentIntent(getPendingIntent(extras))); + } } else { appLog(R.string.app_log_backup_failed_general_error, state.getDetailedErrorMessage(getResources())); if (shouldNotifyUser(state)) { - notifyUser(android.R.drawable.stat_sys_warning, - NOTIFICATION_ID_WARNING, + notifyUser(NOTIFICATION_ID_WARNING, notificationBuilder(stat_sys_warning, getString(R.string.notification_general_error), - state.getErrorMessage(getResources())); + state.getErrorMessage(getResources()))); } } } @@ -242,7 +273,7 @@ private void notifyAboutBackup(BackupState state) { NotificationCompat.Builder builder = createNotification(R.string.status_backup); notification = builder.setContentTitle(getString(R.string.status_backup)) .setContentText(state.getNotificationLabel(getResources())) - .setContentIntent(getPendingIntent()) + .setContentIntent(getPendingIntent(null)) .build(); startForeground(BACKUP_ID, notification); } @@ -261,18 +292,20 @@ private void scheduleNextBackup(BackupState state) { } // else job already persisted } - protected void notifyUser(int icon, int notificationId, String title, String text) { - Notification n = new NotificationCompat.Builder(this) - .setSmallIcon(icon) - .setWhen(System.currentTimeMillis()) - .setOnlyAlertOnce(true) - .setAutoCancel(true) - .setContentText(text) - .setTicker(getString(R.string.app_name)) - .setContentTitle(title) - .setContentIntent(getPendingIntent()) - .build(); - getNotifier().notify(notificationId, n); + void notifyUser(int notificationId, NotificationCompat.Builder builder) { + getNotifier().notify(notificationId, builder.build()); + } + + private NotificationCompat.Builder notificationBuilder(int icon, String title, String text) { + return new NotificationCompat.Builder(this) + .setSmallIcon(icon) + .setWhen(System.currentTimeMillis()) + .setOnlyAlertOnce(true) + .setAutoCancel(true) + .setContentText(text) + .setTicker(getString(R.string.app_name)) + .setContentTitle(title) + .setContentIntent(getPendingIntent(null)); } protected BackupJobs getBackupJobs() { diff --git a/app/src/main/java/com/zegoggles/smssync/service/SmsJobService.java b/app/src/main/java/com/zegoggles/smssync/service/SmsJobService.java index 21f20b738..58383713c 100644 --- a/app/src/main/java/com/zegoggles/smssync/service/SmsJobService.java +++ b/app/src/main/java/com/zegoggles/smssync/service/SmsJobService.java @@ -34,6 +34,7 @@ public class SmsJobService extends JobService { + /** job parameters keyed by job tag / {@link BackupType} */ private Map jobs = new HashMap(); @Override @@ -75,9 +76,10 @@ public boolean onStartJob(JobParameters jobParameters) { getBackupJobs().scheduleIncoming(); return false; } else if (shouldRun(jobParameters)) { - startService(new Intent(this, SmsBackupService.class).putExtras(extras)); - final String backupType = extras == null ? jobParameters.getTag() : extras.getString(BackupType.EXTRA); - jobs.put(backupType, jobParameters); + startService(new Intent(this, SmsBackupService.class) + .setAction(jobParameters.getTag()) + .putExtras(extras)); + jobs.put(jobParameters.getTag(), jobParameters); return true; } else { Log.d(TAG, "skipping run"); @@ -109,10 +111,11 @@ public void backupStateChanged(BackupState state) { final JobParameters jobParameters = jobs.remove(state.backupType.name()); if (jobParameters != null) { + final boolean needsReschedule = state.isError() && !state.isPermissionException(); if (LOCAL_LOGV) { - Log.v(TAG, "jobFinished(" + jobParameters + ", isError=" + state.isError() + ")"); + Log.v(TAG, "jobFinished(" + jobParameters + ", isError=" + state.isError() + ", needsReschedule="+needsReschedule+")"); } - jobFinished(jobParameters, state.isError()); + jobFinished(jobParameters, needsReschedule); } else { Log.w(TAG, "unknown job for state "+state); } diff --git a/app/src/main/java/com/zegoggles/smssync/service/SmsRestoreService.java b/app/src/main/java/com/zegoggles/smssync/service/SmsRestoreService.java index dd086654b..6af105a08 100644 --- a/app/src/main/java/com/zegoggles/smssync/service/SmsRestoreService.java +++ b/app/src/main/java/com/zegoggles/smssync/service/SmsRestoreService.java @@ -143,7 +143,7 @@ public boolean accept(File dir, String name) { notification = createNotification(R.string.status_restore) .setContentTitle(getString(R.string.status_restore)) .setContentText(state.getNotificationLabel(getResources())) - .setContentIntent(getPendingIntent()) + .setContentIntent(getPendingIntent(null)) .build(); startForeground(RESTORE_ID, notification); diff --git a/app/src/main/java/com/zegoggles/smssync/service/exception/MissingPermissionException.java b/app/src/main/java/com/zegoggles/smssync/service/exception/MissingPermissionException.java new file mode 100644 index 000000000..4a8004f5c --- /dev/null +++ b/app/src/main/java/com/zegoggles/smssync/service/exception/MissingPermissionException.java @@ -0,0 +1,12 @@ +package com.zegoggles.smssync.service.exception; + + +import java.util.Set; + +public class MissingPermissionException extends Exception { + public final Set permissions; + + public MissingPermissionException(Set permissions) { + this.permissions = permissions; + } +} diff --git a/app/src/main/java/com/zegoggles/smssync/service/state/BackupState.java b/app/src/main/java/com/zegoggles/smssync/service/state/BackupState.java index 14fde8f80..b2e3b3576 100644 --- a/app/src/main/java/com/zegoggles/smssync/service/state/BackupState.java +++ b/app/src/main/java/com/zegoggles/smssync/service/state/BackupState.java @@ -10,8 +10,8 @@ import static com.zegoggles.smssync.service.state.SmsSyncState.INITIAL; public class BackupState extends State { - public final int currentSyncedItems, itemsToSync; public final BackupType backupType; + public final int currentSyncedItems, itemsToSync; public BackupState() { this(INITIAL, 0, 0, UNKNOWN, null, null); diff --git a/app/src/main/java/com/zegoggles/smssync/service/state/State.java b/app/src/main/java/com/zegoggles/smssync/service/state/State.java index bc34a70f4..43e2324e8 100644 --- a/app/src/main/java/com/zegoggles/smssync/service/state/State.java +++ b/app/src/main/java/com/zegoggles/smssync/service/state/State.java @@ -10,6 +10,7 @@ import com.zegoggles.smssync.mail.DataType; import com.zegoggles.smssync.service.exception.ConnectivityException; import com.zegoggles.smssync.service.exception.LocalizableException; +import com.zegoggles.smssync.service.exception.MissingPermissionException; import com.zegoggles.smssync.service.exception.RequiresLoginException; import java.util.EnumSet; @@ -19,7 +20,7 @@ public abstract class State { public final Exception exception; public final @Nullable DataType dataType; - public State(SmsSyncState state, @Nullable DataType dataType, Exception exception) { + State(SmsSyncState state, @Nullable DataType dataType, Exception exception) { this.state = state; this.exception = exception; this.dataType = dataType; @@ -61,11 +62,11 @@ public boolean isInitialState() { public boolean isRunning() { return EnumSet.of( - SmsSyncState.LOGIN, - SmsSyncState.CALC, - SmsSyncState.BACKUP, - SmsSyncState.RESTORE, - SmsSyncState.UPDATING_THREADS).contains(state); + SmsSyncState.LOGIN, + SmsSyncState.CALC, + SmsSyncState.BACKUP, + SmsSyncState.RESTORE, + SmsSyncState.UPDATING_THREADS).contains(state); } public boolean isFinished() { @@ -80,6 +81,10 @@ public boolean isAuthException() { exception instanceof RequiresLoginException; } + public boolean isPermissionException() { + return exception instanceof MissingPermissionException; + } + public boolean isConnectivityError() { return exception instanceof ConnectivityException; } @@ -100,4 +105,13 @@ public String getNotificationLabel(Resources resources) { default: return null; } } + + public String[] getMissingPermissions() { + if (isPermissionException()) { + MissingPermissionException mpe = (MissingPermissionException)exception; + return mpe.permissions.toArray(new String[mpe.permissions.size()]); + } else { + return new String[0]; + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 099982c23..d9ea773bd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,20 @@ Login error SMSBackup+ login error + Permission problem + + + The following permission is required: %1$s. + The following permissions are required: %1$s. + + + Send and view SMS Messages + Read Call Log + Access your contacts + Unknown + + SMSBackup+ permission problem + XOAuth authorization error. Please make sure you enabled IMAP in your Gmail account settings. IMAP authorization error. Make sure login and password are set correctly. diff --git a/app/src/test/java/com/zegoggles/smssync/service/AlarmManagerDriverTest.java b/app/src/test/java/com/zegoggles/smssync/service/AlarmManagerDriverTest.java index 808f4a6cb..2ac565c5f 100644 --- a/app/src/test/java/com/zegoggles/smssync/service/AlarmManagerDriverTest.java +++ b/app/src/test/java/com/zegoggles/smssync/service/AlarmManagerDriverTest.java @@ -97,7 +97,7 @@ private Intent assertAlarmScheduled(String ofExpectedType) { assertThat(shadowPendingIntent.getFlags()).isEqualTo(FLAG_UPDATE_CURRENT); assertThat(shadowPendingIntent.getSavedIntent().getAction()).isNotEmpty(); - assertThat(shadowPendingIntent.getSavedIntent().getStringExtra(BackupType.EXTRA)) + assertThat(shadowPendingIntent.getSavedIntent().getAction()) .isEqualTo(ofExpectedType); return shadowPendingIntent.getSavedIntent(); diff --git a/app/src/test/java/com/zegoggles/smssync/service/BackupConfigTest.java b/app/src/test/java/com/zegoggles/smssync/service/BackupConfigTest.java index 9bd8ebec4..da29cef37 100644 --- a/app/src/test/java/com/zegoggles/smssync/service/BackupConfigTest.java +++ b/app/src/test/java/com/zegoggles/smssync/service/BackupConfigTest.java @@ -18,7 +18,6 @@ public class BackupConfigTest { public void shouldCheckForDataTypesEmpty() throws Exception { new BackupConfig(mock(BackupImapStore.class), 0, - false, -1, ContactGroup.EVERYBODY, BackupType.MANUAL, @@ -32,7 +31,6 @@ public void shouldCheckForDataTypesEmpty() throws Exception { public void shouldCheckForDataTypesNull() throws Exception { new BackupConfig(mock(BackupImapStore.class), 0, - false, -1, ContactGroup.EVERYBODY, BackupType.MANUAL, @@ -45,7 +43,6 @@ public void shouldCheckForDataTypesNull() throws Exception { public void shouldCheckForPositiveTry() throws Exception { new BackupConfig(mock(BackupImapStore.class), -1, - false, -1, ContactGroup.EVERYBODY, BackupType.MANUAL, diff --git a/app/src/test/java/com/zegoggles/smssync/service/BackupTaskTest.java b/app/src/test/java/com/zegoggles/smssync/service/BackupTaskTest.java index b34498f49..eeaee5be7 100644 --- a/app/src/test/java/com/zegoggles/smssync/service/BackupTaskTest.java +++ b/app/src/test/java/com/zegoggles/smssync/service/BackupTaskTest.java @@ -83,7 +83,7 @@ public class BackupTaskTest { } private BackupConfig getBackupConfig(EnumSet types) { - return new BackupConfig(store, 0, false, 100, new ContactGroup(-1), BackupType.MANUAL, types, + return new BackupConfig(store, 0, 100, new ContactGroup(-1), BackupType.MANUAL, types, false ); } @@ -173,7 +173,7 @@ public void shouldBackupMultipleTypes() throws Exception { when(fetcher.getMostRecentTimestamp(any(DataType.class))).thenReturn(-23L); BackupState finalState = task.doInBackground(new BackupConfig( - store, 0, true, 100, new ContactGroup(-1), BackupType.MANUAL, EnumSet.of(SMS), false + store, 0, 100, new ContactGroup(-1), BackupType.SKIP, EnumSet.of(SMS), false ) ); verify(dataTypePreferences).setMaxSyncedDate(DataType.SMS, -23); diff --git a/app/src/test/java/com/zegoggles/smssync/service/SmsBackupServiceTest.java b/app/src/test/java/com/zegoggles/smssync/service/SmsBackupServiceTest.java index e243a8cb9..175034598 100644 --- a/app/src/test/java/com/zegoggles/smssync/service/SmsBackupServiceTest.java +++ b/app/src/test/java/com/zegoggles/smssync/service/SmsBackupServiceTest.java @@ -5,6 +5,7 @@ import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.support.v4.app.NotificationCompat; import android.telephony.TelephonyManager; import com.fsck.k9.mail.MessagingException; import com.zegoggles.smssync.contacts.ContactGroup; @@ -33,7 +34,10 @@ import java.util.EnumSet; import java.util.List; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static com.google.common.truth.Truth.assertThat; +import static com.zegoggles.smssync.service.BackupType.MANUAL; +import static com.zegoggles.smssync.service.BackupType.REGULAR; import static org.mockito.Matchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -43,17 +47,10 @@ @RunWith(RobolectricTestRunner.class) public class SmsBackupServiceTest { - private static class UserNotification { - final String title, text; - private UserNotification(String title, String text) { - this.title = title; - this.text = text; - } - } SmsBackupService service; ShadowConnectivityManager shadowConnectivityManager; ShadowWifiManager shadowWifiManager; - List sentNotifications; + List sentNotifications; @Mock AuthPreferences authPreferences; @Mock Preferences preferences; @@ -63,16 +60,17 @@ private UserNotification(String title, String text) { @Before public void before() { initMocks(this); - sentNotifications = new ArrayList(); + sentNotifications = new ArrayList(); service = new SmsBackupService() { @Override public Context getApplicationContext() { return RuntimeEnvironment.application; } @Override public Resources getResources() { return getApplicationContext().getResources(); } @Override protected BackupTask getBackupTask() { return backupTask; } @Override protected BackupJobs getBackupJobs() { return backupJobs; } @Override protected Preferences getPreferences() { return preferences; } + @Override public int checkPermission(String permission, int pid, int uid) { return PERMISSION_GRANTED; } @Override protected AuthPreferences getAuthPreferences() { return authPreferences; } - @Override protected void notifyUser(int icon, int notificationId, String title, String text) { - sentNotifications.add(new UserNotification(title, text)); + @Override protected void notifyUser(int icon, NotificationCompat.Builder builder) { + sentNotifications.add(builder); } }; shadowConnectivityManager = shadowOf(service.getConnectivityManager()); @@ -93,15 +91,13 @@ private UserNotification(String title, String text) { } @Test public void shouldTriggerBackupWithManualIntent() throws Exception { - Intent intent = new Intent(); - intent.putExtra(BackupType.EXTRA, BackupType.MANUAL.name()); + Intent intent = new Intent(MANUAL.name()); service.handleIntent(intent); verify(backupTask).execute(any(BackupConfig.class)); } @Test public void shouldCheckForConnectivityBeforeBackingUp() throws Exception { - Intent intent = new Intent(); - intent.putExtra(BackupType.EXTRA, BackupType.MANUAL.name()); + Intent intent = new Intent(MANUAL.name()); shadowConnectivityManager.setActiveNetworkInfo(null); service.handleIntent(intent); @@ -113,8 +109,7 @@ private UserNotification(String title, String text) { @Test public void shouldNotCheckForConnectivityBeforeBackingUpWithNewScheduler() throws Exception { when(preferences.isUseOldScheduler()).thenReturn(false); - Intent intent = new Intent(); - intent.putExtra(BackupType.EXTRA, BackupType.REGULAR.name()); + Intent intent = new Intent(REGULAR.name()); shadowConnectivityManager.setActiveNetworkInfo(null); shadowConnectivityManager.setBackgroundDataSetting(true); service.handleIntent(intent); @@ -166,23 +161,20 @@ private UserNotification(String title, String text) { } @Test public void shouldPassInCorrectBackupConfig() throws Exception { - Intent intent = new Intent(); - intent.putExtra(BackupType.EXTRA, BackupType.MANUAL.name()); + Intent intent = new Intent(MANUAL.name()); ArgumentCaptor config = ArgumentCaptor.forClass(BackupConfig.class); service.handleIntent(intent); verify(backupTask).execute(config.capture()); BackupConfig backupConfig = config.getValue(); - assertThat(backupConfig.backupType).isEqualTo(BackupType.MANUAL); + assertThat(backupConfig.backupType).isEqualTo(MANUAL); assertThat(backupConfig.currentTry).isEqualTo(0); - assertThat(backupConfig.skip).isFalse(); } @Test public void shouldScheduleNextRegularBackupAfterFinished() throws Exception { shadowConnectivityManager.setBackgroundDataSetting(true); - Intent intent = new Intent(); - intent.putExtra(BackupType.EXTRA, BackupType.REGULAR.name()); + Intent intent = new Intent(REGULAR.name()); service.handleIntent(intent); verify(backupTask).execute(any(BackupConfig.class)); @@ -191,14 +183,13 @@ private UserNotification(String title, String text) { verify(backupJobs).scheduleRegular(); - assertThat(shadowOf(service).isStoppedBySelf()); - assertThat(shadowOf(service).isForegroundStopped()); + assertThat(shadowOf(service).isStoppedBySelf()).isTrue(); + assertThat(shadowOf(service).isForegroundStopped()).isTrue(); } @Test public void shouldCheckForValidStore() throws Exception { when(authPreferences.getStoreUri()).thenReturn("invalid"); - Intent intent = new Intent(); - intent.putExtra(BackupType.EXTRA, BackupType.MANUAL.name()); + Intent intent = new Intent(MANUAL.name()); service.handleIntent(intent); verifyZeroInteractions(backupTask); @@ -207,24 +198,22 @@ private UserNotification(String title, String text) { @Test public void shouldNotifyUserAboutErrorInManualMode() throws Exception { when(authPreferences.getStoreUri()).thenReturn("invalid"); - Intent intent = new Intent(); - intent.putExtra(BackupType.EXTRA, BackupType.MANUAL.name()); + Intent intent = new Intent(MANUAL.name()); service.handleIntent(intent); verifyZeroInteractions(backupTask); assertNotificationShown("SMSBackup+ error", "No valid IMAP URI: invalid"); - assertThat(shadowOf(service).isStoppedBySelf()); - assertThat(shadowOf(service).isForegroundStopped()); + assertThat(shadowOf(service).isStoppedBySelf()).isTrue(); + assertThat(shadowOf(service).isForegroundStopped()).isTrue(); } - - private void assertNotificationShown(String title, String message) { + private void assertNotificationShown(CharSequence title, CharSequence message) { assertThat(sentNotifications).hasSize(1); - UserNotification u = sentNotifications.get(0); - assertThat(u.title).isEqualTo(title); - assertThat(u.text).isEqualTo(message); + NotificationCompat.Builder u = sentNotifications.get(0); + assertThat(u.mContentTitle).isEqualTo(title); + assertThat(u.mContentText).isEqualTo(message); } private NetworkInfo connectedViaEdge() {