0

I wanted to make a method that determine if the application is started for the very first time, no matter the current version of the application. People suggest that we should use SharedPreferences as seen from this qustion. Below is the function that determine if application is started for the very first time.

companion object {

    const val APP_LAUNCH_FIRST_TIME: Int = 0            // first start ever
    const val APP_LAUNCH_FIRST_TIME_VERSION: Int = 1    // first start in this version (when app is updated)
    const val APP_LAUNCH_NORMAL: Int = 2                // normal app start

    /**
     * Method that checks if the application is started for the very first time, or for the first time
     * of the updated version, or just normal start.
     */
    fun checkForFirstAppStart(context: Context): Int {
        val sharedPreferencesVersionTag = "last_app_version"

        val sharedPreferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(context)
        var appStart = APP_LAUNCH_NORMAL

        try {
            val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
            val lastVersionCode = sharedPreferences.getLong(sharedPreferencesVersionTag, -1L)
            val currentVersionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
            appStart = when {
                lastVersionCode == -1L -> APP_LAUNCH_FIRST_TIME
                lastVersionCode < currentVersionCode -> APP_LAUNCH_FIRST_TIME_VERSION
                lastVersionCode > currentVersionCode -> APP_LAUNCH_NORMAL
                else -> APP_LAUNCH_NORMAL
            }

            // Update version in preferences
            sharedPreferences.edit().putLong(sharedPreferencesVersionTag, currentVersionCode).commit()

        } catch (e: PackageManager.NameNotFoundException) {
            // Unable to determine current app version from package manager. Defensively assuming normal app start
        }
        return appStart
    }
}

Now in my MainActivity I make the check in this way, but strangely enough I always end up inside the if statement, although appLaunch is different from MainActivityHelper.APP_LAUNCH_FIRST_TIME

val appLaunch = MainActivityHelper.checkForFirstAppStart(this)
if (appLaunch == MainActivityHelper.APP_LAUNCH_FIRST_TIME) {
    val c = 299_792_458L
}

Here we see that appLaunch is 2 enter image description here

Here we see that MainActivityHelper.APP_LAUNCH_FIRST_TIME is 0 enter image description here

I am in the main thread I check using Thread.currentThread(), and when I add watches in the debugger (appLaunch == MainActivityHelper.APP_LAUNCH_FIRST_TIME) I get false. So I suggest that there is some delay, and by the time the if check is made the result is changed?

Zain
  • 37,492
  • 7
  • 60
  • 84
slaviboy
  • 1,407
  • 17
  • 27
  • First of all this logic will be broken if you clean your cache. To be 100% sure, you have to save this data on the remote storage(If it's really important) – Eugene Troyanskii Jun 04 '21 at 11:58

1 Answers1

1

There's nothing wrong with the code. I tested it and it works as intended. I get all three return values depending on the circumstances. I simplified the code a bit but the original code should nevertheless works.

enum class AppLaunch {
    LAUNCH_FIRST_TIME,          // first start ever
    FIRST_TIME_VERSION,         // first start in this version (when app is updated)
    NORMAL                      // normal app start
}

/**
 * Method that checks if the application is started for the very first time, or for the first time
 * of the updated version, or just normal start.
 */
fun checkForFirstAppStart(context: Context): AppLaunch {
    val sharedPreferencesVersionTag = "last_app_version"

    val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)

    return try {
        val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
        val lastVersionCode = sharedPreferences.getLong(sharedPreferencesVersionTag, -1L)
        val currentVersionCode = PackageInfoCompat.getLongVersionCode(packageInfo)

        // Update version in preferences
        sharedPreferences.edit().putLong(sharedPreferencesVersionTag, currentVersionCode).commit()

        when (lastVersionCode) {
            -1L -> AppLaunch.LAUNCH_FIRST_TIME
            in 0L until currentVersionCode -> AppLaunch.FIRST_TIME_VERSION
            else -> AppLaunch.NORMAL
        }
    } catch (e: PackageManager.NameNotFoundException) {
        // Unable to determine current app version from package manager. Defensively assuming normal app start
        AppLaunch.NORMAL
    }
}

I experimented a bit and the issue you see looks like a bug in Android Studio. If the code in the if statement is a NOP (no operation) then the debugger seems to stop there. If the code does have a side effect, the debugger doesn't stop. Things like this can be infuriating but with Android, Android Studio and the tooling, bugs like this are pretty common (unfortunately).

