Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Looper
import android.os.UserManager
import android.text.TextUtils
import androidx.annotation.Keep
import androidx.core.app.NotificationManagerCompat
Expand Down Expand Up @@ -41,6 +42,27 @@ object AndroidUtils {
return hasToken && insetsAttached
}

/**
* Retrieve whether the device user is accessible.
*
* On Android 7.0+ (API 24+), encrypted user data is inaccessible until the user unlocks
* the device for the first time after boot. This includes:
* * getSharedPreferences()
* * Any file-based storage in the default credential-encrypted context
*
* Apps that auto-run on boot or background services triggered early may hit this issue.
*/
fun isAndroidUserUnlocked(appContext: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
// Prior to API 24, the device booted into an unlocked state by default
return true
}

val userManager = appContext.getSystemService(Context.USER_SERVICE) as? UserManager
// assume user is unlocked if the Android UserManager is null
return userManager?.isUserUnlocked ?: true
}

fun hasConfigChangeFlag(
activity: Activity,
configChangeFlag: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,16 @@ internal class OneSignalImp(
context: Context,
appId: String?,
): Boolean {
// Check whether current Android user is accessible.
// Return early if it is inaccessible, as we are unable to complete initialization without access
// to device storage like SharedPreferences.
if (!AndroidUtils.isAndroidUserUnlocked(context)) {
Logging.warn("initWithContext called when device storage is locked, no user data is accessible!")
initState = InitState.FAILED
notifyInitComplete()
return false
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part can be in a separate function


initEssentials(context)

val startupService = bootstrapServices()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.ContextWrapper
import android.content.SharedPreferences
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest
import com.onesignal.common.AndroidUtils
import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys.PREFS_LEGACY_APP_ID
import com.onesignal.debug.LogLevel
import com.onesignal.debug.internal.logging.Logging
Expand All @@ -14,6 +15,9 @@ import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.maps.shouldContain
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.unmockkObject
import kotlinx.coroutines.runBlocking
import java.util.concurrent.CountDownLatch

Expand Down Expand Up @@ -98,6 +102,42 @@ class SDKInitTests : FunSpec({
}
}

test("initWithContext returns gracefully when Android user is locked") {
// Given
val context = getApplicationContext<Context>()
val os = OneSignalImp()

mockkObject(AndroidUtils)
every { AndroidUtils.isAndroidUserUnlocked(any()) } returns false

// When
os.initWithContext(context, "appId")

// Then
// returns gracefully but isInitialized should be false
os.isInitialized shouldBe false

unmockkObject(AndroidUtils)
}

test("initWithContext is successful when Android user is unlocked") {
// Given
val context = getApplicationContext<Context>()
val os = OneSignalImp()

mockkObject(AndroidUtils)
every { AndroidUtils.isAndroidUserUnlocked(any()) } returns true

// When
os.initWithContext(context, "appId")

// Then
waitForInitialization(os)
os.isInitialized shouldBe true

unmockkObject(AndroidUtils)
}

test("initWithContext with no appId succeeds when configModel has appId") {
// Given
// block SharedPreference before calling init
Expand Down
Loading