Fix backup job background start restricitions with API31+.

This commit is contained in:
Cody Henthorne
2022-10-12 09:48:40 -04:00
committed by GitHub
parent e1c6dfb73b
commit a8e03e9bf2
18 changed files with 453 additions and 22 deletions

View File

@@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.service.NotificationController;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -43,17 +44,17 @@ public class SQLCipherMigrationHelper {
@NonNull SQLiteDatabase modernDb)
{
modernDb.beginTransaction();
int foregroundId = GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database)).getId();
try {
try (NotificationController controller = 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);
copyTable("recipient_preferences", legacyDb, modernDb, null);
copyTable("group_receipts", legacyDb, modernDb, null);
modernDb.setTransactionSuccessful();
} catch (GenericForegroundService.UnableToStartException e) {
throw new IllegalStateException(e);
} finally {
modernDb.endTransaction();
GenericForegroundService.stopForegroundTask(context, foregroundId);
}
}
@@ -68,8 +69,7 @@ public class SQLCipherMigrationHelper {
modernDb.beginTransaction();
int foregroundId = GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database)).getId();
try {
try (NotificationController controller = GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database))) {
int total = 5000;
copyTable("sms", legacyDb, modernDb, (row, progress) -> {
@@ -176,9 +176,10 @@ public class SQLCipherMigrationHelper {
AttachmentSecretProvider.getInstance(context).setClassicKey(context, masterSecret.getEncryptionKey().getEncoded(), masterSecret.getMacKey().getEncoded());
TextSecurePreferences.setNeedsSqlCipherMigration(context, false);
modernDb.setTransactionSuccessful();
} catch (GenericForegroundService.UnableToStartException e) {
throw new IllegalStateException(e);
} finally {
modernDb.endTransaction();
GenericForegroundService.stopForegroundTask(context, foregroundId);
}
}

View File

@@ -205,7 +205,7 @@ public final class AttachmentCompressionJob extends BaseJob {
return attachment;
}
try (NotificationController notification = GenericForegroundService.startForegroundTask(context, context.getString(R.string.AttachmentUploadJob_compressing_video_start))) {
try (NotificationController notification = ForegroundUtil.requireForegroundTask(context, context.getString(R.string.AttachmentUploadJob_compressing_video_start))) {
notification.setIndeterminateProgress();
@@ -290,7 +290,7 @@ public final class AttachmentCompressionJob extends BaseJob {
throw new UndeliverableMessageException("Failed to transcode and cannot skip due to editing", e);
}
}
} catch (IOException | MmsException e) {
} catch (GenericForegroundService.UnableToStartException | IOException | MmsException e) {
throw new UndeliverableMessageException("Failed to transcode", e);
}
return attachment;

View File

@@ -159,7 +159,12 @@ public final class AttachmentUploadJob extends BaseJob {
private @Nullable NotificationController getNotificationForAttachment(@NonNull Attachment attachment) {
if (attachment.getSize() >= FOREGROUND_LIMIT) {
return GenericForegroundService.startForegroundTask(context, context.getString(R.string.AttachmentUploadJob_uploading_media));
try {
return ForegroundUtil.requireForegroundTask(context, context.getString(R.string.AttachmentUploadJob_uploading_media));
} catch (GenericForegroundService.UnableToStartException e) {
Log.w(TAG, "Unable to start foreground service", e);
return null;
}
} else {
return null;
}

View File

@@ -0,0 +1,74 @@
package org.thoughtcrime.securesms.jobs
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.SystemClock
import org.signal.core.util.PendingIntentFlags
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.service.GenericForegroundService
import org.thoughtcrime.securesms.service.NotificationController
import org.thoughtcrime.securesms.util.ServiceUtil
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* Helps start foreground services from the background.
*/
object ForegroundUtil {
private val TAG = Log.tag(ForegroundUtil::class.java)
private val updateMutex: ReentrantLock = ReentrantLock()
private var activeLatch: CountDownLatch? = null
@Throws(GenericForegroundService.UnableToStartException::class)
@JvmStatic
fun requireForegroundTask(context: Context, task: String): NotificationController {
val alarmManager = ServiceUtil.getAlarmManager(context)
if (Build.VERSION.SDK_INT < 31 || ApplicationDependencies.getAppForegroundObserver().isForegrounded || !alarmManager.canScheduleExactAlarms()) {
return GenericForegroundService.startForegroundTask(context, task)
}
val latch: CountDownLatch? = updateMutex.withLock {
if (activeLatch == null) {
if (alarmManager.canScheduleExactAlarms()) {
activeLatch = CountDownLatch(1)
val pendingIntent = PendingIntent.getBroadcast(context, 0, Intent(context, Receiver::class.java), PendingIntentFlags.mutable())
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 1000, pendingIntent)
} else {
Log.w(TAG, "Unable to schedule alarm")
}
}
activeLatch
}
if (latch != null) {
try {
if (!latch.await(1, TimeUnit.MINUTES)) {
Log.w(TAG, "Time ran out waiting for foreground")
}
} catch (e: InterruptedException) {
Log.w(TAG, "Interrupted while waiting for foreground")
}
}
return GenericForegroundService.startForegroundTask(context, task)
}
class Receiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
updateMutex.withLock {
activeLatch?.countDown()
activeLatch = null
}
}
}
}

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.jobs;
import android.Manifest;
import android.os.Build;
import androidx.annotation.NonNull;
@@ -55,7 +56,7 @@ public final class LocalBackupJob extends BaseJob {
.setQueue(QUEUE)
.setMaxInstancesForFactory(1)
.setMaxAttempts(3);
if (force) {
if (force || Build.VERSION.SDK_INT >= 31) {
jobManager.cancelAllInQueue(QUEUE);
} else {
parameters.addConstraint(ChargingConstraint.KEY);
@@ -164,6 +165,9 @@ public final class LocalBackupJob extends BaseJob {
}
BackupUtil.deleteOldBackups();
} catch (GenericForegroundService.UnableToStartException e) {
Log.w(TAG, "This should not happen on API < 31");
throw new AssertionError(e);
} finally {
EventBus.getDefault().unregister(updater);
updater.setNotification(null);

View File

@@ -171,6 +171,9 @@ public final class LocalBackupJobApi29 extends BaseJob {
}
BackupUtil.deleteOldBackups();
} catch (GenericForegroundService.UnableToStartException e) {
Log.w(TAG, "Unable to start foreground backup service", e);
BackupFileIOError.UNKNOWN.postNotification(context);
} finally {
EventBus.getDefault().unregister(updater);
updater.setNotification(null);

View File

@@ -1,7 +1,9 @@
package org.thoughtcrime.securesms.megaphone;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
@@ -34,6 +36,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LocaleFeatureFlags;
import org.thoughtcrime.securesms.util.PlayServicesUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity;
@@ -105,6 +108,7 @@ public final class Megaphones {
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER);
put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : NEVER);
put(Event.BACKUP_SCHEDULE_PERMISSION, shouldShowBackupSchedulePermissionMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(3)) : NEVER);
put(Event.ONBOARDING, shouldShowOnboardingMegaphone(context) ? ALWAYS : NEVER);
put(Event.TURN_OFF_CENSORSHIP_CIRCUMVENTION, shouldShowTurnOffCircumventionMegaphone() ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(7)) : NEVER);
put(Event.DONATE_Q2_2022, shouldShowDonateMegaphone(context, Event.DONATE_Q2_2022, records) ? ShowForDurationSchedule.showForDays(7) : NEVER);
@@ -138,6 +142,8 @@ public final class Megaphones {
return buildTurnOffCircumventionMegaphone(context);
case REMOTE_MEGAPHONE:
return buildRemoteMegaphone(context);
case BACKUP_SCHEDULE_PERMISSION:
return buildBackupPermissionMegaphone(context);
default:
throw new IllegalArgumentException("Event not handled!");
}
@@ -335,6 +341,21 @@ public final class Megaphones {
}
}
@SuppressLint("InlinedApi")
private static Megaphone buildBackupPermissionMegaphone(@NonNull Context context) {
return new Megaphone.Builder(Event.BACKUP_SCHEDULE_PERMISSION, Megaphone.Style.BASIC)
.setTitle(R.string.BackupSchedulePermissionMegaphone__cant_back_up_chats)
.setImage(R.drawable.ic_cant_backup_megaphone)
.setBody(R.string.BackupSchedulePermissionMegaphone__your_chats_are_no_longer_being_automatically_backed_up)
.setActionButton(R.string.BackupSchedulePermissionMegaphone__back_up_chats, (megaphone, controller) -> {
controller.onMegaphoneDialogFragmentRequested(new ReenableBackupsDialogFragment());
})
.setSecondaryButton(R.string.BackupSchedulePermissionMegaphone__not_now, (megaphone, controller) -> {
controller.onMegaphoneSnooze(Event.BACKUP_SCHEDULE_PERMISSION);
})
.build();
}
private static boolean shouldShowDonateMegaphone(@NonNull Context context, @NonNull Event event, @NonNull Map<Event, MegaphoneRecord> records) {
long timeSinceLastDonatePrompt = timeSinceLastDonatePrompt(event, records);
@@ -398,6 +419,10 @@ public final class Megaphones {
return RemoteMegaphoneRepository.hasRemoteMegaphoneToShow(canShowLocalDonate);
}
private static boolean shouldShowBackupSchedulePermissionMegaphone(@NonNull Context context) {
return Build.VERSION.SDK_INT >= 31 && SignalStore.settings().isBackupEnabled() && !ServiceUtil.getAlarmManager(context).canScheduleExactAlarms();
}
/**
* Unfortunately lastSeen is only set today upon snoozing, which never happens to donate prompts.
* So we use firstVisible as a proxy.
@@ -426,7 +451,8 @@ public final class Megaphones {
BECOME_A_SUSTAINER("become_a_sustainer"),
DONATE_Q2_2022("donate_q2_2022"),
TURN_OFF_CENSORSHIP_CIRCUMVENTION("turn_off_censorship_circumvention"),
REMOTE_MEGAPHONE("remote_megaphone");
REMOTE_MEGAPHONE("remote_megaphone"),
BACKUP_SCHEDULE_PERMISSION("backup_schedule_permission");
private final String key;

View File

@@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.megaphone
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
/**
* Bottom sheet dialog to prompt user to enable schedule alarms permission for triggering backups.
*/
class ReenableBackupsDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
override val peekHeightPercentage: Float = 1f
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.reenable_backups_dialog_fragment, container, false)
}
@SuppressLint("InlinedApi")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val launcher: ActivityResultLauncher<Intent> = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
dismissAllowingStateLoss()
}
}
view.findViewById<View>(R.id.reenable_backups_go_to_settings).setOnClickListener {
launcher.launch(Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM, Uri.parse("package:" + requireContext().packageName)))
}
}
}

