Allow multiple messages on the Generic Foreground Service. Show the oldest still active.

This commit is contained in:
Alan Evans 2019-06-27 12:18:52 -04:00 committed by GitHub
parent cfcb9a8cdb
commit c089d6cd43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 327 additions and 82 deletions

View file

@ -63,13 +63,16 @@
<string name="DraftDatabase_Draft_location_snippet">(location)</string>
<string name="DraftDatabase_Draft_quote_snippet">(reply)</string>
<!-- AttchmentManager -->
<!-- AttachmentManager -->
<string name="AttachmentManager_cant_open_media_selection">Can\'t find an app to select media.</string>
<string name="AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio">Signal requires the Storage permission in order to attach photos, videos, or audio, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Storage\".</string>
<string name="AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information">Signal requires Contacts permission in order to attach contact information, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Contacts\".</string>
<string name="AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location">Signal requires Location permission in order to attach a location, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Location\".</string>
<string name="AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied">Signal requires the Camera permission in order to take photos, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Camera\".</string>
<!-- AttachmentUploadJob -->
<string name="AttachmentUploadJob_uploading_media">Uploading media...</string>
<!-- AudioSlidePlayer -->
<string name="AudioSlidePlayer_error_playing_audio">Error playing audio!</string>

View file

@ -40,8 +40,8 @@ public class SQLCipherMigrationHelper {
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb)
{
modernDb.beginTransaction();
int foregroundId = GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database)).getId();
try {
GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database));
copyTable("identities", legacyDb, modernDb, null);
copyTable("push", legacyDb, modernDb, null);
copyTable("groups", legacyDb, modernDb, null);
@ -50,7 +50,7 @@ public class SQLCipherMigrationHelper {
modernDb.setTransactionSuccessful();
} finally {
modernDb.endTransaction();
GenericForegroundService.stopForegroundTask(context);
GenericForegroundService.stopForegroundTask(context, foregroundId);
}
}
@ -65,8 +65,8 @@ public class SQLCipherMigrationHelper {
modernDb.beginTransaction();
int foregroundId = GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database)).getId();
try {
GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database));
int total = 5000;
copyTable("sms", legacyDb, modernDb, (row, progress) -> {
@ -175,7 +175,7 @@ public class SQLCipherMigrationHelper {
modernDb.setTransactionSuccessful();
} finally {
modernDb.endTransaction();
GenericForegroundService.stopForegroundTask(context);
GenericForegroundService.stopForegroundTask(context, foregroundId);
}
}

View file

@ -1,8 +1,10 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
@ -19,6 +21,8 @@ import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MediaStream;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.service.NotificationController;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.libsignal.util.guava.Optional;
@ -41,6 +45,11 @@ public class AttachmentUploadJob extends BaseJob implements InjectableType {
private static final String KEY_ROW_ID = "row_id";
private static final String KEY_UNIQUE_ID = "unique_id";
/**
* Foreground notification shows while uploading attachments above this.
*/
private static final int FOREGROUND_LIMIT = 10 * 1024 * 1024;
private AttachmentId attachmentId;
@Inject SignalServiceMessageSender messageSender;
@ -79,13 +88,24 @@ public class AttachmentUploadJob extends BaseJob implements InjectableType {
throw new IllegalStateException("Cannot find the specified attachment.");
}
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment);
SignalServiceAttachment localAttachment = getAttachmentFor(scaledAttachment);
SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream(), databaseAttachment.isSticker());
Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.getFastPreflightId()).get();
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment);
database.updateAttachmentAfterUpload(databaseAttachment.getAttachmentId(), attachment);
try (NotificationController notification = getNotificationForAttachment(scaledAttachment)) {
SignalServiceAttachment localAttachment = getAttachmentFor(scaledAttachment, notification);
SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream(), databaseAttachment.isSticker());
Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.getFastPreflightId()).get();
database.updateAttachmentAfterUpload(databaseAttachment.getAttachmentId(), attachment);
}
}
private @Nullable NotificationController getNotificationForAttachment(@NonNull Attachment attachment) {
if (attachment.getSize() >= FOREGROUND_LIMIT) {
return GenericForegroundService.startForegroundTask(context, context.getString(R.string.AttachmentUploadJob_uploading_media));
} else {
return null;
}
}
@Override
@ -96,7 +116,7 @@ public class AttachmentUploadJob extends BaseJob implements InjectableType {
return exception instanceof IOException;
}
private SignalServiceAttachment getAttachmentFor(Attachment attachment) {
private SignalServiceAttachment getAttachmentFor(Attachment attachment, @Nullable NotificationController notification) {
try {
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
@ -109,7 +129,12 @@ public class AttachmentUploadJob extends BaseJob implements InjectableType {
.withWidth(attachment.getWidth())
.withHeight(attachment.getHeight())
.withCaption(attachment.getCaption())
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)))
.withListener((total, progress) -> {
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress));
if (notification != null) {
notification.setProgress(total, progress);
}
})
.build();
} catch (IOException ioe) {
Log.w(TAG, "Couldn't open attachment", ioe);

View file

@ -2,21 +2,22 @@ package org.thoughtcrime.securesms.jobs;
import android.Manifest;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.backup.BackupPassphrase;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.backup.BackupPassphrase;
import org.thoughtcrime.securesms.backup.FullBackupExporter;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.service.NotificationController;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.StorageUtil;
@ -62,12 +63,13 @@ public class LocalBackupJob extends BaseJob {
throw new IOException("No external storage permission!");
}
GenericForegroundService.startForegroundTask(context,
context.getString(R.string.LocalBackupJob_creating_backup),
NotificationChannels.BACKUPS,
R.drawable.ic_signal_backup);
try (NotificationController notification = GenericForegroundService.startForegroundTask(context,
context.getString(R.string.LocalBackupJob_creating_backup),
NotificationChannels.BACKUPS,
R.drawable.ic_signal_backup))
{
notification.setIndeterminateProgress();
try {
String backupPassword = BackupPassphrase.get(context);
File backupDirectory = StorageUtil.getBackupDirectory();
String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(new Date());
@ -96,8 +98,6 @@ public class LocalBackupJob extends BaseJob {
}
BackupUtil.deleteOldBackups();
} finally {
GenericForegroundService.stopForegroundTask(context);
}
}

View file

@ -1,11 +1,12 @@
package org.thoughtcrime.securesms.service;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -16,111 +17,237 @@ import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.whispersystems.libsignal.util.guava.Preconditions;
public class GenericForegroundService extends Service {
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
private static final String TAG = GenericForegroundService.class.getSimpleName();
public final class GenericForegroundService extends Service {
private static final int NOTIFICATION_ID = 827353982;
private static final String EXTRA_TITLE = "extra_title";
private static final String EXTRA_CHANNEL_ID = "extra_channel_id";
private static final String EXTRA_ICON_RES = "extra_icon_res";
private static final String TAG = Log.tag(GenericForegroundService.class);
private final IBinder binder = new LocalBinder();
private static final int NOTIFICATION_ID = 827353982;
private static final String EXTRA_TITLE = "extra_title";
private static final String EXTRA_CHANNEL_ID = "extra_channel_id";
private static final String EXTRA_ICON_RES = "extra_icon_res";
private static final String EXTRA_ID = "extra_id";
private static final String EXTRA_PROGRESS = "extra_progress";
private static final String EXTRA_PROGRESS_MAX = "extra_progress_max";
private static final String EXTRA_PROGRESS_INDETERMINATE = "extra_progress_indeterminate";
private static final String ACTION_START = "start";
private static final String ACTION_STOP = "stop";
private int foregroundCount;
private String activeTitle;
private String activeChannelId;
private int activeIconRes;
private static final AtomicInteger NEXT_ID = new AtomicInteger();
@Override
public void onCreate() {
private final LinkedHashMap<Integer, Entry> allActiveMessages = new LinkedHashMap<>();
}
private static final Entry DEFAULTS = new Entry("", NotificationChannels.OTHER, R.drawable.ic_signal_grey_24dp, -1, 0, 0, false);
private @Nullable Entry lastPosted;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
throw new IllegalStateException("Intent needs to be non-null.");
}
synchronized (GenericForegroundService.class) {
if (intent != null && ACTION_START.equals(intent.getAction())) handleStart(intent);
else if (intent != null && ACTION_STOP.equals(intent.getAction())) handleStop();
else throw new IllegalStateException("Action needs to be START or STOP.");
String action = intent.getAction();
if (ACTION_START.equals(action)) handleStart(intent);
else if (ACTION_STOP .equals(action)) handleStop(intent);
else throw new IllegalStateException(String.format("Action needs to be %s or %s.", ACTION_START, ACTION_STOP));
updateNotification();
return START_NOT_STICKY;
}
}
private synchronized void updateNotification() {
Iterator<Entry> iterator = allActiveMessages.values().iterator();
private void handleStart(@NonNull Intent intent) {
String title = Preconditions.checkNotNull(intent.getStringExtra(EXTRA_TITLE));
String channelId = Preconditions.checkNotNull(intent.getStringExtra(EXTRA_CHANNEL_ID));
int iconRes = intent.getIntExtra(EXTRA_ICON_RES, R.drawable.ic_signal_grey_24dp);
Log.i(TAG, "handleStart() Title: " + title + " ChannelId: " + channelId);
foregroundCount++;
if (foregroundCount == 1) {
Log.d(TAG, "First request. Title: " + title + " ChannelId: " + channelId);
activeTitle = title;
activeChannelId = channelId;
activeIconRes = iconRes;
}
postObligatoryForegroundNotification(activeTitle, activeChannelId, activeIconRes);
}
private void handleStop() {
Log.i(TAG, "handleStop()");
postObligatoryForegroundNotification(activeTitle, activeChannelId, activeIconRes);
foregroundCount--;
if (foregroundCount == 0) {
if (iterator.hasNext()) {
postObligatoryForegroundNotification(iterator.next());
} else {
Log.d(TAG, "Last request. Ending foreground service.");
postObligatoryForegroundNotification(lastPosted != null ? lastPosted : DEFAULTS);
stopForeground(true);
stopSelf();
}
}
private void postObligatoryForegroundNotification(String title, String channelId, @DrawableRes int iconRes) {
startForeground(NOTIFICATION_ID, new NotificationCompat.Builder(this, channelId)
.setSmallIcon(iconRes)
.setContentTitle(title)
private synchronized void handleStart(@NonNull Intent intent) {
Entry entry = Entry.fromIntent(intent);
Log.i(TAG, String.format(Locale.US, "handleStart() %s", entry));
allActiveMessages.put(entry.id, entry);
}
private synchronized void handleStop(@NonNull Intent intent) {
Log.i(TAG, "handleStop()");
int id = intent.getIntExtra(EXTRA_ID, -1);
Entry removed = allActiveMessages.remove(id);
if (removed == null) {
Log.w(TAG, "Could not find entry to remove");
}
}
private void postObligatoryForegroundNotification(@NonNull Entry active) {
lastPosted = active;
startForeground(NOTIFICATION_ID, new NotificationCompat.Builder(this, active.channelId)
.setSmallIcon(active.iconRes)
.setContentTitle(active.title)
.setProgress(active.progressMax, active.progress, active.indeterminate)
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ConversationListActivity.class), 0))
.build());
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
return binder;
}
public static void startForegroundTask(@NonNull Context context, @NonNull String task) {
startForegroundTask(context, task, NotificationChannels.OTHER);
public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task) {
return startForegroundTask(context, task, DEFAULTS.channelId);
}
public static void startForegroundTask(@NonNull Context context, @NonNull String task, @NonNull String channelId) {
startForegroundTask(context, task, channelId, R.drawable.ic_signal_grey_24dp);
public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task, @NonNull String channelId) {
return startForegroundTask(context, task, channelId, DEFAULTS.iconRes);
}
public static void startForegroundTask(@NonNull Context context, @NonNull String task, @NonNull String channelId, @DrawableRes int iconRes) {
public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task, @NonNull String channelId, @DrawableRes int iconRes) {
final int id = NEXT_ID.getAndIncrement();
Intent intent = new Intent(context, GenericForegroundService.class);
intent.setAction(ACTION_START);
intent.putExtra(EXTRA_TITLE, task);
intent.putExtra(EXTRA_CHANNEL_ID, channelId);
intent.putExtra(EXTRA_ICON_RES, iconRes);
intent.putExtra(EXTRA_ID, id);
ContextCompat.startForegroundService(context, intent);
return new NotificationController(context, id);
}
public static void stopForegroundTask(@NonNull Context context) {
public static void stopForegroundTask(@NonNull Context context, int id) {
Intent intent = new Intent(context, GenericForegroundService.class);
intent.setAction(ACTION_STOP);
intent.putExtra(EXTRA_ID, id);
ContextCompat.startForegroundService(context, intent);
}
synchronized void replaceProgress(int id, int progressMax, int progress, boolean indeterminate) {
Entry oldEntry = allActiveMessages.get(id);
if (oldEntry == null) {
Log.w(TAG, "Failed to replace notification, it was not found");
return;
}
Entry newEntry = new Entry(oldEntry.title, oldEntry.channelId, oldEntry.iconRes, oldEntry.id, progressMax, progress, indeterminate);
if (oldEntry.equals(newEntry)) {
Log.d(TAG, String.format("handleReplace() skip, no change %s", newEntry));
return;
}
Log.i(TAG, String.format("handleReplace() %s", newEntry));
allActiveMessages.put(newEntry.id, newEntry);
updateNotification();
}
private static class Entry {
final @NonNull String title;
final @NonNull String channelId;
final int id;
final @DrawableRes int iconRes;
final int progress;
final int progressMax;
final boolean indeterminate;
private Entry(@NonNull String title, @NonNull String channelId, @DrawableRes int iconRes, int id, int progressMax, int progress, boolean indeterminate) {
this.title = title;
this.channelId = channelId;
this.iconRes = iconRes;
this.id = id;
this.progress = progress;
this.progressMax = progressMax;
this.indeterminate = indeterminate;
}
private static Entry fromIntent(@NonNull Intent intent) {
int id = intent.getIntExtra(EXTRA_ID, DEFAULTS.id);
String title = intent.getStringExtra(EXTRA_TITLE);
if (title == null) title = DEFAULTS.title;
String channelId = intent.getStringExtra(EXTRA_CHANNEL_ID);
if (channelId == null) channelId = DEFAULTS.channelId;
int iconRes = intent.getIntExtra(EXTRA_ICON_RES, DEFAULTS.iconRes);
int progress = intent.getIntExtra(EXTRA_PROGRESS, DEFAULTS.progress);
int progressMax = intent.getIntExtra(EXTRA_PROGRESS_MAX, DEFAULTS.progressMax);
boolean indeterminate = intent.getBooleanExtra(EXTRA_PROGRESS_INDETERMINATE, DEFAULTS.indeterminate);
return new Entry(title, channelId, iconRes, id, progressMax, progress, indeterminate);
}
@Override
public @NonNull String toString() {
return String.format(Locale.US, "ChannelId: %s Id: %d Progress: %d/%d %s", channelId, id, progress, progressMax, indeterminate ? "indeterminate" : "determinate");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Entry entry = (Entry) o;
return id == entry.id &&
iconRes == entry.iconRes &&
progress == entry.progress &&
progressMax == entry.progressMax &&
indeterminate == entry.indeterminate &&
Objects.equals(title, entry.title) &&
Objects.equals(channelId, entry.channelId);
}
@Override
public int hashCode() {
int hashCode = title.hashCode();
hashCode *= 31;
hashCode += channelId.hashCode();
hashCode *= 31;
hashCode += id;
hashCode *= 31;
hashCode += iconRes;
hashCode *= 31;
hashCode += progress;
hashCode *= 31;
hashCode += progressMax;
hashCode *= 31;
hashCode += indeterminate ? 1 : 0;
return hashCode;
}
}
class LocalBinder extends Binder {
GenericForegroundService getService() {
// Return this instance of LocalService so clients can call public methods
return GenericForegroundService.this;
}
}
}

View file

@ -0,0 +1,90 @@
package org.thoughtcrime.securesms.service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import androidx.annotation.NonNull;
import java.util.concurrent.atomic.AtomicReference;
public final class NotificationController implements AutoCloseable {
private final @NonNull Context context;
private final int id;
private int progress;
private int progressMax;
private boolean indeterminate;
private long percent = -1;
private final AtomicReference<GenericForegroundService> service = new AtomicReference<>();
NotificationController(@NonNull Context context, int id) {
this.context = context;
this.id = id;
bindToService();
}
private void bindToService() {
context.bindService(new Intent(context, GenericForegroundService.class), new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
GenericForegroundService.LocalBinder binder = (GenericForegroundService.LocalBinder) service;
GenericForegroundService genericForegroundService = binder.getService();
NotificationController.this.service.set(genericForegroundService);
updateProgressOnService();
}
@Override
public void onServiceDisconnected(ComponentName name) {
service.set(null);
}
}, Context.BIND_AUTO_CREATE);
}
public int getId() {
return id;
}
@Override
public void close() {
GenericForegroundService.stopForegroundTask(context, id);
}
public void setIndeterminateProgress() {
setProgress(0, 0, true);
}
public void setProgress(long newProgressMax, long newProgress) {
setProgress((int) newProgressMax, (int) newProgress, false);
}
private synchronized void setProgress(int newProgressMax, int newProgress, boolean indeterminant) {
int newPercent = newProgressMax != 0 ? 100 * newProgress / newProgressMax : -1;
boolean same = newPercent == percent && indeterminate == indeterminant;
percent = newPercent;
progress = newProgress;
progressMax = newProgressMax;
indeterminate = indeterminant;
if (same) return;
updateProgressOnService();
}
private synchronized void updateProgressOnService() {
GenericForegroundService genericForegroundService = service.get();
if (genericForegroundService == null) return;
genericForegroundService.replaceProgress(id, progressMax, progress, indeterminate);
}
}