Skip to content

Commit

Permalink
Added VersionCheck class and support for verifying data and showing a…
Browse files Browse the repository at this point in the history
… default dialog (#5, #6, #7)

* Added VersionData and VersionDataConveter
* Added DisplayState and Status data classes
* Added URLFetcher interface and NetworkURLFetcher implementation
* Added UpgradeDialog and default implementation
* Removed unused libraries
* Fixed containsBlockedVersion logic and added test case
* Added VersionCheck which exposes flows for state and status
* UpgradeDialog collects flow and reacts to activity lifecycle changes
* Added unit tests
  • Loading branch information
ssawchenko authored Oct 1, 2021
1 parent 7d88556 commit 1c51994
Show file tree
Hide file tree
Showing 24 changed files with 945 additions and 75 deletions.
11 changes: 4 additions & 7 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,10 @@ android {
}

dependencies {
implementation project(':versioncheckkotlin')

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation "com.google.android.material:material:$material"
implementation "androidx.activity:activity-ktx:$ktx_activity"
implementation "androidx.lifecycle:lifecycle-process:$lifecycles"
}
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package="com.steamclock.versioncheckkotlinsample">

<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
Expand Down
56 changes: 56 additions & 0 deletions app/src/main/java/com/steamclock/versioncheckkotlinsample/App.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.steamclock.versioncheckkotlinsample

import android.app.Application
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.ProcessLifecycleOwner
import com.steamclock.versioncheckkotlin.VersionCheck
import com.steamclock.versioncheckkotlin.VersionCheckConfig
import com.steamclock.versioncheckkotlin.interfaces.DefaultUpgradeDialog
import com.steamclock.versioncheckkotlin.interfaces.URLFetcher
import kotlinx.coroutines.*
import java.net.URL
import kotlin.random.Random

class App: Application(), LifecycleObserver {

override fun onCreate() {
super.onCreate()
setupVersionCheck()
}

private fun setupVersionCheck() {
val versionChecker = VersionCheck(
VersionCheckConfig(
appVersionName = BuildConfig.VERSION_NAME,
appVersionCode = BuildConfig.VERSION_CODE,
url = "https://doesn't_matter",
urlFetcher = MockURLFetcher
)
)
val upgradeDialog = DefaultUpgradeDialog(versionChecker.displayStateFlow)

ProcessLifecycleOwner.get().lifecycle.addObserver(versionChecker)
registerActivityLifecycleCallbacks(upgradeDialog)
}
}

object MockURLFetcher: URLFetcher {
override fun getData(url: URL): String? {
return """
{
"ios" : {
"minimumVersion": "1.1",
"blockedVersions": ["1.2.0", "1.2.1", "@301"],
"latestTestVersion": "1.4.2@400"
},
"android" : {
"minimumVersion": "1.1",
"blockedVersions": ["1.2.0", "1.2.1", "@301"],
"latestTestVersion": "1.4.2@400"
},
"serverForceVersionFailure": false,
"serverMaintenance": false
}
"""
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.steamclock.versioncheckkotlinsample

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand Down
12 changes: 11 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.3.72"
ext.kotlin_version = "1.5.30"

ext.ktx_core = "1.6.0"
ext.ktx_activity = "1.3.1"
ext.lifecycles = "2.3.1"
ext.material = "1.4.0"

ext.junit = "4.13.2"
ext.arch_test_core = "2.1.0"
ext.turbine = "0.6.1"

repositories {
google()
jcenter()
Expand Down
16 changes: 10 additions & 6 deletions versioncheckkotlin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ android {
consumerProguardFiles "consumer-rules.pro"
}

testOptions {
// Allow us to test JSONObject
unitTests.returnDefaultValues = true
}

buildTypes {
release {
minifyEnabled false
Expand All @@ -35,10 +40,9 @@ android {
dependencies {

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation "androidx.activity:activity-ktx:$ktx_activity"

testImplementation "junit:junit:$junit"
testImplementation "org.json:json:20180813" // Allow us to test JSONObject
testImplementation "app.cash.turbine:turbine:$turbine" // Testing StateFlows
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package com.steamclock.versioncheckkotlin.interfaces

import android.app.Activity
import android.app.AlertDialog
import android.app.Application
import android.content.Context
import android.os.Bundle
import com.steamclock.versioncheckkotlin.models.DisplayState
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference

class DefaultUpgradeDialog(private val versionDisplayState: StateFlow<DisplayState>) : Application.ActivityLifecycleCallbacks {
private var dialog: AlertDialog? = null
private val coroutineScope = MainScope()
private var needToShowDialog = false
private var currentActivityContext: WeakReference<Context>? = null

init {
coroutineScope.launch {
versionDisplayState.collect { displayState ->
when (val ctx = currentActivityContext?.get()) {
null -> needToShowDialog = true
else -> showForState(ctx, displayState)
}
}
}
}

private fun dismissDialog() {
if (dialog?.isShowing == true) {
dialog?.dismiss()
}
}

private fun reshowDialog() {
dismissDialog()
dialog?.show()
}

private fun showForState(context: Context, state: DisplayState) {
dismissDialog()
when (state) {
DisplayState.ForceUpdate -> {
createBasicDialog(
context,
"Must Update",
"The version of the application is out of date and cannot run. Please update to the latest version from the Play Store.",
requiresUpdate = true,
canDismiss = false
)
}
DisplayState.SuggestUpdate -> {
createBasicDialog(
context,
"Should Update",
"The version of the application is out of date and should not run. Please update to the latest version from the Play Store.",
requiresUpdate = true,
canDismiss = true
)
}
// todo 2021-09 Handle down for maintenance
else -> {
dialog?.dismiss()
dialog = null
}
}
dialog?.show()
needToShowDialog = false
}

private fun createBasicDialog(
context: Context,
title: String,
message: String,
requiresUpdate: Boolean,
canDismiss: Boolean
) {

val builder = AlertDialog.Builder(context).apply {
setTitle(title)
setMessage(message)
setCancelable(canDismiss)

if (requiresUpdate) {
// Do not set click listener here, we do not want the button to dismiss the dialog.
setNeutralButton("Upgrade", null)
}

if (canDismiss) {
setPositiveButton("OK") { _, _ ->
dialog = null
/* todo */
}
}
}

dialog = builder.create()

// Tap dance a little to override the click listener so the dialog will not dismiss
dialog?.apply {
setOnShowListener {
getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener {
// todo Proxy out to Play Store.
}
}
}
}

//-----------------------------------------------------------------
// Using ActivityLifecycleCallbacks to launch version checks and collect
// state flows.
//-----------------------------------------------------------------
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
/* No op */
}

override fun onActivityStarted(activity: Activity) {
/* No op */
}

override fun onActivityResumed(activity: Activity) {
currentActivityContext = WeakReference(activity)
if (needToShowDialog) {
showForState(activity, versionDisplayState.value)
} else {
reshowDialog()
}
}

override fun onActivityPaused(activity: Activity) {
currentActivityContext = null
dismissDialog()
}

override fun onActivityStopped(activity: Activity) {
/* No op */
}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
/* No op */
}

override fun onActivityDestroyed(activity: Activity) {
/* No op */
}
}
Loading

0 comments on commit 1c51994

Please sign in to comment.