if (appLaunch == APP_LAUNCH_FIRST_TIME) {
    val c = 299_792_458L
}

translates to the following byte code:

L3 (the if statement)
    LINENUMBER 32 L3
    ILOAD 4
    IFNE L4
L5
    LINENUMBER 33 L5
    LDC 299792458
    LSTORE 2

Converting c to a var

var c = 1L
if (appLaunch == APP_LAUNCH_FIRST_TIME) {
    c = 299_792_458L
}

results in identical byte code so it's certainly not a code problem but an issue with Android Studio.

Update

If you need fast writes with enums you can use something like this:

fun appLaunchById(id: Int, def: AppLaunch = AppLaunch.NORMAL) = AppLaunch.values().find { it.id == id } ?: def

enum class AppLaunch(val id: Int) {
    LAUNCH_FIRST_TIME(0),          // first start ever
    FIRST_TIME_VERSION(1),         // first start in this version (when app is updated)
    NORMAL(2);                     // normal app start
}

^^^ writes an Int so fast and short. Reading is certainly not super fast though.

Update 2

Generic version of the enum solution:

inline fun <reified T : Enum<*>> enumById(hash: Int, def: T) = enumValues<T>()
    .find { it.hashCode() == hash }
    ?: def

enum class AppLaunch {
    LAUNCH_FIRST_TIME,          // first start ever
    FIRST_TIME_VERSION,         // first start in this version (when app is updated)
    NORMAL                      // normal app start
}

Usage:

val enum = enumById(value.hashCode(), AppLaunch.NORMAL)
Emanuel Moecklin
  • 28,488
  • 11
  • 69
  • 85
  • Yes [appLaunch = 2] and [MainActivityHelper.APP_LAUNCH_FIRST_TIME = 0], so how do I always end up inside the if scope when its impossible we have the check statement if(2 == 0){ } and it is False. But if I call another method inside the if scope, it starts working normally and the breakpoint is not called!!! – slaviboy Jun 04 '21 at 20:07
  • Thank you for your simplification, but I usually try to avoid using Enums, and the reason is if at any moment I decide I want to save the last statement of the 'appLaunch' for example in a bundle when the device is rotated or process death occurred. It is easier to save primitive data types such as Int, Long, Double... and then restore them. At first I mainly used the Enums, but then I saw how the Google team writes their code and I started using constant primitive values. – slaviboy Jun 04 '21 at 20:19
  • @slaviboy I added my observations to the answer. I would not spend sleepless nights over this (unless you already have ;-). It's most likely a bug in Android Studio. – Emanuel Moecklin Jun 05 '21 at 00:00
  • @slaviboy re: Enums. With all due respect I disagree. It's easy to serialize an enum (as string with Enum.name) and deserialize using Enum.valueOf() -> in Kotlin there's ways to define a default value should this fail). Google is a bad example imo, most of their libraries I've seen so far for Android have a terrible design, picking Const over Enums is just another example of their poor design choices. If you run a simple Google search "Const vs Enum Java" (or Kotlin) you'll find plenty of opinions, most of them vavouring the use of Enums over Const. – Emanuel Moecklin Jun 05 '21 at 00:06
  • Yea, you are right it depends if you want to serialize it as json for example you can use Enums. But I usually write custom file and it is significantly faster to use primitives instead of String especially if you implement autosave, where you save a bunch of settings every 10-20 second. And when you use primitive values the file size is also significantly smaller then writing strings, especially if you are like me and like naming your variables using long string values. – slaviboy Jun 05 '21 at 03:36
  • @slaviboy ok got it. If writing must be fast but reading doesn't happen often I have a last argument for enums ;-). See my Update in the response above. – Emanuel Moecklin Jun 05 '21 at 04:20
  • Thank you, yeah it is possible to sign a primitive value to the Enums, but it also requires more code imagine having to implement the same logic for 20 different enums. It looks uglier in my opinion, I prefer simply adding static values. Also if you need different types for a given class I can place them all inside companion object, and it is easier to find them when you always put them at the same place, instead of creating Enum for each type. – slaviboy Jun 05 '21 at 05:44
  • @slaviboy see my generic solution, requires only one function for all enums but I get it, you want to use constants ;-) – Emanuel Moecklin Jun 06 '21 at 02:43