diff options
Diffstat (limited to 'mobile/android/android-components/samples/glean')
26 files changed, 1024 insertions, 0 deletions
diff --git a/mobile/android/android-components/samples/glean/README.md b/mobile/android/android-components/samples/glean/README.md new file mode 100644 index 0000000000..eb2f5abbc1 --- /dev/null +++ b/mobile/android/android-components/samples/glean/README.md @@ -0,0 +1,20 @@ +# [Android Components](../../README.md) > Samples > Glean + +![](src/main/res/mipmap-xhdpi/ic_launcher.png) + +A simple app showcasing Glean, the mobile telemetry SDK & the Experiments library. + +## Concepts + +The main concepts shown in the sample app are: + +* Usage of the `metrics.yaml` file. +* Integration between Glean and the application's build process to generate the specific metrics API. +* Usage of the generated specific metrics API. +* Integration of the Nimbus experimentation library. + +## License + + 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/ diff --git a/mobile/android/android-components/samples/glean/build.gradle b/mobile/android/android-components/samples/glean/build.gradle new file mode 100644 index 0000000000..b4922d6b53 --- /dev/null +++ b/mobile/android/android-components/samples/glean/build.gradle @@ -0,0 +1,84 @@ +/* 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/. */ + +buildscript { + repositories { + gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository -> + maven { + url repository + if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) { + allowInsecureProtocol = true + } + } + } + + dependencies { + classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}" + } + } +} + +plugins { + id "com.jetbrains.python.envs" version "$python_envs_plugin" +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + applicationId "org.mozilla.samples.glean" + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + debug { + applicationIdSuffix ".debug" + } + } + + buildFeatures { + viewBinding true + buildConfig true + } + + namespace 'org.mozilla.samples.glean' +} + +dependencies { + implementation project(':service-glean') + implementation project(':service-nimbus') + implementation project(':support-base') + implementation project(':support-rusthttp') + implementation project(':support-rustlog') + implementation project(':lib-fetch-httpurlconnection') + + implementation ComponentsDependencies.kotlin_coroutines + + implementation ComponentsDependencies.androidx_appcompat + implementation ComponentsDependencies.androidx_browser + + implementation project(':samples-glean-library') + + androidTestImplementation ComponentsDependencies.androidx_test_core + androidTestImplementation ComponentsDependencies.androidx_test_runner + androidTestImplementation ComponentsDependencies.androidx_test_rules + androidTestImplementation ComponentsDependencies.androidx_test_junit + androidTestImplementation ComponentsDependencies.androidx_test_uiautomator + androidTestImplementation ComponentsDependencies.androidx_espresso_core + androidTestImplementation ComponentsDependencies.androidx_work_testing + androidTestImplementation ComponentsDependencies.testing_mockwebserver +} + +apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" diff --git a/mobile/android/android-components/samples/glean/metrics.yaml b/mobile/android/android-components/samples/glean/metrics.yaml new file mode 100644 index 0000000000..5759c626b2 --- /dev/null +++ b/mobile/android/android-components/samples/glean/metrics.yaml @@ -0,0 +1,116 @@ +# 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/. + +# This file defines the metrics that are recorded by glean telemetry. They are +# automatically converted to Kotlin code at build time using the `glean_parser` +# PyPI package. +--- + +$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0 + +browser.engagement: + click: + type: event + description: > + Just testing events + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + extra_keys: + key1: + type: string + description: "This is key one" + key2: + type: string + description: "This is key two" + expires: 2100-01-01 + + event_no_keys: + type: event + description: > + Just testing events without keys + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + expires: 2100-01-01 + +basic: + os: + type: string + description: > + The name of the os + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + expires: 2100-01-01 + +test: + string_list: + type: string_list + description: > + Testing StringList ping + send_in_pings: + - test-string-list + lifetime: user + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + expires: 2100-01-01 + + counter: + type: counter + description: > + Testing counter + send_in_pings: + - test-string-list + lifetime: user + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + expires: 2100-01-01 + + timespan: + type: timespan + description: > + Testing a timespan + time_unit: microsecond + lifetime: application + bugs: + - https://bugzilla.mozilla.org/1508948 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + expires: 2100-01-01 + +custom: + counter: + type: counter + description: > + A custom counter that goes on a custom ping + lifetime: ping + send_in_pings: + - sample + bugs: + - https://bugzilla.mozilla.org/1547330 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@test-only.com + expires: 2100-01-01 diff --git a/mobile/android/android-components/samples/glean/pings.yaml b/mobile/android/android-components/samples/glean/pings.yaml new file mode 100644 index 0000000000..7f998f7748 --- /dev/null +++ b/mobile/android/android-components/samples/glean/pings.yaml @@ -0,0 +1,21 @@ +# 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/. + +# This file defines the built-in pings that are recorded by glean telemetry. +# They are # automatically converted to Kotlin code at build time +# using the `glean_parser` PyPI package. +--- + +$schema: moz://mozilla.org/schemas/glean/pings/2-0-0 + +sample: + description: | + A sample custom ping. + include_client_id: true + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com diff --git a/mobile/android/android-components/samples/glean/proguard-rules.pro b/mobile/android/android-components/samples/glean/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/samples/glean/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/README.md b/mobile/android/android-components/samples/glean/samples-glean-library/README.md new file mode 100644 index 0000000000..97a4de14b6 --- /dev/null +++ b/mobile/android/android-components/samples/glean/samples-glean-library/README.md @@ -0,0 +1,5 @@ +samples_glean_library is a toy library that uses glean to record metrics. It +exists simply to show how libraries that are neither the application or glean +itself can record metrics, and those metrics will be stored and sent as part of +the [main pings](../../../components/service/glean/docs/pings.md) that glean +provides. diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/build.gradle b/mobile/android/android-components/samples/glean/samples-glean-library/build.gradle new file mode 100644 index 0000000000..68df279fab --- /dev/null +++ b/mobile/android/android-components/samples/glean/samples-glean-library/build.gradle @@ -0,0 +1,55 @@ +/* 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/. */ + +buildscript { + repositories { + gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository -> + maven { + url repository + if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) { + allowInsecureProtocol = true + } + } + } + + dependencies { + classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}" + } + } +} + +plugins { + id "com.jetbrains.python.envs" version "$python_envs_plugin" +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + buildConfig true + } + + namespace 'mozilla.samples.glean.library' +} + +dependencies { + implementation project(':service-glean') +} + +apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" + diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/metrics.yaml b/mobile/android/android-components/samples/glean/samples-glean-library/metrics.yaml new file mode 100644 index 0000000000..c999c586cf --- /dev/null +++ b/mobile/android/android-components/samples/glean/samples-glean-library/metrics.yaml @@ -0,0 +1,27 @@ +# 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/. + +# This file defines the built-in pings that are recorded by glean telemetry. +# They are # automatically converted to Kotlin code at build time +# using the `glean_parser` PyPI package. +--- + +$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0 + +sample_metrics: + test: + type: counter + description: > + A simple counter defined in a third-party library + # To see the result immediately when pressing the "send ping" button in the + # interface, uncomment the following: + # send_in_pings: + # - baseline + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + expires: 2100-01-01 diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/glean/samples-glean-library/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e16cda1d34 --- /dev/null +++ b/mobile/android/android-components/samples/glean/samples-glean-library/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ +<!-- 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 /> diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/src/main/java/org/mozilla/samples/glean/library/SamplesGleanLibrary.kt b/mobile/android/android-components/samples/glean/samples-glean-library/src/main/java/org/mozilla/samples/glean/library/SamplesGleanLibrary.kt new file mode 100644 index 0000000000..d9d635ddd4 --- /dev/null +++ b/mobile/android/android-components/samples/glean/samples-glean-library/src/main/java/org/mozilla/samples/glean/library/SamplesGleanLibrary.kt @@ -0,0 +1,31 @@ +/* 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.library + +import mozilla.components.service.glean.Glean +import mozilla.samples.glean.library.GleanMetrics.SampleMetrics + +/** + * These are just simple functions to test calling the Glean API + * from a third-party library. + */ +object SamplesGleanLibrary { + /** + * Record to a metric defined in *this* library's metrics.yaml file. + */ + fun recordMetric() { + SampleMetrics.test.add() + } + + /** + * Notate an active experiment. + */ + fun recordExperiment() { + Glean.setExperimentActive( + "third_party_library", + "enabled", + ) + } +} 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 Binary files differnew file mode 100644 index 0000000000..a2f5908281 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-hdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..ff10afd6e1 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-mdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..dcd3cd8083 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..8ca12fe024 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..b824ebdd48 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxxhdpi/ic_launcher.png 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 |