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 @@