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() {