diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55883783d..32630b92c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -93,6 +93,8 @@ + + + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherMigrationHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherMigrationHelper.java index 145d67a85..98ea3f5af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherMigrationHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherMigrationHelper.java @@ -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); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java index f56328680..ee1e8b946 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java index ee98db179..fc48e94d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java @@ -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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ForegroundUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ForegroundUtil.kt new file mode 100644 index 000000000..5f91f8ddd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ForegroundUtil.kt @@ -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 + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java index 6debaad49..fa76ae9a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java index e9ec4f2f3..0988a23c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index 7d7f120a6..50dd51bbc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -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 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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/ReenableBackupsDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/megaphone/ReenableBackupsDialogFragment.kt new file mode 100644 index 000000000..a59acfa9f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/ReenableBackupsDialogFragment.kt @@ -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 = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == Activity.RESULT_OK) { + dismissAllowingStateLoss() + } + } + + view.findViewById(R.id.reenable_backups_go_to_settings).setOnClickListener { + launcher.launch(Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM, Uri.parse("package:" + requireContext().packageName))) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java index 7c7365663..03bc49575 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java b/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java index 22efd34f4..37a9717ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java @@ -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. *

* 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); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java index ebab241c8..1b59d5881 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java index d35a3e77a..36f158667 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java @@ -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; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/JavaTimeExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/util/JavaTimeExtensions.kt index ceda89be0..2fbcd17ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/JavaTimeExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/JavaTimeExtensions.kt @@ -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)) } diff --git a/app/src/main/res/drawable-night/ic_cant_backup_megaphone.xml b/app/src/main/res/drawable-night/ic_cant_backup_megaphone.xml new file mode 100644 index 000000000..6d80254b6 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_cant_backup_megaphone.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_cant_backup_megaphone.xml b/app/src/main/res/drawable/ic_cant_backup_megaphone.xml new file mode 100644 index 000000000..9c2e7345e --- /dev/null +++ b/app/src/main/res/drawable/ic_cant_backup_megaphone.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/layout/reenable_backups_dialog_fragment.xml b/app/src/main/res/layout/reenable_backups_dialog_fragment.xml new file mode 100644 index 000000000..97a5d4848 --- /dev/null +++ b/app/src/main/res/layout/reenable_backups_dialog_fragment.xml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b8dea45b5..0467e432b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5345,6 +5345,24 @@ Next + + + Can\'t back up chats + + Your chats are no longer being automatically backed up. + + Back up chats + + Not now + + To re-enable backups: + + Tap the \"Go to settings\" button below + + Turn on \"Allow settings alarms and reminders.\" + + Go to settings +