View File

@@ -38,7 +38,6 @@ import org.thoughtcrime.securesms.util.StorageUtil;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public class BackupsPreferenceFragment extends Fragment {
@@ -85,7 +84,6 @@ public class BackupsPreferenceFragment extends Fragment {
EventBus.getDefault().register(this);
}
@SuppressWarnings("ConstantConditions")
@Override
public void onResume() {
super.onResume();
@@ -241,7 +239,7 @@ public class BackupsPreferenceFragment extends Fragment {
@RequiresApi(29)
private void onCreateClickedApi29() {
Log.i(TAG, "Queing backup...");
Log.i(TAG, "Queueing backup...");
LocalBackupJob.enqueue(true);
}

View File

@@ -1,10 +1,12 @@
package org.thoughtcrime.securesms.service;
import android.app.ForegroundServiceStartNotAllowedException;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.DrawableRes;
@@ -18,6 +20,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.whispersystems.signalservice.api.util.Preconditions;
import java.util.Iterator;
import java.util.LinkedHashMap;
@@ -126,20 +129,39 @@ public final class GenericForegroundService extends Service {
* Waits for {@param delayMillis} ms before starting the foreground task.
* <p>
* The delayed notification controller can also shown on demand and promoted to a regular notification controller to update the message etc.
*
* Do not call this method on API > 31
*/
public static DelayedNotificationController startForegroundTaskDelayed(@NonNull Context context, @NonNull String task, long delayMillis, @DrawableRes int iconRes) {
return DelayedNotificationController.create(delayMillis, () -> startForegroundTask(context, task, DEFAULTS.channelId, iconRes));
Preconditions.checkArgument(Build.VERSION.SDK_INT < 31);
return DelayedNotificationController.create(delayMillis, () -> {
try {
return startForegroundTask(context, task, DEFAULTS.channelId, iconRes);
} catch (UnableToStartException e) {
Log.w(TAG, "This should not happen on API < 31", e);
throw new AssertionError(e.getCause());
}
});
}
public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task) {
public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task) throws UnableToStartException {
return startForegroundTask(context, task, DEFAULTS.channelId);
}
public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task, @NonNull String channelId) {
public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task, @NonNull String channelId)
throws UnableToStartException
{
return startForegroundTask(context, task, channelId, DEFAULTS.iconRes);
}
public static NotificationController 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)
throws UnableToStartException
{
final int id = NEXT_ID.getAndIncrement();
Intent intent = new Intent(context, GenericForegroundService.class);
@@ -150,7 +172,17 @@ public final class GenericForegroundService extends Service {
intent.putExtra(EXTRA_ID, id);
Log.i(TAG, String.format(Locale.US, "Starting foreground service (%s) id=%d", task, id));
ContextCompat.startForegroundService(context, intent);
if (Build.VERSION.SDK_INT < 31) {
ContextCompat.startForegroundService(context, intent);
} else {
try {
ContextCompat.startForegroundService(context, intent);
} catch (ForegroundServiceStartNotAllowedException e) {
Log.e(TAG, "Unable to start foreground service", e);
throw new UnableToStartException(e);
}
}
return new NotificationController(context, id);
}
@@ -289,4 +321,10 @@ public final class GenericForegroundService extends Service {
return GenericForegroundService.this;
}
}
public static final class UnableToStartException extends Exception {
public UnableToStartException(Throwable cause) {
super(cause);
}
}
}

