diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
commit | da4c7e7ed675c3bf405668739c3012d140856109 (patch) | |
tree | cdd868dba063fecba609a1d819de271f0d51b23e /mobile/android/android-components/samples/sync | |
parent | Adding upstream version 125.0.3. (diff) | |
download | firefox-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')
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 Binary files differnew file mode 100644 index 0000000000..e2edeb6cbe --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-hdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..0c12478a8e --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-mdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..abdaf95771 --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..0c8508a62b --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..ba2b17573d --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png 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…</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 |