diff --git a/app/src/main/java/org/tasks/jobs/WorkManager.java b/app/src/main/java/org/tasks/jobs/WorkManager.java deleted file mode 100644 index b78240c51..000000000 --- a/app/src/main/java/org/tasks/jobs/WorkManager.java +++ /dev/null @@ -1,281 +0,0 @@ -package org.tasks.jobs; - -import static com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo; -import static com.todoroo.andlib.utility.DateUtilities.now; -import static io.reactivex.Single.just; -import static io.reactivex.Single.zip; -import static org.tasks.date.DateTimeUtils.midnight; -import static org.tasks.date.DateTimeUtils.newDateTime; -import static org.tasks.db.DbUtils.batch; -import static org.tasks.jobs.ReverseGeocodeWork.PLACE_ID; -import static org.tasks.time.DateTimeUtils.currentTimeMillis; -import static org.tasks.time.DateTimeUtils.printDuration; -import static org.tasks.time.DateTimeUtils.printTimestamp; - -import android.annotation.SuppressLint; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import androidx.annotation.Nullable; -import androidx.work.BackoffPolicy; -import androidx.work.Constraints; -import androidx.work.Data; -import androidx.work.ExistingPeriodicWorkPolicy; -import androidx.work.ExistingWorkPolicy; -import androidx.work.NetworkType; -import androidx.work.OneTimeWorkRequest; -import androidx.work.OneTimeWorkRequest.Builder; -import androidx.work.PeriodicWorkRequest; -import androidx.work.Worker; -import com.google.common.primitives.Longs; -import com.todoroo.astrid.data.Task; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.util.Random; -import java.util.concurrent.TimeUnit; -import javax.inject.Inject; -import org.tasks.BuildConfig; -import org.tasks.R; -import org.tasks.data.CaldavDao; -import org.tasks.data.GoogleTaskListDao; -import org.tasks.data.Place; -import org.tasks.injection.ApplicationScope; -import org.tasks.injection.ForApplication; -import org.tasks.preferences.Preferences; -import timber.log.Timber; - -@ApplicationScope -public class WorkManager { - - public static final long REMOTE_CONFIG_INTERVAL_HOURS = BuildConfig.DEBUG ? 1 : 12; - private static final int MAX_CLEANUP_LENGTH = 500; - private static final String TAG_BACKUP = "tag_backup"; - private static final String TAG_REFRESH = "tag_refresh"; - private static final String TAG_MIDNIGHT_REFRESH = "tag_midnight_refresh"; - private static final String TAG_SYNC = "tag_sync"; - private static final String TAG_BACKGROUND_SYNC = "tag_background_sync"; - private static final String TAG_REMOTE_CONFIG = "tag_remote_config"; - - private final Context context; - private final Preferences preferences; - private final GoogleTaskListDao googleTaskListDao; - private final CaldavDao caldavDao; - private final AlarmManager alarmManager; - private final androidx.work.WorkManager workManager; - - @Inject - public WorkManager( - @ForApplication Context context, - Preferences preferences, - GoogleTaskListDao googleTaskListDao, - CaldavDao caldavDao) { - this.context = context; - this.preferences = preferences; - this.googleTaskListDao = googleTaskListDao; - this.caldavDao = caldavDao; - alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - workManager = androidx.work.WorkManager.getInstance(context); - } - - public void afterSave(Task current, Task original) { - workManager.enqueue( - new OneTimeWorkRequest.Builder(AfterSaveWork.class) - .setInputData(AfterSaveWork.getInputData(current, original)) - .build()); - } - - public void cleanup(Iterable ids) { - batch( - ids, - MAX_CLEANUP_LENGTH, - b -> - workManager.enqueue( - new Builder(CleanupWork.class) - .setInputData( - new Data.Builder() - .putLongArray(CleanupWork.EXTRA_TASK_IDS, Longs.toArray(b)) - .build()) - .build())); - } - - public void sync(boolean immediate) { - Constraints constraints = - new Constraints.Builder() - .setRequiredNetworkType( - !immediate - && preferences.getBoolean(R.string.p_background_sync_unmetered_only, false) - ? NetworkType.UNMETERED - : NetworkType.CONNECTED) - .build(); - Builder builder = - new Builder(SyncWork.class) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) - .setConstraints(constraints); - if (!immediate) { - builder.setInitialDelay(1, TimeUnit.MINUTES); - } - OneTimeWorkRequest request = builder.build(); - workManager.beginUniqueWork(TAG_SYNC, ExistingWorkPolicy.REPLACE, request).enqueue(); - } - - public void reverseGeocode(Place place) { - if (BuildConfig.DEBUG && place.getId() == 0) { - throw new RuntimeException("Missing id"); - } - workManager.enqueue( - new Builder(ReverseGeocodeWork.class) - .setInputData(new Data.Builder().putLong(PLACE_ID, place.getId()).build()) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) - .setConstraints( - new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) - .build()); - } - - public void updateBackgroundSync() { - updateBackgroundSync(null, null, null); - } - - @SuppressLint("CheckResult") - public void updateBackgroundSync( - @Nullable Boolean forceAccountPresent, - @Nullable Boolean forceBackgroundEnabled, - @Nullable Boolean forceOnlyOnUnmetered) { - boolean backgroundEnabled = - forceBackgroundEnabled == null - ? preferences.getBoolean(R.string.p_background_sync, true) - : forceBackgroundEnabled; - boolean onlyOnWifi = - forceOnlyOnUnmetered == null - ? preferences.getBoolean(R.string.p_background_sync_unmetered_only, false) - : forceOnlyOnUnmetered; - - //noinspection ResultOfMethodCallIgnored - (forceAccountPresent == null - ? zip( - googleTaskListDao.accountCount(), - Single.fromCallable(caldavDao::accountCount), - (googleCount, caldavCount) -> googleCount > 0 || caldavCount > 0) - : just(forceAccountPresent)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - accountsPresent -> - scheduleBackgroundSync(backgroundEnabled && accountsPresent, onlyOnWifi)); - } - - private void scheduleBackgroundSync(boolean enabled, boolean onlyOnUnmetered) { - Timber.d("background sync enabled: %s, onlyOnUnmetered: %s", enabled, onlyOnUnmetered); - if (enabled) { - workManager.enqueueUniquePeriodicWork( - TAG_BACKGROUND_SYNC, - ExistingPeriodicWorkPolicy.KEEP, - new PeriodicWorkRequest.Builder(SyncWork.class, 1, TimeUnit.HOURS) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) - .setConstraints(getNetworkConstraints(onlyOnUnmetered)) - .build()); - } else { - workManager.cancelUniqueWork(TAG_BACKGROUND_SYNC); - } - } - - public void scheduleRefresh(long time) { - enqueueUnique(TAG_REFRESH, RefreshWork.class, time); - } - - public void scheduleMidnightRefresh() { - enqueueUnique(TAG_MIDNIGHT_REFRESH, MidnightRefreshWork.class, midnight()); - } - - @SuppressWarnings("WeakerAccess") - public void scheduleNotification(long time) { - time = Math.max(now(), time); - - if (time < currentTimeMillis()) { - Intent intent = getNotificationIntent(); - if (atLeastOreo()) { - context.startForegroundService(intent); - } else { - context.startService(intent); - } - } else { - PendingIntent pendingIntent = getNotificationPendingIntent(); - alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pendingIntent); - } - } - - public void scheduleBackup() { - long lastBackup = preferences.getLong(R.string.p_last_backup, 0L); - enqueueUnique( - TAG_BACKUP, - BackupWork.class, - Math.min(newDateTime(lastBackup).plusDays(1).getMillis(), midnight())); - } - - public void scheduleConfigRefresh() { - workManager.enqueueUniquePeriodicWork( - TAG_REMOTE_CONFIG, - ExistingPeriodicWorkPolicy.KEEP, - new PeriodicWorkRequest.Builder( - RemoteConfigWork.class, REMOTE_CONFIG_INTERVAL_HOURS, TimeUnit.HOURS) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) - .setConstraints( - new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) - .build()); - } - - public void scheduleDriveUpload(Uri uri, boolean purge) { - if (!preferences.getBoolean(R.string.p_google_drive_backup, false)) { - return; - } - - Builder builder = - new Builder(DriveUploader.class) - .setInputData(DriveUploader.getInputData(uri, purge)) - .setConstraints(getNetworkConstraints()); - if (purge) { - builder.setInitialDelay(new Random().nextInt(3600), TimeUnit.SECONDS); - } - workManager.enqueue(builder.build()); - } - - private Constraints getNetworkConstraints() { - return getNetworkConstraints( - preferences.getBoolean(R.string.p_background_sync_unmetered_only, false)); - } - - private Constraints getNetworkConstraints(boolean unmeteredOnly) { - return new Constraints.Builder() - .setRequiredNetworkType(unmeteredOnly ? NetworkType.UNMETERED : NetworkType.CONNECTED) - .build(); - } - - private void enqueueUnique(String key, Class c, long time) { - long delay = time - now(); - OneTimeWorkRequest.Builder builder = new Builder(c); - if (delay > 0) { - builder.setInitialDelay(delay, TimeUnit.MILLISECONDS); - } - Timber.d("%s: %s (%s)", key, printTimestamp(time), printDuration(delay)); - workManager.beginUniqueWork(key, ExistingWorkPolicy.REPLACE, builder.build()).enqueue(); - } - - @SuppressWarnings("WeakerAccess") - public void cancelNotifications() { - Timber.d("cancelNotifications"); - alarmManager.cancel(getNotificationPendingIntent()); - } - - private Intent getNotificationIntent() { - return new Intent(context, NotificationService.class); - } - - private PendingIntent getNotificationPendingIntent() { - Intent intent = getNotificationIntent(); - return atLeastOreo() - ? PendingIntent.getForegroundService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) - : PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } -} diff --git a/app/src/main/java/org/tasks/jobs/WorkManager.kt b/app/src/main/java/org/tasks/jobs/WorkManager.kt new file mode 100644 index 000000000..37b87320f --- /dev/null +++ b/app/src/main/java/org/tasks/jobs/WorkManager.kt @@ -0,0 +1,228 @@ +package org.tasks.jobs + +import android.annotation.SuppressLint +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.work.* +import androidx.work.WorkManager +import com.todoroo.andlib.utility.AndroidUtilities +import com.todoroo.andlib.utility.DateUtilities +import com.todoroo.astrid.data.Task +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.functions.BiFunction +import io.reactivex.schedulers.Schedulers +import org.tasks.BuildConfig +import org.tasks.R +import org.tasks.data.CaldavDao +import org.tasks.data.GoogleTaskListDao +import org.tasks.data.Place +import org.tasks.date.DateTimeUtils.midnight +import org.tasks.date.DateTimeUtils.newDateTime +import org.tasks.injection.ApplicationScope +import org.tasks.injection.ForApplication +import org.tasks.preferences.Preferences +import org.tasks.time.DateTimeUtils +import timber.log.Timber +import java.util.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import kotlin.math.max + +@ApplicationScope +class WorkManager @Inject constructor( + @param:ForApplication private val context: Context, + private val preferences: Preferences, + private val googleTaskListDao: GoogleTaskListDao, + private val caldavDao: CaldavDao) { + + private val alarmManager: AlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + private val workManager: WorkManager = WorkManager.getInstance(context) + + fun afterSave(current: Task, original: Task?) = + workManager.enqueue( + OneTimeWorkRequest.Builder(AfterSaveWork::class.java) + .setInputData(AfterSaveWork.getInputData(current, original)) + .build()) + + fun cleanup(ids: Iterable) = ids.chunked(MAX_CLEANUP_LENGTH) { + workManager.enqueue( + OneTimeWorkRequest.Builder(CleanupWork::class.java) + .setInputData( + Data.Builder() + .putLongArray(CleanupWork.EXTRA_TASK_IDS, it.toLongArray()) + .build()) + .build()) + } + + fun sync(immediate: Boolean) { + val constraints = Constraints.Builder() + .setRequiredNetworkType( + if (!immediate && preferences.getBoolean(R.string.p_background_sync_unmetered_only, false)) { + NetworkType.UNMETERED + } else { + NetworkType.CONNECTED + }) + .build() + val builder = OneTimeWorkRequest.Builder(SyncWork::class.java) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) + .setConstraints(constraints) + if (!immediate) { + builder.setInitialDelay(1, TimeUnit.MINUTES) + } + val request = builder.build() + workManager.beginUniqueWork(TAG_SYNC, ExistingWorkPolicy.REPLACE, request).enqueue() + } + + fun reverseGeocode(place: Place) { + if (BuildConfig.DEBUG && place.id == 0L) { + throw RuntimeException("Missing id") + } + workManager.enqueue( + OneTimeWorkRequest.Builder(ReverseGeocodeWork::class.java) + .setInputData(Data.Builder().putLong(ReverseGeocodeWork.PLACE_ID, place.id).build()) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) + .setConstraints( + Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) + .build()) + } + + fun updateBackgroundSync() = updateBackgroundSync(null, null, null) + + @SuppressLint("CheckResult") + fun updateBackgroundSync( + forceAccountPresent: Boolean?, + forceBackgroundEnabled: Boolean?, + forceOnlyOnUnmetered: Boolean?) { + val backgroundEnabled = forceBackgroundEnabled + ?: preferences.getBoolean(R.string.p_background_sync, true) + val onlyOnWifi = forceOnlyOnUnmetered + ?: preferences.getBoolean(R.string.p_background_sync_unmetered_only, false) + (if (forceAccountPresent == null) Single.zip( + googleTaskListDao.accountCount(), + Single.fromCallable { caldavDao.accountCount() }, + BiFunction { googleCount: Int, caldavCount: Int -> googleCount > 0 || caldavCount > 0 }) else Single.just(forceAccountPresent)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { accountsPresent: Boolean -> scheduleBackgroundSync(backgroundEnabled && accountsPresent, onlyOnWifi) } + } + + private fun scheduleBackgroundSync(enabled: Boolean, onlyOnUnmetered: Boolean) { + Timber.d("background sync enabled: %s, onlyOnUnmetered: %s", enabled, onlyOnUnmetered) + if (enabled) { + workManager.enqueueUniquePeriodicWork( + TAG_BACKGROUND_SYNC, + ExistingPeriodicWorkPolicy.KEEP, + PeriodicWorkRequest.Builder(SyncWork::class.java, 1, TimeUnit.HOURS) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) + .setConstraints(getNetworkConstraints(onlyOnUnmetered)) + .build()) + } else { + workManager.cancelUniqueWork(TAG_BACKGROUND_SYNC) + } + } + + fun scheduleRefresh(time: Long) = enqueueUnique(TAG_REFRESH, RefreshWork::class.java, time) + + fun scheduleMidnightRefresh() = + enqueueUnique(TAG_MIDNIGHT_REFRESH, MidnightRefreshWork::class.java, midnight()) + + fun scheduleNotification(scheduledTime: Long) { + val time = max(DateUtilities.now(), scheduledTime) + if (time < DateTimeUtils.currentTimeMillis()) { + val intent = notificationIntent + if (AndroidUtilities.atLeastOreo()) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } else { + val pendingIntent = notificationPendingIntent + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pendingIntent) + } + } + + fun scheduleBackup() { + enqueueUnique( + TAG_BACKUP, + BackupWork::class.java, + newDateTime(preferences.getLong(R.string.p_last_backup, 0L)) + .plusDays(1) + .millis + .coerceAtMost(midnight())) + } + + fun scheduleConfigRefresh() = + workManager.enqueueUniquePeriodicWork( + TAG_REMOTE_CONFIG, + ExistingPeriodicWorkPolicy.KEEP, + PeriodicWorkRequest.Builder( + RemoteConfigWork::class.java, REMOTE_CONFIG_INTERVAL_HOURS, TimeUnit.HOURS) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) + .setConstraints( + Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) + .build()) + + fun scheduleDriveUpload(uri: Uri?, purge: Boolean) { + if (!preferences.getBoolean(R.string.p_google_drive_backup, false)) { + return + } + val builder = OneTimeWorkRequest.Builder(DriveUploader::class.java) + .setInputData(DriveUploader.getInputData(uri, purge)) + .setConstraints(networkConstraints) + if (purge) { + builder.setInitialDelay(Random().nextInt(3600).toLong(), TimeUnit.SECONDS) + } + workManager.enqueue(builder.build()) + } + + private val networkConstraints: Constraints + get() = getNetworkConstraints( + preferences.getBoolean(R.string.p_background_sync_unmetered_only, false)) + + private fun getNetworkConstraints(unmeteredOnly: Boolean) = + Constraints.Builder() + .setRequiredNetworkType(if (unmeteredOnly) NetworkType.UNMETERED else NetworkType.CONNECTED) + .build() + + private fun enqueueUnique(key: String, c: Class, time: Long) { + val delay = time - DateUtilities.now() + val builder = OneTimeWorkRequest.Builder(c) + if (delay > 0) { + builder.setInitialDelay(delay, TimeUnit.MILLISECONDS) + } + Timber.d("$key: ${DateTimeUtils.printTimestamp(time)} (${DateTimeUtils.printDuration(delay)})") + workManager.beginUniqueWork(key, ExistingWorkPolicy.REPLACE, builder.build()).enqueue() + } + + fun cancelNotifications() { + Timber.d("cancelNotifications") + alarmManager.cancel(notificationPendingIntent) + } + + private val notificationIntent: Intent + get() = Intent(context, NotificationService::class.java) + + private val notificationPendingIntent: PendingIntent + get() { + return if (AndroidUtilities.atLeastOreo()) { + PendingIntent.getForegroundService(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT) + } else { + PendingIntent.getService(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT) + } + } + + companion object { + val REMOTE_CONFIG_INTERVAL_HOURS = if (BuildConfig.DEBUG) 1 else 12.toLong() + private const val MAX_CLEANUP_LENGTH = 500 + private const val TAG_BACKUP = "tag_backup" + private const val TAG_REFRESH = "tag_refresh" + private const val TAG_MIDNIGHT_REFRESH = "tag_midnight_refresh" + private const val TAG_SYNC = "tag_sync" + private const val TAG_BACKGROUND_SYNC = "tag_background_sync" + private const val TAG_REMOTE_CONFIG = "tag_remote_config" + } +} \ No newline at end of file