View File

@@ -3,19 +3,27 @@ package org.thoughtcrime.securesms.service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.jobs.LocalBackupJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.JavaTimeExtensionsKt;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
public class LocalBackupListener extends PersistentAlarmManagerListener {
private static final long INTERVAL = TimeUnit.DAYS.toMillis(1);
@Override
protected boolean scheduleExact() {
return Build.VERSION.SDK_INT >= 31;
}
@Override
protected long getNextScheduledExecutionTime(Context context) {
return TextSecurePreferences.getNextBackupTime(context);
@@ -24,7 +32,7 @@ public class LocalBackupListener extends PersistentAlarmManagerListener {
@Override
protected long onAlarm(Context context, long scheduledTime) {
if (SignalStore.settings().isBackupEnabled()) {
LocalBackupJob.enqueue(false);
LocalBackupJob.enqueue(scheduleExact());
}
return setNextBackupTimeToIntervalFromNow(context);
@@ -37,7 +45,20 @@ public class LocalBackupListener extends PersistentAlarmManagerListener {
}
public static long setNextBackupTimeToIntervalFromNow(@NonNull Context context) {
long nextTime = System.currentTimeMillis() + INTERVAL;
long nextTime;
if (Build.VERSION.SDK_INT < 31) {
nextTime = System.currentTimeMillis() + INTERVAL;
} else {
LocalDateTime now = LocalDateTime.now();
LocalDateTime next = now.withHour(2).withMinute(0).withSecond(0);
if (now.getHour() > 2) {
next = next.plusDays(1);
}
nextTime = JavaTimeExtensionsKt.toMillis(next);
}
TextSecurePreferences.setNextBackupTime(context, nextTime);
return nextTime;

View File

@@ -6,6 +6,7 @@ import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import org.signal.core.util.PendingIntentFlags;
import org.signal.core.util.logging.Log;
@@ -35,9 +36,21 @@ public abstract class PersistentAlarmManagerListener extends BroadcastReceiver {
if (pendingIntent != null) {
alarmManager.cancel(pendingIntent);
alarmManager.set(AlarmManager.RTC_WAKEUP, scheduledTime, pendingIntent);
if (scheduleExact() && Build.VERSION.SDK_INT >= 31) {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, scheduledTime, pendingIntent);
} else {
Log.w(TAG, "Unable to schedule exact alarm, permissionAllowed: " + alarmManager.canScheduleExactAlarms());
}
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, scheduledTime, pendingIntent);
}
} else {
Log.i(TAG, "PendingIntent somehow null, skipping");
}
}
protected boolean scheduleExact() {
return false;
}
}

View File

@@ -26,6 +26,7 @@ fun ZoneId.toOffset(): ZoneOffset {
/**
* Convert [LocalDateTime] to be same as [System.currentTimeMillis]
*/
@JvmOverloads
fun LocalDateTime.toMillis(zoneOffset: ZoneOffset = ZoneId.systemDefault().toOffset()): Long {
return TimeUnit.SECONDS.toMillis(toEpochSecond(zoneOffset))
}