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 @@ -4,6 +4,7 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.SystemClock
import com.onesignal.common.AndroidUtils
import com.onesignal.common.IDManager
import com.onesignal.core.internal.application.IApplicationLifecycleHandler
import com.onesignal.core.internal.application.IApplicationService
Expand All @@ -28,7 +29,9 @@ import com.onesignal.logger.ObservabilityEvent
* only rate rule; six cycles fit inside it at round trips of five seconds or faster.
*
* [FeatureFlag.SDK_DEVICE_GESTURE_DISABLED] turns the gesture off. Absent means enabled, so a
* device that has never fetched flags still has it.
* device that has never fetched flags still has it. An app can also opt out for good with the
* manifest meta-data [MANIFEST_DISABLED_KEY]; then the detector never starts, so nothing is
* counted or recorded.
*
* Every recognised gesture also records [ObservabilityEvent.DEVICE_GESTURE], with its outcome
* and the copied ID, so the gesture's usage can be measured.
Expand All @@ -53,6 +56,11 @@ internal class DeviceGestureDetector(
private val cycleTimestamps = mutableListOf<Long>()

override fun start() {
// The app owner's opt-out.
if (AndroidUtils.getManifestMetaBoolean(applicationService.appContext, MANIFEST_DISABLED_KEY)) {
Logging.info("DeviceGestureDetector: disabled by $MANIFEST_DISABLED_KEY in the manifest, not starting")
Comment thread
nan-li marked this conversation as resolved.
Resolved
return
}
applicationService.addApplicationLifecycleHandler(this)
}

Expand Down Expand Up @@ -180,6 +188,7 @@ internal class DeviceGestureDetector(
/** Shortest background phase a human can produce; anything faster is synthetic. */
internal const val MIN_BACKGROUND_DWELL_MS = 250L

private const val MANIFEST_DISABLED_KEY = "com.onesignal.subscriptionIdCopyDisabled"
private const val CLIP_LABEL = "OneSignal subscription ID"
private const val CLIP_PREFIX = "os: "
internal const val NO_SUBSCRIPTION_CLIP_TEXT = CLIP_PREFIX + "no subscription ID yet"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.content.ContextWrapper
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest
import com.onesignal.common.AndroidUtils
import com.onesignal.core.internal.application.IApplicationLifecycleHandler
import com.onesignal.core.internal.application.IApplicationService
import com.onesignal.core.internal.features.IFeatureManager
Expand All @@ -20,7 +21,10 @@ import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.slot
import io.mockk.unmockkObject
import io.mockk.verify
import org.robolectric.annotation.Config

private const val SUBSCRIPTION_ID = "aaaabbbb-cccc-dddd-eeee-ffff00001111"
Expand Down Expand Up @@ -76,12 +80,12 @@ private class Harness(
val context: Context = ApplicationProvider.getApplicationContext()
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val recorder = RecorderSpy()
val applicationService: IApplicationService = mockk()

private val handlerSlot = slot<IApplicationLifecycleHandler>()
val detector: DeviceGestureDetector

init {
val applicationService = mockk<IApplicationService>()
every { applicationService.appContext } returns if (clipboardAvailable) context else NoClipboardContext(context)
every { applicationService.addApplicationLifecycleHandler(capture(handlerSlot)) } answers {
// Mirrors ApplicationService.addApplicationLifecycleHandler when the app is
Expand Down Expand Up @@ -252,6 +256,27 @@ class DeviceGestureDetectorTests : FunSpec({

// ===== Boundaries and failure paths =====

test("the manifest opt-out keeps the detector from starting") {
// The app owner said no: no lifecycle handler is registered, so nothing is counted,
// copied or recorded, and the only trace is one INFO line at start.
// The key is spelled out rather than read from the detector's constant because the
// string is the whole contract an app has. A typo in the constant then misses this stub,
// the real read finds no meta-data, and the handler gets added, so the test fails.
mockkObject(AndroidUtils)
try {
every {
AndroidUtils.getManifestMetaBoolean(any(), "com.onesignal.subscriptionIdCopyDisabled")
} returns true
val harness = Harness()

verify(exactly = 0) { harness.applicationService.addApplicationLifecycleHandler(any()) }
harness.clipText() shouldBe null
harness.recorder.recorded.shouldBeEmpty()
} finally {
unmockkObject(AndroidUtils)
}
}

test("a 249ms background is a blip and a 250ms one is a cycle") {
// The floor is inclusive: exactly the minimum counts. Six blips leave the window empty,
// so the six real cycles right after still need all six.
Expand Down