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