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.ExistingWorkPolicy.APPEND_OR_REPLACE import androidx.work.ExistingWorkPolicy.REPLACE import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.DateUtilities import com.todoroo.astrid.data.Task import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.tasks.BuildConfig import org.tasks.R import org.tasks.data.* import org.tasks.data.CaldavAccount.Companion.TYPE_CALDAV import org.tasks.data.CaldavAccount.Companion.TYPE_ETEBASE import org.tasks.data.CaldavAccount.Companion.TYPE_OPENTASKS import org.tasks.data.CaldavAccount.Companion.TYPE_TASKS import org.tasks.date.DateTimeUtils.midnight import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.jobs.MigrateLocalWork.Companion.EXTRA_ACCOUNT import org.tasks.jobs.SyncWork.Companion.EXTRA_BACKGROUND import org.tasks.jobs.SyncWork.Companion.EXTRA_IMMEDIATE import org.tasks.jobs.WorkManager.Companion.MAX_CLEANUP_LENGTH import org.tasks.jobs.WorkManager.Companion.REMOTE_CONFIG_INTERVAL_HOURS import org.tasks.jobs.WorkManager.Companion.TAG_BACKGROUND_SYNC_CALDAV import org.tasks.jobs.WorkManager.Companion.TAG_BACKGROUND_SYNC_ETEBASE import org.tasks.jobs.WorkManager.Companion.TAG_BACKGROUND_SYNC_GOOGLE_TASKS import org.tasks.jobs.WorkManager.Companion.TAG_BACKGROUND_SYNC_OPENTASKS import org.tasks.jobs.WorkManager.Companion.TAG_BACKUP import org.tasks.jobs.WorkManager.Companion.TAG_MIDNIGHT_REFRESH import org.tasks.jobs.WorkManager.Companion.TAG_MIGRATE_LOCAL import org.tasks.jobs.WorkManager.Companion.TAG_REFRESH import org.tasks.jobs.WorkManager.Companion.TAG_REMOTE_CONFIG import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_CALDAV import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_ETEBASE import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_ETESYNC import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_GOOGLE_TASKS import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_OPENTASK import org.tasks.jobs.WorkManager.Companion.TAG_UPDATE_PURCHASES import org.tasks.notifications.Throttle import org.tasks.preferences.Preferences import org.tasks.time.DateTimeUtils import timber.log.Timber import java.util.* import java.util.concurrent.TimeUnit import kotlin.math.max class WorkManagerImpl constructor( private val context: Context, private val preferences: Preferences, private val googleTaskListDao: GoogleTaskListDao, private val caldavDao: CaldavDao, private val openTaskDao: OpenTaskDao ): WorkManager { private val throttle = Throttle(200, 60000, "WORK") private val alarmManager: AlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager private val workManager = androidx.work.WorkManager.getInstance(context) override fun scheduleRepeat(task: Task) { enqueue( OneTimeWorkRequest.Builder(AfterSaveWork::class.java) .setInputData(Data.Builder() .putLong(AfterSaveWork.EXTRA_ID, task.id) .build())) } override fun updateCalendar(task: Task) { enqueue( OneTimeWorkRequest.Builder(UpdateCalendarWork::class.java) .setInputData(Data.Builder() .putLong(UpdateCalendarWork.EXTRA_ID, task.id) .build())) } @SuppressLint("EnqueueWork") override fun migrateLocalTasks(caldavAccount: CaldavAccount) { val builder = OneTimeWorkRequest.Builder(MigrateLocalWork::class.java) .setInputData(Data.Builder().putString(EXTRA_ACCOUNT, caldavAccount.uuid).build()) .setConstraints(networkConstraints) enqueue(workManager.beginUniqueWork(TAG_MIGRATE_LOCAL, APPEND_OR_REPLACE, builder.build())) } override fun cleanup(ids: Iterable) { ids.chunked(MAX_CLEANUP_LENGTH) { enqueue( OneTimeWorkRequest.Builder(CleanupWork::class.java) .setInputData( Data.Builder() .putLongArray(CleanupWork.EXTRA_TASK_IDS, it.toLongArray()) .build())) } } override suspend fun googleTaskSync(immediate: Boolean) = sync(immediate, TAG_SYNC_GOOGLE_TASKS, SyncGoogleTasksWork::class.java) override suspend fun caldavSync(immediate: Boolean) = sync(immediate, TAG_SYNC_CALDAV, SyncCaldavWork::class.java) override suspend fun eteSync(immediate: Boolean) = sync(immediate, TAG_SYNC_ETESYNC, SyncEteSyncWork::class.java) override suspend fun eteBaseSync(immediate: Boolean) = sync(immediate, TAG_SYNC_ETEBASE, SyncEtebaseWork::class.java) override suspend fun openTaskSync(immediate: Boolean) = sync(immediate, TAG_SYNC_OPENTASK, SyncOpenTasksWork::class.java, false) @SuppressLint("EnqueueWork") private suspend fun sync(immediate: Boolean, tag: String, c: Class, requireNetwork: Boolean = true) { Timber.d("sync(immediate = $immediate, $tag, $c, requireNetwork = $requireNetwork)") val builder = OneTimeWorkRequest.Builder(c) .setInputData(Data.Builder().putBoolean(EXTRA_IMMEDIATE, immediate).build()) if (requireNetwork) { builder.setConstraints(networkConstraints) } if (!immediate) { builder.setInitialDelay(1, TimeUnit.MINUTES) } val append = withContext(Dispatchers.IO) { workManager.getWorkInfosByTag(tag).get().any { it.state == WorkInfo.State.RUNNING } } enqueue(workManager.beginUniqueWork( tag, if (append) APPEND_OR_REPLACE else REPLACE, builder.build()) ) } override fun reverseGeocode(place: Place) { if (BuildConfig.DEBUG && place.id == 0L) { throw RuntimeException("Missing id") } enqueue( OneTimeWorkRequest.Builder(ReverseGeocodeWork::class.java) .setInputData(Data.Builder().putLong(ReverseGeocodeWork.PLACE_ID, place.id).build()) .setConstraints(networkConstraints)) } override fun updateBackgroundSync() { throttle.run { scheduleBackgroundSync( TAG_BACKGROUND_SYNC_GOOGLE_TASKS, SyncGoogleTasksWork::class.java, googleTaskListDao.accountCount() > 0) } throttle.run { scheduleBackgroundSync( TAG_BACKGROUND_SYNC_CALDAV, SyncCaldavWork::class.java, caldavDao.getAccounts(TYPE_CALDAV, TYPE_TASKS).isNotEmpty()) } throttle.run { scheduleBackgroundSync( TAG_BACKGROUND_SYNC_ETEBASE, SyncEtebaseWork::class.java, caldavDao.getAccounts(TYPE_ETEBASE).isNotEmpty()) } throttle.run { scheduleBackgroundSync( TAG_BACKGROUND_SYNC_OPENTASKS, SyncOpenTasksWork::class.java, caldavDao.getAccounts(TYPE_OPENTASKS).isNotEmpty() || openTaskDao.newAccounts().isNotEmpty()) } } private fun scheduleBackgroundSync(tag: String, c: Class, enabled: Boolean) { Timber.d("scheduleBackgroundSync($tag, $c, enabled = $enabled)") if (enabled) { val builder = PeriodicWorkRequest.Builder(c, 1, TimeUnit.HOURS) .setInputData(Data.Builder().putBoolean(EXTRA_BACKGROUND, true).build()) .setConstraints(networkConstraints) workManager.enqueueUniquePeriodicWork( tag, ExistingPeriodicWorkPolicy.KEEP, builder.build()) } else { workManager.cancelUniqueWork(tag) } } override fun scheduleRefresh(time: Long) = enqueueUnique(TAG_REFRESH, RefreshWork::class.java, time) override fun scheduleMidnightRefresh() = enqueueUnique(TAG_MIDNIGHT_REFRESH, MidnightRefreshWork::class.java, midnight()) override 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) } } override fun scheduleBackup() = enqueueUnique( TAG_BACKUP, BackupWork::class.java, newDateTime(preferences.getLong(R.string.p_last_backup, 0L)) .plusDays(1) .millis .coerceAtMost(midnight())) override fun scheduleConfigRefresh() { throttle.run { workManager.enqueueUniquePeriodicWork( TAG_REMOTE_CONFIG, ExistingPeriodicWorkPolicy.KEEP, PeriodicWorkRequest.Builder( RemoteConfigWork::class.java, REMOTE_CONFIG_INTERVAL_HOURS, TimeUnit.HOURS) .setConstraints(networkConstraints) .build()) } } override 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) } enqueue(builder) } private val networkConstraints: Constraints get() = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() override fun cancelNotifications() { alarmManager.cancel(notificationPendingIntent) } override fun updatePurchases() = enqueueUnique(TAG_UPDATE_PURCHASES, UpdatePurchaseWork::class.java) @SuppressLint("EnqueueWork") private fun enqueueUnique(key: String, c: Class, time: Long = 0) { 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)})") enqueue(workManager.beginUniqueWork(key, REPLACE, builder.build())) } private fun enqueue(builder: WorkRequest.Builder<*, *>) { throttle.run { workManager.enqueue(builder.build()) } } private fun enqueue(continuation: WorkContinuation) { throttle.run { continuation.enqueue() } } 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) } } }