summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/samples/sync
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:34:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:34:42 +0000
commitda4c7e7ed675c3bf405668739c3012d140856109 (patch)
treecdd868dba063fecba609a1d819de271f0d51b23e /mobile/android/android-components/samples/sync
parentAdding upstream version 125.0.3. (diff)
downloadfirefox-da4c7e7ed675c3bf405668739c3012d140856109.tar.xz
firefox-da4c7e7ed675c3bf405668739c3012d140856109.zip
Adding upstream version 126.0.upstream/126.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/samples/sync')
-rw-r--r--mobile/android/android-components/samples/sync/README.md25
-rw-r--r--mobile/android/android-components/samples/sync/build.gradle58
-rw-r--r--mobile/android/android-components/samples/sync/gradle.properties0
-rw-r--r--mobile/android/android-components/samples/sync/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/samples/sync/src/main/AndroidManifest.xml40
-rw-r--r--mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceFragment.kt85
-rw-r--r--mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceRecyclerViewAdapter.kt68
-rw-r--r--mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/LoginFragment.kt108
-rw-r--r--mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt464
-rw-r--r--mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/SampleFxAEntryPoint.kt14
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/layout/activity_main.xml146
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device.xml24
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device_list.xml15
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/layout/fragment_view.xml11
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 2954 bytes
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2061 bytes
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4368 bytes
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 6037 bytes
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 8179 bytes
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/values/dimens.xml8
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/values/strings.xml39
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/xml/backup_rules.xml8
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/xml/data_extraction_rules.xml9
23 files changed, 1143 insertions, 0 deletions
diff --git a/mobile/android/android-components/samples/sync/README.md b/mobile/android/android-components/samples/sync/README.md
new file mode 100644
index 0000000000..1790aaca5b
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/README.md
@@ -0,0 +1,25 @@
+# [Android Components](../../README.md) > Samples > Firefox Sync
+
+![](src/main/res/mipmap-xhdpi/ic_launcher.png)
+
+A simple app showcasing how to use `FxaAccountManager` together with `BackgroundSyncManager` and `browser-storage-sync` components
+to (periodically) synchronize FxA data in a background worker.
+
+## Concepts
+
+This app demonstrates how to synchronize Firefox Account data (bookmarks, history, ...).
+
+This app could be "easily" generalized to synchronize other types of data stores, if another implementation of `concept-storage`
+is used.
+
+Following basic bits of functionality are present:
+
+* Configuring `FxaAccountManager` and `BackgroundSyncManager`
+* Making `Syncable` stores (such as `PlacesHistoryStorage`) available to background workers via `GlobalSyncableStoreProvider`
+* Configuring listeners to monitor account status (logged in, logged out) and sync status (in-progress, finished, querying local data)
+
+## 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/sync/build.gradle b/mobile/android/android-components/samples/sync/build.gradle
new file mode 100644
index 0000000000..bb99c4220b
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/build.gradle
@@ -0,0 +1,58 @@
+/* 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/. */
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ applicationId "org.mozilla.samples.sync"
+ 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'
+ }
+ }
+
+ splits {
+ abi {
+ enable true
+ reset()
+ include 'x86', 'arm64-v8a', 'armeabi-v7a'
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ buildConfig true
+ }
+
+ namespace 'org.mozilla.samples.sync'
+}
+
+dependencies {
+ implementation project(':concept-storage')
+ implementation project(':concept-toolbar')
+ implementation project(':browser-storage-sync')
+ implementation project(':service-firefox-accounts')
+ implementation project(':service-sync-logins')
+ implementation project(':service-sync-autofill')
+ implementation project(':support-rustlog')
+ implementation project(':support-rusthttp')
+ implementation project(':lib-fetch-httpurlconnection')
+ implementation project(':lib-dataprotect')
+
+ implementation ComponentsDependencies.kotlin_reflect
+ implementation ComponentsDependencies.androidx_fragment
+ implementation ComponentsDependencies.androidx_recyclerview
+}
diff --git a/mobile/android/android-components/samples/sync/gradle.properties b/mobile/android/android-components/samples/sync/gradle.properties
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/gradle.properties
diff --git a/mobile/android/android-components/samples/sync/proguard-rules.pro b/mobile/android/android-components/samples/sync/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/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/sync/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/sync/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..eb03b1d407
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?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" />
+
+ <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"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ tools:targetApi="s"
+ tools:ignore="DataExtractionRules">
+ <activity
+ android:name=".MainActivity"
+ android:launchMode="singleTask"
+ android:windowSoftInputMode="adjustResize"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data
+ android:host="*"
+ android:scheme="fxaclient" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceFragment.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceFragment.kt
new file mode 100644
index 0000000000..a2fa4c15b6
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceFragment.kt
@@ -0,0 +1,85 @@
+/* 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.sync
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.sync.Device
+
+/**
+ * A fragment representing a list of Items.
+ * Activities containing this fragment MUST implement the
+ * [DeviceFragment.OnDeviceListInteractionListener] interface.
+ */
+class DeviceFragment : Fragment() {
+
+ private var listenerDevice: OnDeviceListInteractionListener? = null
+
+ private val adapter = DeviceRecyclerViewAdapter(listenerDevice)
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View? {
+ val view = inflater.inflate(R.layout.fragment_device_list, container, false)
+
+ // Set the adapter
+ if (view is RecyclerView) {
+ with(view) {
+ layoutManager = LinearLayoutManager(context)
+ adapter = this@DeviceFragment.adapter
+ }
+ }
+ return view
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ if (context is OnDeviceListInteractionListener) {
+ listenerDevice = context
+ adapter.mListenerDevice = context
+ } else {
+ throw IllegalArgumentException("$context must implement OnDeviceListInteractionListener")
+ }
+ }
+
+ override fun onDetach() {
+ super.onDetach()
+ listenerDevice = null
+ }
+
+ /**
+ * Updates the list of devices.
+ */
+ @SuppressLint("NotifyDataSetChanged")
+ fun updateDevices(devices: List<Device>) {
+ adapter.devices.clear()
+ adapter.devices.addAll(devices)
+ adapter.notifyDataSetChanged()
+ }
+
+ /**
+ * This interface must be implemented by activities that contain this
+ * fragment to allow an interaction in this fragment to be communicated
+ * to the activity and potentially other fragments contained in that
+ * activity.
+ *
+ *
+ * See the Android Training lesson
+ * [Communicating with Other Fragments](http://developer.android.com/training/basics/fragments/communicating.html)
+ * for more information.
+ */
+ interface OnDeviceListInteractionListener {
+ fun onDeviceInteraction(item: Device)
+ }
+}
diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceRecyclerViewAdapter.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceRecyclerViewAdapter.kt
new file mode 100644
index 0000000000..f240b793b9
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceRecyclerViewAdapter.kt
@@ -0,0 +1,68 @@
+/* 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.sync
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceType
+import org.mozilla.samples.sync.DeviceFragment.OnDeviceListInteractionListener
+import org.mozilla.samples.sync.databinding.FragmentDeviceBinding
+
+/**
+ * [RecyclerView.Adapter] that can display a [DummyItem] and makes a call to the
+ * specified [OnDeviceListInteractionListener].
+ */
+class DeviceRecyclerViewAdapter(
+ var mListenerDevice: OnDeviceListInteractionListener?,
+) : RecyclerView.Adapter<DeviceRecyclerViewAdapter.ViewHolder>() {
+
+ val devices = mutableListOf<Device>()
+
+ private val mOnClickListener: View.OnClickListener
+
+ init {
+ mOnClickListener = View.OnClickListener { v ->
+ val item = v.tag as Device
+ // Notify the active callbacks interface (the activity, if the fragment is attached to
+ // one) that an item has been selected.
+ mListenerDevice?.onDeviceInteraction(item)
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val binding = FragmentDeviceBinding
+ .inflate(LayoutInflater.from(parent.context), parent, false)
+ return ViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = devices[position]
+ holder.nameView.text = item.displayName
+ holder.typeView.text = when (item.deviceType) {
+ DeviceType.DESKTOP -> "Desktop"
+ DeviceType.MOBILE -> "Mobile"
+ DeviceType.TABLET -> "Tablet"
+ DeviceType.TV -> "TV"
+ DeviceType.VR -> "VR"
+ DeviceType.UNKNOWN -> "Unknown"
+ }
+
+ with(holder.itemView) {
+ tag = item
+ setOnClickListener(mOnClickListener)
+ }
+ }
+
+ override fun getItemCount(): Int = devices.size
+
+ inner class ViewHolder(binding: FragmentDeviceBinding) : RecyclerView.ViewHolder(binding.root) {
+ val nameView: TextView = binding.deviceName
+ val typeView: TextView = binding.deviceType
+ }
+}
diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/LoginFragment.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/LoginFragment.kt
new file mode 100644
index 0000000000..ac3802c0c6
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/LoginFragment.kt
@@ -0,0 +1,108 @@
+/* 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.sync
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.CookieManager
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.fragment.app.Fragment
+
+class LoginFragment : Fragment() {
+
+ private lateinit var authUrl: String
+ private lateinit var redirectUrl: String
+ private var mWebView: WebView? = null
+ private var listener: OnLoginCompleteListener? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ arguments?.let {
+ authUrl = it.getString(AUTH_URL)!!
+ redirectUrl = it.getString(REDIRECT_URL)!!
+ }
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ val view: View = inflater.inflate(R.layout.fragment_view, container, false)
+ val webView = view.findViewById<WebView>(R.id.webview)
+ // Need JS, cookies and localStorage.
+ webView.settings.domStorageEnabled = true
+ webView.settings.javaScriptEnabled = true
+ CookieManager.getInstance().setAcceptCookie(true)
+
+ webView.webViewClient = object : WebViewClient() {
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ if (url != null && url.startsWith(redirectUrl)) {
+ val uri = Uri.parse(url)
+ val code = uri.getQueryParameter("code")
+ val state = uri.getQueryParameter("state")
+ val action = uri.getQueryParameter("action")
+ if (code != null && state != null && action != null) {
+ listener?.onLoginComplete(code, state, action, this@LoginFragment)
+ }
+ }
+
+ super.onPageStarted(view, url, favicon)
+ }
+ }
+ webView.loadUrl(authUrl)
+
+ mWebView?.destroy()
+ mWebView = webView
+
+ return view
+ }
+
+ @Suppress("TooGenericExceptionThrown")
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ if (context is OnLoginCompleteListener) {
+ listener = context
+ } else {
+ throw IllegalStateException("$context must implement OnLoginCompleteListener")
+ }
+ }
+
+ override fun onDetach() {
+ super.onDetach()
+ listener = null
+ }
+
+ override fun onPause() {
+ super.onPause()
+ mWebView?.onPause()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ mWebView?.onResume()
+ }
+
+ interface OnLoginCompleteListener {
+ fun onLoginComplete(code: String, state: String, action: String, fragment: LoginFragment)
+ }
+
+ companion object {
+ const val AUTH_URL = "authUrl"
+ const val REDIRECT_URL = "redirectUrl"
+
+ fun create(authUrl: String, redirectUrl: String): LoginFragment =
+ LoginFragment().apply {
+ arguments = Bundle().apply {
+ putString(AUTH_URL, authUrl)
+ putString(REDIRECT_URL, redirectUrl)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt
new file mode 100644
index 0000000000..06f889d441
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt
@@ -0,0 +1,464 @@
+/* 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.sync
+
+import android.os.Bundle
+import android.text.method.ScrollingMovementMethod
+import android.view.View
+import android.widget.TextView
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.appservices.fxaclient.FxaServer
+import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
+import mozilla.components.browser.storage.sync.PlacesHistoryStorage
+import mozilla.components.concept.storage.BookmarkNode
+import mozilla.components.concept.sync.AccountEvent
+import mozilla.components.concept.sync.AccountEventsObserver
+import mozilla.components.concept.sync.AccountObserver
+import mozilla.components.concept.sync.AuthFlowError
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceCapability
+import mozilla.components.concept.sync.DeviceCommandIncoming
+import mozilla.components.concept.sync.DeviceCommandOutgoing
+import mozilla.components.concept.sync.DeviceConfig
+import mozilla.components.concept.sync.DeviceConstellationObserver
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.Profile
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.service.fxa.FxaAuthData
+import mozilla.components.service.fxa.PeriodicSyncConfig
+import mozilla.components.service.fxa.ServerConfig
+import mozilla.components.service.fxa.SyncConfig
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider
+import mozilla.components.service.fxa.sync.SyncReason
+import mozilla.components.service.fxa.sync.SyncStatusObserver
+import mozilla.components.service.fxa.toAuthType
+import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage
+import mozilla.components.service.sync.logins.SyncableLoginsStorage
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.log.sink.AndroidLogSink
+import mozilla.components.support.rusthttp.RustHttpConfig
+import mozilla.components.support.rustlog.RustLog
+import org.mozilla.samples.sync.databinding.ActivityMainBinding
+import java.lang.Exception
+import kotlin.coroutines.CoroutineContext
+
+class MainActivity :
+ AppCompatActivity(),
+ LoginFragment.OnLoginCompleteListener,
+ DeviceFragment.OnDeviceListInteractionListener,
+ CoroutineScope {
+ private val historyStorage = lazy {
+ PlacesHistoryStorage(this)
+ }
+
+ private val bookmarksStorage = lazy {
+ PlacesBookmarksStorage(this)
+ }
+
+ private val securePreferences by lazy { SecureAbove22Preferences(this, "key_store") }
+
+ private val passwordsStorage = lazy {
+ SyncableLoginsStorage(this, lazy { securePreferences })
+ }
+
+ private val creditCardsAddressesStorage = lazy {
+ AutofillCreditCardsAddressesStorage(this, lazy { securePreferences })
+ }
+
+ private val creditCardKeyProvider by lazy { creditCardsAddressesStorage.value.crypto }
+ private val passwordsKeyProvider by lazy { passwordsStorage.value.crypto }
+
+ private val accountManager by lazy {
+ FxaAccountManager(
+ this,
+ ServerConfig(FxaServer.Release, CLIENT_ID, REDIRECT_URL),
+ DeviceConfig(
+ name = "A-C Sync Sample - ${System.currentTimeMillis()}",
+ type = DeviceType.MOBILE,
+ capabilities = setOf(DeviceCapability.SEND_TAB),
+ secureStateAtRest = true,
+ ),
+ SyncConfig(
+ setOf(
+ SyncEngine.History,
+ SyncEngine.Bookmarks,
+ SyncEngine.Passwords,
+ SyncEngine.Addresses,
+ SyncEngine.CreditCards,
+ ),
+ periodicSyncConfig = PeriodicSyncConfig(periodMinutes = 15, initialDelayMinutes = 5),
+ ),
+ )
+ }
+
+ private var job = Job()
+ override val coroutineContext: CoroutineContext
+ get() = Dispatchers.Main + job
+
+ companion object {
+ const val CLIENT_ID = "3c49430b43dfba77"
+ const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID"
+ }
+
+ private val logger = Logger("SampleSync")
+
+ private lateinit var binding: ActivityMainBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+
+ RustLog.enable()
+ RustHttpConfig.setClient(lazy { HttpURLConnectionClient() })
+
+ Log.addSink(AndroidLogSink())
+
+ setContentView(binding.root)
+
+ findViewById<View>(R.id.buttonSignIn).setOnClickListener {
+ launch {
+ accountManager.beginAuthentication(entrypoint = SampleFxAEntryPoint.HomeMenu)?.let { openWebView(it) }
+ }
+ }
+
+ findViewById<View>(R.id.buttonLogout).setOnClickListener {
+ launch { accountManager.logout() }
+ }
+
+ findViewById<View>(R.id.refreshDevice).setOnClickListener {
+ launch { accountManager.authenticatedAccount()?.deviceConstellation()?.refreshDevices() }
+ }
+
+ findViewById<View>(R.id.sendTab).setOnClickListener {
+ launch {
+ accountManager.authenticatedAccount()?.deviceConstellation()?.let { constellation ->
+ // Ignore devices that can't receive tabs.
+ val targets = constellation.state()?.otherDevices?.filter {
+ it.capabilities.contains(DeviceCapability.SEND_TAB)
+ }
+
+ targets?.forEach {
+ constellation.sendCommandToDevice(
+ it.id,
+ DeviceCommandOutgoing.SendTab("Sample tab", "https://www.mozilla.org"),
+ )
+ }
+
+ Toast.makeText(
+ this@MainActivity,
+ "Sent sample tab to ${targets?.size ?: 0} device(s)",
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ }
+ }
+
+ // NB: ObserverRegistry takes care of unregistering this observer when appropriate, and
+ // cleaning up any internal references to 'observer' and 'owner'.
+ // Observe changes to the account and profile.
+ accountManager.register(accountObserver, owner = this, autoPause = true)
+ // Observe sync state changes.
+ accountManager.registerForSyncEvents(syncObserver, owner = this, autoPause = true)
+ // Observe incoming device commands.
+ accountManager.registerForAccountEvents(accountEventsObserver, owner = this, autoPause = true)
+
+ GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
+ GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarksStorage)
+ GlobalSyncableStoreProvider.configureStore(
+ storePair = SyncEngine.Passwords to passwordsStorage,
+ keyProvider = lazy { passwordsKeyProvider },
+ )
+ GlobalSyncableStoreProvider.configureStore(
+ storePair = SyncEngine.CreditCards to creditCardsAddressesStorage,
+ keyProvider = lazy { creditCardKeyProvider },
+ )
+ GlobalSyncableStoreProvider.configureStore(SyncEngine.Addresses to creditCardsAddressesStorage)
+
+ launch {
+ // Now that our account state observer is registered, we can kick off the account manager.
+ accountManager.start()
+ }
+
+ findViewById<View>(R.id.buttonSync).setOnClickListener {
+ launch {
+ accountManager.syncNow(SyncReason.User)
+ accountManager.authenticatedAccount()?.deviceConstellation()?.pollForCommands()
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ accountManager.close()
+ job.cancel()
+ }
+
+ override fun onLoginComplete(code: String, state: String, action: String, fragment: LoginFragment) {
+ launch {
+ supportFragmentManager.popBackStack()
+ accountManager.finishAuthentication(
+ FxaAuthData(action.toAuthType(), code = code, state = state),
+ )
+ }
+ }
+
+ override fun onDeviceInteraction(item: Device) {
+ Toast.makeText(
+ this@MainActivity,
+ getString(
+ R.string.full_device_details,
+ item.id,
+ item.displayName,
+ item.deviceType,
+ item.subscriptionExpired,
+ item.subscription,
+ item.capabilities,
+ item.lastAccessTime,
+ ),
+ Toast.LENGTH_LONG,
+ ).show()
+ }
+
+ private fun openWebView(url: String) {
+ supportFragmentManager.beginTransaction().apply {
+ replace(R.id.container, LoginFragment.create(url, REDIRECT_URL))
+ addToBackStack(null)
+ commit()
+ }
+ }
+
+ private val deviceConstellationObserver = object : DeviceConstellationObserver {
+ override fun onDevicesUpdate(constellation: ConstellationState) {
+ launch {
+ val currentDevice = constellation.currentDevice
+
+ val currentDeviceView: TextView = findViewById(R.id.currentDevice)
+ if (currentDevice != null) {
+ currentDeviceView.text = getString(
+ R.string.full_device_details,
+ currentDevice.id,
+ currentDevice.displayName,
+ currentDevice.deviceType,
+ currentDevice.subscriptionExpired,
+ currentDevice.subscription,
+ currentDevice.capabilities,
+ currentDevice.lastAccessTime,
+ )
+ } else {
+ currentDeviceView.text = getString(R.string.current_device_unknown)
+ }
+
+ val devicesFragment = supportFragmentManager.findFragmentById(R.id.devices_fragment) as DeviceFragment
+ devicesFragment.updateDevices(constellation.otherDevices)
+
+ Toast.makeText(this@MainActivity, "Devices updated", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ @Suppress("SetTextI18n", "NestedBlockDepth")
+ private val accountEventsObserver = object : AccountEventsObserver {
+ override fun onEvents(events: List<AccountEvent>) {
+ val txtView: TextView = findViewById(R.id.latestTabs)
+ events.forEach {
+ when (it) {
+ is AccountEvent.DeviceCommandIncoming -> {
+ when (it.command) {
+ is DeviceCommandIncoming.TabReceived -> {
+ val cmd = it.command as DeviceCommandIncoming.TabReceived
+ var tabsStringified = "Tab(s) from: ${cmd.from?.displayName}\n"
+ cmd.entries.forEach { tab ->
+ tabsStringified += "${tab.title}: ${tab.url}\n"
+ }
+ txtView.text = tabsStringified
+ }
+ }
+ }
+ is AccountEvent.ProfileUpdated -> {
+ txtView.text = "The user's profile was updated"
+ }
+ is AccountEvent.AccountAuthStateChanged -> {
+ txtView.text = "The account auth state changed"
+ }
+ is AccountEvent.AccountDestroyed -> {
+ txtView.text = "The account was destroyed"
+ }
+ is AccountEvent.DeviceConnected -> {
+ txtView.text = "Another device connected to the account"
+ }
+ is AccountEvent.DeviceDisconnected -> {
+ if (it.isLocalDevice) {
+ txtView.text = "This device disconnected"
+ } else {
+ txtView.text = "The device ${it.deviceId} disconnected"
+ }
+ }
+ is AccountEvent.Unknown -> {
+ // Unknown events are ignored to allow supporting new
+ // account events
+ }
+ }
+ }
+ }
+ }
+
+ private val accountObserver = object : AccountObserver {
+ lateinit var lastAuthType: AuthType
+
+ override fun onLoggedOut() {
+ logger.info("onLoggedOut")
+
+ launch {
+ val txtView: TextView = findViewById(R.id.fxaStatusView)
+ txtView.text = getString(R.string.logged_out)
+
+ val historyResultTextView: TextView = findViewById(R.id.historySyncResult)
+ historyResultTextView.text = ""
+ val bookmarksResultTextView: TextView = findViewById(R.id.bookmarksSyncResult)
+ bookmarksResultTextView.text = ""
+ val currentDeviceTextView: TextView = findViewById(R.id.currentDevice)
+ currentDeviceTextView.text = ""
+
+ val devicesFragment = supportFragmentManager.findFragmentById(
+ R.id.devices_fragment,
+ ) as DeviceFragment
+ devicesFragment.updateDevices(listOf())
+
+ findViewById<View>(R.id.buttonLogout).visibility = View.INVISIBLE
+ findViewById<View>(R.id.buttonSignIn).visibility = View.VISIBLE
+ findViewById<View>(R.id.buttonSync).visibility = View.INVISIBLE
+ findViewById<View>(R.id.refreshDevice).visibility = View.INVISIBLE
+ findViewById<View>(R.id.sendTab).visibility = View.INVISIBLE
+ }
+ }
+
+ override fun onAuthenticationProblems() {
+ logger.info("onAuthenticationProblems")
+
+ launch {
+ val txtView: TextView = findViewById(R.id.fxaStatusView)
+ txtView.text = getString(R.string.need_reauth)
+
+ findViewById<View>(R.id.buttonSignIn).visibility = View.VISIBLE
+ }
+ }
+
+ override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
+ logger.info("onAuthenticated")
+
+ launch {
+ lastAuthType = authType
+
+ val txtView: TextView = findViewById(R.id.fxaStatusView)
+ txtView.text = getString(R.string.signed_in_waiting_for_profile, authType::class.simpleName)
+
+ findViewById<View>(R.id.buttonLogout).visibility = View.VISIBLE
+ findViewById<View>(R.id.buttonSignIn).visibility = View.INVISIBLE
+ findViewById<View>(R.id.buttonSync).visibility = View.VISIBLE
+ findViewById<View>(R.id.refreshDevice).visibility = View.VISIBLE
+ findViewById<View>(R.id.sendTab).visibility = View.VISIBLE
+
+ account.deviceConstellation().registerDeviceObserver(
+ deviceConstellationObserver,
+ this@MainActivity,
+ true,
+ )
+ }
+ }
+
+ override fun onProfileUpdated(profile: Profile) {
+ logger.info("onProfileUpdated")
+
+ launch {
+ val txtView: TextView = findViewById(R.id.fxaStatusView)
+ txtView.text = getString(
+ R.string.signed_in_with_profile,
+ lastAuthType::class.simpleName,
+ "${profile.displayName ?: ""} ${profile.email}",
+ )
+ }
+ }
+
+ override fun onFlowError(error: AuthFlowError) {
+ launch {
+ val txtView: TextView = findViewById(R.id.fxaStatusView)
+ txtView.text = getString(
+ R.string.account_error,
+ when (error) {
+ AuthFlowError.FailedToBeginAuth -> "Failed to begin authentication"
+ AuthFlowError.FailedToCompleteAuth -> "Failed to complete authentication"
+ },
+ )
+ }
+ }
+ }
+
+ private val syncObserver = object : SyncStatusObserver {
+ override fun onStarted() {
+ logger.info("onSyncStarted")
+ CoroutineScope(Dispatchers.Main).launch {
+ binding.syncStatus.text = getString(R.string.syncing)
+ }
+ }
+
+ override fun onIdle() {
+ logger.info("onSyncIdle")
+ CoroutineScope(Dispatchers.Main).launch {
+ binding.syncStatus.text = getString(R.string.sync_idle)
+
+ val historyResultTextView: TextView = findViewById(R.id.historySyncResult)
+ val visitedCount = withContext(Dispatchers.IO) { historyStorage.value.getVisited().size }
+ // visitedCount is passed twice: to get the correct plural form, and then as
+ // an argument for string formatting.
+ historyResultTextView.text = resources.getQuantityString(
+ R.plurals.visited_url_count,
+ visitedCount,
+ visitedCount,
+ )
+
+ val bookmarksResultTextView: TextView = findViewById(R.id.bookmarksSyncResult)
+ bookmarksResultTextView.setHorizontallyScrolling(true)
+ bookmarksResultTextView.movementMethod = ScrollingMovementMethod.getInstance()
+ bookmarksResultTextView.text = withContext(Dispatchers.IO) {
+ val bookmarksRoot = bookmarksStorage.value.getTree("root________", recursive = true)
+ if (bookmarksRoot == null) {
+ getString(R.string.no_bookmarks_root)
+ } else {
+ var bookmarksRootAndChildren = "BOOKMARKS\n"
+ fun addTreeNode(node: BookmarkNode, depth: Int) {
+ val desc = " ".repeat(depth * 2) + "${node.title} - ${node.url} (${node.guid})\n"
+ bookmarksRootAndChildren += desc
+ node.children?.forEach {
+ addTreeNode(it, depth + 1)
+ }
+ }
+ addTreeNode(bookmarksRoot, 0)
+ bookmarksRootAndChildren
+ }
+ }
+ }
+ }
+
+ override fun onError(error: Exception?) {
+ logger.error("onSyncError", error)
+ CoroutineScope(Dispatchers.Main).launch {
+ binding.syncStatus.text = getString(R.string.sync_error, error)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/SampleFxAEntryPoint.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/SampleFxAEntryPoint.kt
new file mode 100644
index 0000000000..3ea2fe51a4
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/SampleFxAEntryPoint.kt
@@ -0,0 +1,14 @@
+/* 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.sync
+
+import mozilla.components.concept.sync.FxAEntryPoint
+
+/**
+ * An implementation of [FxAEntryPoint] for the sample application.
+ */
+enum class SampleFxAEntryPoint(override val entryName: String) : FxAEntryPoint {
+ HomeMenu("home-menu"),
+}
diff --git a/mobile/android/android-components/samples/sync/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/sync/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000000..816c5cd0e6
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/layout/activity_main.xml
@@ -0,0 +1,146 @@
+<?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/. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:id="@+id/container"
+ tools:context=".MainActivity">
+
+ <ScrollView
+ tools:ignore="UselessParent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <RelativeLayout
+ android:id="@+id/stuff"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <Button
+ android:id="@+id/buttonSignIn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sign_in"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/buttonSync"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/buttonSignIn"
+ android:text="@string/sync"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/refreshDevice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/buttonSync"
+ android:text="@string/refresh_device"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/sendTab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/refreshDevice"
+ android:text="@string/send_tab"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/buttonLogout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/sendTab"
+ android:text="@string/log_out"
+ android:textAlignment="center" />
+
+ <TextView
+ android:id="@+id/fxaStatusView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/buttonLogout"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/syncStatus"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/fxaStatusView"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/historySyncResult"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/syncStatus"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/bookmarksSyncResult"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/historySyncResult"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/currentDeviceLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/bookmarksSyncResult"
+ style="?android:attr/listSeparatorTextViewStyle"
+ android:text="@string/current_device" />
+
+ <TextView
+ android:id="@+id/currentDevice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/currentDeviceLabel"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/latestTabsLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/currentDevice"
+ style="?android:attr/listSeparatorTextViewStyle"
+ android:text="@string/latest_tabs" />
+
+ <TextView
+ android:id="@+id/latestTabs"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/latestTabsLabel"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/devicesLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/latestTabs"
+ style="?android:attr/listSeparatorTextViewStyle"
+ android:text="@string/devices" />
+
+ <androidx.fragment.app.FragmentContainerView android:name="org.mozilla.samples.sync.DeviceFragment"
+ android:id="@+id/devices_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/devicesLabel" />
+ </RelativeLayout>
+ </ScrollView>
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device.xml b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device.xml
new file mode 100644
index 0000000000..1cd0773ff9
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device.xml
@@ -0,0 +1,24 @@
+<?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"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/device_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem" />
+
+ <TextView
+ android:id="@+id/device_type"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem" />
+</LinearLayout>
diff --git a/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device_list.xml b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device_list.xml
new file mode 100644
index 0000000000..4af98d4db2
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device_list.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+
+<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/list"
+ android:name="org.mozilla.samples.sync.DeviceFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ tools:context=".DeviceFragment"
+ tools:listitem="@layout/fragment_device" /> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_view.xml b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_view.xml
new file mode 100644
index 0000000000..44536f4f68
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_view.xml
@@ -0,0 +1,11 @@
+<?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/. -->
+
+<WebView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/webview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".LoginFragment" />
diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..e2edeb6cbe
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..0c12478a8e
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..abdaf95771
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..0c8508a62b
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..ba2b17573d
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync/src/main/res/values/dimens.xml b/mobile/android/android-components/samples/sync/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..3544b113bd
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/values/dimens.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>
+ <dimen name="text_margin">16dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/samples/sync/src/main/res/values/strings.xml b/mobile/android/android-components/samples/sync/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..b3f00174e9
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/values/strings.xml
@@ -0,0 +1,39 @@
+<?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">Firefox Sync Demo</string>
+ <string name="sign_in">FxA sign in</string>
+ <string name="signed_in_waiting_for_profile">Signed in (type=%1$s); waiting for profile</string>
+ <string name="signed_in_with_profile">Authenticated (type=%1$s) as %2$s</string>
+ <string name="account_error">FxA error: %1$s</string>
+ <string name="sync_idle">Sync is idle</string>
+ <string name="syncing">Syncing&#8230;</string>
+ <string name="sync_error">Sync error: %1$s</string>
+ <plurals name="visited_url_count">
+ <item quantity="zero">There are no visited URLs</item>
+ <item quantity="one">There is %d visited URL</item>
+ <item quantity="other">There are %d visited URLs</item>
+ </plurals>
+ <string name="no_bookmarks_root">No Bookmarks Root node</string>
+ <string name="log_out">FxA Log Out</string>
+ <string name="logged_out">Logged out!</string>
+ <string name="need_reauth">Need to re-authenticate</string>
+ <string name="sync">Sync</string>
+ <string name="refresh_device">Refresh device</string>
+ <string name="send_tab">Send tab</string>
+ <string name="current_device">Current device</string>
+ <string name="current_device_unknown">Unknown</string>
+ <string name="full_device_details">
+ ID: %1$s\n
+ Name: %2$s\n
+ Type: %3$s\n
+ Subscription expired: %4$b\n
+ Subscription: %5$s\n
+ Capabilities: %6$s\n
+ Last access: %7$d
+ </string>
+ <string name="latest_tabs">Latest tabs</string>
+ <string name="devices">Devices</string>
+</resources>
diff --git a/mobile/android/android-components/samples/sync/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/sync/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000..820ae61afa
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/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/sync/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/sync/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..55da967560
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/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