From db9c2640c7e8068b1b8adec37ef4c05b48c0ff17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 15 Oct 2020 01:00:41 +0200 Subject: [PATCH] Add in-app updates support (#914) --- app/build.gradle | 4 +- .../io/github/wulkanowy/utils/UpdateHelper.kt | 17 +++ .../ui/modules/login/LoginActivity.kt | 16 +++ .../wulkanowy/ui/modules/main/MainActivity.kt | 17 +++ app/src/main/res/values/strings.xml | 6 + .../io/github/wulkanowy/utils/UpdateHelper.kt | 104 ++++++++++++++++++ build.gradle | 2 +- 7 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt create mode 100644 app/src/play/java/io/github/wulkanowy/utils/UpdateHelper.kt diff --git a/app/build.gradle b/app/build.gradle index 903a3128..b2ec7b52 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -112,6 +112,7 @@ play { serviceAccountCredentials = file('key.p12') defaultToAppBundles = false track = 'alpha' + updatePriority = 0 } ext { @@ -183,11 +184,12 @@ dependencies { implementation "io.github.wulkanowy:AppKillerManager:3.0.0" implementation 'me.xdrop:fuzzywuzzy:1.3.1' - playImplementation 'com.google.firebase:firebase-analytics:17.5.0' + playImplementation 'com.google.firebase:firebase-analytics:17.6.0' playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.1.1' playImplementation "com.google.firebase:firebase-inappmessaging-ktx:19.1.1" playImplementation 'com.google.firebase:firebase-messaging:20.3.0' playImplementation 'com.google.firebase:firebase-crashlytics:17.2.2' + playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt new file mode 100644 index 00000000..3abab962 --- /dev/null +++ b/app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt @@ -0,0 +1,17 @@ +package io.github.wulkanowy.utils + +import android.app.Activity +import android.view.View +import javax.inject.Inject + +@Suppress("UNUSED_PARAMETER") +class UpdateHelper @Inject constructor() { + + lateinit var messageContainer: View + + fun checkAndInstallUpdates(activity: Activity) {} + + fun onActivityResult(requestCode: Int, resultCode: Int) {} + + fun onResume(activity: Activity) {} +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt index 749989e3..aff1c84c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt @@ -14,6 +14,7 @@ import io.github.wulkanowy.ui.modules.login.form.LoginFormFragment import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment import io.github.wulkanowy.ui.modules.login.studentselect.LoginStudentSelectFragment import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment +import io.github.wulkanowy.utils.UpdateHelper import io.github.wulkanowy.utils.setOnSelectPageListener import javax.inject.Inject @@ -25,6 +26,9 @@ class LoginActivity : BaseActivity(), Logi private val loginAdapter = BaseFragmentPagerAdapter(supportFragmentManager) + @Inject + lateinit var updateHelper: UpdateHelper + companion object { fun getStartIntent(context: Context) = Intent(context, LoginActivity::class.java) @@ -37,8 +41,20 @@ class LoginActivity : BaseActivity(), Logi setContentView(ActivityLoginBinding.inflate(layoutInflater).apply { binding = this }.root) setSupportActionBar(binding.loginToolbar) messageContainer = binding.loginContainer + updateHelper.messageContainer = binding.loginContainer presenter.onAttachView(this) + updateHelper.checkAndInstallUpdates(this) + } + + override fun onResume() { + super.onResume() + updateHelper.onResume(this) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + updateHelper.onActivityResult(requestCode, resultCode) } override fun initView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index 53ac1631..abc002de 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -40,6 +40,7 @@ import io.github.wulkanowy.ui.modules.note.NoteFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.FirebaseAnalyticsHelper +import io.github.wulkanowy.utils.UpdateHelper import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.safelyPopFragments @@ -56,6 +57,9 @@ class MainActivity : BaseActivity(), MainVie @Inject lateinit var analytics: FirebaseAnalyticsHelper + @Inject + lateinit var updateHelper: UpdateHelper + @Inject lateinit var appInfo: AppInfo @@ -100,6 +104,7 @@ class MainActivity : BaseActivity(), MainVie setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root) setSupportActionBar(binding.mainToolbar) messageContainer = binding.mainFragmentContainer + updateHelper.messageContainer = binding.mainFragmentContainer presenter.onAttachView(this, MainView.Section.values().singleOrNull { it.id == intent.getIntExtra(EXTRA_START_MENU, -1) }) @@ -107,6 +112,18 @@ class MainActivity : BaseActivity(), MainVie initialize(startMenuIndex, savedInstanceState) pushFragment(moreMenuFragments[startMenuMoreIndex]) } + updateHelper.checkAndInstallUpdates(this) + } + + override fun onResume() { + super.onResume() + updateHelper.onResume(this) + } + + @SuppressLint("NewApi") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + updateHelper.onActivityResult(requestCode, resultCode) if (appInfo.systemVersion >= Build.VERSION_CODES.N_MR1) initShortcuts() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bf4727be..79d987d0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -472,6 +472,12 @@ Copied Undo + + Start downloading update… + An update has just been downloaded. + Restart + Update failed! Wulkanowy may not function properly. Consider updating + No internet connection diff --git a/app/src/play/java/io/github/wulkanowy/utils/UpdateHelper.kt b/app/src/play/java/io/github/wulkanowy/utils/UpdateHelper.kt new file mode 100644 index 00000000..eb9e854e --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/utils/UpdateHelper.kt @@ -0,0 +1,104 @@ +package io.github.wulkanowy.utils + +import android.app.Activity +import android.app.Activity.RESULT_OK +import android.content.Context +import android.view.View +import android.widget.Toast +import com.google.android.material.snackbar.Snackbar +import com.google.android.play.core.appupdate.AppUpdateInfo +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.install.InstallStateUpdatedListener +import com.google.android.play.core.install.model.AppUpdateType.FLEXIBLE +import com.google.android.play.core.install.model.AppUpdateType.IMMEDIATE +import com.google.android.play.core.install.model.InstallStatus.DOWNLOADED +import com.google.android.play.core.install.model.InstallStatus.PENDING +import com.google.android.play.core.install.model.UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS +import com.google.android.play.core.install.model.UpdateAvailability.UPDATE_AVAILABLE +import com.google.android.play.core.ktx.isFlexibleUpdateAllowed +import com.google.android.play.core.ktx.isImmediateUpdateAllowed +import io.github.wulkanowy.R +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class UpdateHelper @Inject constructor(private val context: Context) { + + lateinit var messageContainer: View + + companion object { + const val IN_APP_UPDATE_REQUEST_CODE = 1721 + + const val DAYS_FOR_FLEXIBLE_UPDATE = 7 + const val HIGH_PRIORITY_UPDATE = 4 + } + + private val appUpdateManager by lazy { AppUpdateManagerFactory.create(context) } + + private val flexibleUpdateListener = InstallStateUpdatedListener { state -> + when (state.installStatus()) { + PENDING -> Toast.makeText(context, R.string.update_download_started, Toast.LENGTH_SHORT).show() + DOWNLOADED -> popupSnackBarForCompleteUpdate() + else -> Timber.d("Update state: ${state.installStatus()}") + } + } + + private inline val AppUpdateInfo.isImmediateUpdateAvailable: Boolean + get() = updateAvailability() == UPDATE_AVAILABLE && isImmediateUpdateAllowed && + updatePriority() >= HIGH_PRIORITY_UPDATE + + private inline val AppUpdateInfo.isFlexibleUpdateAvailable: Boolean + get() = updateAvailability() == UPDATE_AVAILABLE && isFlexibleUpdateAllowed && + clientVersionStalenessDays() ?: 0 >= DAYS_FOR_FLEXIBLE_UPDATE + + fun checkAndInstallUpdates(activity: Activity) { + Timber.d("Checking for updates...") + appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo -> + when { + appUpdateInfo.isImmediateUpdateAvailable -> startUpdate(activity, appUpdateInfo, IMMEDIATE) + appUpdateInfo.isFlexibleUpdateAvailable -> { + appUpdateManager.registerListener(flexibleUpdateListener) + startUpdate(activity, appUpdateInfo, FLEXIBLE) + } + else -> Timber.d("No update available") + } + } + } + + private fun startUpdate(activity: Activity, appUpdateInfo: AppUpdateInfo, updateType: Int) { + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, updateType, activity, IN_APP_UPDATE_REQUEST_CODE + ) + } + + fun onActivityResult(requestCode: Int, resultCode: Int) { + if (requestCode == IN_APP_UPDATE_REQUEST_CODE && resultCode != RESULT_OK) { + Timber.e("Update failed! Result code: $resultCode") + Toast.makeText(context, R.string.update_failed, Toast.LENGTH_LONG).show() + } + } + + fun onResume(activity: Activity) { + appUpdateManager.appUpdateInfo.addOnSuccessListener { info -> + Timber.d("InAppUpdate.onResume() listener: $info") + + when { + DOWNLOADED == info.installStatus() -> popupSnackBarForCompleteUpdate() + DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS == info.updateAvailability() -> { + startUpdate(activity, info, if (info.isImmediateUpdateAvailable) IMMEDIATE else FLEXIBLE) + } + } + } + } + + private fun popupSnackBarForCompleteUpdate() { + Snackbar.make(messageContainer, R.string.update_download_success, Snackbar.LENGTH_INDEFINITE).apply { + setAction(R.string.update_download_success_button) { + appUpdateManager.completeUpdate() + appUpdateManager.unregisterListener(flexibleUpdateListener) + } + show() + } + } +} diff --git a/build.gradle b/build.gradle index 37a3c899..608e3ce5 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.4' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0' - classpath "com.github.triplet.gradle:play-publisher:2.7.5" + classpath "com.github.triplet.gradle:play-publisher:2.8.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.0" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries"