From 53aec6a313e9d626c4aaf373a911b05d2586726f Mon Sep 17 00:00:00 2001 From: Catfriend1 Date: Sat, 7 Jul 2018 20:35:18 +0200 Subject: [PATCH] Fix recurring device, folder accept notifications, add ignore action (fixes #679) (#1177) --- .../activities/DeviceActivity.java | 12 +- .../activities/FolderActivity.java | 10 +- .../syncthingandroid/model/Config.java | 3 +- .../service/EventProcessor.java | 139 +++++++++++------- .../service/NotificationHandler.java | 42 +++++- .../syncthingandroid/service/RestApi.java | 26 ++++ .../service/SyncthingService.java | 42 ++++++ app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values/strings.xml | 8 +- 9 files changed, 222 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/DeviceActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/DeviceActivity.java index cc2eb482..b7971ae5 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/DeviceActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/DeviceActivity.java @@ -48,8 +48,12 @@ import static com.nutomic.syncthingandroid.util.Compression.METADATA; */ public class DeviceActivity extends SyncthingActivity implements View.OnClickListener { + public static final String EXTRA_NOTIFICATION_ID = + "com.nutomic.syncthingandroid.activities.DeviceActivity.NOTIFICATION_ID"; public static final String EXTRA_DEVICE_ID = "com.nutomic.syncthingandroid.activities.DeviceActivity.DEVICE_ID"; + public static final String EXTRA_DEVICE_NAME = + "com.nutomic.syncthingandroid.activities.DeviceActivity.DEVICE_NAME"; public static final String EXTRA_IS_CREATE = "com.nutomic.syncthingandroid.activities.DeviceActivity.IS_CREATE"; @@ -215,6 +219,7 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis super.onDestroy(); SyncthingService syncthingService = getService(); if (syncthingService != null) { + syncthingService.getNotificationHandler().cancelConsentNotification(getIntent().getIntExtra(EXTRA_NOTIFICATION_ID, 0)); syncthingService.unregisterOnServiceStateChangeListener(this::onServiceStateChange); } mIdView.removeTextChangedListener(mIdTextWatcher); @@ -253,7 +258,10 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis } private void onServiceConnected() { - getService().registerOnServiceStateChangeListener(this::onServiceStateChange); + Log.v(TAG, "onServiceConnected"); + SyncthingService syncthingService = (SyncthingService) getService(); + syncthingService.getNotificationHandler().cancelConsentNotification(getIntent().getIntExtra(EXTRA_NOTIFICATION_ID, 0)); + syncthingService.registerOnServiceStateChangeListener(this::onServiceStateChange); } /** @@ -394,7 +402,7 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis private void initDevice() { mDevice = new Device(); - mDevice.name = ""; + mDevice.name = getIntent().getStringExtra(EXTRA_DEVICE_NAME); mDevice.deviceID = getIntent().getStringExtra(EXTRA_DEVICE_ID); mDevice.addresses = DYNAMIC_ADDRESS; mDevice.compression = METADATA.getValue(this); diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderActivity.java index b0672650..9756b1b7 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderActivity.java @@ -54,8 +54,10 @@ import static com.nutomic.syncthingandroid.service.SyncthingService.State.ACTIVE public class FolderActivity extends SyncthingActivity implements SyncthingActivity.OnServiceConnectedListener, SyncthingService.OnServiceStateChangeListener { + public static final String EXTRA_NOTIFICATION_ID = + "com.nutomic.syncthingandroid.activities.FolderActivity.NOTIFICATION_ID"; public static final String EXTRA_IS_CREATE = - "com.nutomic.syncthingandroid.activities.DeviceActivity.IS_CREATE"; + "com.nutomic.syncthingandroid.activities.FolderActivity.IS_CREATE"; public static final String EXTRA_FOLDER_ID = "com.nutomic.syncthingandroid.activities.FolderActivity.FOLDER_ID"; public static final String EXTRA_FOLDER_LABEL = @@ -237,6 +239,7 @@ public class FolderActivity extends SyncthingActivity super.onDestroy(); SyncthingService syncthingService = getService(); if (syncthingService != null) { + syncthingService.getNotificationHandler().cancelConsentNotification(getIntent().getIntExtra(EXTRA_NOTIFICATION_ID, 0)); syncthingService.unregisterOnServiceStateChangeListener(this::onServiceStateChange); } mLabelView.removeTextChangedListener(mTextWatcher); @@ -272,7 +275,10 @@ public class FolderActivity extends SyncthingActivity */ @Override public void onServiceConnected() { - getService().registerOnServiceStateChangeListener(this); + Log.v(TAG, "onServiceConnected"); + SyncthingService syncthingService = (SyncthingService) getService(); + syncthingService.getNotificationHandler().cancelConsentNotification(getIntent().getIntExtra(EXTRA_NOTIFICATION_ID, 0)); + syncthingService.registerOnServiceStateChangeListener(this); } @Override diff --git a/app/src/main/java/com/nutomic/syncthingandroid/model/Config.java b/app/src/main/java/com/nutomic/syncthingandroid/model/Config.java index 257a4440..d9195dfe 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/model/Config.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/model/Config.java @@ -4,11 +4,12 @@ import java.util.List; public class Config { public int version; - public String[] ignoredDevices; public List devices; public List folders; public Gui gui; public Options options; + public List ignoredFolders; + public List ignoredDevices; public class Gui { public boolean enabled; diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java b/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java index ba8acff7..05b75fcb 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java @@ -94,9 +94,6 @@ public class EventProcessor implements Runnable, RestApi.OnReceiveEventListener */ @Override public void onEvent(Event event) { - String deviceId; - String folderId; - switch (event.type) { case "ConfigSaved": if (mApi != null) { @@ -105,56 +102,26 @@ public class EventProcessor implements Runnable, RestApi.OnReceiveEventListener } break; case "DeviceRejected": - deviceId = (String) event.data.get("device"); - Log.d(TAG, "Unknown device " + deviceId + " wants to connect"); - - Intent intent = new Intent(mContext, DeviceActivity.class) - .putExtra(DeviceActivity.EXTRA_IS_CREATE, true) - .putExtra(DeviceActivity.EXTRA_DEVICE_ID, deviceId); - // HACK: Use a random, deterministic ID to make multiple PendingIntents - // distinguishable - int requestCode = deviceId.hashCode(); - PendingIntent pi = PendingIntent.getActivity(mContext, requestCode, intent, 0); - - String title = mContext.getString(R.string.device_rejected, - deviceId.substring(0, 7)); - - notify(title, pi); + onDeviceRejected( + (String) event.data.get("device"), // deviceId + (String) event.data.get("name") // deviceName + ); break; case "FolderCompletion": - deviceId = (String) event.data.get("device"); - folderId = (String) event.data.get("folder"); CompletionInfo completionInfo = new CompletionInfo(); completionInfo.completion = (Double) event.data.get("completion"); - mApi.setCompletionInfo(deviceId, folderId, completionInfo); + mApi.setCompletionInfo( + (String) event.data.get("device"), // deviceId + (String) event.data.get("folder"), // folderId + completionInfo + ); break; case "FolderRejected": - deviceId = (String) event.data.get("device"); - folderId = (String) event.data.get("folder"); - String folderLabel = (String) event.data.get("folderLabel"); - Log.d(TAG, "Device " + deviceId + " wants to share folder " + folderId); - - boolean isNewFolder = Stream.of(mApi.getFolders()) - .noneMatch(f -> f.id.equals(folderId)); - intent = new Intent(mContext, FolderActivity.class) - .putExtra(FolderActivity.EXTRA_IS_CREATE, isNewFolder) - .putExtra(FolderActivity.EXTRA_DEVICE_ID, deviceId) - .putExtra(FolderActivity.EXTRA_FOLDER_ID, folderId) - .putExtra(FolderActivity.EXTRA_FOLDER_LABEL, folderLabel); - // HACK: Use a random, deterministic ID to make multiple PendingIntents - // distinguishable - requestCode = (deviceId + folderId + folderLabel).hashCode(); - pi = PendingIntent.getActivity(mContext, requestCode, intent, 0); - - String deviceName = null; - for (Device d : mApi.getDevices(false)) { - if (d.deviceID.equals(deviceId)) - deviceName = d.getDisplayName(); - } - title = mContext.getString(R.string.folder_rejected, deviceName, - folderLabel.isEmpty() ? folderId : folderLabel + " (" + folderId + ")"); - - notify(title, pi); + onFolderRejected( + (String) event.data.get("device"), // deviceId + (String) event.data.get("folder"), // folderId + (String) event.data.get("folderLabel") // folderLabel + ); break; case "ItemFinished": String folder = (String) event.data.get("folder"); @@ -242,10 +209,78 @@ public class EventProcessor implements Runnable, RestApi.OnReceiveEventListener } } - private void notify(String text, PendingIntent pi) { - // HACK: Use a random, deterministic ID between 1000 and 2000 to avoid duplicate - // notifications. - int notificationId = 1000 + text.hashCode() % 1000; - mNotificationHandler.showEventNotification(text, pi, notificationId); + private void onDeviceRejected(String deviceId, String deviceName) { + if (deviceId == null) { + return; + } + Log.d(TAG, "Unknown device " + deviceName + "(" + deviceId + ") wants to connect"); + + String title = mContext.getString(R.string.device_rejected, + deviceName.isEmpty() ? deviceId.substring(0, 7) : deviceName); + int notificationId = mNotificationHandler.getNotificationIdFromText(title); + + // Prepare "accept" action. + Intent intentAccept = new Intent(mContext, DeviceActivity.class) + .putExtra(DeviceActivity.EXTRA_NOTIFICATION_ID, notificationId) + .putExtra(DeviceActivity.EXTRA_IS_CREATE, true) + .putExtra(DeviceActivity.EXTRA_DEVICE_ID, deviceId) + .putExtra(DeviceActivity.EXTRA_DEVICE_NAME, deviceName); + PendingIntent piAccept = PendingIntent.getActivity(mContext, notificationId, + intentAccept, PendingIntent.FLAG_UPDATE_CURRENT); + + // Prepare "ignore" action. + Intent intentIgnore = new Intent(mContext, SyncthingService.class) + .putExtra(SyncthingService.EXTRA_NOTIFICATION_ID, notificationId) + .putExtra(SyncthingService.EXTRA_DEVICE_ID, deviceId); + intentIgnore.setAction(SyncthingService.ACTION_IGNORE_DEVICE); + PendingIntent piIgnore = PendingIntent.getService(mContext, 0, + intentIgnore, PendingIntent.FLAG_UPDATE_CURRENT); + + // Show notification. + mNotificationHandler.showConsentNotification(notificationId, title, piAccept, piIgnore); + } + + private void onFolderRejected(String deviceId, String folderId, + String folderLabel) { + if (deviceId == null || folderId == null) { + return; + } + Log.d(TAG, "Device " + deviceId + " wants to share folder " + + folderLabel + " (" + folderId + ")"); + + // Find the deviceName corresponding to the deviceId + String deviceName = null; + for (Device d : mApi.getDevices(false)) { + if (d.deviceID.equals(deviceId)) { + deviceName = d.getDisplayName(); + break; + } + } + String title = mContext.getString(R.string.folder_rejected, deviceName, + folderLabel.isEmpty() ? folderId : folderLabel + " (" + folderId + ")"); + int notificationId = mNotificationHandler.getNotificationIdFromText(title); + + // Prepare "accept" action. + boolean isNewFolder = Stream.of(mApi.getFolders()) + .noneMatch(f -> f.id.equals(folderId)); + Intent intentAccept = new Intent(mContext, FolderActivity.class) + .putExtra(FolderActivity.EXTRA_NOTIFICATION_ID, notificationId) + .putExtra(FolderActivity.EXTRA_IS_CREATE, isNewFolder) + .putExtra(FolderActivity.EXTRA_DEVICE_ID, deviceId) + .putExtra(FolderActivity.EXTRA_FOLDER_ID, folderId) + .putExtra(FolderActivity.EXTRA_FOLDER_LABEL, folderLabel); + PendingIntent piAccept = PendingIntent.getActivity(mContext, notificationId, + intentAccept, PendingIntent.FLAG_UPDATE_CURRENT); + + // Prepare "ignore" action. + Intent intentIgnore = new Intent(mContext, SyncthingService.class) + .putExtra(SyncthingService.EXTRA_NOTIFICATION_ID, notificationId) + .putExtra(SyncthingService.EXTRA_FOLDER_ID, folderId); + intentIgnore.setAction(SyncthingService.ACTION_IGNORE_FOLDER); + PendingIntent piIgnore = PendingIntent.getService(mContext, 0, + intentIgnore, PendingIntent.FLAG_UPDATE_CURRENT); + + // Show notification. + mNotificationHandler.showConsentNotification(notificationId, title, piAccept, piIgnore); } } diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/NotificationHandler.java b/app/src/main/java/com/nutomic/syncthingandroid/service/NotificationHandler.java index ee2f703d..0d091694 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/NotificationHandler.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/NotificationHandler.java @@ -10,6 +10,7 @@ import android.content.SharedPreferences; import android.os.Build; import android.support.annotation.StringRes; import android.support.v4.app.NotificationCompat; +import android.util.Log; import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.SyncthingApp; @@ -22,6 +23,7 @@ import javax.inject.Inject; public class NotificationHandler { + private static final String TAG = "NotificationHandler"; private static final int ID_PERSISTENT = 1; private static final int ID_PERSISTENT_WAITING = 4; private static final int ID_RESTART = 2; @@ -178,17 +180,51 @@ public class NotificationHandler { } } - public void showEventNotification(String text, PendingIntent pi, int id) { + /** + * Calculate a deterministic ID between 1000 and 2000 to avoid duplicate + * notification ids for different device, folder consent popups triggered + * by {@link EventProcessor}. + */ + public int getNotificationIdFromText(String text) { + return 1000 + text.hashCode() % 1000; + } + + /** + * Closes a notification. Required after the user hit an action button. + */ + public void cancelConsentNotification(int notificationId) { + if (notificationId == 0) { + return; + } + Log.v(TAG, "Cancelling notification with id " + notificationId); + mNotificationManager.cancel(notificationId); + } + + /** + * Used by {@link EventProcessor} + */ + public void showConsentNotification(int notificationId, + String text, + PendingIntent piAccept, + PendingIntent piIgnore) { + /** + * As we know the id for a specific notification text, + * we'll dismiss this notification as it may be outdated. + * This is also valid if the notification does not exist. + */ + mNotificationManager.cancel(notificationId); Notification n = getNotificationBuilder(mInfoChannel) .setContentTitle(mContext.getString(R.string.app_name)) .setContentText(text) .setStyle(new NotificationCompat.BigTextStyle() .bigText(text)) - .setContentIntent(pi) + .setContentIntent(piAccept) + .addAction(R.drawable.ic_stat_notify, mContext.getString(R.string.accept), piAccept) + .addAction(R.drawable.ic_stat_notify, mContext.getString(R.string.ignore), piIgnore) .setSmallIcon(R.drawable.ic_stat_notify) .setAutoCancel(true) .build(); - mNotificationManager.notify(id, n); + mNotificationManager.notify(notificationId, n); } public void showStoragePermissionRevokedNotification() { diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java b/app/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java index 384ae9d4..bd41f236 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java @@ -242,6 +242,32 @@ public class RestApi { } } + /** + * Permanently ignore a device when it tries to connect. + * Ignored devices will not trigger the "DeviceRejected" event + * in {@link EventProcessor#onEvent}. + */ + public void ignoreDevice(String deviceId) { + if (!mConfig.ignoredDevices.contains(deviceId)) { + mConfig.ignoredDevices.add(deviceId); + sendConfig(); + Log.d(TAG, "Ignored device [" + deviceId + "]"); + } + } + + /** + * Permanently ignore a folder share request. + * Ignored folders will not trigger the "FolderRejected" event + * in {@link EventProcessor#onEvent}. + */ + public void ignoreFolder(String folderId) { + if (!mConfig.ignoredFolders.contains(folderId)) { + mConfig.ignoredFolders.add(folderId); + sendConfig(); + Log.d(TAG, "Ignored folder [" + folderId + "]"); + } + } + /** * Sends current config to Syncthing. * Will result in a "ConfigSaved" event. diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java index 50db28eb..cfd7f1d3 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java @@ -59,6 +59,36 @@ public class SyncthingService extends Service { public static final String ACTION_REFRESH_NETWORK_INFO = "com.nutomic.syncthingandroid.service.SyncthingService.REFRESH_NETWORK_INFO"; + /** + * Intent action to permanently ignore a device connection request. + */ + public static final String ACTION_IGNORE_DEVICE = + "com.nutomic.syncthingandroid.service.SyncthingService.IGNORE_DEVICE"; + + /** + * Intent action to permanently ignore a folder share request. + */ + public static final String ACTION_IGNORE_FOLDER = + "com.nutomic.syncthingandroid.service.SyncthingService.IGNORE_FOLDER"; + + /** + * Extra used together with ACTION_IGNORE_DEVICE, ACTION_IGNORE_FOLDER. + */ + public static final String EXTRA_NOTIFICATION_ID = + "com.nutomic.syncthingandroid.service.SyncthingService.EXTRA_NOTIFICATION_ID"; + + /** + * Extra used together with ACTION_IGNORE_DEVICE + */ + public static final String EXTRA_DEVICE_ID = + "com.nutomic.syncthingandroid.service.SyncthingService.EXTRA_DEVICE_ID"; + + /** + * Extra used together with ACTION_IGNORE_FOLDER + */ + public static final String EXTRA_FOLDER_ID = + "com.nutomic.syncthingandroid.service.SyncthingService.EXTRA_FOLDER_ID"; + public interface OnServiceStateChangeListener { void onServiceStateChange(State currentState); } @@ -204,6 +234,14 @@ public class SyncthingService extends Service { }); } else if (ACTION_REFRESH_NETWORK_INFO.equals(intent.getAction())) { mDeviceStateHolder.updateShouldRunDecision(); + } else if (ACTION_IGNORE_DEVICE.equals(intent.getAction()) && mCurrentState == State.ACTIVE) { + // mApi is not null due to State.ACTIVE + mApi.ignoreDevice(intent.getStringExtra(EXTRA_DEVICE_ID)); + mNotificationHandler.cancelConsentNotification(intent.getIntExtra(EXTRA_NOTIFICATION_ID, 0)); + } else if (ACTION_IGNORE_FOLDER.equals(intent.getAction()) && mCurrentState == State.ACTIVE) { + // mApi is not null due to State.ACTIVE + mApi.ignoreFolder(intent.getStringExtra(EXTRA_FOLDER_ID)); + mNotificationHandler.cancelConsentNotification(intent.getIntExtra(EXTRA_NOTIFICATION_ID, 0)); } return START_STICKY; } @@ -527,6 +565,10 @@ public class SyncthingService extends Service { return mCurrentState; } + public NotificationHandler getNotificationHandler() { + return mNotificationHandler; + } + /** * Exports the local config and keys to {@link Constants#EXPORT_PATH}. */ diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 829ea704..d033e382 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -25,8 +25,8 @@ Bitte melden Sie auftretende Probleme via GitHub. Nein Webseite öffnen Diese App benötigt Schreibzugriff auf den Gerätespeicher - Gerät %1$s möchte sich verbinden - Gerät %1$s möchte Verzeichnis %2$s teilen + Gerät \"%1$s\" möchte sich verbinden + Gerät \"%1$s\" möchte Verzeichnis \"%2$s\" teilen Batterielaufzeit Optimierung Android kann die Synchronisation nach einiger Zeit stoppen. Um dies zu verhindern kann der Energiesparmodus deaktiviert werden.\n\nEinige Geräte haben Apps vorinstalliert, welche Hintergrundaktivitäten unterbinden. Syncthing sollte deshalb auch in die Ausnahmeliste hinzugefügt werden. Später diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d30df4b6..b9613eea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,6 +21,10 @@ Please report any problems you encounter via Github. Example + Accept + + Ignore + @@ -46,9 +50,9 @@ Please report any problems you encounter via Github. Write storage permission is required for this app - Device %1$s wants to connect + Device \"%1$s\" wants to connect - Device %1$s wants to share folder %2$s + Device \"%1$s\" wants to share folder \"%2$s\" Battery Optimization Android may stop synchronization after some time. To prevent this, turn off battery optimization.\n\nSome devices have additional task-killing apps preinstalled. You should add Syncthing to their whitelist, as well.