summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/samples/glean/src
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/android-components/samples/glean/src')
-rw-r--r--mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/MainActivityTest.kt39
-rw-r--r--mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/pings/BaselinePingTest.kt138
-rw-r--r--mobile/android/android-components/samples/glean/src/main/AndroidManifest.xml42
-rw-r--r--mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/GleanApplication.kt115
-rw-r--r--mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/MainActivity.kt125
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/layout/activity_main.xml75
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3056 bytes
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2096 bytes
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4569 bytes
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 6464 bytes
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 9250 bytes
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/raw/initial_experiments.json60
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/values/endpoints.xml8
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/values/strings.xml21
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/xml/backup_rules.xml8
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/xml/data_extraction_rules.xml9
16 files changed, 640 insertions, 0 deletions
diff --git a/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/MainActivityTest.kt b/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/MainActivityTest.kt
new file mode 100644
index 0000000000..1a145e61c6
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/MainActivityTest.kt
@@ -0,0 +1,39 @@
+/* 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 org.mozilla.samples.glean
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.samples.glean.GleanMetrics.Test as GleanTestMetrics
+
+class MainActivityTest {
+ @get:Rule
+ val activityRule: ActivityScenarioRule<MainActivity> = ActivityScenarioRule(MainActivity::class.java)
+
+ @Test
+ fun checkGleanClickData() {
+ // We don't reset the storage in this test as the GleanTestRule does not
+ // work nicely in instrumented test. Just check the current value, increment
+ // by one and make it the expected value.
+ val expectedValue = if (GleanTestMetrics.counter.testGetValue() != null) {
+ GleanTestMetrics.counter.testGetValue()!! + 1
+ } else {
+ 1
+ }
+
+ // Simulate a click on the button.
+ onView(withId(R.id.buttonGenerateData)).perform(click())
+
+ // Use the Glean testing API to check if the expected data was recorded.
+ assertNotNull(GleanTestMetrics.counter.testGetValue())
+ assertEquals(expectedValue, GleanTestMetrics.counter.testGetValue())
+ }
+}
diff --git a/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/pings/BaselinePingTest.kt b/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/pings/BaselinePingTest.kt
new file mode 100644
index 0000000000..61057039d0
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/pings/BaselinePingTest.kt
@@ -0,0 +1,138 @@
+/* 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 org.mozilla.samples.glean.pings
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import mozilla.components.service.glean.testing.GleanTestLocalServer
+import okhttp3.mockwebserver.Dispatcher
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import okhttp3.mockwebserver.RecordedRequest
+import org.json.JSONObject
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.samples.glean.MainActivity
+import java.io.BufferedReader
+import java.io.ByteArrayInputStream
+import java.util.concurrent.TimeUnit
+import java.util.zip.GZIPInputStream
+
+/**
+ * Decompress the GZIP returned by the glean-core layer.
+ *
+ * @param data the gzipped [ByteArray] to decompress
+ * @return a [String] containing the uncompressed data.
+ */
+fun decompressGZIP(data: ByteArray): String {
+ return GZIPInputStream(ByteArrayInputStream(data)).bufferedReader().use(BufferedReader::readText)
+}
+
+/**
+ * Convenience method to get the body of a request as a String.
+ * The UTF8 representation of the request body will be returned.
+ * If the request body is gzipped, it will be decompressed first.
+ *
+ * @return a [String] containing the body of the request.
+ */
+fun RecordedRequest.getPlainBody(): String {
+ return if (this.getHeader("Content-Encoding") == "gzip") {
+ val bodyInBytes = this.body.readByteArray()
+ decompressGZIP(bodyInBytes)
+ } else {
+ this.body.readUtf8()
+ }
+}
+
+class BaselinePingTest {
+ private val server = createMockWebServer()
+
+ @get:Rule
+ val activityRule: ActivityScenarioRule<MainActivity> = ActivityScenarioRule(MainActivity::class.java)
+
+ @get:Rule
+ val gleanRule = GleanTestLocalServer(context, server.port)
+
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ /**
+ * Create a mock webserver that accepts all requests and replies with "OK".
+ * @return a [MockWebServer] instance
+ */
+ private fun createMockWebServer(): MockWebServer {
+ val server = MockWebServer()
+ server.dispatcher =
+ object : Dispatcher() {
+ override fun dispatch(request: RecordedRequest): MockResponse {
+ return MockResponse().setBody("OK")
+ }
+ }
+
+ return server
+ }
+
+ private fun waitForPingContent(
+ pingName: String,
+ pingReason: String?,
+ maxAttempts: Int = 3,
+ ): JSONObject? {
+ var attempts = 0
+ do {
+ attempts += 1
+ val request = server.takeRequest(20L, TimeUnit.SECONDS)
+ val docType = request?.path?.split("/")?.get(3)
+ if (pingName == docType) {
+ val parsedPayload = JSONObject(request.getPlainBody())
+ if (pingReason == null) {
+ return parsedPayload
+ }
+
+ // If we requested a specific ping reason, look for it.
+ val reason = parsedPayload.getJSONObject("ping_info").getString("reason")
+ if (reason == pingReason) {
+ return parsedPayload
+ }
+ }
+ } while (attempts < maxAttempts)
+
+ return null
+ }
+
+ @Test
+ fun validateBaselinePing() {
+ // Wait for the app to be idle/ready.
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ device.waitForIdle()
+
+ // Wait for 1 second: this should guarantee we have some valid duration in the
+ // ping.
+ Thread.sleep(1000)
+
+ // Move it to background.
+ device.pressHome()
+
+ // Validate the received data.
+ val baselinePing = waitForPingContent("baseline", "inactive")!!
+ val metrics = baselinePing.getJSONObject("metrics")
+
+ // Make sure we have a 'duration' field with a reasonable value: it should be >= 1, since
+ // we slept for 1000ms.
+ val timespans = metrics.getJSONObject("timespan")
+ assertTrue(timespans.getJSONObject("glean.baseline.duration").getLong("value") >= 1L)
+
+ // Make sure there's no errors.
+ val errors = metrics.optJSONObject("labeled_counter")?.keys()
+ errors?.forEach {
+ assertFalse(it.startsWith("glean.error."))
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/glean/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/glean/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..0fb5a873cb
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+ tools:ignore="ScopedStorage" />
+
+ <!-- Note: the usesCleartextTraffic is only required for making instrumentation
+ tests work on API 23+. Also note that this requires tools:ignore="UnusedAttribute"
+ for stopping the linter from complaining on API 21 <= x < 23. -->
+ <application
+ android:allowBackup="true"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
+ android:usesCleartextTraffic="true"
+ tools:ignore="DataExtractionRules,UnusedAttribute"
+ android:name=".GleanApplication"
+ android:dataExtractionRules="@xml/data_extraction_rules">
+ <activity android:name="org.mozilla.samples.glean.MainActivity"
+ android:windowSoftInputMode="adjustResize"
+ android:launchMode="singleTask"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/GleanApplication.kt b/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/GleanApplication.kt
new file mode 100644
index 0000000000..bf0ff5f641
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/GleanApplication.kt
@@ -0,0 +1,115 @@
+/* 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 org.mozilla.samples.glean
+
+import android.app.Application
+import android.content.Context
+import android.net.Uri
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.service.glean.Glean
+import mozilla.components.service.glean.config.Configuration
+import mozilla.components.service.glean.net.ConceptFetchHttpUploader
+import mozilla.components.service.nimbus.Nimbus
+import mozilla.components.service.nimbus.NimbusApi
+import mozilla.components.service.nimbus.NimbusAppInfo
+import mozilla.components.service.nimbus.NimbusServerSettings
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.sink.AndroidLogSink
+import mozilla.components.support.rusthttp.RustHttpConfig
+import mozilla.components.support.rustlog.RustLog
+import org.mozilla.samples.glean.GleanMetrics.Basic
+import org.mozilla.samples.glean.GleanMetrics.Custom
+import org.mozilla.samples.glean.GleanMetrics.GleanBuildInfo
+import org.mozilla.samples.glean.GleanMetrics.Pings
+import org.mozilla.samples.glean.GleanMetrics.Test
+
+class GleanApplication : Application() {
+
+ companion object {
+ lateinit var nimbus: NimbusApi
+
+ const val SAMPLE_GLEAN_PREFERENCES = "sample_glean_preferences"
+ const val PREF_IS_FIRST_RUN = "isFirstRun"
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ val settings = applicationContext.getSharedPreferences(SAMPLE_GLEAN_PREFERENCES, Context.MODE_PRIVATE)
+ val isFirstRun = settings.getBoolean(PREF_IS_FIRST_RUN, true)
+
+ // We want the log messages of all builds to go to Android logcat
+
+ Log.addSink(AndroidLogSink())
+
+ // Register the sample application's custom pings.
+ Glean.registerPings(Pings)
+
+ // Initialize the Glean library. Ideally, this is the first thing that
+ // must be done right after enabling logging.
+ val client by lazy { HttpURLConnectionClient() }
+ val httpClient = ConceptFetchHttpUploader.fromClient(client)
+ val config = Configuration(httpClient = httpClient)
+ Glean.initialize(
+ applicationContext,
+ uploadEnabled = true,
+ configuration = config,
+ buildInfo = GleanBuildInfo.buildInfo,
+ )
+
+ /** Begin Nimbus component specific code. Note: this is not relevant to Glean */
+ initNimbus(isFirstRun)
+ /** End Nimbus specific code. */
+
+ Test.timespan.start()
+
+ Custom.counter.add()
+
+ // Set a sample value for a metric.
+ Basic.os.set("Android")
+
+ settings
+ .edit()
+ .putBoolean(PREF_IS_FIRST_RUN, false)
+ .apply()
+ }
+
+ /**
+ * Initialize the Nimbus experiments library. This is only relevant to the Nimbus library, aside
+ * from recording the experiment in Glean.
+ */
+ private fun initNimbus(isFirstRun: Boolean) {
+ RustLog.enable()
+ RustHttpConfig.setClient(lazy { HttpURLConnectionClient() })
+ val url = Uri.parse(getString(R.string.nimbus_default_endpoint))
+ val appInfo = NimbusAppInfo(
+ appName = "samples-glean",
+ channel = "samples",
+ )
+ nimbus = Nimbus(
+ context = this,
+ appInfo = appInfo,
+ server = NimbusServerSettings(url),
+ ).also { nimbus ->
+ if (isFirstRun) {
+ // This file is bundled with the app, but derived from the server at build time.
+ // We'll use it now, on first run.
+ nimbus.setExperimentsLocally(R.raw.initial_experiments)
+ }
+ // Apply the experiments downloaded on last run, but on first run, it will
+ // use the contents of `R.raw.initial_experiments`.
+ nimbus.applyPendingExperiments()
+
+ // In a real application, we might want to fetchExperiments() here.
+ //
+ // We won't do that in this app because:
+ // * the server's experiments will overwrite the current ones
+ // * it's not clear that the server will have a `test-color` experiment
+ // by the time you run this
+ // * an update experiments button is in `MainActivity`
+ //
+ // nimbus.fetchExperiments()
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/MainActivity.kt b/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/MainActivity.kt
new file mode 100644
index 0000000000..8020022d87
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/MainActivity.kt
@@ -0,0 +1,125 @@
+/* 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 org.mozilla.samples.glean
+
+import android.graphics.Color
+import android.os.Bundle
+import androidx.annotation.MainThread
+import androidx.appcompat.app.AppCompatActivity
+import org.mozilla.experiments.nimbus.EnrolledExperiment
+import org.mozilla.experiments.nimbus.NimbusInterface
+import org.mozilla.samples.glean.GleanMetrics.BrowserEngagement
+import org.mozilla.samples.glean.GleanMetrics.Test
+import org.mozilla.samples.glean.databinding.ActivityMainBinding
+import org.mozilla.samples.glean.library.SamplesGleanLibrary
+
+/**
+ * Main Activity of the glean-sample-app
+ */
+open class MainActivity : AppCompatActivity(), NimbusInterface.Observer {
+ private lateinit var binding: ActivityMainBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+
+ setContentView(binding.root)
+
+ // Generate an event when user clicks on the button.
+ binding.buttonGenerateData.setOnClickListener {
+ // These first two actions, adding to the string list and incrementing the counter are
+ // tied to a user lifetime metric which is persistent from launch to launch.
+
+ // Adds the EditText's text content as a new string in the string list metric from the
+ // metrics.yaml file.
+ Test.stringList.add(binding.etStringListInput.text.toString())
+ // Clear current text to help indicate something happened
+ binding.etStringListInput.setText("")
+
+ // Increments the test_counter metric from the metrics.yaml file.
+ Test.counter.add()
+
+ // This is referencing the event ping named 'click' from the metrics.yaml file. In
+ // order to illustrate adding extra information to the event, it is also adding to the
+ // 'extras' field a dictionary of values. Note that the dictionary keys must be
+ // declared in the metrics.yaml file under the 'extra_keys' section of an event metric.
+ BrowserEngagement.click.record(
+ BrowserEngagement.ClickExtra(
+ key1 = "extra_value_1",
+ key2 = "extra_value_2",
+ ),
+ )
+ }
+
+ Test.timespan.stop()
+
+ // Update some metrics from a third-party library
+ SamplesGleanLibrary.recordMetric()
+ SamplesGleanLibrary.recordExperiment()
+
+ // The following is not relevant to the Glean SDK, but to the Nimbus experiments library
+ setupNimbusExperiments()
+ }
+
+ /** Begin Nimbus component specific functions */
+
+ /**
+ * This sets up the update receiver and sets the onClickListener for the "Update Experiments"
+ * button. This is not relevant to the Glean SDK, but to the Nimbus experiments library.
+ */
+ private fun setupNimbusExperiments() {
+ // Register the main activity as a Nimbus observer
+ GleanApplication.nimbus.register(this)
+
+ // Attach the click listener for the experiments button to the updateExperiments function
+ binding.buttonCheckExperiments.setOnClickListener {
+ // Once the experiments are fetched, then the activity's (a Nimbus observer)
+ // `onExperimentFetched()` method is called.
+ GleanApplication.nimbus.fetchExperiments()
+ }
+
+ configureButton()
+ }
+
+ /**
+ * Event to indicate that the experiments have been fetched from the endpoint
+ */
+ override fun onExperimentsFetched() {
+ println("Experiments fetched")
+ GleanApplication.nimbus.applyPendingExperiments()
+ }
+
+ /**
+ * Event to indicate that the experiment enrollments have been applied. Developers normally
+ * shouldn't care to observe this and rather rely on `onExperimentsFetched` and `withExperiment`
+ */
+ override fun onUpdatesApplied(updated: List<EnrolledExperiment>) {
+ runOnUiThread {
+ configureButton()
+ }
+ }
+
+ @MainThread
+ private fun configureButton() {
+ val nimbus = GleanApplication.nimbus
+ val branch = nimbus.getExperimentBranch("sample-experiment-feature")
+
+ val color = when (branch) {
+ "blue" -> Color.BLUE
+ "red" -> Color.RED
+ "control" -> Color.DKGRAY
+ else -> Color.WHITE
+ }
+ val text = when (branch) {
+ null -> getString(R.string.experiment_not_active)
+ else -> getString(R.string.experiment_active_branch, branch)
+ }
+
+ binding.textViewExperimentStatus.setBackgroundColor(color)
+ binding.textViewExperimentStatus.text = text
+ }
+
+ /** End Nimbus component functions */
+}
diff --git a/mobile/android/android-components/samples/glean/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/glean/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000000..87e3025d0f
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/layout/activity_main.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/buttonList"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:padding="10dp"
+ tools:context="org.mozilla.samples.glean.MainActivity">
+
+ <!-- This is a dummy linear layout to capture focus and prevent the keyboard from popping
+ when the app first launches. This is a known issue of linear layouts and a common
+ workaround. -->
+ <LinearLayout android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:layout_width="0px"
+ android:layout_height="0px" >
+ <requestFocus />
+ </LinearLayout>
+
+ <EditText
+ android:id="@+id/etStringListInput"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:hint="@string/string_list_input_hint"
+ android:inputType="textPersonName"
+ android:autofillHints=""
+ tools:ignore="UnusedAttribute" />
+
+ <Button
+ android:id="@+id/buttonGenerateData"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="5dp"
+ android:text="@string/generate_data"
+ android:textAlignment="center" />
+
+ <TextView
+ android:id="@+id/textView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="5dp"
+ android:text="@string/counter_metric_info" />
+
+ <Button
+ android:id="@+id/buttonCheckExperiments"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/check_experiments"
+ android:textAlignment="center" />
+
+ <TextView
+ android:id="@+id/textViewExperimentStatus"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/white"
+ android:text="@string/experiment_not_active"
+ android:textStyle="italic" />
+
+ <TextView
+ android:id="@+id/textView3"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/check_experiments_btn_description" />
+
+</LinearLayout>
diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..a2f5908281
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..ff10afd6e1
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..dcd3cd8083
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..8ca12fe024
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..b824ebdd48
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/glean/src/main/res/raw/initial_experiments.json b/mobile/android/android-components/samples/glean/src/main/res/raw/initial_experiments.json
new file mode 100644
index 0000000000..e4eadb052b
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/raw/initial_experiments.json
@@ -0,0 +1,60 @@
+{
+ "data": [{
+ "slug": "test-color",
+ "endDate": null,
+ "branches": [
+ {
+ "slug": "control",
+ "ratio": 1,
+ "feature": {
+ "featureId": "sample-experiment-feature",
+ "enabled": true,
+ "value": null
+ }
+ },
+ {
+ "slug": "red",
+ "ratio": 1,
+ "feature": {
+ "featureId": "sample-experiment-feature",
+ "enabled": true,
+ "value": null
+ }
+ },
+ {
+ "slug": "blue",
+ "ratio": 1,
+ "feature": {
+ "featureId": "sample-experiment-feature",
+ "enabled": true,
+ "value": null
+ }
+ }
+ ],
+ "featureIds": [
+ "sample-experiment-feature"
+ ],
+ "probeSets": [],
+ "startDate": null,
+ "targeting": "true",
+ "appName": "samples-glean",
+ "appId": "org.mozilla.samples.glean.debug",
+ "channel": "samples",
+ "bucketConfig": {
+ "count": 10000,
+ "start": 0,
+ "total": 10000,
+ "namespace": "test-color-1",
+ "randomizationUnit": "nimbus_id"
+ },
+ "schemaVersion": "1.1.0",
+ "userFacingName": "Button color sample experiment",
+ "referenceBranch": "control",
+ "proposedDuration": null,
+ "isEnrollmentPaused": false,
+ "proposedEnrollment": 7,
+ "userFacingDescription": "A sample experiment to determine the color of a button",
+ "id": "test-color",
+ "last_modified": 1607613885917
+ }]
+}
diff --git a/mobile/android/android-components/samples/glean/src/main/res/values/endpoints.xml b/mobile/android/android-components/samples/glean/src/main/res/values/endpoints.xml
new file mode 100644
index 0000000000..1a6c4363ca
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/values/endpoints.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<resources>
+ <string name="nimbus_default_endpoint" translatable="false">https://settings.stage.mozaws.net</string>
+</resources>
diff --git a/mobile/android/android-components/samples/glean/src/main/res/values/strings.xml b/mobile/android/android-components/samples/glean/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..ed86142323
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<resources>
+ <string name="app_name">Glean - Metrics Demo</string>
+ <string name="generate_data">Record Text</string>
+ <string name="string_list_input_hint">Enter some text here</string>
+ <string name="counter_metric_info">
+ Every time you click on the button above, the text is added to the test_string_list metric
+ and a counter metric type called testCounter is incremented. Both of these will be included
+ when the ping is sent. The counter and the string list are utilizing the `user` lifetime
+ and should persist from launch to launch of the app.\n\nAn event metric is also being used
+ to attach a dictionary of values to the extras of the event ping. See MainActivity for
+ where all of this is happening.
+ </string>
+ <string name="experiment_not_active">Experiment not active</string>
+ <string name="experiment_active_branch">Experiment active, branch: %1$s</string>
+ <string name="check_experiments">Check experiments</string>
+ <string name="check_experiments_btn_description">The CHECK EXPERIMENTS button checks the experiments status of the test-colorexperiment and sets the color accordingly.</string>
+</resources>
diff --git a/mobile/android/android-components/samples/glean/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/glean/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000..820ae61afa
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<full-backup-content>
+ <include domain="sharedpref" path="."/>
+</full-backup-content> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/glean/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/glean/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..55da967560
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<data-extraction-rules>
+ <cloud-backup>
+ <include domain="sharedpref" path="."/>
+ </cloud-backup>
+</data-extraction-rules> \ No newline at end of file