diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt index 824f9f06943fb8d1588e9155f00c26fcbff8bfcf..7a96494ec5fa66bcf1ea1fb7e4ab195e8dd9a20b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt @@ -5,24 +5,28 @@ import android.net.Uri import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import de.rki.coronawarnapp.http.DynamicURLs import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity +import de.rki.coronawarnapp.update.UpdateChecker +import kotlinx.coroutines.launch class LauncherActivity : AppCompatActivity() { companion object { private val TAG: String? = LauncherActivity::class.simpleName } + private lateinit var updateChecker: UpdateChecker + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retrieveCustomURLsFromSchema(intent.data) + updateChecker = UpdateChecker(this) - if (LocalData.isOnboarded()) { - startMainActivity() - } else { - startOnboardingActivity() + lifecycleScope.launch { + updateChecker.checkForUpdate() } } @@ -54,15 +58,31 @@ class LauncherActivity : AppCompatActivity() { } } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + updateChecker.onActivityResult(requestCode, resultCode) + } + + fun navigateToActivities() { + if (LocalData.isOnboarded()) { + startMainActivity() + } else { + startOnboardingActivity() + } + } + private fun startOnboardingActivity() { val onboardingActivity = Intent(this, OnboardingActivity::class.java) startActivity(onboardingActivity) + this.overridePendingTransition(0, 0) finish() } private fun startMainActivity() { val mainActivityIntent = Intent(this, MainActivity::class.java) startActivity(mainActivityIntent) + this.overridePendingTransition(0, 0) finish() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt new file mode 100644 index 0000000000000000000000000000000000000000..07bf9cc0ede200968366a53ff982de42d4e04618 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt @@ -0,0 +1,160 @@ +package de.rki.coronawarnapp.update + +import android.app.Activity.RESULT_CANCELED +import android.app.Activity.RESULT_OK +import android.content.IntentSender.SendIntentException +import android.util.Log +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +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.install.model.ActivityResult.RESULT_IN_APP_UPDATE_FAILED +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.UpdateAvailability +import de.rki.coronawarnapp.BuildConfig +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.server.protocols.ApplicationConfigurationOuterClass +import de.rki.coronawarnapp.service.applicationconfiguration.ApplicationConfigurationService +import de.rki.coronawarnapp.ui.LauncherActivity +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class UpdateChecker(private val activity: LauncherActivity) { + + companion object { + val TAG: String? = UpdateChecker::class.simpleName + private const val REQUEST_CODE = 100 + } + + suspend fun checkForUpdate() { + + // check if an update is needed based on server config + val updateNeededFromServer: Boolean = try { + checkIfUpdatesNeededFromServer() + } + // TODO replace with signature exception + catch (exception: Exception) { + true + } + + // get AppUpdateManager + val baseContext = activity.baseContext + val appUpdateManager = AppUpdateManagerFactory.create(baseContext) + + var appUpdateInfo: AppUpdateInfo? = null + + val updateAvailableFromGooglePlay = try { + appUpdateInfo = checkForGooglePlayUpdate(appUpdateManager) + + val availability = appUpdateInfo.updateAvailability() + val immediateUpdateAllowed = + appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) + + availability == UpdateAvailability.UPDATE_AVAILABLE && immediateUpdateAllowed + } catch (exception: Exception) { + false + } + + if (updateNeededFromServer && updateAvailableFromGooglePlay && appUpdateInfo != null) { + Log.i(TAG, "show update dialog") + showUpdateAvailableDialog(appUpdateManager, appUpdateInfo) + } else { + activity.navigateToActivities() + } + } + + private fun showUpdateAvailableDialog( + appUpdateManager: AppUpdateManager, + appUpdateInfo: AppUpdateInfo + ) { + AlertDialog.Builder(activity) + .setTitle(activity.getString(R.string.update_dialog_title)) + .setMessage(activity.getString(R.string.update_dialog_message)) + .setPositiveButton(activity.getString(R.string.update_dialog_button)) { _, _ -> + startGooglePlayUpdateFlow(appUpdateManager, appUpdateInfo) + } + .create().show() + } + + private fun startGooglePlayUpdateFlow( + appUpdateManager: AppUpdateManager, + appUpdateInfo: AppUpdateInfo + ) { + try { + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, + AppUpdateType.IMMEDIATE, + activity, + REQUEST_CODE + ) + } catch (exception: SendIntentException) { + Log.i(TAG, exception.toString()) + } + } + + fun onActivityResult(requestCode: Int, resultCode: Int) { + if (REQUEST_CODE == requestCode) { + + // TODO react to these + when (resultCode) { + RESULT_OK -> { + Log.i(TAG, "startFlowResult RESULT_OK") + activity.navigateToActivities() + } + RESULT_CANCELED -> { + Log.i(TAG, "startFlowResult RESULT_CANCELED") + } + RESULT_IN_APP_UPDATE_FAILED -> { + Log.i(TAG, "startFlowResult RESULT_IN_APP_UPDATE_FAILED") + val toast = Toast.makeText(activity, "In app update failed", Toast.LENGTH_LONG) + toast.show() + activity.navigateToActivities() + } + } + } + } + + private suspend fun checkIfUpdatesNeededFromServer(): Boolean { + + val applicationConfigurationFromServer = + ApplicationConfigurationService.asyncRetrieveApplicationConfiguration() + + val minVersionFromServer = applicationConfigurationFromServer.appVersion.android.min + val minVersionFromServerString = + constructSemanticVersionString(minVersionFromServer) + Log.i( + TAG, + "minVersionStringFromServer:" + constructSemanticVersionString( + minVersionFromServer + ) + ) + Log.i(TAG, "Current app version:" + BuildConfig.VERSION_NAME) + + val needsImmediateUpdate = VersionComparator.isVersionOlder( + BuildConfig.VERSION_NAME, + minVersionFromServerString + ) + Log.i(TAG, "needs update:" + needsImmediateUpdate) + return true + } + + private fun constructSemanticVersionString( + semanticVersion: ApplicationConfigurationOuterClass.SemanticVersion + ): String { + return semanticVersion.major.toString() + "." + + semanticVersion.minor.toString() + "." + + semanticVersion.patch.toString() + } + + private suspend fun checkForGooglePlayUpdate(appUpdateManager: AppUpdateManager) = + suspendCoroutine<AppUpdateInfo> { cont -> + val appUpdateInfoTask = appUpdateManager.appUpdateInfo + appUpdateInfoTask.addOnSuccessListener { + cont.resume(it) + }.addOnFailureListener { + cont.resumeWithException(it) + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/VersionComparator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/VersionComparator.kt new file mode 100644 index 0000000000000000000000000000000000000000..1df0b5dde051662beb2912bb2c008701666b6d8e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/VersionComparator.kt @@ -0,0 +1,30 @@ +package de.rki.coronawarnapp.update + +object VersionComparator { + + fun isVersionOlder(currentVersion: String, versionToCompareTo: String): Boolean { + var isVersionOlder = false + + val delimiter = "." + + val currentVersionParts = currentVersion.split(delimiter) + val currentVersionMajor = currentVersionParts[0].toInt() + val currentVersionMinor = currentVersionParts[1].toInt() + val currentVersionPatch = currentVersionParts[2].toInt() + + val versionToCompareParts = versionToCompareTo.split(delimiter) + val versionToCompareMajor = versionToCompareParts[0].toInt() + val versionToCompareMinor = versionToCompareParts[1].toInt() + val versionToComparePatch = versionToCompareParts[2].toInt() + + if (versionToCompareMajor > currentVersionMajor) { + isVersionOlder = true + } else if (versionToCompareMinor > currentVersionMinor) { + isVersionOlder = true + } else if (versionToComparePatch > currentVersionPatch) { + isVersionOlder = true + } + + return isVersionOlder + } +} diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index f9c2f4e33671dc207ee3aeca11957f7e790c44e7..0add622529cc819a528466e4e4aecf3db38d8e2b 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -150,6 +150,14 @@ <string name="notification_headline">Corona-Warn App</string> <string name="notification_body">Es gibt Neuigkeiten von Ihrer Corona-Warn-App</string> + <!-- #################################### + App Auto Update + ###################################### --> + + <string name="update_dialog_title">Update verfügbar</string> + <string name="update_dialog_message">Bitte die app updaten</string> + <string name="update_dialog_button">Update</string> + <!-- #################################### Risk Card ###################################### --> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/VersionComparatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/VersionComparatorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..421e73c5ad404152e5a23cc0408f688aad8d7ffc --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/VersionComparatorTest.kt @@ -0,0 +1,51 @@ +package de.rki.coronawarnapp.update + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` +import org.junit.Test + +class VerificationServiceTest { + + @Test + fun testVersionMajorOlder() { + val result = VersionComparator.isVersionOlder("1.0.0", "2.0.0") + assertThat(result, `is`(true)) + } + + @Test + fun testVersionMinorOlder() { + val result = VersionComparator.isVersionOlder("1.0.0", "1.1.0") + assertThat(result, `is`(true)) + } + + @Test + fun testVersionPatchOlder() { + val result = VersionComparator.isVersionOlder("1.0.1", "1.0.2") + assertThat(result, `is`(true)) + } + + @Test + fun testVersionMajorNewer() { + val result = VersionComparator.isVersionOlder("2.0.0", "1.0.0") + assertThat(result, `is`(false)) + } + + @Test + fun testVersionMinorNewer() { + val result = VersionComparator.isVersionOlder("1.2.0", "1.1.0") + assertThat(result, `is`(false)) + } + + @Test + fun testVersionPatchNewer() { + val result = VersionComparator.isVersionOlder("1.0.3", "1.0.2") + assertThat(result, `is`(false)) + } + + @Test + fun testSameVersion() { + val result = VersionComparator.isVersionOlder("1.0.1", "1.0.1") + assertThat(result, `is`(false)) + } + +} \ No newline at end of file