From bde0541efd4887925364721c1f4139147f48d554 Mon Sep 17 00:00:00 2001 From: Grisha Kruglov Date: Thu, 26 Sep 2019 11:52:06 -0700 Subject: [PATCH] Combined PR: FxA+Sync+Send Tab integration & Bookmarks navigation (#1417) * Closes #1395: Ability to navigate in and out of folders * Closes #1395: Add "Desktop Bookmarks" virtual folder when at the top level * Closes #717: FxA, Sync and Send Tab integrations This PR integrates FxA account manager and adds just enough code to allow signing-in via settings, signing out, synchronizing bookmarks and receiving tabs sent from other Firefox devices. TODO: - bookmarks UI needs folder support - better account management UI, currently there are just sign-in/sign-out buttons - megazord configuration? * Notify any BookmarkStore listeners of changes after sync is finished This makes sure we see synced bookmarks in the library right after signing-in. * Add history storage and configure it to be synchronized * Rebase fixes --- app/build.gradle | 4 + .../mozilla/vrbrowser/VRBrowserActivity.java | 38 ++++++ .../vrbrowser/VRBrowserApplication.java | 7 ++ .../vrbrowser/browser/BookmarksStore.kt | 90 +++++++++++++- .../org/mozilla/vrbrowser/browser/Services.kt | 117 ++++++++++++++++++ .../browser/engine/SessionStack.java | 3 + .../suggestions/SuggestionsProvider.java | 4 +- .../vrbrowser/ui/views/BookmarksView.java | 3 +- .../ui/widgets/settings/AccountsHelper.kt | 49 ++++++++ .../ui/widgets/settings/SettingsWidget.java | 77 ++++++++++++ app/src/main/res/layout/bookmark_item.xml | 1 + app/src/main/res/layout/bookmarks.xml | 36 +++--- app/src/main/res/layout/settings.xml | 8 ++ app/src/main/res/values/strings.xml | 22 ++++ versions.gradle | 6 + 15 files changed, 442 insertions(+), 23 deletions(-) create mode 100644 app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt create mode 100644 app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/AccountsHelper.kt diff --git a/app/build.gradle b/app/build.gradle index 212411d7bb..5a15ce5615 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -437,10 +437,14 @@ dependencies { implementation deps.android_components.browser_search implementation deps.android_components.browser_storage implementation deps.android_components.browser_domains + implementation deps.android_components.service_accounts implementation deps.android_components.ui_autocomplete implementation deps.android_components.concept_fetch implementation deps.android_components.lib_fetch + // TODO this should not be necessary at all, see Services.kt + implementation deps.work.runtime + // Kotlin dependency implementation deps.kotlin.stdlib implementation deps.kotlin.coroutines diff --git a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java index 31112f635e..c6ebdbf079 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java @@ -72,6 +72,10 @@ import org.mozilla.vrbrowser.utils.ServoUtils; import org.mozilla.vrbrowser.utils.SystemUtils; +import mozilla.components.concept.sync.AccountObserver; +import mozilla.components.concept.sync.OAuthAccount; +import mozilla.components.concept.sync.Profile; + import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -180,6 +184,23 @@ public void onGlobalFocusChanged(View oldFocus, View newFocus) { } }; + private AccountObserver accountObserver = new AccountObserver() { + @Override + public void onLoggedOut() {} + + @Override + public void onAuthenticated(@NonNull OAuthAccount account) { + // Check if we have any new device events (e.g. tabs). + account.deviceConstellation().refreshDeviceStateAsync(); + } + + @Override + public void onProfileUpdated(@NonNull Profile profile) {} + + @Override + public void onAuthenticationProblems() {} + }; + @Override protected void attachBaseContext(Context base) { super.attachBaseContext(LocaleUtils.setLocale(base)); @@ -263,6 +284,12 @@ protected void onCreate(Bundle savedInstanceState) { mConnectivityReceiver = new ConnectivityReceiver(); mPoorPerformanceWhiteList = new HashSet<>(); checkForCrash(); + + // Monitor FxA account state. + ((VRBrowserApplication) this.getApplicationContext()) + .getServices() + .getAccountManager() + .register(accountObserver); } protected void initializeWidgets() { @@ -385,6 +412,17 @@ protected void onResume() { } handleConnectivityChange(); mConnectivityReceiver.register(this, () -> runOnUiThread(() -> handleConnectivityChange())); + + // If we're signed-in, poll for any new device events (e.g. received tabs) on activity resume. + // There's no push support right now, so this helps with the perception of speedy tab delivery. + OAuthAccount account = ((VRBrowserApplication) this.getApplicationContext()) + .getServices() + .getAccountManager() + .authenticatedAccount(); + if (account != null) { + account.deviceConstellation().refreshDeviceStateAsync(); + } + super.onResume(); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java index cdb1648a66..070d08df75 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java @@ -10,6 +10,7 @@ import android.content.res.Configuration; import org.mozilla.vrbrowser.browser.Places; +import org.mozilla.vrbrowser.browser.Services; import org.mozilla.vrbrowser.db.AppDatabase; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.utils.LocaleUtils; @@ -17,6 +18,7 @@ public class VRBrowserApplication extends Application { private AppExecutors mAppExecutors; + private Services mServices; private Places mPlaces; @Override @@ -25,6 +27,7 @@ public void onCreate() { mAppExecutors = new AppExecutors(); mPlaces = new Places(this); + mServices = new Services(this, mPlaces); TelemetryWrapper.init(this); } @@ -49,6 +52,10 @@ public DataRepository getRepository() { return DataRepository.getInstance(getDatabase(), mAppExecutors); } + public Services getServices() { + return mServices; + } + public Places getPlaces() { return mPlaces; } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/BookmarksStore.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/BookmarksStore.kt index 93c994df2d..b2d6c271f6 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/BookmarksStore.kt +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/BookmarksStore.kt @@ -8,16 +8,73 @@ package org.mozilla.vrbrowser.browser import android.content.Context import android.os.Handler import android.os.Looper +import android.util.Log +import androidx.lifecycle.ProcessLifecycleOwner import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.future.future import mozilla.appservices.places.BookmarkRoot import mozilla.components.concept.storage.BookmarkNode import org.mozilla.vrbrowser.VRBrowserApplication import java.util.concurrent.CompletableFuture +import mozilla.components.concept.storage.BookmarkNodeType +import mozilla.components.service.fxa.sync.SyncStatusObserver +import org.mozilla.vrbrowser.R + +const val DESKTOP_ROOT = "fake_desktop_root" class BookmarksStore constructor(val context: Context) { + companion object { + private val coreRoots = listOf( + DESKTOP_ROOT, + BookmarkRoot.Mobile.id, + BookmarkRoot.Unfiled.id, + BookmarkRoot.Toolbar.id, + BookmarkRoot.Menu.id + ) + + @JvmStatic + fun allowDeletion(guid: String): Boolean { + return coreRoots.contains(guid) + } + + /** + * User-friendly titles for various internal bookmark folders. + */ + fun rootTitles(context: Context): Map { + return mapOf( + // "Virtual" desktop folder. + DESKTOP_ROOT to context.getString(R.string.bookmarks_desktop_folder_title), + // Our main root, in actuality the "mobile" root: + BookmarkRoot.Mobile.id to context.getString(R.string.bookmarks_title), + // What we consider the "desktop" roots: + BookmarkRoot.Menu.id to context.getString(R.string.bookmarks_desktop_menu_title), + BookmarkRoot.Toolbar.id to context.getString(R.string.bookmarks_desktop_toolbar_title), + BookmarkRoot.Unfiled.id to context.getString(R.string.bookmarks_desktop_unfiled_title) + ) + } + } + private val listeners = ArrayList() private val storage = (context.applicationContext as VRBrowserApplication).places.bookmarks + private val titles = rootTitles(context) + + // Bookmarks might have changed during sync, so notify our listeners. + private val syncStatusObserver = object : SyncStatusObserver { + override fun onStarted() {} + + override fun onIdle() { + Log.d("BookmarksStore", "Detected that sync is finished, notifying listeners") + notifyListeners() + } + + override fun onError(error: Exception?) {} + } + + init { + (context.applicationContext as VRBrowserApplication).services.accountManager.registerForSyncEvents( + syncStatusObserver, ProcessLifecycleOwner.get(), false + ) + } interface BookmarkListener { fun onBookmarksUpdated() @@ -38,8 +95,37 @@ class BookmarksStore constructor(val context: Context) { listeners.clear() } - fun getBookmarks(): CompletableFuture?> = GlobalScope.future { - storage.getTree(BookmarkRoot.Mobile.id)?.children?.toMutableList() + fun getBookmarks(guid: String): CompletableFuture?> = GlobalScope.future { + when (guid) { + BookmarkRoot.Mobile.id -> { + // Construct a "virtual" desktop folder as the first bookmark item in the list. + val withDesktopFolder = mutableListOf( + BookmarkNode( + BookmarkNodeType.FOLDER, + DESKTOP_ROOT, + BookmarkRoot.Mobile.id, + title = titles[DESKTOP_ROOT], + children = emptyList(), + position = null, + url = null + ) + ) + // Append all of the bookmarks in the mobile root. + storage.getTree(BookmarkRoot.Mobile.id)?.children?.let { withDesktopFolder.addAll(it) } + withDesktopFolder + } + DESKTOP_ROOT -> { + val root = storage.getTree(BookmarkRoot.Root.id) + root?.children + ?.filter { it.guid != BookmarkRoot.Mobile.id } + ?.map { + it.copy(title = titles[it.guid]) + } + } + else -> { + storage.getTree(guid)?.children?.toList() + } + } } fun addBookmark(aURL: String, aTitle: String) = GlobalScope.future { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt new file mode 100644 index 0000000000..1848dd55ae --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt @@ -0,0 +1,117 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * 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.vrbrowser.browser + +import android.content.Context +import android.net.Uri +import android.os.Build +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.work.Configuration +import androidx.work.WorkManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mozilla.components.concept.sync.DeviceCapability +import mozilla.components.concept.sync.DeviceEvent +import mozilla.components.concept.sync.DeviceEventsObserver +import mozilla.components.concept.sync.DeviceType +import mozilla.components.service.fxa.DeviceConfig +import mozilla.components.service.fxa.ServerConfig +import mozilla.components.service.fxa.SyncConfig +import mozilla.components.service.fxa.manager.FxaAccountManager +import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider +import mozilla.components.support.base.log.Log +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.base.log.sink.AndroidLogSink +import org.mozilla.vrbrowser.browser.engine.SessionStore +import java.lang.IllegalStateException + +class Services(context: Context, places: Places) { + companion object { + // TODO this is from a sample app, get a real client id before shipping. + const val CLIENT_ID = "3c49430b43dfba77" + const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID" + } + + // This makes bookmarks storage accessible to background sync workers. + init { + // Make sure we get logs out of our android-components. + Log.addSink(AndroidLogSink()) + + GlobalSyncableStoreProvider.configureStore("bookmarks" to places.bookmarks) + GlobalSyncableStoreProvider.configureStore("history" to places.history) + + // TODO this really shouldn't be necessary, since WorkManager auto-initializes itself, unless + // auto-initialization is disabled in the manifest file. We don't disable the initialization, + // but i'm seeing crashes locally because WorkManager isn't initialized correctly... + // Maybe this is a race of sorts? We're trying to access it before it had a chance to auto-initialize? + // It's not well-documented _when_ that auto-initialization is supposed to happen. + + // For now, let's just manually initialize it here, and swallow failures (it's already initialized). + try { + WorkManager.initialize( + context, + Configuration.Builder().setMinimumLoggingLevel(android.util.Log.INFO).build() + ) + } catch (e: IllegalStateException) {} + } + + // Process received device events, only handling received tabs for now. + // They'll come from other FxA devices (e.g. Firefox Desktop). + private val deviceEventObserver = object : DeviceEventsObserver { + private val logTag = "DeviceEventsObserver" + + override fun onEvents(events: List) { + CoroutineScope(Dispatchers.Main).launch { + Logger(logTag).info("Received ${events.size} device event(s)") + events.filterIsInstance(DeviceEvent.TabReceived::class.java).forEach { + // Just load the first tab that was sent. + // TODO is there a notifications API of sorts here? + SessionStore.get().activeStore.loadUri(it.entries[0].url) + } + } + } + } + + val accountManager = FxaAccountManager( + context = context, + serverConfig = ServerConfig.release(CLIENT_ID, REDIRECT_URL), + deviceConfig = DeviceConfig( + // This is a default name, and can be changed once user is logged in. + // E.g. accountManager.authenticatedAccount()?.deviceConstellation()?.setDeviceNameAsync("new name") + name = "Firefox Reality on ${Build.MANUFACTURER} ${Build.MODEL}", + // TODO need a new device type! "VR" + type = DeviceType.MOBILE, + capabilities = setOf(DeviceCapability.SEND_TAB) + ), + // If background syncing is desired, pass in a 'syncPeriodInMinutes' parameter. + // As-is, sync will run on app startup. + syncConfig = SyncConfig(setOf("bookmarks", "history")) + ).also { + it.registerForDeviceEvents(deviceEventObserver, ProcessLifecycleOwner.get(), true) + } + + init { + CoroutineScope(Dispatchers.Main).launch { + accountManager.initAsync().await() + } + } + + /** + * Call this for every loaded URL to enable FxA sign-in to finish. It's a bit of a hack, but oh well. + */ + fun interceptFxaUrl(uri: String) { + if (!uri.startsWith(REDIRECT_URL)) return + val parsedUri = Uri.parse(uri) + + parsedUri.getQueryParameter("code")?.let { code -> + val state = parsedUri.getQueryParameter("state") as String + + // Notify the state machine about our success. + accountManager.finishAuthenticationAsync(code, state) + } + } +} \ No newline at end of file diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStack.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStack.java index f3bb338d40..6edb9f9476 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStack.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStack.java @@ -27,6 +27,7 @@ import org.mozilla.geckoview.MediaElement; import org.mozilla.geckoview.WebRequestError; import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.VRBrowserApplication; import org.mozilla.vrbrowser.browser.Media; import org.mozilla.vrbrowser.browser.SessionChangeListener; import org.mozilla.vrbrowser.browser.SettingsStore; @@ -941,6 +942,8 @@ public void onCanGoForward(@NonNull GeckoSession aSession, boolean aCanGoForward Log.d(LOGTAG, "onLoadRequest: " + uri); + ((VRBrowserApplication) mContext.getApplicationContext()).getServices().interceptFxaUrl(uri); + String uriOverride = SessionUtils.checkYoutubeOverride(uri); if (uriOverride != null) { aSession.loadUri(uriOverride); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/search/suggestions/SuggestionsProvider.java b/app/src/common/shared/org/mozilla/vrbrowser/search/suggestions/SuggestionsProvider.java index 30cfeb0d52..b63b117a86 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/search/suggestions/SuggestionsProvider.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/search/suggestions/SuggestionsProvider.java @@ -16,6 +16,8 @@ import java.util.List; import java.util.concurrent.CompletableFuture; +import mozilla.appservices.places.BookmarkRoot; + public class SuggestionsProvider { private static final String LOGTAG = SuggestionsProvider.class.getSimpleName(); @@ -79,7 +81,7 @@ public void setComparator(Comparator comparator) { public CompletableFuture> getBookmarkSuggestions(@NonNull List items) { CompletableFuture future = new CompletableFuture(); - SessionStore.get().getBookmarkStore().getBookmarks().thenAcceptAsync((bookmarks) -> { + SessionStore.get().getBookmarkStore().getBookmarks(BookmarkRoot.Root.getId()).thenAcceptAsync((bookmarks) -> { bookmarks.stream(). filter(b -> b.getUrl().toLowerCase().contains(mFilterText) || b.getTitle().toLowerCase().contains(mFilterText)) diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/BookmarksView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/BookmarksView.java index d568bd163a..88f9937818 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/BookmarksView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/BookmarksView.java @@ -30,6 +30,7 @@ import java.util.List; +import mozilla.appservices.places.BookmarkRoot; import mozilla.components.concept.storage.BookmarkNode; public class BookmarksView extends FrameLayout implements BookmarksStore.BookmarkListener { @@ -135,7 +136,7 @@ public void setBookmarksCallback(@NonNull BookmarksCallback callback) { } private void syncBookmarks() { - SessionStore.get().getBookmarkStore().getBookmarks().thenAcceptAsync(this::showBookmarks, new UIThreadExecutor()); + SessionStore.get().getBookmarkStore().getBookmarks(BookmarkRoot.Root.getId()).thenAcceptAsync(this::showBookmarks, new UIThreadExecutor()); } private void showBookmarks(List aBookmarks) { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/AccountsHelper.kt b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/AccountsHelper.kt new file mode 100644 index 0000000000..5c185e29ff --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/AccountsHelper.kt @@ -0,0 +1,49 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * 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.vrbrowser.ui.widgets.settings + +import android.content.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.future +import mozilla.components.concept.sync.AccountObserver +import mozilla.components.concept.sync.OAuthAccount +import mozilla.components.concept.sync.Profile +import org.mozilla.vrbrowser.VRBrowserApplication +import org.mozilla.vrbrowser.browser.Services +import java.util.concurrent.CompletableFuture + +class AccountsHelper(private val settingsWidget: SettingsWidget) { + val accountObserver = object : AccountObserver { + override fun onAuthenticated(account: OAuthAccount) { + settingsWidget.updateCurrentAccountState() + } + + override fun onAuthenticationProblems() { + settingsWidget.updateCurrentAccountState() + } + + override fun onLoggedOut() { + settingsWidget.updateCurrentAccountState() + } + + override fun onProfileUpdated(profile: Profile) { + settingsWidget.updateCurrentAccountState() + } + } + + fun authUrlAsync(): CompletableFuture? { + val context = settingsWidget.context ?: return null + + return CoroutineScope(Dispatchers.Main).future { + context.services().accountManager.beginAuthenticationAsync().await() + } + } + + private fun Context.services(): Services { + return (this.applicationContext as VRBrowserApplication).services + } +} \ No newline at end of file diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/SettingsWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/SettingsWidget.java index c5081b4f9e..d296ef77e9 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/SettingsWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/SettingsWidget.java @@ -8,6 +8,8 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.Looper; import android.graphics.Point; import android.util.AttributeSet; import android.util.Log; @@ -22,7 +24,9 @@ import org.mozilla.vrbrowser.BuildConfig; import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.VRBrowserApplication; import org.mozilla.vrbrowser.audio.AudioEngine; +import org.mozilla.vrbrowser.browser.Services; import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.browser.engine.SessionStack; import org.mozilla.vrbrowser.ui.views.HoneycombButton; @@ -39,6 +43,11 @@ import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.GregorianCalendar; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; + +import mozilla.components.service.fxa.manager.FxaAccountManager; public class SettingsWidget extends UIDialog implements WidgetManagerDelegate.WorldClickListener, SettingsView.Delegate { private AudioEngine mAudio; @@ -50,6 +59,8 @@ public class SettingsWidget extends UIDialog implements WidgetManagerDelegate.Wo private int mRestartDialogHandle = -1; private int mAlertDialogHandle = -1; + private AccountsHelper accountHelper = new AccountsHelper(this); + class VersionGestureListener extends GestureDetector.SimpleOnGestureListener { private boolean mIsHash; @@ -166,6 +177,16 @@ private void initialize(Context aContext) { onDismiss(); }); + HoneycombButton fxaButton = findViewById(R.id.fxaButton); + fxaButton.setOnClickListener(view -> + onTurnOnSyncClick() + ); + + updateCurrentAccountState(); + + // Monitor account state changes. + getServices().getAccountManager().register(accountHelper.getAccountObserver()); + HoneycombButton developerOptionsButton = findViewById(R.id.developerOptionsButton); developerOptionsButton.setOnClickListener(view -> { if (mAudio != null) { @@ -244,6 +265,58 @@ private void onSettingsReportClick() { onDismiss(); } + private void onTurnOnSyncClick() { + FxaAccountManager manager = getServices().getAccountManager(); + // If we're already logged-in, and not in a "need to reconnect" state, logout. + if (manager.authenticatedAccount() != null && !manager.accountNeedsReauth()) { + manager.logoutAsync(); + return; + } + + // Otherwise, obtain an authentication URL and load it in the gecko session. + // Recovering from "need to reconnect" state is treated the same as just logging in. + CompletableFuture futureUrl = accountHelper.authUrlAsync(); + if (futureUrl == null) { + Log.w(LOGTAG, "Got a 'null' futureUrl"); + return; + } + + Executors.newSingleThreadExecutor().submit(() -> { + try { + String url = futureUrl.get(); + if (url == null) { + Log.w(LOGTAG, "Got a 'null' url after resolving futureUrl"); + return; + } + Log.i(LOGTAG, "Got an auth url: " + url); + + // Actually process the url on the main thread. + new Handler(Looper.getMainLooper()).post(() -> { + Log.i(LOGTAG, "Loading url..."); + SessionStore.get().getActiveStore().loadUri(url); + hide(REMOVE_WIDGET); + }); + } catch (ExecutionException | InterruptedException e) { + Log.e(LOGTAG, "Error obtaining auth url", e); + } + }); + } + + // TODO we can also set profile display name, email and authentication problem states. + void updateCurrentAccountState() { + HoneycombButton fxaButton = findViewById(R.id.fxaButton); + FxaAccountManager manager = getServices().getAccountManager(); + if (manager.authenticatedAccount() != null) { + if (manager.accountNeedsReauth()) { + ((TextView) fxaButton.findViewById(R.id.settings_button_text)).setText(R.string.settings_accounts_reconnect); + } else { + ((TextView) fxaButton.findViewById(R.id.settings_button_text)).setText(R.string.settings_accounts_sign_out); + } + } else { + ((TextView) fxaButton.findViewById(R.id.settings_button_text)).setText(R.string.settings_accounts_sign_in); + } + } + private void onDeveloperOptionsClick() { showDeveloperOptionsDialog(); } @@ -433,4 +506,8 @@ private boolean isLanguagesSubView(View view) { return false; } + + private Services getServices() { + return ((VRBrowserApplication) getContext().getApplicationContext()).getServices(); + } } diff --git a/app/src/main/res/layout/bookmark_item.xml b/app/src/main/res/layout/bookmark_item.xml index 708497091d..530fb1ec5b 100644 --- a/app/src/main/res/layout/bookmark_item.xml +++ b/app/src/main/res/layout/bookmark_item.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools"> + - + android:orientation="horizontal"> - + + android:layout_marginBottom="10dp" + android:src="@drawable/ic_icon_back" + android:tint="@color/midnight" + android:visibility="gone" /> + - + + + + Help + + Sign in + + + Sign out + + + Reconnect + Restart Required @@ -634,6 +644,18 @@ Loading Bookmarks + + Desktop Bookmarks + + + Menu + + + Toolbar + + + Other + diff --git a/versions.gradle b/versions.gradle index 0cb2d393b3..2f57da8c6a 100644 --- a/versions.gradle +++ b/versions.gradle @@ -57,6 +57,7 @@ android_components.browser_errorpages = "org.mozilla.components:browser-errorpag android_components.browser_search = "org.mozilla.components:browser-search:$versions.android_components" android_components.browser_storage = "org.mozilla.components:browser-storage-sync:$versions.android_components" android_components.browser_domains = "org.mozilla.components:browser-domains:$versions.android_components" +android_components.service_accounts = "org.mozilla.components:service-firefox-accounts:$versions.android_components" android_components.ui_autocomplete = "org.mozilla.components:ui-autocomplete:$versions.android_components" android_components.concept_fetch = "org.mozilla.components:concept-fetch:$versions.android_components" android_components.lib_fetch = "org.mozilla.components:lib-fetch-httpurlconnection:$versions.android_components" @@ -82,6 +83,11 @@ support.core_utils = "androidx.legacy:legacy-support-core-utils:$versions.suppor support.vector_drawable = "androidx.vectordrawable:vectordrawable:$versions.support" deps.support = support +// TODO this should not be necessary at all, see Services.kt +def work = [:] +work.runtime = "androidx.work:work-runtime-ktx:$versions.work" +deps.work = work + def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room"