diff --git a/app/src/main/java/org/wikipedia/activity/BaseActivity.kt b/app/src/main/java/org/wikipedia/activity/BaseActivity.kt index c72a823f8ec..9e5eec93779 100644 --- a/app/src/main/java/org/wikipedia/activity/BaseActivity.kt +++ b/app/src/main/java/org/wikipedia/activity/BaseActivity.kt @@ -96,7 +96,7 @@ abstract class BaseActivity : AppCompatActivity(), ConnectionStateMonitor.Callba } // Conditionally execute all recurring tasks - RecurringTasksExecutor().run() + RecurringTasksExecutor.schedule() if (Prefs.isReadingListsFirstTimeSync && AccountUtil.isLoggedIn) { Prefs.isReadingListsFirstTimeSync = false Prefs.isReadingListSyncEnabled = true diff --git a/app/src/main/java/org/wikipedia/alphaupdater/AlphaUpdateChecker.kt b/app/src/main/java/org/wikipedia/alphaupdater/AlphaUpdateChecker.kt deleted file mode 100644 index 1c8252d32d6..00000000000 --- a/app/src/main/java/org/wikipedia/alphaupdater/AlphaUpdateChecker.kt +++ /dev/null @@ -1,76 +0,0 @@ -package org.wikipedia.alphaupdater - -import android.Manifest -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.app.PendingIntentCompat -import androidx.core.content.ContextCompat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import okhttp3.Request -import org.wikipedia.dataclient.okhttp.OkHttpConnectionFactory -import org.wikipedia.notifications.NotificationCategory -import org.wikipedia.recurring.RecurringTask -import org.wikipedia.settings.PrefsIoUtil -import java.io.IOException -import java.util.Date -import java.util.concurrent.TimeUnit - -class AlphaUpdateChecker(private val context: Context) : RecurringTask() { - override val name = "alpha-update-checker" - - override fun shouldRun(lastRun: Date): Boolean { - return System.currentTimeMillis() - lastRun.time >= RUN_INTERVAL_MILLI - } - - override suspend fun run(lastRun: Date) { - // Check for updates! - var hashString: String? = null - withContext(Dispatchers.IO) { - try { - val request: Request = Request.Builder().url(ALPHA_BUILD_DATA_URL).build() - OkHttpConnectionFactory.client.newCall(request).execute().use { - hashString = it.body?.string() - } - } catch (e: IOException) { - // It's ok, we can do nothing. - } - } - hashString?.let { - if (PrefsIoUtil.getString(PREFERENCE_KEY_ALPHA_COMMIT, "") != it) { - showNotification() - } - PrefsIoUtil.setString(PREFERENCE_KEY_ALPHA_COMMIT, it) - } - } - - private fun showNotification() { - if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - return - } - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(ALPHA_BUILD_APK_URL)) - val pendingIntent = PendingIntentCompat.getActivity(context, 0, intent, 0, false) - - val notificationManagerCompat = NotificationManagerCompat.from(context) - val notificationCategory = NotificationCategory.ALPHA_BUILD_CHECKER - - val notificationBuilder = NotificationCompat.Builder(context, notificationCategory.id) - .setContentTitle(context.getString(notificationCategory.title)) - .setContentText(context.getString(notificationCategory.description)) - .setContentIntent(pendingIntent) - .setAutoCancel(true) - notificationBuilder.setSmallIcon(notificationCategory.iconResId) - notificationManagerCompat.notify(1, notificationBuilder.build()) - } - - companion object { - private val RUN_INTERVAL_MILLI = TimeUnit.DAYS.toMillis(1) - private const val PREFERENCE_KEY_ALPHA_COMMIT = "alpha_last_checked_commit" - private const val ALPHA_BUILD_APK_URL = "https://github.com/wikimedia/apps-android-wikipedia/releases/download/latest/app-alpha-universal-release.apk" - private const val ALPHA_BUILD_DATA_URL = "https://github.com/wikimedia/apps-android-wikipedia/releases/download/latest/rev-hash.txt" - } -} diff --git a/app/src/main/java/org/wikipedia/alphaupdater/AlphaUpdateWorker.kt b/app/src/main/java/org/wikipedia/alphaupdater/AlphaUpdateWorker.kt new file mode 100644 index 00000000000..0083d30520a --- /dev/null +++ b/app/src/main/java/org/wikipedia/alphaupdater/AlphaUpdateWorker.kt @@ -0,0 +1,71 @@ +package org.wikipedia.alphaupdater + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.PendingIntentCompat +import androidx.core.content.ContextCompat +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.Request +import okio.IOException +import org.wikipedia.dataclient.okhttp.OkHttpConnectionFactory.client +import org.wikipedia.notifications.NotificationCategory +import org.wikipedia.settings.PrefsIoUtil + +class AlphaUpdateWorker( + appContext: Context, + params: WorkerParameters +) : CoroutineWorker(appContext, params) { + override suspend fun doWork(): Result { + val hashString = withContext(Dispatchers.IO) { + val request = Request.Builder().url(ALPHA_BUILD_DATA_URL).build() + try { + client.newCall(request).execute().body!!.use { it.string() } + } catch (e: IOException) { + // It's ok, we can do nothing. + null + } + } + if (hashString == null) { + return Result.failure() + } + if (PrefsIoUtil.getString(PREFERENCE_KEY_ALPHA_COMMIT, "") != hashString) { + showNotification() + } + PrefsIoUtil.setString(PREFERENCE_KEY_ALPHA_COMMIT, hashString) + return Result.success() + } + + private fun showNotification() { + if (ContextCompat.checkSelfPermission(applicationContext, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + return + } + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(ALPHA_BUILD_APK_URL)) + val pendingIntent = PendingIntentCompat.getActivity(applicationContext, 0, intent, 0, false) + + val notificationManagerCompat = NotificationManagerCompat.from(applicationContext) + val notificationCategory = NotificationCategory.ALPHA_BUILD_CHECKER + + val notificationBuilder = NotificationCompat.Builder(applicationContext, notificationCategory.id) + .setContentTitle(applicationContext.getString(notificationCategory.title)) + .setContentText(applicationContext.getString(notificationCategory.description)) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + notificationBuilder.setSmallIcon(notificationCategory.iconResId) + notificationManagerCompat.notify(1, notificationBuilder.build()) + } + + companion object { + private const val PREFERENCE_KEY_ALPHA_COMMIT = "alpha_last_checked_commit" + private const val ALPHA_BUILD_APK_URL = "https://github.com/wikimedia/apps-android-wikipedia/releases/download/latest/app-alpha-universal-release.apk" + private const val ALPHA_BUILD_DATA_URL = "https://github.com/wikimedia/apps-android-wikipedia/releases/download/latest/rev-hash.txt" + } +} diff --git a/app/src/main/java/org/wikipedia/notifications/PollNotificationWorker.kt b/app/src/main/java/org/wikipedia/notifications/PollNotificationWorker.kt index 108a70e75a8..d957d7a8839 100644 --- a/app/src/main/java/org/wikipedia/notifications/PollNotificationWorker.kt +++ b/app/src/main/java/org/wikipedia/notifications/PollNotificationWorker.kt @@ -61,11 +61,8 @@ class PollNotificationWorker( companion object { fun schedulePollNotificationJob(context: Context) { - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() val workRequest = OneTimeWorkRequestBuilder() - .setConstraints(constraints) + .setConstraints(Constraints(NetworkType.CONNECTED)) .build() WorkManager.getInstance(context).enqueue(workRequest) } diff --git a/app/src/main/java/org/wikipedia/recurring/DailyEventTask.kt b/app/src/main/java/org/wikipedia/recurring/DailyEventTask.kt deleted file mode 100644 index c2b004ad3b7..00000000000 --- a/app/src/main/java/org/wikipedia/recurring/DailyEventTask.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.wikipedia.recurring - -import org.wikipedia.R -import org.wikipedia.WikipediaApp -import org.wikipedia.analytics.eventplatform.DailyStatsEvent -import org.wikipedia.analytics.eventplatform.EventPlatformClient -import java.util.Date -import java.util.concurrent.TimeUnit - -class DailyEventTask(private val app: WikipediaApp) : RecurringTask() { - override val name = app.getString(R.string.preference_key_daily_event_time_task_name) - - override fun shouldRun(lastRun: Date): Boolean { - return millisSinceLastRun(lastRun) > TimeUnit.DAYS.toMillis(1) - } - - override suspend fun run(lastRun: Date) { - DailyStatsEvent.log(app) - EventPlatformClient.refreshStreamConfigs() - } -} diff --git a/app/src/main/java/org/wikipedia/recurring/DailyEventWorker.kt b/app/src/main/java/org/wikipedia/recurring/DailyEventWorker.kt new file mode 100644 index 00000000000..c111d1494f7 --- /dev/null +++ b/app/src/main/java/org/wikipedia/recurring/DailyEventWorker.kt @@ -0,0 +1,19 @@ +package org.wikipedia.recurring + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import org.wikipedia.WikipediaApp +import org.wikipedia.analytics.eventplatform.DailyStatsEvent +import org.wikipedia.analytics.eventplatform.EventPlatformClient + +class DailyEventWorker( + appContext: Context, + params: WorkerParameters +) : CoroutineWorker(appContext, params) { + override suspend fun doWork(): Result { + DailyStatsEvent.log(WikipediaApp.instance) + EventPlatformClient.refreshStreamConfigs() + return Result.success() + } +} diff --git a/app/src/main/java/org/wikipedia/recurring/RecurringTask.kt b/app/src/main/java/org/wikipedia/recurring/RecurringTask.kt deleted file mode 100644 index 1d346eb024e..00000000000 --- a/app/src/main/java/org/wikipedia/recurring/RecurringTask.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.wikipedia.recurring - -import org.wikipedia.settings.Prefs -import org.wikipedia.util.log.L -import java.util.Date -import kotlin.math.max -import kotlin.math.min - -/** - * Represents a task that needs to be run periodically. - * - * Usually an expensive task, that is run Async. Do not do anything - * that requires access to the UI thread on these tasks. - * - * Since it is an expensive task, there's a separate method that detects - * if the task should be run or not, and then runs it if necessary. The - * last run times are tracked automatically by the base class. - */ -abstract class RecurringTask { - suspend fun runIfNecessary() { - val lastRunDate = lastRunDate - val lastExecutionLog = "$name. Last execution was $lastRunDate." - if (shouldRun(lastRunDate)) { - L.d("Executing recurring task, $lastExecutionLog") - run(lastRunDate) - Prefs.setLastRunTime(name, absoluteTime) - } else { - L.d("Skipping recurring task, $lastExecutionLog") - } - } - - protected abstract fun shouldRun(lastRun: Date): Boolean - protected abstract suspend fun run(lastRun: Date) - - protected abstract val name: String - - protected val absoluteTime: Long - get() = System.currentTimeMillis() - private val lastRunDate: Date - get() = Date(Prefs.getLastRunTime(name)) - - protected fun millisSinceLastRun(lastRun: Date): Long { - return min(Int.MAX_VALUE.toLong(), max(0, absoluteTime - lastRun.time)) - } -} diff --git a/app/src/main/java/org/wikipedia/recurring/RecurringTasksExecutor.kt b/app/src/main/java/org/wikipedia/recurring/RecurringTasksExecutor.kt index 6e5f53f0704..6ae576703b0 100644 --- a/app/src/main/java/org/wikipedia/recurring/RecurringTasksExecutor.kt +++ b/app/src/main/java/org/wikipedia/recurring/RecurringTasksExecutor.kt @@ -1,26 +1,58 @@ package org.wikipedia.recurring -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.launch +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import org.wikipedia.R import org.wikipedia.WikipediaApp -import org.wikipedia.alphaupdater.AlphaUpdateChecker -import org.wikipedia.settings.RemoteConfigRefreshTask +import org.wikipedia.alphaupdater.AlphaUpdateWorker +import org.wikipedia.settings.PrefsIoUtil +import org.wikipedia.settings.RemoteConfigRefreshWorker import org.wikipedia.util.ReleaseUtil -import org.wikipedia.util.log.L - -class RecurringTasksExecutor() { - fun run() { - val app = WikipediaApp.instance - MainScope().launch(CoroutineExceptionHandler { _, throwable -> - L.e(throwable) - }) { - RemoteConfigRefreshTask().runIfNecessary() - DailyEventTask(app).runIfNecessary() - TalkOfflineCleanupTask(app).runIfNecessary() - if (ReleaseUtil.isAlphaRelease) { - AlphaUpdateChecker(app).runIfNecessary() - } +import java.util.concurrent.TimeUnit + +object RecurringTasksExecutor { + fun schedule() { + // Clean up now-unused task time values, as WorkManager handles scheduling + PrefsIoUtil.remove("alpha-update-checker") + PrefsIoUtil.remove("remote-config-refresher") + PrefsIoUtil.remove(R.string.preference_key_talk_offline_cleanup_task_name) + PrefsIoUtil.remove(R.string.preference_key_daily_event_time_task_name) + + val networkConstraints = Constraints( + requiredNetworkType = NetworkType.CONNECTED, + requiresBatteryNotLow = true + ) + + val remoteConfigRefreshRequest = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) + .setConstraints(networkConstraints) + .build() + + val dailyEventRequest = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) + .setConstraints(networkConstraints) + .build() + + val offlineCleanupRequest = PeriodicWorkRequestBuilder(7, TimeUnit.DAYS) + .setConstraints(Constraints(requiresBatteryNotLow = true)) + .build() + + val tasks = mutableMapOf( + "REMOTE_CONFIG" to remoteConfigRefreshRequest, + "DAILY_EVENT" to dailyEventRequest, + "OFFLINE_CLEANUP" to offlineCleanupRequest + ) + + if (ReleaseUtil.isAlphaRelease) { + tasks["ALPHA_UPDATE"] = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) + .setConstraints(networkConstraints) + .build() + } + + tasks.forEach { (taskName, task) -> + WorkManager.getInstance(WikipediaApp.instance) + .enqueueUniquePeriodicWork(taskName, ExistingPeriodicWorkPolicy.KEEP, task) } } } diff --git a/app/src/main/java/org/wikipedia/recurring/TalkOfflineCleanupTask.kt b/app/src/main/java/org/wikipedia/recurring/TalkOfflineCleanupWorker.kt similarity index 58% rename from app/src/main/java/org/wikipedia/recurring/TalkOfflineCleanupTask.kt rename to app/src/main/java/org/wikipedia/recurring/TalkOfflineCleanupWorker.kt index 9d7b3b1ee44..861d8cbfad1 100644 --- a/app/src/main/java/org/wikipedia/recurring/TalkOfflineCleanupTask.kt +++ b/app/src/main/java/org/wikipedia/recurring/TalkOfflineCleanupWorker.kt @@ -1,32 +1,32 @@ package org.wikipedia.recurring import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.wikipedia.R import org.wikipedia.database.AppDatabase import java.io.File -import java.util.Date -import java.util.concurrent.TimeUnit +import java.time.Instant +import java.time.temporal.ChronoUnit -class TalkOfflineCleanupTask(context: Context) : RecurringTask() { - override val name = context.getString(R.string.preference_key_talk_offline_cleanup_task_name) - - override fun shouldRun(lastRun: Date): Boolean { - return millisSinceLastRun(lastRun) > TimeUnit.DAYS.toMillis(CLEANUP_MAX_AGE_DAYS) - } - - override suspend fun run(lastRun: Date) { +class TalkOfflineCleanupWorker( + appContext: Context, + params: WorkerParameters +) : CoroutineWorker(appContext, params) { + override suspend fun doWork(): Result { withContext(Dispatchers.IO) { AppDatabase.instance.offlineObjectDao() .searchForOfflineObjects(CLEANUP_URL_SEARCH_KEY) .filter { - (absoluteTime - File(it.path + ".0").lastModified()) > TimeUnit.DAYS.toMillis(CLEANUP_MAX_AGE_DAYS) + val lastModified = Instant.ofEpochMilli(File("${it.path}.0").lastModified()) + ChronoUnit.DAYS.between(lastModified, Instant.now()) > CLEANUP_MAX_AGE_DAYS }.forEach { AppDatabase.instance.offlineObjectDao().deleteOfflineObject(it) AppDatabase.instance.offlineObjectDao().deleteFilesForObject(it) } } + return Result.success() } companion object { diff --git a/app/src/main/java/org/wikipedia/settings/RemoteConfigRefreshTask.kt b/app/src/main/java/org/wikipedia/settings/RemoteConfigRefreshTask.kt deleted file mode 100644 index 37e27d342d3..00000000000 --- a/app/src/main/java/org/wikipedia/settings/RemoteConfigRefreshTask.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.wikipedia.settings - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import okhttp3.Request -import okhttp3.Response -import okhttp3.internal.closeQuietly -import org.wikipedia.WikipediaApp -import org.wikipedia.dataclient.ServiceFactory -import org.wikipedia.dataclient.okhttp.OkHttpConnectionFactory.client -import org.wikipedia.recurring.RecurringTask -import org.wikipedia.util.log.L -import java.util.Date -import java.util.concurrent.TimeUnit - -class RemoteConfigRefreshTask : RecurringTask() { - override val name = "remote-config-refresher" - - override fun shouldRun(lastRun: Date): Boolean { - return millisSinceLastRun(lastRun) >= TimeUnit.DAYS.toMillis(RUN_INTERVAL_DAYS) - } - - override suspend fun run(lastRun: Date) { - withContext(Dispatchers.IO) { - var response: Response? = null - try { - val request = Request.Builder().url(REMOTE_CONFIG_URL).build() - response = client.newCall(request).execute() - val configStr = response.body!!.string() - RemoteConfig.updateConfig(configStr) - L.d(configStr) - } catch (e: Exception) { - L.e(e) - } finally { - response?.closeQuietly() - } - - val userInfo = ServiceFactory.get(WikipediaApp.instance.wikiSite).getUserInfo() - // This clumsy comparison is necessary because the field is an integer value when enabled, but an empty string when disabled. - // Since we want the default to lean towards opt-in, we check very specifically for an empty string, to make sure the user has opted out. - val fundraisingOptOut = userInfo.query?.userInfo?.options?.fundraisingOptIn?.toString()?.replace("\"", "")?.isEmpty() - Prefs.donationBannerOptIn = fundraisingOptOut != true - } - } - - companion object { - private const val REMOTE_CONFIG_URL = "https://meta.wikimedia.org/w/extensions/MobileApp/config/android.json" - private const val RUN_INTERVAL_DAYS = 1L - } -} diff --git a/app/src/main/java/org/wikipedia/settings/RemoteConfigRefreshWorker.kt b/app/src/main/java/org/wikipedia/settings/RemoteConfigRefreshWorker.kt new file mode 100644 index 00000000000..16d3f2d1fd0 --- /dev/null +++ b/app/src/main/java/org/wikipedia/settings/RemoteConfigRefreshWorker.kt @@ -0,0 +1,47 @@ +package org.wikipedia.settings + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.Request +import okio.IOException +import org.wikipedia.WikipediaApp +import org.wikipedia.dataclient.ServiceFactory +import org.wikipedia.dataclient.okhttp.OkHttpConnectionFactory.client +import org.wikipedia.util.log.L + +class RemoteConfigRefreshWorker( + appContext: Context, + params: WorkerParameters +) : CoroutineWorker(appContext, params) { + override suspend fun doWork(): Result { + val configStr = withContext(Dispatchers.IO) { + val request = Request.Builder().url(REMOTE_CONFIG_URL).build() + try { + client.newCall(request).execute().body!!.use { it.string() } + } catch (e: IOException) { + L.e(e) + null + } + } + if (configStr == null) { + return Result.failure() + } + RemoteConfig.updateConfig(configStr) + L.d(configStr) + + val userInfo = ServiceFactory.get(WikipediaApp.instance.wikiSite).getUserInfo() + // This clumsy comparison is necessary because the field is an integer value when enabled, but an empty string when disabled. + // Since we want the default to lean towards opt-in, we check very specifically for an empty string, to make sure the user has opted out. + val fundraisingOptOut = userInfo.query?.userInfo?.options?.fundraisingOptIn?.toString()?.replace("\"", "")?.isEmpty() + Prefs.donationBannerOptIn = fundraisingOptOut != true + + return Result.success() + } + + companion object { + private const val REMOTE_CONFIG_URL = "https://meta.wikimedia.org/w/extensions/MobileApp/config/android.json" + } +}