diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml
index 20c5f550b..6bea7ff0a 100644
--- a/OpenKeychain/src/main/AndroidManifest.xml
+++ b/OpenKeychain/src/main/AndroidManifest.xml
@@ -95,15 +95,6 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Keychain.Light">
-
-
-
-
-
-
@@ -1070,21 +1061,6 @@
android:resource="@xml/sync_adapter_contacts_structure" />
-
-
-
-
-
-
-
-
-
.
+ */
+
+package org.sufficientlysecure.keychain.keysync;
+
+
+import java.util.concurrent.TimeUnit;
+
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+
+import androidx.work.Constraints.Builder;
+import androidx.work.NetworkType;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.PeriodicWorkRequest;
+import androidx.work.WorkManager;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Preferences;
+import timber.log.Timber;
+
+
+public class KeyserverSyncManager {
+ private static final long SYNC_INTERVAL = 3;
+ private static final TimeUnit SYNC_INTERVAL_UNIT = TimeUnit.DAYS;
+
+ private static final String WORK_TAG = "keyserverSync";
+
+ public static void updateKeyserverSyncSchedule(Context context, boolean forceReschedule) {
+ Preferences prefs = Preferences.getPreferences(context);
+ if (!forceReschedule && prefs.isKeyserverSyncScheduled() != prefs.isKeyserverSyncEnabled()) {
+ return;
+ }
+ WorkManager workManager = WorkManager.getInstance();
+ if (workManager == null) {
+ Timber.e("WorkManager unavailable!");
+ return;
+ }
+ workManager.cancelAllWorkByTag(WORK_TAG);
+
+ if (!prefs.isKeyserverSyncEnabled()) {
+ return;
+ }
+
+ Builder constraints = new Builder()
+ .setRequiredNetworkType(prefs.getWifiOnlySync() ? NetworkType.UNMETERED : NetworkType.CONNECTED)
+ .setRequiresBatteryNotLow(true);
+ if (VERSION.SDK_INT >= VERSION_CODES.M) {
+ constraints.setRequiresDeviceIdle(true);
+ }
+
+ PeriodicWorkRequest workRequest =
+ new PeriodicWorkRequest.Builder(KeyserverSyncWorker.class, SYNC_INTERVAL, SYNC_INTERVAL_UNIT)
+ .setConstraints(constraints.build())
+ .addTag(WORK_TAG)
+ .build();
+ workManager.enqueue(workRequest);
+
+ prefs.setKeyserverSyncScheduled(true);
+ }
+
+ public static void runSyncNow() {
+ WorkManager workManager = WorkManager.getInstance();
+ if (workManager == null) {
+ Timber.e("WorkManager unavailable!");
+ return;
+ }
+
+ OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(KeyserverSyncWorker.class).build();
+ workManager.enqueue(workRequest);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keysync/KeyserverSyncWorker.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keysync/KeyserverSyncWorker.java
new file mode 100644
index 000000000..240389d83
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keysync/KeyserverSyncWorker.java
@@ -0,0 +1,198 @@
+package org.sufficientlysecure.keychain.keysync;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import androidx.work.Worker;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
+import org.sufficientlysecure.keychain.network.orbot.OrbotHelper;
+import org.sufficientlysecure.keychain.operations.ImportOperation;
+import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult;
+import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
+import org.sufficientlysecure.keychain.provider.LastUpdateInteractor;
+import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
+import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
+import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.Preferences;
+import timber.log.Timber;
+
+
+public class KeyserverSyncWorker extends Worker {
+ // time since last update after which a key should be updated again, in s
+ private static final long KEY_STALE_THRESHOLD_MILLIS =
+ Constants.DEBUG_KEYSERVER_SYNC ? 1 : TimeUnit.DAYS.toMillis(7);
+ // Time taken by Orbot before a new circuit is created
+ private static final int ORBOT_CIRCUIT_TIMEOUT_SECONDS =
+ Constants.DEBUG_KEYSERVER_SYNC ? 2 : (int) TimeUnit.MINUTES.toSeconds(10);
+
+ private AtomicBoolean cancellationSignal = new AtomicBoolean(false);
+ private LastUpdateInteractor lastUpdateInteractor;
+ private KeyWritableRepository keyWritableRepository;
+ private Preferences preferences;
+
+ @NonNull
+ @Override
+ public WorkerResult doWork() {
+ lastUpdateInteractor = LastUpdateInteractor.create(getApplicationContext());
+ keyWritableRepository = KeyWritableRepository.create(getApplicationContext());
+ preferences = Preferences.getPreferences(getApplicationContext());
+
+ Timber.d("Starting key sync…");
+ ImportKeyResult result = updateKeysFromKeyserver(getApplicationContext());
+ return handleUpdateResult(result);
+ }
+
+ private ImportKeyResult updateKeysFromKeyserver(Context context) {
+ long staleKeyThreshold = System.currentTimeMillis() - KEY_STALE_THRESHOLD_MILLIS;
+ List staleKeyFingerprints =
+ lastUpdateInteractor.getFingerprintsForKeysOlderThan(staleKeyThreshold, TimeUnit.MILLISECONDS);
+ List staleKeyParcelableKeyRings = fingerprintListToParcelableKeyRings(staleKeyFingerprints);
+
+ if (isStopped()) { // if we've already been cancelled
+ return new ImportKeyResult(OperationResult.RESULT_CANCELLED,
+ new OperationResult.OperationLog());
+ }
+
+ // no explicit proxy, retrieve from preferences. Check if we should do a staggered sync
+ CryptoInputParcel cryptoInputParcel = CryptoInputParcel.createCryptoInputParcel();
+ if (preferences.getParcelableProxy().isTorEnabled()) {
+ return staggeredUpdate(context, staleKeyParcelableKeyRings, cryptoInputParcel);
+ } else {
+ return directUpdate(context, staleKeyParcelableKeyRings, cryptoInputParcel);
+ }
+ }
+
+ private List fingerprintListToParcelableKeyRings(List staleKeyFingerprints) {
+ ArrayList result = new ArrayList<>(staleKeyFingerprints.size());
+ for (byte[] fingerprint : staleKeyFingerprints) {
+ Timber.d("Keyserver sync: Updating %s", KeyFormattingUtils.beautifyKeyId(fingerprint));
+ result.add(ParcelableKeyRing.createFromReference(fingerprint, null, null, null));
+ }
+ return result;
+ }
+
+ private ImportKeyResult directUpdate(Context context, List keyList,
+ CryptoInputParcel cryptoInputParcel) {
+ Timber.d("Starting normal update");
+ ImportOperation importOp = new ImportOperation(context, keyWritableRepository, null);
+ return importOp.execute(
+ ImportKeyringParcel.createImportKeyringParcel(keyList, preferences.getPreferredKeyserver()),
+ cryptoInputParcel
+ );
+ }
+
+ /**
+ * Since we're returning START_REDELIVER_INTENT in onStartCommand, we need to remember to call
+ * stopSelf(int) to prevent the Intent from being redelivered if our work is already done
+ *
+ * @param result
+ * result of keyserver sync
+ */
+ private WorkerResult handleUpdateResult(ImportKeyResult result) {
+ if (result.isPending()) {
+ Timber.d("Orbot required for sync but not running, attempting to start");
+ // result is pending due to Orbot not being started
+ // try to start it silently, if disabled show notifications
+ new OrbotHelper.SilentStartManager() {
+ @Override
+ protected void onOrbotStarted() {
+ }
+
+ @Override
+ protected void onSilentStartDisabled() {
+ OrbotRequiredDialogActivity.showOrbotRequiredNotification(getApplicationContext());
+ }
+ }.startOrbotAndListen(getApplicationContext(), false);
+ return WorkerResult.RETRY;
+ } else if (isStopped()) {
+ Timber.d("Keyserver sync cancelled");
+ return WorkerResult.FAILURE;
+ } else {
+ Timber.d("Keyserver sync completed: Updated: %d, Failed: %d", result.mUpdatedKeys, result.mBadKeys);
+ return WorkerResult.SUCCESS;
+ }
+ }
+
+ /**
+ * will perform a staggered update of user's keys using delays to ensure new Tor circuits, as
+ * performed by parcimonie. Relevant issue and method at:
+ * https://github.com/open-keychain/open-keychain/issues/1337
+ *
+ * @return result of the sync
+ */
+ private ImportKeyResult staggeredUpdate(Context context, List keyList,
+ CryptoInputParcel cryptoInputParcel) {
+ Timber.d("Starting staggered update");
+ // final int WEEK_IN_SECONDS = (int) TimeUnit.DAYS.toSeconds(7);
+ // we are limiting our randomness to ORBOT_CIRCUIT_TIMEOUT_SECONDS for now
+ final int WEEK_IN_SECONDS = 0;
+
+ ImportOperation.KeyImportAccumulator accumulator
+ = new ImportOperation.KeyImportAccumulator(keyList.size(), null);
+
+ // so that the first key can be updated without waiting. This is so that there isn't a
+ // large gap between a "Start Orbot" notification and the next key update
+ boolean first = true;
+
+ for (ParcelableKeyRing keyRing : keyList) {
+ int waitTime;
+ int staggeredTime = new Random().nextInt(1 + 2 * (WEEK_IN_SECONDS / keyList.size()));
+ if (staggeredTime >= ORBOT_CIRCUIT_TIMEOUT_SECONDS) {
+ waitTime = staggeredTime;
+ } else {
+ waitTime = ORBOT_CIRCUIT_TIMEOUT_SECONDS
+ + new Random().nextInt(1 + ORBOT_CIRCUIT_TIMEOUT_SECONDS);
+ }
+
+ if (first) {
+ waitTime = 0;
+ first = false;
+ }
+
+ Timber.d("Updating key with a wait time of %d seconds", waitTime);
+ try {
+ Thread.sleep(waitTime * 1000);
+ } catch (InterruptedException e) {
+ Timber.e(e, "Exception during sleep between key updates");
+ // skip this one
+ continue;
+ }
+ ArrayList keyWrapper = new ArrayList<>();
+ keyWrapper.add(keyRing);
+ if (isStopped()) {
+ return new ImportKeyResult(ImportKeyResult.RESULT_CANCELLED,
+ new OperationResult.OperationLog());
+ }
+ ImportKeyResult result =
+ new ImportOperation(context, keyWritableRepository, null, cancellationSignal)
+ .execute(
+ ImportKeyringParcel.createImportKeyringParcel(
+ keyWrapper,
+ preferences.getPreferredKeyserver()
+ ),
+ cryptoInputParcel
+ );
+ if (result.isPending()) {
+ return result;
+ }
+ accumulator.accumulateKeyImport(result);
+ }
+ return accumulator.getConsolidatedResult();
+ }
+
+ @Override
+ public void onStopped() {
+ super.onStopped();
+ cancellationSignal.set(true);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/NetworkReceiver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/NetworkReceiver.java
deleted file mode 100644
index ec3cebf0f..000000000
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/NetworkReceiver.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2017 Schürmann & Breitmoser GbR
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.sufficientlysecure.keychain.network;
-
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-
-import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
-import timber.log.Timber;
-
-
-public class NetworkReceiver extends BroadcastReceiver {
-
- @Override
- public void onReceive(Context context, Intent intent) {
-
- ConnectivityManager conn = (ConnectivityManager)
- context.getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo networkInfo = conn.getActiveNetworkInfo();
- boolean isTypeWifi = (networkInfo != null) &&
- (networkInfo.getType() == ConnectivityManager.TYPE_WIFI);
- boolean isConnected = (networkInfo != null) && networkInfo.isConnected();
-
- if (isTypeWifi && isConnected) {
-
- // broadcaster receiver disabled
- setWifiReceiverComponent(false, context);
- Intent serviceIntent = new Intent(context, KeyserverSyncAdapterService.class);
- serviceIntent.setAction(KeyserverSyncAdapterService.ACTION_SYNC_NOW);
- context.startService(serviceIntent);
- }
- }
-
- public void setWifiReceiverComponent(Boolean isEnabled, Context context) {
-
- PackageManager pm = context.getPackageManager();
- ComponentName compName = new ComponentName(context,
- NetworkReceiver.class);
-
- if (isEnabled) {
- pm.setComponentEnabledSetting(compName,
- PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
- Timber.d("Wifi Receiver is enabled!");
- } else {
- pm.setComponentEnabledSetting(compName,
- PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
- Timber.d("Wifi Receiver is disabled!");
- }
- }
-}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java
index 2f1847a17..fce2c131c 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java
@@ -41,6 +41,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
import timber.log.Timber;
@@ -284,7 +285,7 @@ public class KeyRepository {
Cursor lastUpdatedCursor = contentResolver.query(
UpdatedKeys.CONTENT_URI,
new String[] { UpdatedKeys.LAST_UPDATED },
- UpdatedKeys.MASTER_KEY_ID + " = ?",
+ Tables.UPDATED_KEYS + "." + UpdatedKeys.MASTER_KEY_ID + " = ?",
new String[] { "" + masterKeyId },
null
);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
index b97bb44cc..396384dd7 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
@@ -55,6 +55,7 @@ public class KeychainContract {
String MASTER_KEY_ID = "master_key_id"; // not a database id
String LAST_UPDATED = "last_updated"; // time since epoch in seconds
String SEEN_ON_KEYSERVERS = "seen_on_keyservers";
+ String FINGERPRINT = "fingerprint";
}
interface KeySignaturesColumns {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
index d581de0f5..f1d2b3326 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
@@ -788,14 +788,23 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
case UPDATED_KEYS:
case UPDATED_KEYS_SPECIFIC: {
HashMap projectionMap = new HashMap<>();
- qb.setTables(Tables.UPDATED_KEYS);
- projectionMap.put(UpdatedKeys.MASTER_KEY_ID, Tables.UPDATED_KEYS + "." + UpdatedKeys.MASTER_KEY_ID);
- projectionMap.put(UpdatedKeys.LAST_UPDATED, Tables.UPDATED_KEYS + "." + UpdatedKeys.LAST_UPDATED);
+ projectionMap.put(UpdatedKeys.MASTER_KEY_ID,
+ Tables.UPDATED_KEYS + "." + UpdatedKeys.MASTER_KEY_ID + " AS " + UpdatedKeys.MASTER_KEY_ID);
+ projectionMap.put(UpdatedKeys.LAST_UPDATED,
+ Tables.UPDATED_KEYS + "." + UpdatedKeys.LAST_UPDATED + " AS " + UpdatedKeys.LAST_UPDATED);
projectionMap.put(UpdatedKeys.SEEN_ON_KEYSERVERS,
- Tables.UPDATED_KEYS + "." + UpdatedKeys.SEEN_ON_KEYSERVERS);
+ Tables.UPDATED_KEYS + "." + UpdatedKeys.SEEN_ON_KEYSERVERS + " AS " + UpdatedKeys.SEEN_ON_KEYSERVERS);
+ projectionMap.put(UpdatedKeys.FINGERPRINT,
+ Tables.KEYS + "." + Keys.FINGERPRINT + " AS " + UpdatedKeys.FINGERPRINT);
qb.setProjectionMap(projectionMap);
+
+ qb.setTables(Tables.UPDATED_KEYS +
+ " LEFT JOIN " + Tables.KEYS +
+ " ON (" + Tables.KEYS + "." + Keys.KEY_ID + " = " + Tables.UPDATED_KEYS + "." + UpdatedKeys.MASTER_KEY_ID + ")"
+ );
+
if (match == UPDATED_KEYS_SPECIFIC) {
- qb.appendWhere(UpdatedKeys.MASTER_KEY_ID + " = ");
+ qb.appendWhere(Tables.UPDATED_KEYS + "." + UpdatedKeys.MASTER_KEY_ID + " = ");
qb.appendWhereEscapeString(uri.getPathSegments().get(1));
}
break;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LastUpdateInteractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LastUpdateInteractor.java
index 89520cb1a..268df323d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LastUpdateInteractor.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LastUpdateInteractor.java
@@ -1,7 +1,10 @@
package org.sufficientlysecure.keychain.provider;
+import java.util.ArrayList;
import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -11,6 +14,7 @@ import android.net.Uri;
import android.support.annotation.Nullable;
import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
public class LastUpdateInteractor {
@@ -32,7 +36,7 @@ public class LastUpdateInteractor {
Cursor cursor = contentResolver.query(
UpdatedKeys.CONTENT_URI,
new String[] { UpdatedKeys.SEEN_ON_KEYSERVERS },
- UpdatedKeys.MASTER_KEY_ID + " = ?",
+ Tables.UPDATED_KEYS + "." + UpdatedKeys.MASTER_KEY_ID + " = ?",
new String[] { "" + masterKeyId },
null
);
@@ -75,4 +79,27 @@ public class LastUpdateInteractor {
databaseNotifyManager.notifyKeyserverStatusChange(masterKeyId);
return insert;
}
+
+ public List getFingerprintsForKeysOlderThan(long olderThan, TimeUnit timeUnit) {
+ Cursor outdatedKeysCursor = contentResolver.query(
+ KeychainContract.UpdatedKeys.CONTENT_URI,
+ new String[] { KeychainContract.UpdatedKeys.FINGERPRINT, },
+ KeychainContract.UpdatedKeys.LAST_UPDATED + " < ?",
+ new String[] { Long.toString(timeUnit.toSeconds(olderThan)) },
+ null
+ );
+
+ List fingerprintList = new ArrayList<>();
+ if (outdatedKeysCursor == null) {
+ return fingerprintList;
+ }
+
+ while (outdatedKeysCursor.moveToNext()) {
+ byte[] fingerprint = outdatedKeysCursor.getBlob(0);
+ fingerprintList.add(fingerprint);
+ }
+ outdatedKeysCursor.close();
+
+ return fingerprintList;
+ }
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java
deleted file mode 100644
index 6799b8d0e..000000000
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java
+++ /dev/null
@@ -1,606 +0,0 @@
-/*
- * Copyright (C) 2017 Schürmann & Breitmoser GbR
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.sufficientlysecure.keychain.service;
-
-
-import java.util.ArrayList;
-import java.util.GregorianCalendar;
-import java.util.Random;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import android.accounts.Account;
-import android.app.AlarmManager;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.AbstractThreadedSyncAdapter;
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SyncResult;
-import android.database.Cursor;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.PowerManager;
-import android.os.SystemClock;
-import android.support.v4.app.NotificationCompat;
-
-import org.sufficientlysecure.keychain.Constants;
-import org.sufficientlysecure.keychain.KeychainApplication;
-import org.sufficientlysecure.keychain.R;
-import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
-import org.sufficientlysecure.keychain.network.NetworkReceiver;
-import org.sufficientlysecure.keychain.network.orbot.OrbotHelper;
-import org.sufficientlysecure.keychain.operations.ImportOperation;
-import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
-import org.sufficientlysecure.keychain.operations.results.OperationResult;
-import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
-import org.sufficientlysecure.keychain.provider.KeychainContract;
-import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
-import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity;
-import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
-import org.sufficientlysecure.keychain.util.ParcelableProxy;
-import org.sufficientlysecure.keychain.util.Preferences;
-import org.sufficientlysecure.keychain.util.ResourceUtils;
-import timber.log.Timber;
-
-
-public class KeyserverSyncAdapterService extends Service {
-
- // how often a sync should be initiated, in s
- public static final long SYNC_INTERVAL =
- Constants.DEBUG_KEYSERVER_SYNC
- ? TimeUnit.MINUTES.toSeconds(1) : TimeUnit.DAYS.toSeconds(3);
- // time since last update after which a key should be updated again, in s
- public static final long KEY_UPDATE_LIMIT =
- Constants.DEBUG_KEYSERVER_SYNC ? 1 : TimeUnit.DAYS.toSeconds(7);
- // time by which a sync is postponed in case screen is on
- public static final long SYNC_POSTPONE_TIME =
- Constants.DEBUG_KEYSERVER_SYNC ? 30 * 1000 : TimeUnit.MINUTES.toMillis(5);
- // Time taken by Orbot before a new circuit is created
- public static final int ORBOT_CIRCUIT_TIMEOUT_SECONDS =
- Constants.DEBUG_KEYSERVER_SYNC ? 2 : (int) TimeUnit.MINUTES.toSeconds(10);
-
-
- private static final String ACTION_IGNORE_TOR = "ignore_tor";
- private static final String ACTION_UPDATE_ALL = "update_all";
- public static final String ACTION_SYNC_NOW = "sync_now";
- private static final String ACTION_DISMISS_NOTIFICATION = "cancel_sync";
- private static final String ACTION_START_ORBOT = "start_orbot";
- private static final String ACTION_CANCEL = "cancel";
-
- private AtomicBoolean mCancelled = new AtomicBoolean(false);
-
- @Override
- public int onStartCommand(final Intent intent, int flags, final int startId) {
- if (intent == null || intent.getAction() == null) {
- // introduced due to https://github.com/open-keychain/open-keychain/issues/1573
- return START_NOT_STICKY; // we can't act on this Intent and don't want it redelivered
- }
-
- if (!isSyncEnabled()) {
- // if we have initiated a sync, but the user disabled it in preferences since
- return START_NOT_STICKY;
- }
-
- switch (intent.getAction()) {
- case ACTION_CANCEL: {
- mCancelled.set(true);
- return START_NOT_STICKY;
- }
- // the reason for the separation betweyeen SYNC_NOW and UPDATE_ALL is so that starting
- // the sync directly from the notification is possible while the screen is on with
- // UPDATE_ALL, but a postponed sync is only started if screen is off
- case ACTION_SYNC_NOW: {
- // this checks for screen on/off before sync, and postpones the sync if on
- ContentResolver.requestSync(
- new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE),
- Constants.PROVIDER_AUTHORITY,
- new Bundle()
- );
- return START_NOT_STICKY;
- }
- case ACTION_UPDATE_ALL: {
- // does not check for screen on/off
- asyncKeyUpdate(this, CryptoInputParcel.createCryptoInputParcel(), startId);
- // we depend on handleUpdateResult to call stopSelf when it is no longer necessary
- // for the intent to be redelivered
- return START_REDELIVER_INTENT;
- }
- case ACTION_IGNORE_TOR: {
- NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT);
- asyncKeyUpdate(this, CryptoInputParcel.createCryptoInputParcel(ParcelableProxy.getForNoProxy()),
- startId);
- // we depend on handleUpdateResult to call stopSelf when it is no longer necessary
- // for the intent to be redelivered
- return START_REDELIVER_INTENT;
- }
- case ACTION_START_ORBOT: {
- NotificationManager manager = (NotificationManager)
- getSystemService(NOTIFICATION_SERVICE);
- manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT);
-
- Intent startOrbot = new Intent(this, OrbotRequiredDialogActivity.class);
- startOrbot.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startOrbot.putExtra(OrbotRequiredDialogActivity.EXTRA_START_ORBOT, true);
-
- Messenger messenger = new Messenger(
- new Handler() {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case OrbotRequiredDialogActivity.MESSAGE_ORBOT_STARTED: {
- startServiceWithUpdateAll();
- break;
- }
- case OrbotRequiredDialogActivity.MESSAGE_ORBOT_IGNORE:
- case OrbotRequiredDialogActivity.MESSAGE_DIALOG_CANCEL: {
- // not possible since we proceed to Orbot's Activity
- // directly, by starting OrbotRequiredDialogActivity with
- // EXTRA_START_ORBOT set to true
- break;
- }
- }
- }
- }
- );
- startOrbot.putExtra(OrbotRequiredDialogActivity.EXTRA_MESSENGER, messenger);
- startActivity(startOrbot);
- // since we return START_NOT_STICKY, we also postpone the sync as a backup in case
- // the service is killed before OrbotRequiredDialogActivity can get back to us
- postponeSync();
- // if use START_REDELIVER_INTENT, we might annoy the user by repeatedly starting the
- // Orbot Activity when our service is killed and restarted
- return START_NOT_STICKY;
- }
- case ACTION_DISMISS_NOTIFICATION: {
- NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT);
- return START_NOT_STICKY;
- }
- }
- return START_NOT_STICKY;
- }
-
- private class KeyserverSyncAdapter extends AbstractThreadedSyncAdapter {
-
- public KeyserverSyncAdapter() {
- super(KeyserverSyncAdapterService.this, true);
- }
-
- @Override
- public void onPerformSync(Account account, Bundle extras, String authority,
- ContentProviderClient provider, SyncResult syncResult) {
-
- Preferences prefs = Preferences.getPreferences(getContext());
-
- // for a wifi-ONLY sync
- if (prefs.getWifiOnlySync()) {
-
- ConnectivityManager connMgr = (ConnectivityManager)
- getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
- boolean isNotOnWifi = !(networkInfo.getType() == ConnectivityManager.TYPE_WIFI);
- boolean isNotConnected = !(networkInfo.isConnected());
-
- // if Wi-Fi connection doesn't exist then receiver is enabled
- if (isNotOnWifi && isNotConnected) {
- new NetworkReceiver().setWifiReceiverComponent(true, getContext());
- return;
- }
- }
- Timber.d("Performing a keyserver sync!");
- PowerManager pm = (PowerManager) KeyserverSyncAdapterService.this
- .getSystemService(Context.POWER_SERVICE);
- @SuppressWarnings("deprecation") // our min is API 15, deprecated only in 20
- boolean isScreenOn = pm.isScreenOn();
-
- if (!isScreenOn) {
- startServiceWithUpdateAll();
- } else {
- postponeSync();
- }
- }
-
- @Override
- public void onSyncCanceled() {
- super.onSyncCanceled();
- cancelUpdates(KeyserverSyncAdapterService.this);
- }
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return new KeyserverSyncAdapter().getSyncAdapterBinder();
- }
-
- /**
- * Since we're returning START_REDELIVER_INTENT in onStartCommand, we need to remember to call
- * stopSelf(int) to prevent the Intent from being redelivered if our work is already done
- *
- * @param result result of keyserver sync
- * @param startId startId provided to the onStartCommand call which resulted in this sync
- */
- private void handleUpdateResult(ImportKeyResult result, final int startId) {
- if (result.isPending()) {
- Timber.d("Orbot required for sync but not running, attempting to start");
- // result is pending due to Orbot not being started
- // try to start it silently, if disabled show notifications
- new OrbotHelper.SilentStartManager() {
- @Override
- protected void onOrbotStarted() {
- // retry the update
- startServiceWithUpdateAll();
- stopSelf(startId); // startServiceWithUpdateAll will deliver a new Intent
- }
-
- @Override
- protected void onSilentStartDisabled() {
- // show notification
- NotificationManager manager =
- (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- manager.notify(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT,
- getOrbotNoification(KeyserverSyncAdapterService.this));
- // further action on user interaction with notification, intent should not be
- // redelivered, therefore:
- stopSelf(startId);
- }
- }.startOrbotAndListen(this, false);
- // if we're killed before we get a response from Orbot, we need the intent to be
- // redelivered, so no stopSelf(int) here
- } else if (isUpdateCancelled()) {
- Timber.d("Keyserver sync cancelled, postponing by" + SYNC_POSTPONE_TIME
- + "ms");
- postponeSync();
- // postponeSync creates a new intent, so we don't need this to be redelivered
- stopSelf(startId);
- } else {
- Timber.d("Keyserver sync completed: Updated: " + result.mUpdatedKeys
- + " Failed: " + result.mBadKeys);
- // key sync completed successfully, we can stop
- stopSelf(startId);
- }
- }
-
- private void postponeSync() {
- AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
- Intent serviceIntent = new Intent(this, KeyserverSyncAdapterService.class);
- serviceIntent.setAction(ACTION_SYNC_NOW);
- PendingIntent pi = PendingIntent.getService(this, 0, serviceIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- alarmManager.set(
- AlarmManager.ELAPSED_REALTIME_WAKEUP,
- SystemClock.elapsedRealtime() + SYNC_POSTPONE_TIME,
- pi
- );
- }
-
- private void asyncKeyUpdate(final Context context,
- final CryptoInputParcel cryptoInputParcel, final int startId) {
- new Thread(new Runnable() {
- @Override
- public void run() {
- ImportKeyResult result = updateKeysFromKeyserver(context, cryptoInputParcel);
- handleUpdateResult(result, startId);
- }
- }).start();
- }
-
- private synchronized ImportKeyResult updateKeysFromKeyserver(final Context context,
- final CryptoInputParcel cryptoInputParcel) {
- mCancelled.set(false);
-
- ArrayList keyList = getKeysToUpdate(context);
-
- if (isUpdateCancelled()) { // if we've already been cancelled
- return new ImportKeyResult(OperationResult.RESULT_CANCELLED,
- new OperationResult.OperationLog());
- }
-
- if (cryptoInputParcel.getParcelableProxy() == null) {
- // no explicit proxy, retrieve from preferences. Check if we should do a staggered sync
- if (Preferences.getPreferences(context).getParcelableProxy().isTorEnabled()) {
- return staggeredUpdate(context, keyList, cryptoInputParcel);
- } else {
- return directUpdate(context, keyList, cryptoInputParcel);
- }
- } else {
- return directUpdate(context, keyList, cryptoInputParcel);
- }
- }
-
- private ImportKeyResult directUpdate(Context context, ArrayList keyList,
- CryptoInputParcel cryptoInputParcel) {
- Timber.d("Starting normal update");
- ImportOperation importOp = new ImportOperation(context,
- KeyWritableRepository.create(context), null);
- return importOp.execute(
- ImportKeyringParcel.createImportKeyringParcel(keyList,
- Preferences.getPreferences(context).getPreferredKeyserver()),
- cryptoInputParcel
- );
- }
-
- /**
- * will perform a staggered update of user's keys using delays to ensure new Tor circuits, as
- * performed by parcimonie. Relevant issue and method at:
- * https://github.com/open-keychain/open-keychain/issues/1337
- *
- * @return result of the sync
- */
- private ImportKeyResult staggeredUpdate(Context context, ArrayList keyList,
- CryptoInputParcel cryptoInputParcel) {
- Timber.d("Starting staggered update");
- // final int WEEK_IN_SECONDS = (int) TimeUnit.DAYS.toSeconds(7);
- // we are limiting our randomness to ORBOT_CIRCUIT_TIMEOUT_SECONDS for now
- final int WEEK_IN_SECONDS = 0;
-
- ImportOperation.KeyImportAccumulator accumulator
- = new ImportOperation.KeyImportAccumulator(keyList.size(), null);
-
- // so that the first key can be updated without waiting. This is so that there isn't a
- // large gap between a "Start Orbot" notification and the next key update
- boolean first = true;
-
- for (ParcelableKeyRing keyRing : keyList) {
- int waitTime;
- int staggeredTime = new Random().nextInt(1 + 2 * (WEEK_IN_SECONDS / keyList.size()));
- if (staggeredTime >= ORBOT_CIRCUIT_TIMEOUT_SECONDS) {
- waitTime = staggeredTime;
- } else {
- waitTime = ORBOT_CIRCUIT_TIMEOUT_SECONDS
- + new Random().nextInt(1 + ORBOT_CIRCUIT_TIMEOUT_SECONDS);
- }
-
- if (first) {
- waitTime = 0;
- first = false;
- }
-
- Timber.d("Updating key with a wait time of " + waitTime + "s");
- try {
- Thread.sleep(waitTime * 1000);
- } catch (InterruptedException e) {
- Timber.e(e, "Exception during sleep between key updates");
- // skip this one
- continue;
- }
- ArrayList keyWrapper = new ArrayList<>();
- keyWrapper.add(keyRing);
- if (isUpdateCancelled()) {
- return new ImportKeyResult(ImportKeyResult.RESULT_CANCELLED,
- new OperationResult.OperationLog());
- }
- ImportKeyResult result =
- new ImportOperation(context, KeyWritableRepository.create(context), null, mCancelled)
- .execute(
- ImportKeyringParcel.createImportKeyringParcel(
- keyWrapper,
- Preferences.getPreferences(context)
- .getPreferredKeyserver()
- ),
- cryptoInputParcel
- );
- if (result.isPending()) {
- return result;
- }
- accumulator.accumulateKeyImport(result);
- }
- return accumulator.getConsolidatedResult();
- }
-
- /**
- * 1. Get keys which have been updated recently and therefore do not need to
- * be updated now
- * 2. Get list of all keys and filter out ones that don't need to be updated
- * 3. Return keys to be updated
- *
- * @return list of keys that require update
- */
- private ArrayList getKeysToUpdate(Context context) {
-
- // 1. Get keys which have been updated recently and don't need to updated now
- final int INDEX_UPDATED_KEYS_MASTER_KEY_ID = 0;
- final int INDEX_LAST_UPDATED = 1;
-
- // all time in seconds not milliseconds
- final long CURRENT_TIME = GregorianCalendar.getInstance().getTimeInMillis() / 1000;
- Cursor updatedKeysCursor = context.getContentResolver().query(
- KeychainContract.UpdatedKeys.CONTENT_URI,
- new String[]{
- KeychainContract.UpdatedKeys.MASTER_KEY_ID,
- KeychainContract.UpdatedKeys.LAST_UPDATED
- },
- "? - " + KeychainContract.UpdatedKeys.LAST_UPDATED + " < " + KEY_UPDATE_LIMIT,
- new String[]{"" + CURRENT_TIME},
- null
- );
-
- ArrayList ignoreMasterKeyIds = new ArrayList<>();
- while (updatedKeysCursor != null && updatedKeysCursor.moveToNext()) {
- long masterKeyId = updatedKeysCursor.getLong(INDEX_UPDATED_KEYS_MASTER_KEY_ID);
- Timber.d("Keyserver sync: Ignoring {" + masterKeyId + "} last updated at {"
- + updatedKeysCursor.getLong(INDEX_LAST_UPDATED) + "}s");
- ignoreMasterKeyIds.add(masterKeyId);
- }
- if (updatedKeysCursor != null) {
- updatedKeysCursor.close();
- }
-
- // 2. Make a list of public keys which should be updated
- final int INDEX_MASTER_KEY_ID = 0;
- final int INDEX_FINGERPRINT = 1;
- Cursor keyCursor = context.getContentResolver().query(
- KeychainContract.KeyRings.buildUnifiedKeyRingsUri(),
- new String[]{
- KeychainContract.KeyRings.MASTER_KEY_ID,
- KeychainContract.KeyRings.FINGERPRINT
- },
- null,
- null,
- null
- );
-
- if (keyCursor == null) {
- return new ArrayList<>();
- }
-
- ArrayList keyList = new ArrayList<>();
- while (keyCursor.moveToNext()) {
- long keyId = keyCursor.getLong(INDEX_MASTER_KEY_ID);
- if (ignoreMasterKeyIds.contains(keyId)) {
- continue;
- }
- Timber.d("Keyserver sync: Updating {" + keyId + "}");
- byte[] fingerprint = keyCursor.getBlob(INDEX_FINGERPRINT);
- String hexKeyId = KeyFormattingUtils.convertKeyIdToHex(keyId);
- // we aren't updating from keybase as of now
- keyList.add(ParcelableKeyRing.createFromReference(fingerprint, hexKeyId, null, null));
- }
- keyCursor.close();
-
- return keyList;
- }
-
- private boolean isUpdateCancelled() {
- return mCancelled.get();
- }
-
- /**
- * will cancel an update already in progress. We send an Intent to cancel it instead of simply
- * modifying a static variable since the service is running in a process that is different from
- * the default application process where the UI code runs.
- *
- * @param context used to send an Intent to the service requesting cancellation.
- */
- public static void cancelUpdates(Context context) {
- Intent intent = new Intent(context, KeyserverSyncAdapterService.class);
- intent.setAction(ACTION_CANCEL);
- context.startService(intent);
- }
-
- private Notification getOrbotNoification(Context context) {
- NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
- builder.setSmallIcon(R.drawable.ic_stat_notify_24dp)
- .setLargeIcon(ResourceUtils.getDrawableAsNotificationBitmap(context, R.mipmap.ic_launcher))
- .setContentTitle(context.getString(R.string.keyserver_sync_orbot_notif_title))
- .setContentText(context.getString(R.string.keyserver_sync_orbot_notif_msg))
- .setAutoCancel(true);
-
- // In case the user decides to not use tor
- Intent ignoreTorIntent = new Intent(context, KeyserverSyncAdapterService.class);
- ignoreTorIntent.setAction(ACTION_IGNORE_TOR);
- PendingIntent ignoreTorPi = PendingIntent.getService(
- context,
- 0, // security not issue since we're giving this pending intent to Notification Manager
- ignoreTorIntent,
- PendingIntent.FLAG_CANCEL_CURRENT
- );
-
- builder.addAction(R.drawable.ic_stat_tor_off,
- context.getString(R.string.keyserver_sync_orbot_notif_ignore),
- ignoreTorPi);
-
- Intent startOrbotIntent = new Intent(context, KeyserverSyncAdapterService.class);
- startOrbotIntent.setAction(ACTION_START_ORBOT);
- PendingIntent startOrbotPi = PendingIntent.getService(
- context,
- 0, // security not issue since we're giving this pending intent to Notification Manager
- startOrbotIntent,
- PendingIntent.FLAG_CANCEL_CURRENT
- );
-
- builder.addAction(R.drawable.ic_stat_tor,
- context.getString(R.string.keyserver_sync_orbot_notif_start),
- startOrbotPi
- );
- builder.setContentIntent(startOrbotPi);
-
- return builder.build();
- }
-
- public static void enableKeyserverSync(Context context) {
- Account account = KeychainApplication.createAccountIfNecessary(context);
-
- if (account == null) {
- // account failed to be created for some reason, nothing we can do here
- return;
- }
-
- ContentResolver.setIsSyncable(account, Constants.PROVIDER_AUTHORITY, 1);
- ContentResolver.setSyncAutomatically(account, Constants.PROVIDER_AUTHORITY, true);
-
- updateInterval(context);
- }
-
- /**
- * creates a new sync if one does not exist, or updates an existing sync if the sync interval
- * has changed.
- */
- public static void updateInterval(Context context) {
- Account account = KeychainApplication.createAccountIfNecessary(context);
-
- if (account == null) {
- // account failed to be created for some reason, nothing we can do here
- return;
- }
-
- boolean intervalChanged = false;
- boolean syncExists = Preferences.getKeyserverSyncEnabled(context);
-
- if (syncExists) {
- long oldInterval = ContentResolver.getPeriodicSyncs(
- account, Constants.PROVIDER_AUTHORITY).get(0).period;
- if (oldInterval != SYNC_INTERVAL) {
- intervalChanged = true;
- }
- }
-
- if (!syncExists || intervalChanged) {
- ContentResolver.addPeriodicSync(
- account,
- Constants.PROVIDER_AUTHORITY,
- new Bundle(),
- SYNC_INTERVAL
- );
- }
- }
-
- private boolean isSyncEnabled() {
- Account account = KeychainApplication.createAccountIfNecessary(this);
-
- // if account is null, it could not be created for some reason, so sync cannot exist
- return account != null
- && ContentResolver.getSyncAutomatically(account, Constants.PROVIDER_AUTHORITY);
- }
-
- private void startServiceWithUpdateAll() {
- Intent serviceIntent = new Intent(this, KeyserverSyncAdapterService.class);
- serviceIntent.setAction(ACTION_UPDATE_ALL);
- this.startService(serviceIntent);
- }
-}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java
index c1565fc9d..b6df0f1db 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java
@@ -17,7 +17,11 @@
package org.sufficientlysecure.keychain.ui;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.app.ProgressDialog;
+import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
@@ -25,14 +29,17 @@ import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.NotificationCompat;
import android.view.ContextThemeWrapper;
+import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
import org.sufficientlysecure.keychain.util.ParcelableProxy;
import org.sufficientlysecure.keychain.network.orbot.OrbotHelper;
+import org.sufficientlysecure.keychain.util.ResourceUtils;
import timber.log.Timber;
@@ -169,4 +176,36 @@ public class OrbotRequiredDialogActivity extends FragmentActivity
}
}
}
+
+ public static void showOrbotRequiredNotification(Context context) {
+ NotificationManager manager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
+ if (manager != null) {
+ manager.notify(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT, createOrbotNotification(context));
+ }
+ }
+
+ private static Notification createOrbotNotification(Context context) {
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
+ builder.setSmallIcon(R.drawable.ic_stat_notify_24dp)
+ .setLargeIcon(ResourceUtils.getDrawableAsNotificationBitmap(context, R.mipmap.ic_launcher))
+ .setContentTitle(context.getString(R.string.keyserver_sync_orbot_notif_title))
+ .setContentText(context.getString(R.string.keyserver_sync_orbot_notif_msg))
+ .setAutoCancel(true);
+
+ Intent startOrbotIntent = new Intent(context, OrbotRequiredDialogActivity.class);
+ startOrbotIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startOrbotIntent.putExtra(OrbotRequiredDialogActivity.EXTRA_START_ORBOT, true);
+ PendingIntent startOrbotPi = PendingIntent.getActivity(
+ context, 0, startOrbotIntent, PendingIntent.FLAG_CANCEL_CURRENT
+ );
+
+ builder.addAction(R.drawable.ic_stat_tor,
+ context.getString(R.string.keyserver_sync_orbot_notif_start),
+ startOrbotPi
+ );
+ builder.setContentIntent(startOrbotPi);
+
+ return builder.build();
+ }
+
}
\ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java
index 4bcbba382..cc064aa26 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java
@@ -18,6 +18,11 @@
package org.sufficientlysecure.keychain.ui;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.util.ArrayList;
+import java.util.List;
+
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
@@ -48,20 +53,16 @@ import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.KeychainApplication;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.AppCompatPreferenceActivity;
+import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress;
+import org.sufficientlysecure.keychain.keysync.KeyserverSyncManager;
+import org.sufficientlysecure.keychain.network.orbot.OrbotHelper;
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
-import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress;
import org.sufficientlysecure.keychain.util.Preferences;
-import org.sufficientlysecure.keychain.network.orbot.OrbotHelper;
import timber.log.Timber;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.util.ArrayList;
-import java.util.List;
-
public class SettingsActivity extends AppCompatPreferenceActivity {
public static final int REQUEST_CODE_KEYSERVER_PREF = 0x00007005;
@@ -398,6 +399,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
* This fragment shows the keyserver/wifi-only-sync/contacts sync preferences
*/
public static class SyncPrefsFragment extends PresetPreferenceFragment {
+ boolean syncPrefChanged = false;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -405,6 +407,22 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.sync_preferences);
+
+ findPreference(Constants.Pref.SYNC_KEYSERVER).setOnPreferenceChangeListener(
+ (preference, newValue) -> {
+ syncPrefChanged = true;
+ return true;
+ });
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ if (syncPrefChanged) {
+ KeyserverSyncManager.updateKeyserverSyncSchedule(getActivity(), true);
+ syncPrefChanged = false;
+ }
}
@Override
@@ -413,12 +431,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
// this needs to be done in onResume since the user can change sync values from Android
// settings and we need to reflect that change when the user navigates back
final Account account = KeychainApplication.createAccountIfNecessary(getActivity());
- // for keyserver sync
- initializeSyncCheckBox(
- (SwitchPreference) findPreference(Constants.Pref.SYNC_KEYSERVER),
- account,
- Constants.PROVIDER_AUTHORITY
- );
// for contacts sync
initializeSyncCheckBox(
(SwitchPreference) findPreference(Constants.Pref.SYNC_CONTACTS),
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java
index bb1949537..626bd8fb9 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java
@@ -33,7 +33,6 @@ import android.widget.TextView;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
-import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
/**
@@ -77,7 +76,6 @@ public abstract class BaseActivity extends AppCompatActivity {
}
public static void onResumeChecks(Context context) {
- KeyserverSyncAdapterService.cancelUpdates(context);
// in case user has disabled sync from Android account settings
ContactSyncAdapterService.deleteIfSyncDisabled(context);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java
index bb5f7b105..c9ef239d6 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java
@@ -363,6 +363,11 @@ public class KeyFormattingUtils {
return idHex;
}
+ public static String beautifyKeyId(byte[] fingerprint) {
+ long keyId = KeyFormattingUtils.convertFingerprintToKeyId(fingerprint);
+ return beautifyKeyId(keyId);
+ }
+
/**
* Makes a human-readable version of a key ID, which is usually 64 bits: lower-case, no
* leading 0x, space-separated quartets (for keys whose length in hex is divisible by 4)
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
index a44c101fb..38773c116 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
@@ -18,27 +18,24 @@
package org.sufficientlysecure.keychain.util;
-import android.accounts.Account;
+import java.net.Proxy;
+import java.util.ArrayList;
+import java.util.ListIterator;
+
import android.annotation.SuppressLint;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
+
import com.google.auto.value.AutoValue;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Constants.Pref;
-import org.sufficientlysecure.keychain.KeychainApplication;
import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress;
-import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
import timber.log.Timber;
-import java.net.Proxy;
-import java.util.ArrayList;
-import java.util.ListIterator;
-
/**
* Singleton Implementation of a Preference Helper
@@ -324,23 +321,6 @@ public class Preferences {
}
}
- /**
- * @return true if a periodic sync exists and is set to run automatically, false otherwise
- */
- public static boolean getKeyserverSyncEnabled(Context context) {
- Account account = KeychainApplication.createAccountIfNecessary(context);
-
- if (account == null) {
- // if the account could not be created for some reason, we can't have a sync
- return false;
- }
-
- String authority = Constants.PROVIDER_AUTHORITY;
-
- return ContentResolver.getSyncAutomatically(account, authority) &&
- !ContentResolver.getPeriodicSyncs(account, authority).isEmpty();
- }
-
// cloud prefs
public CloudSearchPrefs getCloudSearchPrefs() {
@@ -361,6 +341,18 @@ public class Preferences {
editor.commit();
}
+ public boolean isKeyserverSyncEnabled() {
+ return mSharedPreferences.getBoolean(Pref.SYNC_KEYSERVER, true);
+ }
+
+ public boolean isKeyserverSyncScheduled() {
+ return mSharedPreferences.getBoolean(Pref.SYNC_IS_SCHEDULED, false);
+ }
+
+ public void setKeyserverSyncScheduled(boolean isScheduled) {
+ mSharedPreferences.edit().putBoolean(Pref.SYNC_IS_SCHEDULED, isScheduled).apply();
+ }
+
@AutoValue
public static abstract class CloudSearchPrefs implements Parcelable {
public abstract boolean isKeyserverEnabled();
@@ -431,7 +423,7 @@ public class Preferences {
editor.commit();
}
- public void upgradePreferences(Context context) {
+ public void upgradePreferences() {
int oldVersion = mSharedPreferences.getInt(Constants.Pref.PREF_VERSION, 0);
boolean requiresUpgrade = oldVersion < Constants.Defaults.PREF_CURRENT_VERSION;
@@ -447,9 +439,7 @@ public class Preferences {
case 4: {
setTheme(Constants.Pref.Theme.DEFAULT);
}
- case 5: {
- KeyserverSyncAdapterService.enableKeyserverSync(context);
- }
+ case 5:
case 6:
case 7: {
addOnionToSks();
diff --git a/OpenKeychain/src/main/res/xml/sync_preferences.xml b/OpenKeychain/src/main/res/xml/sync_preferences.xml
index 600ccc9e8..6e57376e1 100644
--- a/OpenKeychain/src/main/res/xml/sync_preferences.xml
+++ b/OpenKeychain/src/main/res/xml/sync_preferences.xml
@@ -1,7 +1,8 @@