Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move update worker to core #129

Merged
merged 6 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Stores/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ android {
dependencies {
implementation project(path: ':Core')

implementation 'androidx.work:work-runtime-ktx:2.9.0'
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'

def appReviewVersion = "2.0.1"
standardImplementation "com.google.android.play:review:$appReviewVersion"
standardImplementation "com.google.android.play:review-ktx:$appReviewVersion"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Infomaniak Core - Android
* Copyright (C) 2024 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.lib.stores

import android.content.Context
import androidx.work.WorkManager

class AppUpdateScheduler(
appContext: Context,
workManager: WorkManager = WorkManager.getInstance(appContext),
) : UpdateScheduler(appContext, workManager)
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.lib.stores
package com.infomaniak.lib.stores.updatemanagers

import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.infomaniak.lib.core.fdroidTools.FdroidApiTools
import com.infomaniak.lib.core.utils.SentryLog
import com.infomaniak.lib.stores.BaseInAppUpdateManager
import com.infomaniak.lib.stores.StoreUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Infomaniak Core - Android
* Copyright (C) 2024 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.lib.stores.updatemanagers

import android.content.Context

class WorkerUpdateManager(appContext: Context) {

fun installDownloadedUpdate(onInstallFailure: (Exception) -> Unit, onInstallSuccess: () -> Unit) = Unit
}
35 changes: 0 additions & 35 deletions Stores/src/main/java/StoresUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,44 +18,9 @@
package com.infomaniak.lib.stores

import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.infomaniak.lib.core.fdroidTools.FdroidApiTools
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

interface StoresUtils {

//region In-App Updates
fun FragmentActivity.initAppUpdateManager(
onUserChoice: (Boolean) -> Unit,
onUpdateDownloaded: () -> Unit,
onUpdateInstalled: () -> Unit,
) = Unit

fun FragmentActivity.checkUpdateIsAvailable(
appId: String,
versionCode: Int,
onFDroidResult: (updateIsAvailable: Boolean) -> Unit,
) {
lifecycleScope.launch {
val lastVersionCode = FdroidApiTools().getLastRelease(appId)

withContext(Dispatchers.Main) { onFDroidResult(versionCode < lastVersionCode) }
}
}

fun checkStalledUpdate() = Unit

fun installDownloadedUpdate(
onInstallStart: (() -> Unit)? = null,
onSuccess: (() -> Unit)? = null,
onFailure: ((Exception) -> Unit)? = null,
) = Unit

fun unregisterAppUpdateListener() = Unit
//endregion

//region In-App Review
fun FragmentActivity.launchInAppReview() = Unit
//endRegion
Expand Down
31 changes: 31 additions & 0 deletions Stores/src/main/java/UpdateScheduler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Infomaniak Core - Android
* Copyright (C) 2024 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.lib.stores

import android.content.Context
import androidx.work.WorkManager

open class UpdateScheduler(
appContext: Context,
private val workManager: WorkManager = WorkManager.getInstance(appContext),
) {

open fun scheduleWorkIfNeeded() = Unit

open suspend fun cancelWorkIfNeeded() = Unit
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Infomaniak Core - Android
* Copyright (C) 2024 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.lib.stores

import android.content.Context
import androidx.concurrent.futures.CallbackToFutureAdapter
import androidx.work.*
import com.google.common.util.concurrent.ListenableFuture
import com.infomaniak.lib.core.utils.SentryLog
import com.infomaniak.lib.stores.updatemanagers.WorkerUpdateManager
import io.sentry.Sentry
import io.sentry.SentryLevel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit

class AppUpdateScheduler(
appContext: Context,
private val workManager: WorkManager = WorkManager.getInstance(appContext),
) : UpdateScheduler(appContext, workManager) {

private val storesLocalSettings = StoresLocalSettings.getInstance(appContext)

override fun scheduleWorkIfNeeded() {
if (storesLocalSettings.hasAppUpdateDownloaded) {
SentryLog.d(TAG, "Work scheduled")

val workRequest = OneTimeWorkRequestBuilder<AppUpdateWorker>()
.setConstraints(Constraints.Builder().setRequiresBatteryNotLow(true).build())
// We start with a delayed duration, so that when the app quickly come back to foreground because the user
// was just switching apps, the service is not launched
.setInitialDelay(INITIAL_DELAY_SECONDS, TimeUnit.SECONDS)
.build()

workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, workRequest)
}
}

override suspend fun cancelWorkIfNeeded() = withContext(Dispatchers.IO) {

val workInfos = workManager.getWorkInfos(
WorkQuery.Builder.fromUniqueWorkNames(listOf(TAG))
.addStates(listOf(WorkInfo.State.BLOCKED, WorkInfo.State.ENQUEUED))
.build(),
).get()

workInfos.forEachIndexed { index, workInfo ->
workManager.cancelWorkById(workInfo.id)
SentryLog.d(TAG, "Work cancelled")
// TODO: Check this Sentry in approximately 1 month (end of March 2024) to know if the `forEach` is useful or not.
if (index > 0) {
Sentry.withScope { scope ->
scope.level = SentryLevel.WARNING
Sentry.captureMessage("There is more than one work infos, we must keep forEach")
}
}
}
}

internal class AppUpdateWorker(appContext: Context, params: WorkerParameters) : ListenableWorker(appContext, params) {

private val updateManager = WorkerUpdateManager(appContext)

override fun startWork(): ListenableFuture<Result> {
SentryLog.i(TAG, "Work started")

return CallbackToFutureAdapter.getFuture { completer ->
updateManager.installDownloadedUpdate(
onInstallSuccess = { completer.setResult(Result.success()) },
onInstallFailure = { exception ->
// This avoid the user being instantly reprompted to download update
Sentry.captureException(exception)
completer.setResult(Result.failure())
},
)
}
}

private fun CallbackToFutureAdapter.Completer<Result>.setResult(result: Result) {
set(result)
SentryLog.d(TAG, "Work finished")
}
}

companion object {
private const val TAG = "AppUpdateWorker"
private const val INITIAL_DELAY_SECONDS = 10L
}
}
114 changes: 0 additions & 114 deletions Stores/src/standard/java/com.infomaniak.lib.stores/StoreUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,129 +17,15 @@
*/
package com.infomaniak.lib.stores

import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentActivity
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.appupdate.AppUpdateOptions
import com.google.android.play.core.install.InstallStateUpdatedListener
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.InstallStatus
import com.google.android.play.core.install.model.UpdateAvailability
import com.google.android.play.core.review.ReviewManagerFactory
import com.infomaniak.lib.core.utils.SentryLog

object StoreUtils : StoresUtils {

const val APP_UPDATE_TAG = "inAppUpdate"

const val UPDATE_TYPE = AppUpdateType.FLEXIBLE

private lateinit var appUpdateManager: AppUpdateManager
// Result of in app update's bottomSheet user choice
private lateinit var inAppUpdateResultLauncher: ActivityResultLauncher<IntentSenderRequest>
private lateinit var localSettings: StoresLocalSettings
private lateinit var onUpdateDownloaded: () -> Unit
private lateinit var onUpdateInstalled: () -> Unit

// Create a listener to track request state updates.
private val installStateUpdatedListener by lazy {
InstallStateUpdatedListener { state ->
when (state.installStatus()) {
InstallStatus.DOWNLOADED -> {
SentryLog.d(APP_UPDATE_TAG, "OnUpdateDownloaded triggered by InstallStateUpdated listener")
onUpdateDownloaded()
}
InstallStatus.INSTALLED -> {
SentryLog.d(APP_UPDATE_TAG, "OnUpdateInstalled triggered by InstallStateUpdated listener")
onUpdateInstalled()
unregisterAppUpdateListener()
}
else -> Unit
}
}
}

//region In-App Update
override fun FragmentActivity.initAppUpdateManager(
onUserChoice: (Boolean) -> Unit,
onUpdateDownloaded: () -> Unit,
onUpdateInstalled: () -> Unit,
) {
appUpdateManager = AppUpdateManagerFactory.create(this)
localSettings = StoresLocalSettings.getInstance(context = this)
[email protected] = onUpdateDownloaded
[email protected] = onUpdateInstalled

inAppUpdateResultLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
val isUserWantingUpdate = result.resultCode == AppCompatActivity.RESULT_OK
localSettings.isUserWantingUpdates = isUserWantingUpdate
onUserChoice(isUserWantingUpdate)
}
}

override fun FragmentActivity.checkUpdateIsAvailable(
appId: String,
versionCode: Int,
onFDroidResult: (updateIsAvailable: Boolean) -> Unit,
) {
SentryLog.d(APP_UPDATE_TAG, "Checking for update on GPlay")
appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(UPDATE_TYPE)
) {
SentryLog.d(APP_UPDATE_TAG, "Update available on GPlay")
startUpdateFlow(appUpdateInfo, inAppUpdateResultLauncher)
}
}
}

override fun checkStalledUpdate(): Unit = with(appUpdateManager) {
registerListener(installStateUpdatedListener)
appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
SentryLog.d(APP_UPDATE_TAG, "CheckStalledUpdate downloaded")
// If the update is downloaded but not installed, notify the user to complete the update.
onUpdateDownloaded.invoke()
}
}
}

override fun installDownloadedUpdate(
onInstallStart: (() -> Unit)?,
onSuccess: (() -> Unit)?,
onFailure: ((Exception) -> Unit)?,
) {
localSettings.hasAppUpdateDownloaded = false
appUpdateManager.completeUpdate()
.addOnSuccessListener { onSuccess?.invoke() }
.addOnFailureListener {
localSettings.resetUpdateSettings()
onFailure?.invoke(it)
}
}

override fun unregisterAppUpdateListener() {
appUpdateManager.unregisterListener(installStateUpdatedListener)
}

private fun startUpdateFlow(
appUpdateInfo: AppUpdateInfo,
downloadUpdateResultLauncher: ActivityResultLauncher<IntentSenderRequest>,
) = with(appUpdateManager) {
registerListener(installStateUpdatedListener)
startUpdateFlowForResult(
appUpdateInfo,
downloadUpdateResultLauncher,
AppUpdateOptions.newBuilder(UPDATE_TYPE).build(),
)
}
//endregion

//region In-App Review
override fun FragmentActivity.launchInAppReview() {
ReviewManagerFactory.create(this).apply {
Expand Down
Loading
Loading