Only allow the registration of lifecycle observers on the main thread

This commit is contained in:
Michael Droettboom 2019-09-27 14:12:21 -04:00
Родитель f2e6c3ab45
Коммит 808c8d2a4c
5 изменённых файлов: 60 добавлений и 0 удалений

Просмотреть файл

@ -91,6 +91,7 @@ The following steps are required for applications using the Glean SDK, but not l
The Glean SDK should only be initialized from the main application, not individual libraries. If you are adding Glean support to a library, you can safely skip this section.
Please also note that the Glean SDK does not support use across multiple processes, and must only be initialized on the application's main process. Initializing in other processes is a no-op.
Additionally, Glean must be initialized on the main (UI) thread of the applications main process. Failure to do so will throw an `IllegalThreadStateException`.
Before any data collection can take place, the Glean SDK **must** be initialized from the application.
An excellent place to perform this operation is within the `onCreate` method of the class that extends Android's `Application` class.

Просмотреть файл

@ -10,6 +10,7 @@ import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Process
import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.ProcessLifecycleOwner
import com.sun.jna.StringArray
@ -33,6 +34,7 @@ import mozilla.telemetry.glean.private.RecordedExperimentData
import mozilla.telemetry.glean.scheduler.GleanLifecycleObserver
import mozilla.telemetry.glean.scheduler.PingUploadWorker
import mozilla.telemetry.glean.scheduler.MetricsPingScheduler
import mozilla.telemetry.glean.utils.ThreadUtils
import org.json.JSONObject
/**
@ -94,6 +96,8 @@ open class GleanInternalAPI internal constructor () {
* A LifecycleObserver will be added to send pings when the application goes
* into the background.
*
* This method must be called from the main thread.
*
* @param applicationContext [Context] to access application features, such
* as shared preferences
* @param configuration A Glean [Configuration] object with global settings.
@ -101,10 +105,17 @@ open class GleanInternalAPI internal constructor () {
@Suppress("ReturnCount", "LongMethod")
@JvmOverloads
@Synchronized
@MainThread
fun initialize(
applicationContext: Context,
configuration: Configuration = Configuration()
) {
// Glean initialization must be called on the main thread, or lifecycle
// registration may fail. This is also enforced at build time by the
// @MainThread decorator, but this run time check is also performed to
// be extra certain.
ThreadUtils.assertOnUiThread()
// In certain situations Glean.initialize may be called from a process other than the main
// process. In this case we want initialize to be a no-op and just return.
if (!isMainProcess(applicationContext)) {
@ -193,6 +204,8 @@ open class GleanInternalAPI internal constructor () {
Dispatchers.API.flushQueuedInitialTasks()
// At this point, all metrics and events can be recorded.
// This should only be called from the main thread. This is enforced by
// the @MainThread decorator and the `assertOnUiThread` call.
ProcessLifecycleOwner.get().lifecycle.addObserver(gleanLifecycleObserver)
}

Просмотреть файл

@ -24,6 +24,7 @@ import mozilla.telemetry.glean.GleanMetrics.Pings
import mozilla.telemetry.glean.utils.getISOTimeString
import mozilla.telemetry.glean.utils.parseISOTimeString
import mozilla.telemetry.glean.private.TimeUnit
import mozilla.telemetry.glean.utils.ThreadUtils
import java.util.Calendar
import java.util.Date
import java.util.concurrent.TimeUnit as AndroidTimeUnit
@ -64,6 +65,15 @@ internal class MetricsPingScheduler(
}
init {
// This should only be called from the main thread.
// We can't enforce this at build time here, since the @MainThread
// decorator can not be applied to a contructor. However, in practice
// this is only called from Glean.initialize which does have that
// decorator. For good measure, we also perform this run time check
// here.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1581556
ThreadUtils.assertOnUiThread()
// When performing the data migration from glean-ac, this scheduler might be
// provided with a date the 'metrics' ping was last sent. If so, save that in
// the new storage and use it in this scheduler.

Просмотреть файл

@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.telemetry.glean.utils
import android.os.Looper
object ThreadUtils {
private val uiThread = Looper.getMainLooper().thread
/**
* Assert that this code is run on the main (UI) thread.
*/
fun assertOnUiThread() {
val currentThread = Thread.currentThread()
val currentThreadId = currentThread.id
val expectedThreadId = uiThread.id
if (currentThreadId == expectedThreadId) {
return
}
throw IllegalThreadStateException("Expected UI thread, but running on " + currentThread.name)
}
}

Просмотреть файл

@ -10,6 +10,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.Dispatchers as KotlinDispatchers
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.runBlocking
import mozilla.telemetry.glean.GleanMetrics.GleanInternalMetrics
@ -522,4 +523,13 @@ class GleanTest {
assertFalse(Glean.isMainProcess(context))
}
@Test(expected = IllegalThreadStateException::class)
fun `Glean initialize must be called on the main thread`() {
runBlocking(KotlinDispatchers.IO) {
val context: Context = ApplicationProvider.getApplicationContext()
Glean.initialize(context)
}
}
}