From e9e348a79757b72929fb8d02f973964c32c8bf05 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Fri, 25 Oct 2019 11:10:49 +0200 Subject: [PATCH] FxA (#1973) * 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 * Added support for Account settings and history/bookmarks updates * Added profile picture to the settings icon * Support for going back to sign in origin after login * Updated to AC v15 for the latest FxA API * Use SyncEnginesStorage to update SyncEngines * Rebase updates and improved library panels scroll performance * Folders support * Set production client Id * Remove unnecessary executePendingBindings * Refactoring * Always sync after signing in and remember sync status Some refactoring too * Style updates * Support responsive UI * Rebase updates * PR review updates #1 * PR review #2 * Rebase updates * Style updates from #2022 * Rebase fixes * Fixes tabs polling --- app/build.gradle | 6 + .../mozilla/vrbrowser/VRBrowserActivity.java | 18 +- .../vrbrowser/VRBrowserApplication.java | 14 + .../org/mozilla/vrbrowser/browser/Accounts.kt | 286 +++++++++++++++ .../vrbrowser/browser/BookmarksStore.kt | 101 +++++- .../mozilla/vrbrowser/browser/HistoryStore.kt | 37 +- .../org/mozilla/vrbrowser/browser/Services.kt | 125 +++++++ .../vrbrowser/browser/SettingsStore.java | 22 ++ .../browser/engine/GeckoViewFetchClient.kt | 124 +++++++ .../vrbrowser/browser/engine/Session.java | 1 - .../browser/engine/SessionStore.java | 16 +- .../browser/engine/SessionUtils.java | 4 +- .../GeolocationLocalizationProvider.java | 25 +- .../suggestions/SuggestionsProvider.java | 5 +- .../ui/adapters/BindingAdapters.java | 41 ++- .../vrbrowser/ui/adapters/Bookmark.java | 147 ++++++++ .../ui/adapters/BookmarkAdapter.java | 336 ++++++++++++------ .../vrbrowser/ui/adapters/HistoryAdapter.java | 2 - .../ui/callbacks/BookmarkItemCallback.java | 11 +- .../callbacks/BookmarkItemFolderCallback.java | 9 + .../ui/callbacks/BookmarksCallback.java | 9 +- .../ui/callbacks/HistoryCallback.java | 7 +- .../vrbrowser/ui/views/BookmarksView.java | 181 +++++++++- .../ui/views/CustomRecyclerView.java | 6 +- .../vrbrowser/ui/views/HistoryView.java | 158 +++++++- .../vrbrowser/ui/views/HoneycombButton.java | 16 +- .../mozilla/vrbrowser/ui/views/TabView.java | 1 - .../ui/views/settings/ButtonSetting.java | 6 + .../vrbrowser/ui/widgets/TabsWidget.java | 2 - .../vrbrowser/ui/widgets/TrayWidget.java | 8 +- .../ui/widgets/WidgetManagerDelegate.java | 1 + .../vrbrowser/ui/widgets/WindowWidget.java | 50 ++- .../mozilla/vrbrowser/ui/widgets/Windows.java | 49 ++- .../settings/FxAAccountOptionsView.java | 178 ++++++++++ .../ui/widgets/settings/SettingsWidget.java | 264 +++++++++----- .../mozilla/vrbrowser/utils/StringUtils.java | 47 +++ .../color/library_panel_button_text_color.xml | 2 +- .../library_panel_folder_title_text_color.xml | 6 + .../content_panel_button_background.xml | 6 + .../res/drawable/ic_icon_settings_sign_in.xml | 12 + .../main/res/drawable/ic_icon_tray_tabs.xml | 2 +- app/src/main/res/layout/bookmark_item.xml | 17 +- .../main/res/layout/bookmark_item_folder.xml | 47 +++ .../main/res/layout/bookmark_separator.xml | 30 ++ app/src/main/res/layout/bookmarks.xml | 79 ++-- app/src/main/res/layout/bookmarks_narrow.xml | 166 +++++++++ app/src/main/res/layout/bookmarks_wide.xml | 156 ++++++++ app/src/main/res/layout/history.xml | 102 +++--- app/src/main/res/layout/history_item.xml | 1 + app/src/main/res/layout/history_narrow.xml | 184 ++++++++++ app/src/main/res/layout/history_wide.xml | 181 ++++++++++ .../main/res/layout/options_fxa_account.xml | 102 ++++++ app/src/main/res/layout/settings.xml | 21 +- app/src/main/res/values/integer.xml | 2 +- app/src/main/res/values/non_L10n.xml | 2 + app/src/main/res/values/strings.xml | 101 ++++++ app/src/main/res/values/styles.xml | 1 + gradle.properties | 1 + versions.gradle | 10 +- 59 files changed, 3146 insertions(+), 398 deletions(-) create mode 100644 app/src/common/shared/org/mozilla/vrbrowser/browser/Accounts.kt create mode 100644 app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt create mode 100644 app/src/common/shared/org/mozilla/vrbrowser/browser/engine/GeckoViewFetchClient.kt create mode 100644 app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/Bookmark.java create mode 100644 app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarkItemFolderCallback.java create mode 100644 app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/FxAAccountOptionsView.java create mode 100644 app/src/main/res/color/library_panel_folder_title_text_color.xml create mode 100644 app/src/main/res/drawable/ic_icon_settings_sign_in.xml create mode 100644 app/src/main/res/layout/bookmark_item_folder.xml create mode 100644 app/src/main/res/layout/bookmark_separator.xml create mode 100644 app/src/main/res/layout/bookmarks_narrow.xml create mode 100644 app/src/main/res/layout/bookmarks_wide.xml create mode 100644 app/src/main/res/layout/history_narrow.xml create mode 100644 app/src/main/res/layout/history_wide.xml create mode 100644 app/src/main/res/layout/options_fxa_account.xml diff --git a/app/build.gradle b/app/build.gradle index 84fdbb147..fe77f0104 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -443,9 +443,15 @@ 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 + implementation deps.android_components.support_rustlog + implementation deps.android_components.support_rusthttp + + // TODO this should not be necessary at all, see Services.kt + implementation deps.work.runtime // Kotlin dependency implementation deps.kotlin.stdlib diff --git a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java index 22f2bf722..dbdb230ea 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java @@ -49,8 +49,6 @@ import org.mozilla.vrbrowser.crashreporting.CrashReporterService; import org.mozilla.vrbrowser.crashreporting.GlobalExceptionHandler; import org.mozilla.vrbrowser.geolocation.GeolocationWrapper; -import org.mozilla.vrbrowser.ui.widgets.prompts.ConfirmPromptWidget; -import org.mozilla.vrbrowser.utils.DeviceType; import org.mozilla.vrbrowser.input.MotionEventGenerator; import org.mozilla.vrbrowser.search.SearchEngineWrapper; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; @@ -67,7 +65,9 @@ import org.mozilla.vrbrowser.ui.widgets.WindowWidget; import org.mozilla.vrbrowser.ui.widgets.Windows; import org.mozilla.vrbrowser.ui.widgets.dialogs.CrashDialogWidget; +import org.mozilla.vrbrowser.ui.widgets.prompts.ConfirmPromptWidget; import org.mozilla.vrbrowser.utils.ConnectivityReceiver; +import org.mozilla.vrbrowser.utils.DeviceType; import org.mozilla.vrbrowser.utils.LocaleUtils; import org.mozilla.vrbrowser.utils.ServoUtils; import org.mozilla.vrbrowser.utils.SystemUtils; @@ -212,6 +212,7 @@ protected void onCreate(Bundle savedInstanceState) { Bundle extras = getIntent() != null ? getIntent().getExtras() : null; SessionStore.get().setContext(this, extras); + SessionStore.get().initializeServices(); SessionStore.get().initializeStores(this); // Create broadcast receiver for getting crash messages from crash process @@ -387,7 +388,13 @@ protected void onResume() { widget.onResume(); } handleConnectivityChange(); - mConnectivityReceiver.register(this, () -> runOnUiThread(() -> handleConnectivityChange())); + mConnectivityReceiver.register(this, () -> runOnUiThread(this::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. + ((VRBrowserApplication)getApplicationContext()).getAccounts().refreshDevicesAsync(); + ((VRBrowserApplication)getApplicationContext()).getAccounts().pollForEventsAsync(); + super.onResume(); } @@ -1401,6 +1408,11 @@ public WindowWidget getFocusedWindow() { return mWindows.getFocusedWindow(); } + @Override + public TrayWidget getTray() { + return mTray; + } + private native void addWidgetNative(int aHandle, WidgetPlacement aPlacement); private native void updateWidgetNative(int aHandle, WidgetPlacement aPlacement); private native void updateVisibleWidgetsNative(); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java index 4ac0292a8..58dbc71b1 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java @@ -9,7 +9,9 @@ import android.content.Context; import android.content.res.Configuration; +import org.mozilla.vrbrowser.browser.Accounts; import org.mozilla.vrbrowser.browser.Places; +import org.mozilla.vrbrowser.browser.Services; import org.mozilla.vrbrowser.db.AppDatabase; import org.mozilla.vrbrowser.db.DataRepository; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; @@ -20,7 +22,9 @@ public class VRBrowserApplication extends Application { private AppExecutors mAppExecutors; private BitmapCache mBitmapCache; + private Services mServices; private Places mPlaces; + private Accounts mAccounts; @Override public void onCreate() { @@ -29,6 +33,8 @@ public void onCreate() { mAppExecutors = new AppExecutors(); mPlaces = new Places(this); mBitmapCache = new BitmapCache(this, mAppExecutors.diskIO(), mAppExecutors.mainThread()); + mServices = new Services(this, mPlaces); + mAccounts = new Accounts(this); TelemetryWrapper.init(this); } @@ -45,6 +51,10 @@ public void onConfigurationChanged(Configuration newConfig) { LocaleUtils.setLocale(this); } + public Services getServices() { + return mServices; + } + public Places getPlaces() { return mPlaces; } @@ -64,4 +74,8 @@ public DataRepository getRepository() { public BitmapCache getBitmapCache() { return mBitmapCache; } + + public Accounts getAccounts() { + return mAccounts; + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/Accounts.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/Accounts.kt new file mode 100644 index 000000000..9a8af24e0 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/Accounts.kt @@ -0,0 +1,286 @@ +/* -*- 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.os.Handler +import android.os.Looper +import androidx.lifecycle.ProcessLifecycleOwner +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.future +import mozilla.components.concept.sync.AccountObserver +import mozilla.components.concept.sync.AuthType +import mozilla.components.concept.sync.OAuthAccount +import mozilla.components.concept.sync.Profile +import mozilla.components.service.fxa.SyncEngine +import mozilla.components.service.fxa.manager.SyncEnginesStorage +import mozilla.components.service.fxa.sync.SyncReason +import mozilla.components.service.fxa.sync.SyncStatusObserver +import mozilla.components.service.fxa.sync.getLastSynced +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.vrbrowser.VRBrowserApplication +import org.mozilla.vrbrowser.utils.SystemUtils +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException +import java.util.concurrent.Executors + +class Accounts constructor(val context: Context) { + + private val LOGTAG = SystemUtils.createLogtag(Accounts::class.java) + + enum class AccountStatus { + SIGNED_IN, + SIGNED_OUT, + NEEDS_RECONNECT + } + + enum class LoginOrigin { + BOOKMARKS, + HISTORY, + SETTINGS, + UNDEFINED + } + + var loginOrigin: LoginOrigin = LoginOrigin.UNDEFINED + var accountStatus = AccountStatus.SIGNED_OUT + private val accountListeners = ArrayList() + private val syncListeners = ArrayList() + private val services = (context.applicationContext as VRBrowserApplication).services + private val syncStorage = SyncEnginesStorage(context) + var isSyncing = false + + private val syncStatusObserver = object : SyncStatusObserver { + override fun onStarted() { + isSyncing = true + syncListeners.toMutableList().forEach { + Handler(Looper.getMainLooper()).post { + it.onStarted() + } + } + } + + override fun onIdle() { + isSyncing = false + syncListeners.toMutableList().forEach { + Handler(Looper.getMainLooper()).post { + it.onIdle() + } + } + } + + override fun onError(error: Exception?) { + isSyncing = false + syncListeners.toMutableList().forEach { + Handler(Looper.getMainLooper()).post { + it.onError(error) + } + } + } + } + + private val accountObserver = object : AccountObserver { + override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { + accountStatus = AccountStatus.SIGNED_IN + + // Enable syncing after signing in + syncStorage.setStatus(SyncEngine.Bookmarks, SettingsStore.getInstance(context).isBookmarksSyncEnabled) + syncStorage.setStatus(SyncEngine.History, SettingsStore.getInstance(context).isHistorySyncEnabled) + services.accountManager.syncNowAsync(SyncReason.EngineChange, false) + + account.deviceConstellation().refreshDevicesAsync() + accountListeners.toMutableList().forEach { + Handler(Looper.getMainLooper()).post { + it.onAuthenticated(account, authType) + } + } + } + + override fun onAuthenticationProblems() { + accountStatus = AccountStatus.NEEDS_RECONNECT + accountListeners.toMutableList().forEach { + Handler(Looper.getMainLooper()).post { + it.onAuthenticationProblems() + } + } + } + + override fun onLoggedOut() { + accountStatus = AccountStatus.SIGNED_OUT + accountListeners.toMutableList().forEach { + Handler(Looper.getMainLooper()).post { + it.onLoggedOut() + } + } + } + + override fun onProfileUpdated(profile: Profile) { + accountListeners.toMutableList().forEach { + Handler(Looper.getMainLooper()).post { + it.onProfileUpdated(profile) + } + } + } + } + + init { + services.accountManager.registerForSyncEvents( + syncStatusObserver, ProcessLifecycleOwner.get(), false + ) + services.accountManager.register(accountObserver) + accountStatus = if (services.accountManager.authenticatedAccount() != null) { + if (services.accountManager.accountNeedsReauth()) { + AccountStatus.NEEDS_RECONNECT + + } else { + AccountStatus.SIGNED_IN + } + + } else { + AccountStatus.SIGNED_OUT + } + } + + + fun addAccountListener(aListener: AccountObserver) { + if (!accountListeners.contains(aListener)) { + accountListeners.add(aListener) + } + } + + fun removeAccountListener(aListener: AccountObserver) { + accountListeners.remove(aListener) + } + + fun removeAllAccountListeners() { + accountListeners.clear() + } + + fun addSyncListener(aListener: SyncStatusObserver) { + if (!syncListeners.contains(aListener)) { + syncListeners.add(aListener) + } + } + + fun removeSyncListener(aListener: SyncStatusObserver) { + syncListeners.remove(aListener) + } + + fun removeAllSyncListeners() { + syncListeners.clear() + } + + fun authUrlAsync(): CompletableFuture? { + return CoroutineScope(Dispatchers.Main).future { + services.accountManager.beginAuthenticationAsync().await() + } + } + + fun refreshDevicesAsync(): CompletableFuture? { + return CoroutineScope(Dispatchers.Main).future { + services.accountManager.authenticatedAccount()?.deviceConstellation()?.refreshDevicesAsync()?.await() + } + } + + fun pollForEventsAsync(): CompletableFuture? { + return CoroutineScope(Dispatchers.Main).future { + services.accountManager.authenticatedAccount()?.deviceConstellation()?.pollForEventsAsync()?.await() + } + } + + fun updateProfileAsync(): CompletableFuture? { + return CoroutineScope(Dispatchers.Main).future { + services.accountManager.updateProfileAsync().await() + } + } + + fun syncNowAsync(reason: SyncReason = SyncReason.User, + debounce: Boolean = false): CompletableFuture?{ + return CoroutineScope(Dispatchers.Main).future { + services.accountManager.syncNowAsync(reason, debounce).await() + } + } + + fun setSyncStatus(engine: SyncEngine, value: Boolean) { + + when(engine) { + SyncEngine.Bookmarks -> SettingsStore.getInstance(context).isBookmarksSyncEnabled = value + SyncEngine.History -> SettingsStore.getInstance(context).isHistorySyncEnabled = value + } + + syncStorage.setStatus(engine, value) + } + + fun accountProfile(): Profile? { + return services.accountManager.accountProfile() + } + + fun logoutAsync(): CompletableFuture? { + return CoroutineScope(Dispatchers.Main).future { + services.accountManager.logoutAsync().await() + } + } + + fun getAuthenticationUrlAsync(): CompletableFuture { + val future: CompletableFuture = CompletableFuture() + + // If we're already logged-in, and not in a "need to reconnect" state, logout. + if (services.accountManager.authenticatedAccount() != null && !services.accountManager.accountNeedsReauth()) { + services.accountManager.logoutAsync() + future.complete(null) + } + + // 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. + val futureUrl = authUrlAsync() + if (futureUrl == null) { + Logger(LOGTAG).debug("Got a 'null' futureUrl") + services.accountManager.logoutAsync() + future.complete(null) + } + + Executors.newSingleThreadExecutor().submit { + try { + val url = futureUrl!!.get() + if (url == null) { + Logger(LOGTAG).debug("Got a 'null' url after resolving futureUrl") + services.accountManager.logoutAsync() + future.complete(null) + } + Logger(LOGTAG).debug("Got an auth url: " + url!!) + + // Actually process the url on the main thread. + Handler(Looper.getMainLooper()).post { + Logger(LOGTAG).debug("We got an authentication url, we can continue...") + future.complete(url) + } + + } catch (e: ExecutionException) { + Logger(LOGTAG).debug("Error obtaining auth url", e) + future.complete(null) + + } catch (e: InterruptedException) { + Logger(LOGTAG).debug("Error obtaining auth url", e) + future.complete(null) + } + } + + return future + } + + fun isEngineEnabled(engine: SyncEngine): Boolean { + return syncStorage.getStatus()[engine]?: false + } + + fun isSignedIn(): Boolean { + return (accountStatus == AccountStatus.SIGNED_IN) + } + + fun lastSync(): Long { + return getLastSynced(context) + } + +} \ No newline at end of file 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 2eb74ac73..172956137 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,78 @@ package org.mozilla.vrbrowser.browser import android.content.Context import android.os.Handler import android.os.Looper +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 mozilla.components.concept.storage.BookmarkNodeType +import mozilla.components.service.fxa.sync.SyncStatusObserver +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.vrbrowser.R import org.mozilla.vrbrowser.VRBrowserApplication +import org.mozilla.vrbrowser.utils.SystemUtils import java.util.concurrent.CompletableFuture +const val DESKTOP_ROOT = "fake_desktop_root" + class BookmarksStore constructor(val context: Context) { + + private val LOGTAG = SystemUtils.createLogtag(BookmarksStore::class.java) + + 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_mobile_folder_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) + private val accountManager = (context.applicationContext as VRBrowserApplication).services.accountManager + + // Bookmarks might have changed during sync, so notify our listeners. + private val syncStatusObserver = object : SyncStatusObserver { + override fun onStarted() {} + + override fun onIdle() { + Logger(LOGTAG).debug("Detected that sync is finished, notifying listeners") + notifyListeners() + } + + override fun onError(error: Exception?) {} + } + + init { + accountManager.registerForSyncEvents( + syncStatusObserver, ProcessLifecycleOwner.get(), false + ) + } interface BookmarkListener { fun onBookmarksUpdated() @@ -38,8 +100,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 { @@ -64,6 +155,12 @@ class BookmarksStore constructor(val context: Context) { getBookmarkByUrl(aURL) != null } + fun getTree(guid: String, recursive: Boolean): CompletableFuture?> = GlobalScope.future { + storage.getTree(guid, recursive)?.children + ?.map { + it.copy(title = titles[it.guid]) + } + } private suspend fun getBookmarkByUrl(aURL: String): BookmarkNode? { val bookmarks: List? = storage.getBookmarksWithUrl(aURL) diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/HistoryStore.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/HistoryStore.kt index b34f7ebe3..4c05121a8 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/HistoryStore.kt +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/HistoryStore.kt @@ -8,18 +8,44 @@ package org.mozilla.vrbrowser.browser import android.content.Context import android.os.Handler import android.os.Looper +import androidx.lifecycle.ProcessLifecycleOwner import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.future.future import mozilla.components.concept.storage.PageObservation +import mozilla.components.concept.storage.PageVisit import mozilla.components.concept.storage.VisitInfo import mozilla.components.concept.storage.VisitType +import mozilla.components.service.fxa.sync.SyncStatusObserver +import mozilla.components.support.base.log.logger.Logger import org.mozilla.vrbrowser.VRBrowserApplication +import org.mozilla.vrbrowser.utils.SystemUtils import java.util.concurrent.CompletableFuture class HistoryStore constructor(val context: Context) { + + private val LOGTAG = SystemUtils.createLogtag(HistoryStore::class.java) + private var listeners = ArrayList() private val storage = (context.applicationContext as VRBrowserApplication).places.history + // Bookmarks might have changed during sync, so notify our listeners. + private val syncStatusObserver = object : SyncStatusObserver { + override fun onStarted() {} + + override fun onIdle() { + Logger(LOGTAG).debug("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 HistoryListener { fun onHistoryUpdated() } @@ -49,8 +75,15 @@ class HistoryStore constructor(val context: Context) { VisitType.REDIRECT_PERMANENT)) } - fun recordVisit(aURL: String, visitType: VisitType) = GlobalScope.future { - storage.recordVisit(aURL, visitType) + fun getVisitsPaginated(offset: Long, count: Long): CompletableFuture?> = GlobalScope.future { + storage.getVisitsPaginated(offset, count, excludeTypes = listOf( + VisitType.NOT_A_VISIT, + VisitType.REDIRECT_TEMPORARY, + VisitType.REDIRECT_PERMANENT)) + } + + fun recordVisit(aURL: String, pageVisit: PageVisit) = GlobalScope.future { + storage.recordVisit(aURL, pageVisit) notifyListeners() } 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 000000000..11deb7e78 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt @@ -0,0 +1,125 @@ +/* -*- 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.lib.fetch.httpurlconnection.HttpURLConnectionClient +import mozilla.components.service.fxa.* +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 mozilla.components.support.rusthttp.RustHttpConfig +import mozilla.components.support.rustlog.RustLog +import org.mozilla.geckoview.AllowOrDeny +import org.mozilla.geckoview.GeckoResult +import org.mozilla.geckoview.GeckoSession +import org.mozilla.vrbrowser.browser.engine.SessionStore +import org.mozilla.vrbrowser.R + +class Services(context: Context, places: Places): GeckoSession.NavigationDelegate { + companion object { + const val CLIENT_ID = "7ad9917f6c55fb77" + const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID" + } + + // This makes bookmarks storage accessible to background sync workers. + init { + RustLog.enable() + RustHttpConfig.setClient(lazy { HttpURLConnectionClient() }) + + // Make sure we get logs out of our android-components. + Log.addSink(AndroidLogSink()) + + GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to places.bookmarks) + GlobalSyncableStoreProvider.configureStore(SyncEngine.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 Update when there is a push notifications API available + SessionStore.get().activeSession.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 = "${context.getString(R.string.app_name)} on ${Build.MANUFACTURER} ${Build.MODEL}", + // TODO need a new device type! "VR" + type = DeviceType.VR, + capabilities = setOf(DeviceCapability.SEND_TAB) + ), + syncConfig = SyncConfig(setOf(SyncEngine.History, SyncEngine.Bookmarks), syncPeriodInMinutes = 1440L) + + ).also { + it.registerForDeviceEvents(deviceEventObserver, ProcessLifecycleOwner.get(), true) + } + + init { + CoroutineScope(Dispatchers.Main).launch { + accountManager.initAsync().await() + } + } + + override fun onLoadRequest(geckoSession: GeckoSession, loadRequest: GeckoSession.NavigationDelegate.LoadRequest): GeckoResult? { + if (loadRequest.uri.startsWith(REDIRECT_URL)) { + val parsedUri = Uri.parse(loadRequest.uri) + + parsedUri.getQueryParameter("code")?.let { code -> + val state = parsedUri.getQueryParameter("state") as String + val action = parsedUri.getQueryParameter("action") as String + + // Notify the state machine about our success. + accountManager.finishAuthenticationAsync(FxaAuthData(action.toAuthType(), code = code, state = state)) + + return GeckoResult.ALLOW + } + } + + return GeckoResult.DENY + } +} \ No newline at end of file diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java index 105389643..322722dce 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java @@ -75,6 +75,8 @@ SettingsStore getInstance(final @NonNull Context aContext) { public final static boolean DEBUG_LOGGING_DEFAULT = false; public final static boolean POP_UPS_BLOCKING_DEFAULT = true; public final static boolean TELEMETRY_STATUS_UPDATE_SENT_DEFAULT = false; + public final static boolean BOOKMARKS_SYNC_DEFAULT = true; + public final static boolean HISTORY_SYNC_DEFAULT = true; // Enable telemetry by default (opt-out). public final static boolean CRASH_REPORTING_DEFAULT = false; @@ -597,5 +599,25 @@ public void setPopUpsBlockingEnabled(boolean isEnabled) { editor.putBoolean(mContext.getString(R.string.settings_key_pop_up_blocking), isEnabled); editor.commit(); } + public void setBookmarksSyncEnabled(boolean isEnabled) { + SharedPreferences.Editor editor = mPrefs.edit(); + editor.putBoolean(mContext.getString(R.string.settings_key_bookmarks_sync), isEnabled); + editor.commit(); + } + + public boolean isBookmarksSyncEnabled() { + return mPrefs.getBoolean(mContext.getString(R.string.settings_key_bookmarks_sync), BOOKMARKS_SYNC_DEFAULT); + } + + public void setHistorySyncEnabled(boolean isEnabled) { + SharedPreferences.Editor editor = mPrefs.edit(); + editor.putBoolean(mContext.getString(R.string.settings_key_history_sync), isEnabled); + editor.commit(); + } + + public boolean isHistorySyncEnabled() { + return mPrefs.getBoolean(mContext.getString(R.string.settings_key_history_sync), HISTORY_SYNC_DEFAULT); + } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/GeckoViewFetchClient.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/GeckoViewFetchClient.kt new file mode 100644 index 000000000..b5d70edb6 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/GeckoViewFetchClient.kt @@ -0,0 +1,124 @@ +/* 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.engine + +import android.content.Context +import androidx.annotation.VisibleForTesting +import mozilla.components.concept.fetch.Client +import mozilla.components.concept.fetch.Headers +import mozilla.components.concept.fetch.MutableHeaders +import mozilla.components.concept.fetch.Request +import mozilla.components.concept.fetch.Response + +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoWebExecutor +import org.mozilla.geckoview.WebRequest +import org.mozilla.geckoview.WebRequest.CACHE_MODE_DEFAULT +import org.mozilla.geckoview.WebRequest.CACHE_MODE_RELOAD +import org.mozilla.geckoview.WebRequestError +import org.mozilla.geckoview.WebResponse +import java.io.IOException +import java.net.SocketTimeoutException +import java.nio.ByteBuffer +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +/** + * GeckoView ([GeckoWebExecutor]) based implementation of [Client]. + */ +class GeckoViewFetchClient( + context: Context, + runtime: GeckoRuntime = GeckoRuntime.getDefault(context), + private val maxReadTimeOut: Pair = Pair(MAX_READ_TIMEOUT_MINUTES, TimeUnit.MINUTES) +) : Client() { + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal var executor: GeckoWebExecutor = GeckoWebExecutor(runtime) + + @Throws(IOException::class) + override fun fetch(request: Request): Response { + val webRequest = request.toWebRequest(defaultHeaders) + + val readTimeOut = request.readTimeout ?: maxReadTimeOut + val readTimeOutMillis = readTimeOut.let { (timeout, unit) -> + unit.toMillis(timeout) + } + + return try { + var fetchFlags = 0 + if (request.cookiePolicy == Request.CookiePolicy.OMIT) { + fetchFlags += GeckoWebExecutor.FETCH_FLAGS_ANONYMOUS + } + if (request.redirect == Request.Redirect.MANUAL) { + fetchFlags += GeckoWebExecutor.FETCH_FLAGS_NO_REDIRECTS + } + val webResponse = executor.fetch(webRequest, fetchFlags).poll(readTimeOutMillis) + webResponse?.toResponse() ?: throw IOException("Fetch failed with null response") + } catch (e: TimeoutException) { + throw SocketTimeoutException() + } catch (e: WebRequestError) { + throw IOException(e) + } + } + + companion object { + const val MAX_READ_TIMEOUT_MINUTES = 5L + } +} + +private fun Request.toWebRequest(defaultHeaders: Headers): WebRequest = WebRequest.Builder(url) + .method(method.name) + .addHeadersFrom(this, defaultHeaders) + .addBodyFrom(this) + .cacheMode(if (useCaches) CACHE_MODE_DEFAULT else CACHE_MODE_RELOAD) + .build() + +private fun WebRequest.Builder.addHeadersFrom(request: Request, defaultHeaders: Headers): WebRequest.Builder { + defaultHeaders.filter { header -> + request.headers?.contains(header.name) != true + }.forEach { header -> + addHeader(header.name, header.value) + } + + request.headers?.forEach { header -> + addHeader(header.name, header.value) + } + + return this +} + +private fun WebRequest.Builder.addBodyFrom(request: Request): WebRequest.Builder { + request.body?.let { body -> + body.useStream { inStream -> + val bytes = inStream.readBytes() + val buffer = ByteBuffer.allocateDirect(bytes.size) + buffer.put(bytes) + this.body(buffer) + } + } + + return this +} + +private fun WebResponse.toResponse(): Response { + val headers = translateHeaders(this) + return Response( + uri, + statusCode, + headers, + body?.let { + Response.Body(it, headers["Content-Type"]) + } ?: Response.Body.empty() + ) +} + +private fun translateHeaders(webResponse: WebResponse): Headers { + val headers = MutableHeaders() + webResponse.headers.forEach { (k, v) -> + v.split(",").forEach { headers.append(k, it.trim()) } + } + + return headers +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java index 8645945ce..de5fdc78f 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java @@ -46,7 +46,6 @@ import java.util.LinkedList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; import static org.mozilla.vrbrowser.utils.ServoUtils.createServoSession; import static org.mozilla.vrbrowser.utils.ServoUtils.isInstanceOfServoSession; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStore.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStore.java index f73c9ddf5..6f9d102e0 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStore.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStore.java @@ -13,9 +13,11 @@ import org.mozilla.geckoview.GeckoSession; import org.mozilla.geckoview.WebExtension; import org.mozilla.vrbrowser.BuildConfig; +import org.mozilla.vrbrowser.VRBrowserApplication; import org.mozilla.vrbrowser.browser.BookmarksStore; import org.mozilla.vrbrowser.browser.HistoryStore; import org.mozilla.vrbrowser.browser.PermissionDelegate; +import org.mozilla.vrbrowser.browser.Services; import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.crashreporting.CrashReporterService; @@ -24,8 +26,6 @@ public class SessionStore implements GeckoSession.PermissionDelegate { - public final int NO_ACTIVE_STORE_ID = -1; - private static final String[] WEB_EXTENSIONS = new String[] { "webcompat_vimeo", "webcompat_youtube" @@ -47,6 +47,7 @@ public static SessionStore get() { private PermissionDelegate mPermissionDelegate; private BookmarksStore mBookmarksStore; private HistoryStore mHistoryStore; + private Services mServices; private SessionStore() { mSessions = new ArrayList<>(); @@ -94,6 +95,14 @@ public void setContext(Context context, Bundle aExtras) { } } + public GeckoRuntime getRuntime() { + return mRuntime; + } + + public void initializeServices() { + mServices = ((VRBrowserApplication)mContext.getApplicationContext()).getServices(); + } + public void initializeStores(Context context) { mBookmarksStore = new BookmarksStore(context); mHistoryStore = new HistoryStore(context); @@ -106,6 +115,7 @@ public Session createSession(boolean aPrivateMode) { public Session createSession(boolean aPrivateMode, @Nullable SessionSettings aSettings, boolean aOpen) { Session session = new Session(mContext, mRuntime, aPrivateMode, aSettings, aOpen); session.setPermissionDelegate(this); + session.addNavigationListener(mServices); mSessions.add(session); return session; @@ -114,6 +124,7 @@ public Session createSession(boolean aPrivateMode, @Nullable SessionSettings aSe public Session createSession(SessionState aRestoreState) { Session session = new Session(mContext, mRuntime, aRestoreState); session.setPermissionDelegate(this); + session.addNavigationListener(mServices); mSessions.add(session); return session; @@ -123,6 +134,7 @@ public void destroySession(Session aSession) { mSessions.remove(aSession); if (aSession != null) { aSession.setPermissionDelegate(null); + aSession.removeNavigationListener(mServices); aSession.shutdown(); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionUtils.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionUtils.java index 3a6b7e544..e9f3f5872 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionUtils.java @@ -8,8 +8,8 @@ import androidx.annotation.Nullable; import org.mozilla.gecko.GeckoProfile; -import org.mozilla.vrbrowser.BuildConfig; import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.utils.SystemUtils; import java.io.File; import java.io.FileNotFoundException; @@ -18,7 +18,7 @@ class SessionUtils { - private static final String LOGTAG = SessionUtils.class.getCanonicalName(); + private static final String LOGTAG = SystemUtils.createLogtag(SessionUtils.class); public static boolean isLocalizedContent(@Nullable String url) { return url != null && (url.startsWith("about:") || url.startsWith("data:")); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationLocalizationProvider.java b/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationLocalizationProvider.java index 5041b58af..500020da3 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationLocalizationProvider.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationLocalizationProvider.java @@ -1,41 +1,32 @@ package org.mozilla.vrbrowser.search; +import androidx.annotation.NonNull; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.mozilla.vrbrowser.geolocation.GeolocationData; import java.util.Locale; +import kotlin.coroutines.Continuation; +import mozilla.components.browser.search.provider.localization.SearchLocalization; import mozilla.components.browser.search.provider.localization.SearchLocalizationProvider; -public class GeolocationLocalizationProvider extends SearchLocalizationProvider { +public class GeolocationLocalizationProvider implements SearchLocalizationProvider { private String mCountry; private String mLanguage; private String mRegion; - GeolocationLocalizationProvider(GeolocationData data) { + GeolocationLocalizationProvider(@NonNull GeolocationData data) { mCountry = data.getCountryCode(); mLanguage = Locale.getDefault().getLanguage(); mRegion = data.getCountryCode(); } - @NotNull - @Override - public String getCountry() { - return mCountry; - } - - @NotNull - @Override - public String getLanguage() { - return mLanguage; - } - @Nullable @Override - public String getRegion() { - return mRegion; + public SearchLocalization determineRegion(@NotNull Continuation continuation) { + return new SearchLocalization(mLanguage, mCountry, mRegion); } - } 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 30cfeb0d5..05e7fbb26 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,8 @@ public void setComparator(Comparator comparator) { public CompletableFuture> getBookmarkSuggestions(@NonNull List items) { CompletableFuture future = new CompletableFuture(); - SessionStore.get().getBookmarkStore().getBookmarks().thenAcceptAsync((bookmarks) -> { + // Explicitly passing Root will look in all the bookmarks, default is just to look in the mobile 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/adapters/BindingAdapters.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BindingAdapters.java index 8e0c23b36..6ef877cce 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BindingAdapters.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BindingAdapters.java @@ -14,6 +14,9 @@ import androidx.annotation.NonNull; import androidx.databinding.BindingAdapter; +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.ui.views.HoneycombButton; + import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; @@ -32,7 +35,7 @@ public static void showInvisible(@NonNull View view, boolean show) { } @BindingAdapter("typeface") - public static void setTypeface(@NonNull TextView v, String style) { + public static void setTypeface(@NonNull TextView v, @NonNull String style) { switch (style) { case "bold": v.setTypeface(null, Typeface.BOLD); @@ -60,7 +63,7 @@ public static void bindDate(@NonNull TextView textView, long timestamp) { } @BindingAdapter(value={"textDrawable", "textString"}) - public static void setSpannableString(@NonNull TextView textView, Drawable drawable, String text) { + public static void setSpannableString(@NonNull TextView textView, @NonNull Drawable drawable, String text) { SpannableString spannableString = new SpannableString(text); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); ImageSpan span = new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM); @@ -68,10 +71,44 @@ public static void setSpannableString(@NonNull TextView textView, Drawable drawa textView.setText(spannableString); } + @BindingAdapter("honeycombButtonText") + public static void setHoneycombButtonText(@NonNull HoneycombButton button, int resource){ + button.setText(resource); + } + @BindingAdapter("layout_height") public static void setLayoutHeight(@NonNull View view, @NonNull @Dimension float dimen) { ViewGroup.LayoutParams params = view.getLayoutParams(); params.height = (int)dimen; view.setLayoutParams(params); } + + @BindingAdapter("leftMargin") + public static void setLeftMargin(@NonNull View view, @NonNull @Dimension float margin) { + if (view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); + if (params.getMarginStart() != Math.round(margin)) { + params.setMarginStart(Math.round(margin)); + view.setLayoutParams(params); + } + } + } + + @BindingAdapter("lastSync") + public static void setFxALastSync(@NonNull TextView view, long lastSync) { + if (lastSync == 0) { + view.setText(view.getContext().getString(R.string.fxa_account_last_no_synced)); + + } else { + long timeDiff = System.currentTimeMillis() - lastSync; + if (timeDiff < 60000) { + view.setText(view.getContext().getString(R.string.fxa_account_last_synced_now)); + + } else { + view.setText(view.getContext().getString(R.string.fxa_account_last_synced, timeDiff / 60000)); + } + } + } + + } \ No newline at end of file diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/Bookmark.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/Bookmark.java new file mode 100644 index 000000000..4c411499d --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/Bookmark.java @@ -0,0 +1,147 @@ +package org.mozilla.vrbrowser.ui.adapters; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +import mozilla.components.concept.storage.BookmarkNode; +import mozilla.components.concept.storage.BookmarkNodeType; + +public class Bookmark { + + public enum Type { + ITEM, + FOLDER, + SEPARATOR + } + + private boolean mIsExpanded; + private int mLevel; + private String mTitle; + private String mURL; + private String mGuid; + private int mPosition; + private Type mType; + private boolean mHasChildren; + + public Bookmark(@NonNull BookmarkNode node, int level, boolean isExpanded) { + mIsExpanded = isExpanded; + mLevel = level; + + mTitle = node.getTitle(); + mURL = node.getUrl(); + mGuid = node.getGuid(); + mPosition = node.getPosition() != null ? node.getPosition() : 0; + mHasChildren = node.getChildren() != null; + + switch (node.getType()) { + case SEPARATOR: + mType = Type.SEPARATOR; + break; + case FOLDER: + mType = Type.FOLDER; + break; + case ITEM: + mType = Type.ITEM; + break; + } + } + + public boolean isExpanded() { + return mIsExpanded; + } + + public Type getType() { + return mType; + } + + public String getTitle() { + return mTitle; + } + + public String getUrl() { + return mURL; + } + + public String getGuid() { + return mGuid; + } + + public int getLevel() { + return mLevel; + } + + public void setLevel(int level) { + mLevel = level; + } + + public int getPosition() { + return mPosition; + } + + public boolean hasChildren() { + return mHasChildren; + } + + static List getRootDisplayListTree(@NonNull List bookmarkNodes) { + return getDisplayListTree(bookmarkNodes, 0, null); + } + + static List getDisplayListTree(@NonNull List bookmarkNodes, List openFolderGuid) { + return getDisplayListTree(bookmarkNodes, 0, openFolderGuid); + } + + /** + * Returns a display tree for the current open folders + * @param bookmarkNodes The bookmark nodes tree + * @param level The hierarchy level to process + * @param openFolderGuid The list of currently opened folders + * @return A display list with all the visible bookmarks + */ + private static List getDisplayListTree(@NonNull List bookmarkNodes, int level, List openFolderGuid) { + ArrayList children = new ArrayList<>(); + for (BookmarkNode node : bookmarkNodes) { + if (node.getType() == BookmarkNodeType.FOLDER) { + if (openFolderGuid != null && openFolderGuid.contains(node.getGuid())) { + Bookmark bookmark = new Bookmark(node, level, true); + children.add(bookmark); + if (node.getChildren() != null) { + children.addAll(getDisplayListTree(node.getChildren(), level + 1, openFolderGuid)); + } + + } else { + Bookmark bookmark = new Bookmark(node, level, false); + children.add(bookmark); + } + + } else if (node.getTitle() != null && + !node.getUrl().startsWith("place:") && + !node.getUrl().startsWith("about:reader")){ + // Exclude "place" and "about:reader" items as we don't support them right now + Bookmark bookmark = new Bookmark(node, level, false); + children.add(bookmark); + } + } + + return children; + } + + /** + * Traverses the current display list looking for opened folders + * @param displayList + * @return A list with the currently opened folder guids + */ + static List getOpenFoldersGuid(@NonNull List displayList) { + ArrayList result = new ArrayList<>(); + + for (Bookmark item : displayList) { + if (item.isExpanded()) { + result.add(item.getGuid()); + } + } + + return result; + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BookmarkAdapter.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BookmarkAdapter.java index 706cba343..6bad69eaa 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BookmarkAdapter.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BookmarkAdapter.java @@ -16,7 +16,10 @@ import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.databinding.BookmarkItemBinding; +import org.mozilla.vrbrowser.databinding.BookmarkItemFolderBinding; +import org.mozilla.vrbrowser.databinding.BookmarkSeparatorBinding; import org.mozilla.vrbrowser.ui.callbacks.BookmarkItemCallback; +import org.mozilla.vrbrowser.ui.callbacks.BookmarkItemFolderCallback; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; import org.mozilla.vrbrowser.utils.AnimationHelper; import org.mozilla.vrbrowser.utils.SystemUtils; @@ -25,14 +28,16 @@ import java.util.Objects; import mozilla.components.concept.storage.BookmarkNode; +import mozilla.components.concept.storage.BookmarkNodeType; -public class BookmarkAdapter extends RecyclerView.Adapter { +public class BookmarkAdapter extends RecyclerView.Adapter { static final String LOGTAG = SystemUtils.createLogtag(BookmarkAdapter.class); private static final int ICON_ANIMATION_DURATION = 200; - private List mBookmarkList; + private List mBookmarksList; + private List mDisplayList; private int mMinPadding; private int mMaxPadding; @@ -54,7 +59,7 @@ public BookmarkAdapter(@Nullable BookmarkItemCallback clickCallback, Context aCo mIsNarrowLayout = false; - setHasStableIds(true); + setHasStableIds(false); } public void setNarrow(boolean isNarrow) { @@ -64,158 +69,237 @@ public void setNarrow(boolean isNarrow) { } } - public void setBookmarkList(final List bookmarkList) { - if (mBookmarkList == null) { - mBookmarkList = bookmarkList; - notifyItemRangeInserted(0, bookmarkList.size()); + public void setBookmarkList(final List bookmarkList) { + mBookmarksList = bookmarkList; + + List newDisplayList; + if (mDisplayList == null || mDisplayList.isEmpty()) { + newDisplayList = Bookmark.getRootDisplayListTree(mBookmarksList); + mDisplayList = newDisplayList; + notifyItemRangeInserted(0, mDisplayList.size()); } else { - DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { - @Override - public int getOldListSize() { - return mBookmarkList.size(); - } + List openFoldersGuid = Bookmark.getOpenFoldersGuid(mDisplayList); + newDisplayList = Bookmark.getDisplayListTree(mBookmarksList, openFoldersGuid); + notifyDiff(newDisplayList); + } + } - @Override - public int getNewListSize() { - return bookmarkList.size(); - } + private void notifyDiff(List newDisplayList) { + DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { + @Override + public int getOldListSize() { + return mDisplayList.size(); + } - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - return mBookmarkList.get(oldItemPosition).getGuid().equals(bookmarkList.get(newItemPosition).getGuid()); - } + @Override + public int getNewListSize() { + return newDisplayList.size(); + } - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - BookmarkNode newBookmark = bookmarkList.get(newItemPosition); - BookmarkNode oldBookmark = mBookmarkList.get(oldItemPosition); - return newBookmark.getGuid().equals(oldBookmark.getGuid()) - && Objects.equals(newBookmark.getTitle(), oldBookmark.getTitle()) - && Objects.equals(newBookmark.getUrl(), oldBookmark.getUrl()); - } - }); + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return mDisplayList.get(oldItemPosition).getGuid().equals(newDisplayList.get(newItemPosition).getGuid()) && + mDisplayList.get(oldItemPosition).isExpanded() == newDisplayList.get(newItemPosition).isExpanded(); + } - mBookmarkList = bookmarkList; - result.dispatchUpdatesTo(this); - } + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + Bookmark newBookmark = newDisplayList.get(newItemPosition); + Bookmark oldBookmark = mDisplayList.get(oldItemPosition); + return newBookmark.getGuid().equals(oldBookmark.getGuid()) + && Objects.equals(newBookmark.getTitle(), oldBookmark.getTitle()) + && Objects.equals(newBookmark.getUrl(), oldBookmark.getUrl()) + && newBookmark.isExpanded() == oldBookmark.isExpanded(); + } + }); + + mDisplayList = newDisplayList; + result.dispatchUpdatesTo(this); } - public void removeItem(BookmarkNode aBookmark) { - int position = mBookmarkList.indexOf(aBookmark); + public void removeItem(Bookmark aBookmark) { + int position = mDisplayList.indexOf(aBookmark); if (position >= 0) { - mBookmarkList.remove(position); + mDisplayList.remove(position); notifyItemRemoved(position); } } public int itemCount() { - return mBookmarkList != null ? mBookmarkList.size() : 0; + return mDisplayList != null ? mDisplayList.size() : 0; } public int getItemPosition(String id) { - for (int position=0; position { - int ev = motionEvent.getActionMasked(); - switch (ev) { - case MotionEvent.ACTION_HOVER_ENTER: - binding.setIsHovered(true); - return false; - - case MotionEvent.ACTION_HOVER_EXIT: - binding.setIsHovered(false); - return false; - } + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == BookmarkNodeType.ITEM.ordinal()) { + BookmarkItemBinding binding = DataBindingUtil + .inflate(LayoutInflater.from(parent.getContext()), R.layout.bookmark_item, + parent, false); + + binding.setCallback(mBookmarkItemCallback); + binding.setIsHovered(false); + binding.setIsNarrow(mIsNarrowLayout); + binding.layout.setOnHoverListener((view, motionEvent) -> { + int ev = motionEvent.getActionMasked(); + switch (ev) { + case MotionEvent.ACTION_HOVER_ENTER: + binding.setIsHovered(true); + return false; + + case MotionEvent.ACTION_HOVER_EXIT: + binding.setIsHovered(false); + return false; + } - return false; - }); - binding.layout.setOnTouchListener((view, motionEvent) -> { - int ev = motionEvent.getActionMasked(); - switch (ev) { - case MotionEvent.ACTION_UP: - return false; - - case MotionEvent.ACTION_DOWN: - binding.setIsHovered(true); - return false; - - case MotionEvent.ACTION_CANCEL: - binding.setIsHovered(false); - return false; - } - return false; - }); - binding.more.setOnHoverListener(mIconHoverListener); - binding.more.setOnTouchListener((view, motionEvent) -> { - int ev = motionEvent.getActionMasked(); - switch (ev) { - case MotionEvent.ACTION_UP: - binding.setIsHovered(true); - mBookmarkItemCallback.onMore(view, binding.getItem()); - return true; - - case MotionEvent.ACTION_DOWN: - binding.setIsHovered(true); - return true; - } - return false; - }); - binding.trash.setOnHoverListener(mIconHoverListener); - binding.trash.setOnTouchListener((view, motionEvent) -> { - int ev = motionEvent.getActionMasked(); - switch (ev) { - case MotionEvent.ACTION_UP: - binding.setIsHovered(true); - mBookmarkItemCallback.onDelete(view, binding.getItem()); - return true; - - case MotionEvent.ACTION_DOWN: - binding.setIsHovered(true); - return true; - } - return false; - }); - return new BookmarkViewHolder(binding); + return false; + }); + binding.layout.setOnTouchListener((view, motionEvent) -> { + int ev = motionEvent.getActionMasked(); + switch (ev) { + case MotionEvent.ACTION_UP: + return false; + + case MotionEvent.ACTION_DOWN: + binding.setIsHovered(true); + return false; + + case MotionEvent.ACTION_CANCEL: + binding.setIsHovered(false); + return false; + } + return false; + }); + binding.more.setOnHoverListener(mIconHoverListener); + binding.more.setOnTouchListener((view, motionEvent) -> { + int ev = motionEvent.getActionMasked(); + switch (ev) { + case MotionEvent.ACTION_UP: + binding.setIsHovered(true); + mBookmarkItemCallback.onMore(view, binding.getItem()); + return true; + + case MotionEvent.ACTION_DOWN: + binding.setIsHovered(true); + return true; + } + return false; + }); + binding.trash.setOnHoverListener(mIconHoverListener); + binding.trash.setOnTouchListener((view, motionEvent) -> { + int ev = motionEvent.getActionMasked(); + switch (ev) { + case MotionEvent.ACTION_UP: + binding.setIsHovered(true); + mBookmarkItemCallback.onDelete(view, binding.getItem()); + return true; + + case MotionEvent.ACTION_DOWN: + binding.setIsHovered(true); + return true; + } + return false; + }); + + return new BookmarkViewHolder(binding); + + } else if (viewType == BookmarkNodeType.FOLDER.ordinal()) { + BookmarkItemFolderBinding binding = DataBindingUtil + .inflate(LayoutInflater.from(parent.getContext()), R.layout.bookmark_item_folder, + parent, false); + binding.setCallback(mBookmarkItemFolderCallback); + + return new BookmarkFolderViewHolder(binding); + + } else if (viewType == BookmarkNodeType.SEPARATOR.ordinal()) { + BookmarkSeparatorBinding binding = DataBindingUtil + .inflate(LayoutInflater.from(parent.getContext()), R.layout.bookmark_separator, + parent, false); + + return new BookmarkSeparatorViewHolder(binding); + } + + throw new IllegalArgumentException("Invalid view Type"); } @Override - public void onBindViewHolder(@NonNull BookmarkViewHolder holder, int position) { - holder.binding.setItem(mBookmarkList.get(position)); - holder.binding.setIsNarrow(mIsNarrowLayout); - holder.binding.executePendingBindings(); + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + Bookmark item = mDisplayList.get(position); + + if (holder instanceof BookmarkViewHolder) { + BookmarkViewHolder bookmarkHolder = (BookmarkViewHolder) holder; + bookmarkHolder.binding.setItem(item); + bookmarkHolder.binding.setIsNarrow(mIsNarrowLayout); + + } else if (holder instanceof BookmarkFolderViewHolder) { + BookmarkFolderViewHolder bookmarkHolder = (BookmarkFolderViewHolder) holder; + bookmarkHolder.binding.setItem(item); + bookmarkHolder.binding.executePendingBindings(); + + } else if (holder instanceof BookmarkSeparatorViewHolder) { + BookmarkSeparatorViewHolder bookmarkHolder = (BookmarkSeparatorViewHolder) holder; + bookmarkHolder.binding.setItem(item); + } } @Override public int getItemCount() { - return mBookmarkList == null ? 0 : mBookmarkList.size(); + return mDisplayList == null ? 0 : mDisplayList.size(); } @Override public long getItemId(int position) { - BookmarkNode bookmark = mBookmarkList.get(position); - return bookmark.getPosition() != null ? bookmark.getPosition() : RecyclerView.NO_ID; + Bookmark bookmark = mDisplayList.get(position); + return bookmark.getPosition(); } static class BookmarkViewHolder extends RecyclerView.ViewHolder { final BookmarkItemBinding binding; - BookmarkViewHolder(BookmarkItemBinding binding) { + BookmarkViewHolder(@NonNull BookmarkItemBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } + + static class BookmarkFolderViewHolder extends RecyclerView.ViewHolder { + + final BookmarkItemFolderBinding binding; + + BookmarkFolderViewHolder(@NonNull BookmarkItemFolderBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } + + static class BookmarkSeparatorViewHolder extends RecyclerView.ViewHolder { + + final BookmarkSeparatorBinding binding; + + BookmarkSeparatorViewHolder(@NonNull BookmarkSeparatorBinding binding) { super(binding.getRoot()); this.binding = binding; } @@ -245,4 +329,28 @@ static class BookmarkViewHolder extends RecyclerView.ViewHolder { return false; }; + private BookmarkItemFolderCallback mBookmarkItemFolderCallback = new BookmarkItemFolderCallback() { + @Override + public void onClick(View view, Bookmark item) { + List openFoldersGuid = Bookmark.getOpenFoldersGuid(mDisplayList); + + for (Bookmark bookmark : mDisplayList) { + if (bookmark.getGuid().equals(item.getGuid())) { + if (item.isExpanded()) { + openFoldersGuid.remove(bookmark.getGuid()); + + } else { + openFoldersGuid.add(bookmark.getGuid()); + } + break; + } + } + + List newDisplayList = Bookmark.getDisplayListTree(mBookmarksList, openFoldersGuid); + notifyDiff(newDisplayList); + + mBookmarkItemCallback.onFolderOpened(item); + } + }; + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/HistoryAdapter.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/HistoryAdapter.java index ae197e568..162cca1ef 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/HistoryAdapter.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/HistoryAdapter.java @@ -222,12 +222,10 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi HistoryItemViewHolder item = (HistoryItemViewHolder) holder; item.binding.setItem(mHistoryList.get(position)); item.binding.setIsNarrow(mIsNarrowLayout); - item.binding.executePendingBindings(); } else if (holder instanceof HistoryItemViewHeaderHolder) { HistoryItemViewHeaderHolder item = (HistoryItemViewHeaderHolder) holder; item.binding.setTitle(mHistoryList.get(position).getTitle()); - item.binding.executePendingBindings(); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarkItemCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarkItemCallback.java index 930876b32..2363e4f6c 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarkItemCallback.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarkItemCallback.java @@ -2,10 +2,13 @@ import android.view.View; -import mozilla.components.concept.storage.BookmarkNode; +import androidx.annotation.NonNull; + +import org.mozilla.vrbrowser.ui.adapters.Bookmark; public interface BookmarkItemCallback { - void onClick(View view, BookmarkNode item); - void onDelete(View view, BookmarkNode item); - void onMore(View view, BookmarkNode item); + void onClick(@NonNull View view, @NonNull Bookmark item); + void onDelete(@NonNull View view, @NonNull Bookmark item); + void onMore(@NonNull View view, @NonNull Bookmark item); + void onFolderOpened(@NonNull Bookmark item); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarkItemFolderCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarkItemFolderCallback.java new file mode 100644 index 000000000..01aa8b5fe --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarkItemFolderCallback.java @@ -0,0 +1,9 @@ +package org.mozilla.vrbrowser.ui.callbacks; + +import android.view.View; + +import org.mozilla.vrbrowser.ui.adapters.Bookmark; + +public interface BookmarkItemFolderCallback { + void onClick(View view, Bookmark item); +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarksCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarksCallback.java index f33e1a576..9049e3318 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarksCallback.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarksCallback.java @@ -4,9 +4,12 @@ import androidx.annotation.NonNull; -import mozilla.components.concept.storage.BookmarkNode; +import org.mozilla.vrbrowser.ui.adapters.Bookmark; public interface BookmarksCallback { - void onClearBookmarks(@NonNull View view); - void onShowContextMenu(@NonNull View view, BookmarkNode item, boolean isLastVisibleItem); + default void onClearBookmarks(@NonNull View view) {} + default void onSyncBookmarks(@NonNull View view) {} + default void onFxALogin(@NonNull View view) {} + default void onFxASynSettings(@NonNull View view) {} + default void onShowContextMenu(@NonNull View view, Bookmark item, boolean isLastVisibleItem) {} } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryCallback.java index 7877c0265..60cc88652 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryCallback.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryCallback.java @@ -7,6 +7,9 @@ import mozilla.components.concept.storage.VisitInfo; public interface HistoryCallback { - void onClearHistory(@NonNull View view); - void onShowContextMenu(@NonNull View view, @NonNull VisitInfo item, boolean isLastVisibleItem); + default void onClearHistory(@NonNull View view) {} + default void onSyncHistory(@NonNull View view) {} + default void onFxALogin(@NonNull View view) {} + default void onFxASynSettings(@NonNull View view) {} + default void onShowContextMenu(@NonNull View view, @NonNull VisitInfo item, boolean isLastVisibleItem) {} } 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 39588ee25..2a20daee7 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 @@ -17,28 +17,44 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.audio.AudioEngine; +import org.mozilla.vrbrowser.VRBrowserApplication; +import org.mozilla.vrbrowser.browser.Accounts; import org.mozilla.vrbrowser.browser.BookmarksStore; import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.browser.engine.Session; import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.databinding.BookmarksBinding; +import org.mozilla.vrbrowser.ui.adapters.Bookmark; import org.mozilla.vrbrowser.ui.adapters.BookmarkAdapter; +import org.mozilla.vrbrowser.ui.adapters.CustomLinearLayoutManager; import org.mozilla.vrbrowser.ui.callbacks.BookmarkItemCallback; import org.mozilla.vrbrowser.ui.callbacks.BookmarksCallback; import org.mozilla.vrbrowser.utils.UIThreadExecutor; +import java.util.ArrayList; import java.util.List; +import mozilla.appservices.places.BookmarkRoot; import mozilla.components.concept.storage.BookmarkNode; +import mozilla.components.concept.sync.AccountObserver; +import mozilla.components.concept.sync.AuthType; +import mozilla.components.concept.sync.OAuthAccount; +import mozilla.components.concept.sync.Profile; +import mozilla.components.service.fxa.SyncEngine; +import mozilla.components.service.fxa.sync.SyncReason; +import mozilla.components.service.fxa.sync.SyncStatusObserver; public class BookmarksView extends FrameLayout implements BookmarksStore.BookmarkListener { private BookmarksBinding mBinding; + private Accounts mAccounts; private BookmarkAdapter mBookmarkAdapter; - private AudioEngine mAudio; private boolean mIgnoreNextListener; + private ArrayList mBookmarksViewListeners; + private CustomLinearLayoutManager mLayoutManager; public BookmarksView(Context aContext) { super(aContext); @@ -57,21 +73,38 @@ public BookmarksView(Context aContext, AttributeSet aAttrs, int aDefStyle) { @SuppressLint("ClickableViewAccessibility") private void initialize(Context aContext) { - mAudio = AudioEngine.fromContext(aContext); - LayoutInflater inflater = LayoutInflater.from(aContext); + mBookmarksViewListeners = new ArrayList<>(); + // Inflate this data binding layout mBinding = DataBindingUtil.inflate(inflater, R.layout.bookmarks, this, true); + mBinding.setCallback(mBookmarksCallback); mBookmarkAdapter = new BookmarkAdapter(mBookmarkItemCallback, aContext); mBinding.bookmarksList.setAdapter(mBookmarkAdapter); mBinding.bookmarksList.setOnTouchListener((v, event) -> { v.requestFocusFromTouch(); return false; }); + mBinding.bookmarksList.setHasFixedSize(true); + mBinding.bookmarksList.setItemViewCacheSize(20); + mBinding.bookmarksList.setDrawingCacheEnabled(true); + mBinding.bookmarksList.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH); + + mLayoutManager = (CustomLinearLayoutManager) mBinding.bookmarksList.getLayoutManager(); + mBinding.setIsLoading(true); + + mAccounts = ((VRBrowserApplication)getContext().getApplicationContext()).getAccounts(); + mAccounts.addAccountListener(mAccountListener); + mAccounts.addSyncListener(mSyncListener); + + mBinding.setIsSignedIn(mAccounts.isSignedIn()); + mBinding.setIsSyncEnabled(mAccounts.isEngineEnabled(SyncEngine.Bookmarks.INSTANCE)); + mBinding.setIsNarrow(false); mBinding.executePendingBindings(); - syncBookmarks(); + + updateBookmarks(); SessionStore.get().getBookmarkStore().addListener(this); setVisibility(GONE); @@ -82,13 +115,19 @@ private void initialize(Context aContext) { }); } + public void onShow() { + updateLayout(); + } + public void onDestroy() { SessionStore.get().getBookmarkStore().removeListener(this); + mAccounts.removeAccountListener(mAccountListener); + mAccounts.removeSyncListener(mSyncListener); } private final BookmarkItemCallback mBookmarkItemCallback = new BookmarkItemCallback() { @Override - public void onClick(View view, BookmarkNode item) { + public void onClick(@NonNull View view, @NonNull Bookmark item) { mBinding.bookmarksList.requestFocusFromTouch(); Session session = SessionStore.get().getActiveSession(); @@ -96,7 +135,7 @@ public void onClick(View view, BookmarkNode item) { } @Override - public void onDelete(View view, BookmarkNode item) { + public void onDelete(@NonNull View view, @NonNull Bookmark item) { mBinding.bookmarksList.requestFocusFromTouch(); mIgnoreNextListener = true; @@ -110,7 +149,7 @@ public void onDelete(View view, BookmarkNode item) { } @Override - public void onMore(View view, BookmarkNode item) { + public void onMore(@NonNull View view, @NonNull Bookmark item) { mBinding.bookmarksList.requestFocusFromTouch(); int rowPosition = mBookmarkAdapter.getItemPosition(item.getGuid()); @@ -129,14 +168,105 @@ public void onMore(View view, BookmarkNode item) { item, isLastVisibleItem); } + + @Override + public void onFolderOpened(@NonNull Bookmark item) { + int position = mBookmarkAdapter.getItemPosition(item.getGuid()); + mLayoutManager.scrollToPositionWithOffset(position, 20); + } + }; + + private BookmarksCallback mBookmarksCallback = new BookmarksCallback() { + @Override + public void onClearBookmarks(@NonNull View view) { + mBookmarksViewListeners.forEach((listener) -> listener.onClearBookmarks(view)); + } + + @Override + public void onSyncBookmarks(@NonNull View view) { + mAccounts.syncNowAsync(SyncReason.User.INSTANCE, false); + } + + @Override + public void onFxALogin(@NonNull View view) { + mAccounts.getAuthenticationUrlAsync().thenAcceptAsync((url) -> { + if (url != null) { + mAccounts.setLoginOrigin(Accounts.LoginOrigin.BOOKMARKS); + SessionStore.get().getActiveSession().loadUri(url); + } + }); + } + + @Override + public void onFxASynSettings(@NonNull View view) { + mBookmarksViewListeners.forEach((listener) -> listener.onFxASynSettings(view)); + } + + @Override + public void onShowContextMenu(@NonNull View view, Bookmark item, boolean isLastVisibleItem) { + mBookmarksViewListeners.forEach((listener) -> listener.onShowContextMenu(view, item, isLastVisibleItem)); + } }; - public void setBookmarksCallback(@NonNull BookmarksCallback callback) { - mBinding.setCallback(callback); + public void addBookmarksListener(@NonNull BookmarksCallback listener) { + if (!mBookmarksViewListeners.contains(listener)) { + mBookmarksViewListeners.add(listener); + } } - private void syncBookmarks() { - SessionStore.get().getBookmarkStore().getBookmarks().thenAcceptAsync(this::showBookmarks, new UIThreadExecutor()); + public void removeBookmarksListener(@NonNull BookmarksCallback listener) { + mBookmarksViewListeners.remove(listener); + } + + private SyncStatusObserver mSyncListener = new SyncStatusObserver() { + @Override + public void onStarted() { + boolean isSyncEnabled = mAccounts.isEngineEnabled(SyncEngine.Bookmarks.INSTANCE); + mBinding.setIsSyncEnabled(isSyncEnabled); + mBinding.setIsSyncing(true); + mBinding.executePendingBindings(); + } + + @Override + public void onIdle() { + mBinding.setIsSyncing(false); + if (mAccounts.isEngineEnabled(SyncEngine.Bookmarks.INSTANCE)) { + mBinding.setLastSync(mAccounts.lastSync()); + } + } + + @Override + public void onError(@Nullable Exception e) { + mBinding.setIsSyncing(false); + mBinding.setIsSyncEnabled(mAccounts.isEngineEnabled(SyncEngine.Bookmarks.INSTANCE)); + mBinding.executePendingBindings(); + } + }; + + private AccountObserver mAccountListener = new AccountObserver() { + + @Override + public void onAuthenticated(@NotNull OAuthAccount oAuthAccount, @NotNull AuthType authType) { + mBinding.setIsSignedIn(true); + } + + @Override + public void onProfileUpdated(@NotNull Profile profile) { + } + + @Override + public void onLoggedOut() { + mBinding.setIsSignedIn(false); + } + + @Override + public void onAuthenticationProblems() { + mBinding.setIsSignedIn(false); + } + }; + + private void updateBookmarks() { + SessionStore.get().getBookmarkStore().getTree(BookmarkRoot.Root.getId(), true).thenAcceptAsync(this::showBookmarks, new UIThreadExecutor()); } private void showBookmarks(List aBookmarks) { @@ -148,28 +278,41 @@ private void showBookmarks(List aBookmarks) { mBinding.setIsEmpty(false); mBinding.setIsLoading(false); mBookmarkAdapter.setBookmarkList(aBookmarks); - mBinding.bookmarksList.post(() -> mBinding.bookmarksList.smoothScrollToPosition( - mBookmarkAdapter.getItemCount() > 0 ? mBookmarkAdapter.getItemCount() - 1 : 0)); } - mBinding.executePendingBindings(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - double width = Math.ceil(getWidth()/getContext().getResources().getDisplayMetrics().density); - mBookmarkAdapter.setNarrow(width < SettingsStore.WINDOW_WIDTH_DEFAULT); + updateLayout(); + } + + private void updateLayout() { + post(() -> { + double width = Math.ceil(getWidth()/getContext().getResources().getDisplayMetrics().density); + boolean isNarrow = width < SettingsStore.WINDOW_WIDTH_DEFAULT; + + if (isNarrow != mBinding.getIsNarrow()) { + mBookmarkAdapter.setNarrow(isNarrow); + + mBinding.setIsNarrow(isNarrow); + mBinding.executePendingBindings(); + + requestLayout(); + } + }); } // BookmarksStore.BookmarksViewListener + @Override public void onBookmarksUpdated() { if (mIgnoreNextListener) { mIgnoreNextListener = false; return; } - syncBookmarks(); + updateBookmarks(); } @Override @@ -178,6 +321,6 @@ public void onBookmarkAdded() { mIgnoreNextListener = false; return; } - syncBookmarks(); + updateBookmarks(); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomRecyclerView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomRecyclerView.java index eabfe6bf8..f9b224ae4 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomRecyclerView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomRecyclerView.java @@ -59,8 +59,10 @@ public CustomRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs getViewTreeObserver().addOnGlobalLayoutListener(() -> { if (getVisibility() == VISIBLE) { - mFastScroller.updateScrollPosition(computeHorizontalScrollOffset(), - computeVerticalScrollOffset()); + if (mFastScroller != null) { + mFastScroller.updateScrollPosition(computeHorizontalScrollOffset(), + computeVerticalScrollOffset()); + } } }); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HistoryView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HistoryView.java index 243cd98a5..9f363423a 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HistoryView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HistoryView.java @@ -18,7 +18,11 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.VRBrowserApplication; +import org.mozilla.vrbrowser.browser.Accounts; import org.mozilla.vrbrowser.browser.HistoryStore; import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.browser.engine.Session; @@ -30,21 +34,32 @@ import org.mozilla.vrbrowser.utils.SystemUtils; import org.mozilla.vrbrowser.utils.UIThreadExecutor; +import java.util.ArrayList; import java.util.Calendar; +import java.util.Comparator; import java.util.GregorianCalendar; import java.util.List; import java.util.stream.Collectors; import mozilla.components.concept.storage.VisitInfo; import mozilla.components.concept.storage.VisitType; +import mozilla.components.concept.sync.AccountObserver; +import mozilla.components.concept.sync.AuthType; +import mozilla.components.concept.sync.OAuthAccount; +import mozilla.components.concept.sync.Profile; +import mozilla.components.service.fxa.SyncEngine; +import mozilla.components.service.fxa.sync.SyncReason; +import mozilla.components.service.fxa.sync.SyncStatusObserver; public class HistoryView extends FrameLayout implements HistoryStore.HistoryListener { private static final String LOGTAG = SystemUtils.createLogtag(HistoryView.class); private HistoryBinding mBinding; + private Accounts mAccounts; private HistoryAdapter mHistoryAdapter; private boolean mIgnoreNextListener; + private ArrayList mHistoryViewListeners; public HistoryView(Context aContext) { super(aContext); @@ -65,17 +80,34 @@ public HistoryView(Context aContext, AttributeSet aAttrs, int aDefStyle) { private void initialize(Context aContext) { LayoutInflater inflater = LayoutInflater.from(aContext); + mHistoryViewListeners = new ArrayList<>(); + // Inflate this data binding layout mBinding = DataBindingUtil.inflate(inflater, R.layout.history, this, true); + mBinding.setCallback(mHistoryCallback); mHistoryAdapter = new HistoryAdapter(mHistoryItemCallback, aContext); mBinding.historyList.setAdapter(mHistoryAdapter); mBinding.historyList.setOnTouchListener((v, event) -> { v.requestFocusFromTouch(); return false; }); + mBinding.historyList.setHasFixedSize(true); + mBinding.historyList.setItemViewCacheSize(20); + mBinding.historyList.setDrawingCacheEnabled(true); + mBinding.historyList.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH); + mBinding.setIsLoading(true); + + mAccounts = ((VRBrowserApplication)getContext().getApplicationContext()).getAccounts(); + mAccounts.addAccountListener(mAccountListener); + mAccounts.addSyncListener(mSyncListener); + + mBinding.setIsSignedIn(mAccounts.isSignedIn()); + mBinding.setIsSyncEnabled(mAccounts.isEngineEnabled(SyncEngine.History.INSTANCE)); + mBinding.setIsNarrow(false); mBinding.executePendingBindings(); - syncHistory(); + + updateHistory(); SessionStore.get().getHistoryStore().addListener(this); setVisibility(GONE); @@ -88,6 +120,12 @@ private void initialize(Context aContext) { public void onDestroy() { SessionStore.get().getHistoryStore().removeListener(this); + mAccounts.removeAccountListener(mAccountListener); + mAccounts.removeSyncListener(mSyncListener); + } + + public void onShow() { + updateLayout(); } private final HistoryItemCallback mHistoryItemCallback = new HistoryItemCallback() { @@ -135,11 +173,97 @@ public void onMore(View view, VisitInfo item) { } }; - public void setHistoryCallback(@NonNull HistoryCallback callback) { - mBinding.setCallback(callback); + private HistoryCallback mHistoryCallback = new HistoryCallback() { + @Override + public void onClearHistory(@NonNull View view) { + mHistoryViewListeners.forEach((listener) -> listener.onClearHistory(view)); + } + + @Override + public void onSyncHistory(@NonNull View view) { + mAccounts.syncNowAsync(SyncReason.User.INSTANCE, false); + } + + @Override + public void onFxALogin(@NonNull View view) { + mAccounts.getAuthenticationUrlAsync().thenAcceptAsync((url) -> { + if (url != null) { + mAccounts.setLoginOrigin(Accounts.LoginOrigin.HISTORY); + SessionStore.get().getActiveSession().loadUri(url); + } + }); + } + + @Override + public void onFxASynSettings(@NonNull View view) { + mHistoryViewListeners.forEach((listener) -> listener.onFxASynSettings(view)); + } + + @Override + public void onShowContextMenu(@NonNull View view, @NonNull VisitInfo item, boolean isLastVisibleItem) { + mHistoryViewListeners.forEach((listener) -> listener.onShowContextMenu(view, item, isLastVisibleItem)); + } + }; + + public void addHistoryListener(@NonNull HistoryCallback listener) { + if (!mHistoryViewListeners.contains(listener)) { + mHistoryViewListeners.add(listener); + } + } + + public void removeHistoryListener(@NonNull HistoryCallback listener) { + mHistoryViewListeners.remove(listener); } - private void syncHistory() { + private SyncStatusObserver mSyncListener = new SyncStatusObserver() { + @Override + public void onStarted() { + boolean isSyncEnabled = mAccounts.isEngineEnabled(SyncEngine.History.INSTANCE); + mBinding.setIsSyncEnabled(isSyncEnabled); + mBinding.setIsSyncing(true); + mBinding.executePendingBindings(); + } + + @Override + public void onIdle() { + mBinding.setIsSyncing(false); + if (mAccounts.isEngineEnabled(SyncEngine.History.INSTANCE)) { + mBinding.setLastSync(mAccounts.lastSync()); + } + mBinding.executePendingBindings(); + } + + @Override + public void onError(@Nullable Exception e) { + mBinding.setIsSyncing(false); + mBinding.setIsSyncEnabled(mAccounts.isEngineEnabled(SyncEngine.History.INSTANCE)); + mBinding.executePendingBindings(); + } + }; + + private AccountObserver mAccountListener = new AccountObserver() { + + @Override + public void onAuthenticated(@NotNull OAuthAccount oAuthAccount, @NotNull AuthType authType) { + mBinding.setIsSignedIn(true); + } + + @Override + public void onProfileUpdated(@NotNull Profile profile) { + } + + @Override + public void onLoggedOut() { + mBinding.setIsSignedIn(false); + } + + @Override + public void onAuthenticationProblems() { + mBinding.setIsSignedIn(false); + } + }; + + private void updateHistory() { Calendar date = new GregorianCalendar(); date.set(Calendar.HOUR_OF_DAY, 0); date.set(Calendar.MINUTE, 0); @@ -153,7 +277,8 @@ private void syncHistory() { SessionStore.get().getHistoryStore().getDetailedHistory().thenAcceptAsync((items) -> { List orderedItems = items.stream() - .sorted((o1, o2) -> Long.valueOf(o2.getVisitTime() - o1.getVisitTime()).intValue()) + .sorted(Comparator.comparing((VisitInfo mps) -> mps.getVisitTime()) + .reversed()) .collect(Collectors.toList()); addSection(orderedItems, getResources().getString(R.string.history_section_today), Long.MAX_VALUE, todayLimit); @@ -197,24 +322,39 @@ private void showHistory(List historyItems) { mHistoryAdapter.setHistoryList(historyItems); mBinding.historyList.post(() -> mBinding.historyList.smoothScrollToPosition(0)); } - mBinding.executePendingBindings(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - double width = Math.ceil(getWidth()/getContext().getResources().getDisplayMetrics().density); - mHistoryAdapter.setNarrow(width < SettingsStore.WINDOW_WIDTH_DEFAULT); + updateLayout(); + } + + private void updateLayout() { + post(() -> { + double width = Math.ceil(getWidth()/getContext().getResources().getDisplayMetrics().density); + boolean isNarrow = width < SettingsStore.WINDOW_WIDTH_DEFAULT; + + if (isNarrow != mBinding.getIsNarrow()) { + mHistoryAdapter.setNarrow(isNarrow); + + mBinding.setIsNarrow(isNarrow); + mBinding.executePendingBindings(); + + requestLayout(); + } + }); } // HistoryStore.HistoryListener + @Override public void onHistoryUpdated() { if (mIgnoreNextListener) { mIgnoreNextListener = false; return; } - syncHistory(); + updateHistory(); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HoneycombButton.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HoneycombButton.java index 0aa357bf3..7764d2cee 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HoneycombButton.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HoneycombButton.java @@ -12,12 +12,14 @@ import android.widget.LinearLayout; import android.widget.TextView; -import org.mozilla.vrbrowser.utils.DeviceType; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.utils.DeviceType; import org.mozilla.vrbrowser.utils.SystemUtils; -import androidx.annotation.Nullable; - public class HoneycombButton extends LinearLayout { private static final String LOGTAG = SystemUtils.createLogtag(HoneycombButton.class); @@ -131,4 +133,12 @@ public void setOnHoverListener(final OnHoverListener l) { public boolean onInterceptTouchEvent(MotionEvent ev) { return true; } + + public void setText(@StringRes int text) { + mText.setText(text); + } + + public void setImageDrawable(@NonNull Drawable drawable) { + mIcon.setImageDrawable(drawable); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/TabView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/TabView.java index 7ffa66019..992a564af 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/TabView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/TabView.java @@ -6,7 +6,6 @@ import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/settings/ButtonSetting.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/settings/ButtonSetting.java index 853cc555d..d8cd3a5f8 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/settings/ButtonSetting.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/settings/ButtonSetting.java @@ -9,6 +9,8 @@ import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.StringRes; + import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; @@ -86,6 +88,10 @@ public void setButtonText(String aText) { mButton.setText(aText); } + public void setButtonText(@StringRes int aStringRes) { + mButton.setText(aStringRes); + } + public void setDescription(String description) { mDescriptionView.setText(description); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TabsWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TabsWidget.java index 5a26548eb..8f4f71f45 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TabsWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TabsWidget.java @@ -2,14 +2,12 @@ import android.content.Context; import android.graphics.Rect; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; -import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayWidget.java index 2297bbbe0..491a8fb36 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayWidget.java @@ -386,7 +386,11 @@ private void handleSessionState() { } } - private void toggleSettingsDialog() { + public void toggleSettingsDialog() { + toggleSettingsDialog(SettingsWidget.SettingDialog.MAIN); + } + + public void toggleSettingsDialog(SettingsWidget.SettingDialog settingDialog) { UIWidget widget = getChild(mSettingsDialogHandle); if (widget == null) { widget = createChild(SettingsWidget.class, false); @@ -399,7 +403,7 @@ private void toggleSettingsDialog() { if (widget.isVisible()) { widget.hide(REMOVE_WIDGET); } else { - widget.show(REQUEST_FOCUS); + ((SettingsWidget)widget).show(REQUEST_FOCUS, settingDialog); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java index f691b0dc3..058682179 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java @@ -83,4 +83,5 @@ interface WorldClickListener { void openNewWindow(@NonNull String uri); void openNewTab(@NonNull String uri); WindowWidget getFocusedWindow(); + TrayWidget getTray(); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java index 8b3617dcd..0d2041d99 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java @@ -41,6 +41,7 @@ import org.mozilla.vrbrowser.browser.engine.Session; import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; +import org.mozilla.vrbrowser.ui.adapters.Bookmark; import org.mozilla.vrbrowser.ui.callbacks.BookmarksCallback; import org.mozilla.vrbrowser.ui.callbacks.HistoryCallback; import org.mozilla.vrbrowser.ui.callbacks.LibraryItemContextMenuClickCallback; @@ -57,6 +58,7 @@ import org.mozilla.vrbrowser.ui.widgets.prompts.AlertPromptWidget; import org.mozilla.vrbrowser.ui.widgets.prompts.ConfirmPromptWidget; import org.mozilla.vrbrowser.ui.widgets.prompts.PromptWidget; +import org.mozilla.vrbrowser.ui.widgets.settings.SettingsWidget; import org.mozilla.vrbrowser.utils.SystemUtils; import org.mozilla.vrbrowser.utils.ViewUtils; @@ -65,8 +67,9 @@ import java.util.Calendar; import java.util.GregorianCalendar; -import mozilla.components.concept.storage.BookmarkNode; import mozilla.components.concept.storage.PageObservation; +import mozilla.components.concept.storage.PageVisit; +import mozilla.components.concept.storage.RedirectSource; import mozilla.components.concept.storage.VisitInfo; import mozilla.components.concept.storage.VisitType; @@ -159,12 +162,12 @@ private void initialize(Context aContext) { mListeners = new ArrayList<>(); setupListeners(mSession); - mBookmarksView = new BookmarksView(aContext); - mBookmarksView.setBookmarksCallback(mBookmarksCallback); + mBookmarksView = new BookmarksView(aContext); + mBookmarksView.addBookmarksListener(mBookmarksListener); mBookmarksViewListeners = new ArrayList<>(); mHistoryView = new HistoryView(aContext); - mHistoryView.setHistoryCallback(mHistoryCallback); + mHistoryView.addHistoryListener(mHistoryListener); mHistoryViewListeners = new ArrayList<>(); mHandle = ((WidgetManagerDelegate)aContext).newWidgetHandle(); @@ -421,6 +424,7 @@ public void showBookmarks() { public void showBookmarks(boolean switchSurface) { if (mView == null) { setView(mBookmarksView, switchSurface); + mBookmarksView.onShow(); for (BookmarksViewDelegate listener : mBookmarksViewListeners) { listener.onBookmarksShown(this); } @@ -464,6 +468,7 @@ public void showHistory() { public void showHistory(boolean switchSurface) { if (mView == null) { setView(mHistoryView, switchSurface); + mHistoryView.onShow(); for (HistoryViewDelegate listener : mHistoryViewListeners) { listener.onHistoryViewShown(this); } @@ -917,6 +922,8 @@ public void releaseWidget() { mTexture.release(); mTexture = null; } + mBookmarksView.removeBookmarksListener(mBookmarksListener); + mHistoryView.removeHistoryListener(mHistoryListener); super.releaseWidget(); } @@ -1310,15 +1317,9 @@ public void onRemoveFromBookmarks(LibraryItemContextMenu.LibraryContextMenuItem mLibraryItemContextMenu.show(REQUEST_FOCUS); } - private BookmarksCallback mBookmarksCallback = new BookmarksCallback() { - - @Override - public void onClearBookmarks(View view) { - // Not used ATM - } - + private BookmarksCallback mBookmarksListener = new BookmarksCallback() { @Override - public void onShowContextMenu(@NonNull View view, @NotNull BookmarkNode item, boolean isLastVisibleItem) { + public void onShowContextMenu(@NonNull View view, @NotNull Bookmark item, boolean isLastVisibleItem) { showLibraryItemContextMenu( view, new LibraryItemContextMenu.LibraryContextMenuItem( @@ -1327,9 +1328,14 @@ public void onShowContextMenu(@NonNull View view, @NotNull BookmarkNode item, bo LibraryItemContextMenu.LibraryItemType.BOOKMARKS), isLastVisibleItem); } + + @Override + public void onFxASynSettings(@NonNull View view) { + mWidgetManager.getTray().toggleSettingsDialog(SettingsWidget.SettingDialog.FXA); + } }; - private HistoryCallback mHistoryCallback = new HistoryCallback() { + private HistoryCallback mHistoryListener = new HistoryCallback() { @Override public void onClearHistory(@NonNull View view) { view.requestFocusFromTouch(); @@ -1346,6 +1352,11 @@ public void onShowContextMenu(@NonNull View view, @NonNull VisitInfo item, boole LibraryItemContextMenu.LibraryItemType.HISTORY), isLastVisibleItem); } + + @Override + public void onFxASynSettings(@NonNull View view) { + mWidgetManager.getTray().toggleSettingsDialog(SettingsWidget.SettingDialog.FXA); + } }; private void hideContextMenus() { @@ -1481,21 +1492,22 @@ public GeckoResult onVisited(@NonNull GeckoSession geckoSession, @NonNu boolean isReload = lastVisitedURL != null && lastVisitedURL.equals(url); - VisitType visitType; + PageVisit pageVisit; if (isReload) { - visitType = VisitType.RELOAD; + pageVisit = new PageVisit(VisitType.RELOAD, RedirectSource.NOT_A_SOURCE); + } else { if ((flags & VISIT_REDIRECT_SOURCE_PERMANENT) != 0) { - visitType = VisitType.REDIRECT_PERMANENT; + pageVisit = new PageVisit(VisitType.REDIRECT_PERMANENT, RedirectSource.NOT_A_SOURCE); } else if ((flags & VISIT_REDIRECT_SOURCE) != 0) { - visitType = VisitType.REDIRECT_TEMPORARY; + pageVisit = new PageVisit(VisitType.REDIRECT_TEMPORARY, RedirectSource.NOT_A_SOURCE); } else { - visitType = VisitType.LINK; + pageVisit = new PageVisit(VisitType.LINK, RedirectSource.NOT_A_SOURCE); } } SessionStore.get().getHistoryStore().deleteVisitsFor(url).thenAcceptAsync(result -> { - SessionStore.get().getHistoryStore().recordVisit(url, visitType); + SessionStore.get().getHistoryStore().recordVisit(url, pageVisit); }); return GeckoResult.fromValue(true); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Windows.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Windows.java index ca5244c63..5773da9d3 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Windows.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Windows.java @@ -2,7 +2,6 @@ import android.content.Context; import android.util.Log; -import android.widget.TabWidget; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -11,8 +10,11 @@ import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; +import org.jetbrains.annotations.NotNull; import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.VRBrowserApplication; +import org.mozilla.vrbrowser.browser.Accounts; import org.mozilla.vrbrowser.browser.Media; import org.mozilla.vrbrowser.browser.PromptDelegate; import org.mozilla.vrbrowser.browser.SettingsStore; @@ -20,6 +22,7 @@ import org.mozilla.vrbrowser.browser.engine.SessionState; import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; +import org.mozilla.vrbrowser.ui.widgets.settings.SettingsWidget; import org.mozilla.vrbrowser.utils.BitmapCache; import org.mozilla.vrbrowser.utils.SystemUtils; @@ -33,7 +36,10 @@ import java.util.ArrayList; import java.util.stream.Collectors; -import static org.mozilla.vrbrowser.ui.widgets.UIWidget.REQUEST_FOCUS; +import mozilla.components.concept.sync.AccountObserver; +import mozilla.components.concept.sync.AuthType; +import mozilla.components.concept.sync.OAuthAccount; +import mozilla.components.concept.sync.Profile; public class Windows implements TrayListener, TopBarWidget.Delegate, TitleBarWidget.Delegate, GeckoSession.ContentDelegate, WindowWidget.WindowListener, TabsWidget.TabDelegate { @@ -93,6 +99,7 @@ class WindowsState { private boolean mIsPaused = false; private PromptDelegate mPromptDelegate; private TabsWidget mTabsWidget; + private Accounts mAccounts; public enum WindowPlacement{ FRONT(0), @@ -128,6 +135,9 @@ public Windows(Context aContext) { mStoredCurvedMode = SettingsStore.getInstance(mContext).getCylinderDensity() > 0.0f; + mAccounts = ((VRBrowserApplication)mContext.getApplicationContext()).getAccounts(); + mAccounts.addAccountListener(mAccountObserver); + restoreWindows(); } @@ -424,6 +434,7 @@ public void onDestroy() { for (WindowWidget window: mPrivateWindows) { window.close(); } + mAccounts.removeAccountListener(mAccountObserver); } public boolean isInPrivateMode() { @@ -859,6 +870,40 @@ public void exitResizeMode() { } } + private AccountObserver mAccountObserver = new AccountObserver() { + @Override + public void onLoggedOut() { + + } + + @Override + public void onAuthenticated(@NotNull OAuthAccount oAuthAccount, @NotNull AuthType authType) { + switch (mAccounts.getLoginOrigin()) { + case BOOKMARKS: + getFocusedWindow().switchBookmarks(); + break; + + case HISTORY: + getFocusedWindow().switchHistory(); + break; + + case SETTINGS: + mWidgetManager.getTray().toggleSettingsDialog(SettingsWidget.SettingDialog.FXA); + break; + } + } + + @Override + public void onProfileUpdated(@NotNull Profile profile) { + + } + + @Override + public void onAuthenticationProblems() { + + } + }; + // Tray Listener @Override public void onBookmarksClicked() { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/FxAAccountOptionsView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/FxAAccountOptionsView.java new file mode 100644 index 000000000..70361c172 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/FxAAccountOptionsView.java @@ -0,0 +1,178 @@ +/* -*- 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 android.view.LayoutInflater; + +import androidx.databinding.DataBindingUtil; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.VRBrowserApplication; +import org.mozilla.vrbrowser.browser.Accounts; +import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.databinding.OptionsFxaAccountBinding; +import org.mozilla.vrbrowser.ui.views.settings.SwitchSetting; +import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; +import org.mozilla.vrbrowser.utils.SystemUtils; + +import java.util.Objects; + +import mozilla.components.concept.sync.AccountObserver; +import mozilla.components.concept.sync.AuthType; +import mozilla.components.concept.sync.OAuthAccount; +import mozilla.components.concept.sync.Profile; +import mozilla.components.service.fxa.SyncEngine; +import mozilla.components.service.fxa.sync.SyncReason; +import mozilla.components.service.fxa.sync.SyncStatusObserver; + +class FxAAccountOptionsView extends SettingsView { + + private static final String LOGTAG = SystemUtils.createLogtag(FxAAccountOptionsView.class); + + private OptionsFxaAccountBinding mBinding; + private Accounts mAccounts; + + public FxAAccountOptionsView(Context aContext, WidgetManagerDelegate aWidgetManager) { + super(aContext, aWidgetManager); + initialize(aContext); + } + + private void initialize(Context aContext) { + LayoutInflater inflater = LayoutInflater.from(aContext); + + // Inflate this data binding layout + mBinding = DataBindingUtil.inflate(inflater, R.layout.options_fxa_account, this, true); + + mScrollbar = mBinding.scrollbar; + + // Header + mBinding.headerLayout.setBackClickListener(view -> onDismiss()); + + mAccounts = ((VRBrowserApplication)getContext().getApplicationContext()).getAccounts(); + + mBinding.signButton.setOnClickListener(view -> mAccounts.logoutAsync()); + mBinding.bookmarksSyncSwitch.setOnCheckedChangeListener(mBookmarksSyncListener); + + mBinding.historySyncSwitch.setOnCheckedChangeListener(mHistorySyncListener); + + updateCurrentAccountState(); + + // Footer + mBinding.footerLayout.setResetClickListener(v -> resetOptions()); + } + + @Override + public void onShown() { + super.onShown(); + + mAccounts.addAccountListener(mAccountListener); + mAccounts.addSyncListener(mSyncListener); + + mBinding.bookmarksSyncSwitch.setValue(mAccounts.isEngineEnabled(SyncEngine.Bookmarks.INSTANCE), false); + mBinding.historySyncSwitch.setValue(mAccounts.isEngineEnabled(SyncEngine.History.INSTANCE), false); + } + + @Override + public void onHidden() { + super.onHidden(); + + mAccounts.removeAccountListener(mAccountListener); + mAccounts.removeSyncListener(mSyncListener); + } + + private SwitchSetting.OnCheckedChangeListener mBookmarksSyncListener = (compoundButton, value, apply) -> { + mAccounts.setSyncStatus(SyncEngine.Bookmarks.INSTANCE, value); + mAccounts.syncNowAsync(SyncReason.EngineChange.INSTANCE, false); + }; + + private SwitchSetting.OnCheckedChangeListener mHistorySyncListener = (compoundButton, value, apply) -> { + mAccounts.setSyncStatus(SyncEngine.History.INSTANCE, value); + mAccounts.syncNowAsync(SyncReason.EngineChange.INSTANCE, false); + }; + + private void resetOptions() { + mAccounts.setSyncStatus(SyncEngine.Bookmarks.INSTANCE, SettingsStore.BOOKMARKS_SYNC_DEFAULT); + mAccounts.setSyncStatus(SyncEngine.History.INSTANCE, SettingsStore.HISTORY_SYNC_DEFAULT); + mAccounts.syncNowAsync(SyncReason.EngineChange.INSTANCE, false); + } + + private SyncStatusObserver mSyncListener = new SyncStatusObserver() { + @Override + public void onStarted() { + + } + + @Override + public void onIdle() { + mBinding.bookmarksSyncSwitch.setValue(mAccounts.isEngineEnabled(SyncEngine.Bookmarks.INSTANCE), false); + mBinding.historySyncSwitch.setValue(mAccounts.isEngineEnabled(SyncEngine.History.INSTANCE), false); + } + + @Override + public void onError(@Nullable Exception e) { + + } + }; + + void updateCurrentAccountState() { + switch(mAccounts.getAccountStatus()) { + case NEEDS_RECONNECT: + mBinding.signButton.setButtonText(R.string.settings_fxa_account_reconnect); + break; + + case SIGNED_IN: + mBinding.signButton.setButtonText(R.string.settings_fxa_account_sign_out); + Profile profile = mAccounts.accountProfile(); + if (profile != null) { + updateProfile(profile); + + } else { + Objects.requireNonNull(mAccounts.updateProfileAsync()).thenAcceptAsync((u) -> updateProfile(mAccounts.accountProfile())); + } + break; + + case SIGNED_OUT: + mBinding.signButton.setButtonText(R.string.settings_fxa_account_sign_in); + break; + + default: + throw new IllegalStateException("Unexpected value: " + mAccounts.getAccountStatus()); + } + } + + private void updateProfile(Profile profile) { + if (profile != null) { + mBinding.accountEmail.setText(profile.getEmail()); + } + } + + private AccountObserver mAccountListener = new AccountObserver() { + + @Override + public void onAuthenticated(@NotNull OAuthAccount oAuthAccount, @NotNull AuthType authType) { + mBinding.signButton.setButtonText(R.string.settings_fxa_account_sign_out); + } + + @Override + public void onProfileUpdated(@NotNull Profile profile) { + post(() -> mBinding.accountEmail.setText(profile.getEmail())); + } + + @Override + public void onLoggedOut() { + post(FxAAccountOptionsView.this::onDismiss); + } + + @Override + public void onAuthenticationProblems() { + post(FxAAccountOptionsView.this::onDismiss); + } + }; + +} 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 e75092ec1..62f8be6fc 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 @@ -10,46 +10,62 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Point; +import android.graphics.drawable.Drawable; import android.text.Html; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; +import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.databinding.DataBindingUtil; + +import org.jetbrains.annotations.NotNull; +import org.mozilla.gecko.util.ThreadUtils; 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.Accounts; import org.mozilla.vrbrowser.browser.engine.Session; import org.mozilla.vrbrowser.browser.engine.SessionStore; -import org.mozilla.vrbrowser.ui.views.HoneycombButton; +import org.mozilla.vrbrowser.databinding.SettingsBinding; import org.mozilla.vrbrowser.ui.widgets.UIWidget; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; import org.mozilla.vrbrowser.ui.widgets.dialogs.RestartDialogWidget; import org.mozilla.vrbrowser.ui.widgets.dialogs.UIDialog; import org.mozilla.vrbrowser.ui.widgets.prompts.AlertPromptWidget; +import org.mozilla.vrbrowser.utils.StringUtils; +import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.net.URL; import java.net.URLEncoder; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.GregorianCalendar; + +import mozilla.components.concept.sync.AccountObserver; +import mozilla.components.concept.sync.AuthType; +import mozilla.components.concept.sync.OAuthAccount; +import mozilla.components.concept.sync.Profile; public class SettingsWidget extends UIDialog implements WidgetManagerDelegate.WorldClickListener, SettingsView.Delegate { + + public enum SettingDialog { + MAIN, LANGUAGE, DISPLAY, PRIVACY, DEVELOPER, FXA, ENVIRONMENT, CONTROLLER + } + + private SettingsBinding mBinding; private AudioEngine mAudio; private SettingsView mCurrentView; - private TextView mBuildText; - private ViewGroup mMainLayout; private int mViewMarginH; private int mViewMarginV; private int mRestartDialogHandle = -1; private int mAlertDialogHandle = -1; + private Accounts mAccounts; class VersionGestureListener extends GestureDetector.SimpleOnGestureListener { @@ -57,7 +73,7 @@ class VersionGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown (MotionEvent e) { - mBuildText.setText(mIsHash ? versionCodeToDate(BuildConfig.VERSION_CODE) : BuildConfig.GIT_HASH); + mBinding.buildText.setText(mIsHash ? StringUtils.versionCodeToDate(getContext(), BuildConfig.VERSION_CODE) : BuildConfig.GIT_HASH); mIsHash = !mIsHash; @@ -67,28 +83,32 @@ public boolean onDown (MotionEvent e) { public SettingsWidget(Context aContext) { super(aContext); - initialize(aContext); + initialize(); } public SettingsWidget(Context aContext, AttributeSet aAttrs) { super(aContext, aAttrs); - initialize(aContext); + initialize(); } public SettingsWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { super(aContext, aAttrs, aDefStyle); - initialize(aContext); + initialize(); } @SuppressLint("ClickableViewAccessibility") - private void initialize(Context aContext) { - inflate(aContext, R.layout.settings, this); + private void initialize() { + LayoutInflater inflater = LayoutInflater.from(getContext()); + + // Inflate this data binding layout + mBinding = DataBindingUtil.inflate(inflater, R.layout.settings, this, true); mWidgetManager.addWorldClickListener(this); - mMainLayout = findViewById(R.id.optionsLayout); - ImageButton cancelButton = findViewById(R.id.backButton); - cancelButton.setOnClickListener(v -> { + mAccounts = ((VRBrowserApplication)getContext().getApplicationContext()).getAccounts(); + mAccounts.addAccountListener(mAccountObserver); + + mBinding.backButton.setOnClickListener(v -> { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } @@ -96,14 +116,12 @@ private void initialize(Context aContext) { onDismiss(); }); - LinearLayout reportIssue = findViewById(R.id.reportIssueLayout); - reportIssue.setOnClickListener(v -> { + mBinding.reportIssueLayout.setOnClickListener(v -> { onSettingsReportClick(); onDismiss(); }); - HoneycombButton languageButton = findViewById(R.id.languageButton); - languageButton.setOnClickListener(view -> { + mBinding.languageButton.setOnClickListener(view -> { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } @@ -111,8 +129,7 @@ private void initialize(Context aContext) { onLanguageOptionsClick(); }); - HoneycombButton privacyButton = findViewById(R.id.privacyButton); - privacyButton.setOnClickListener(view -> { + mBinding.languageButton.setOnClickListener(view -> { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } @@ -120,8 +137,7 @@ private void initialize(Context aContext) { onSettingsPrivacyClick(); }); - HoneycombButton displayButton = findViewById(R.id.displayButton); - displayButton.setOnClickListener(view -> { + mBinding.displayButton.setOnClickListener(view -> { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } @@ -129,8 +145,7 @@ private void initialize(Context aContext) { onDisplayOptionsClick(); }); - HoneycombButton environmentButton = findViewById(R.id.environmentButton); - environmentButton.setOnClickListener(view -> { + mBinding.environmentButton.setOnClickListener(view -> { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } @@ -138,12 +153,11 @@ private void initialize(Context aContext) { showEnvironmentOptionsDialog(); }); - TextView versionText = findViewById(R.id.versionText); try { PackageInfo pInfo = getContext().getPackageManager().getPackageInfo(getContext().getPackageName(), 0); String app_name = getResources().getString(R.string.app_name); String[] app_name_parts = app_name.split(" "); - versionText.setText(Html.fromHtml("" + app_name_parts[0] + "" + + mBinding.versionText.setText(Html.fromHtml("" + app_name_parts[0] + "" + " " + app_name_parts[1] + " " + " " + pInfo.versionName + "", Html.FROM_HTML_MODE_LEGACY)); @@ -152,26 +166,22 @@ private void initialize(Context aContext) { e.printStackTrace(); } - mBuildText = findViewById(R.id.buildText); - mBuildText.setText(versionCodeToDate(BuildConfig.VERSION_CODE)); + mBinding.buildText.setText(StringUtils.versionCodeToDate(getContext(), BuildConfig.VERSION_CODE)); - TextView settingsMasthead = findViewById(R.id.buildText); final GestureDetector gd = new GestureDetector(getContext(), new VersionGestureListener()); - settingsMasthead.setOnTouchListener((view, motionEvent) -> { + mBinding.settingsMasthead.setOnTouchListener((view, motionEvent) -> { if (gd.onTouchEvent(motionEvent)) { return true; } return view.performClick(); }); - TextView surveyLink = findViewById(R.id.surveyLink); - surveyLink.setOnClickListener(v -> { - SessionStore.get().getActiveSession().loadUri(getResources().getString(R.string.survey_link)); + mBinding.surveyLink.setOnClickListener(v -> { + mWidgetManager.getFocusedWindow().getSession().loadUri(getResources().getString(R.string.survey_link)); exitWholeSettings(); }); - HoneycombButton reportButton = findViewById(R.id.helpButton); - reportButton.setOnClickListener(view -> { + mBinding.helpButton.setOnClickListener(view -> { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } @@ -179,8 +189,11 @@ private void initialize(Context aContext) { onDismiss(); }); - HoneycombButton developerOptionsButton = findViewById(R.id.developerOptionsButton); - developerOptionsButton.setOnClickListener(view -> { + mBinding.fxaButton.setOnClickListener(view -> + manageAccount() + ); + + mBinding.developerOptionsButton.setOnClickListener(view -> { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } @@ -188,8 +201,7 @@ private void initialize(Context aContext) { onDeveloperOptionsClick(); }); - HoneycombButton controllerOptionsButton = findViewById(R.id.controllerOptionsButton); - controllerOptionsButton.setOnClickListener(view -> { + mBinding.controllerOptionsButton.setOnClickListener(view -> { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } @@ -197,7 +209,7 @@ private void initialize(Context aContext) { showControllerOptionsDialog(); }); - mAudio = AudioEngine.fromContext(aContext); + mAudio = AudioEngine.fromContext(getContext()); mViewMarginH = mWidgetPlacement.width - WidgetPlacement.dpDimension(getContext(), R.dimen.options_width); mViewMarginH = WidgetPlacement.convertDpToPixel(getContext(), mViewMarginH); @@ -208,6 +220,7 @@ private void initialize(Context aContext) { @Override public void releaseWidget() { mWidgetManager.removeWorldClickListener(this); + mAccounts.removeAccountListener(mAccountObserver); super.releaseWidget(); } @@ -257,53 +270,97 @@ private void onSettingsReportClick() { onDismiss(); } - private void onDeveloperOptionsClick() { - showDeveloperOptionsDialog(); + private void manageAccount() { + switch(mAccounts.getAccountStatus()) { + case SIGNED_OUT: + case NEEDS_RECONNECT: + mAccounts.getAuthenticationUrlAsync().thenAcceptAsync((url) -> { + if (url != null) { + post(() -> { + mAccounts.setLoginOrigin(Accounts.LoginOrigin.SETTINGS); + SessionStore.get().getActiveSession().loadUri(url); + hide(REMOVE_WIDGET); + }); + } + }); + break; + + case SIGNED_IN: + post(this::showFXAOptionsDialog); + break; + } } - private void onLanguageOptionsClick() { - showLanguageOptionsDialog(); + private void updateCurrentAccountState() { + switch(mAccounts.getAccountStatus()) { + case NEEDS_RECONNECT: + mBinding.fxaButton.setText(R.string.settings_fxa_account_reconnect); + break; + + case SIGNED_IN: + mBinding.fxaButton.setText(R.string.settings_fxa_account_manage); + updateProfile(mAccounts.accountProfile()); + break; + + case SIGNED_OUT: + mBinding.fxaButton.setText(R.string.settings_fxa_account_sign_in); + updateProfile(mAccounts.accountProfile()); + break; + } } - private void onDisplayOptionsClick() { - showDisplayOptionsDialog(); - } + private AccountObserver mAccountObserver = new AccountObserver() { - /** - * The version code is composed like: yDDDHHmm - * * y = Double digit year, with 16 substracted: 2017 -> 17 -> 1 - * * DDD = Day of the year, pad with zeros if needed: September 6th -> 249 - * * HH = Hour in day (00-23) - * * mm = Minute in hour - * - * For September 6th, 2017, 9:41 am this will generate the versionCode: 12490941 (1-249-09-41). - * - * For local debug builds we use a fixed versionCode to not mess with the caching mechanism of the build - * system. The fixed local build number is 1. - * - * @param aVersionCode Application version code minus the leading architecture digit. - * @return String The converted date in the format yyyy-MM-dd - */ - private String versionCodeToDate(final int aVersionCode) { - String versionCode = Integer.toString(aVersionCode); - - String formatted; - try { - int year = Integer.parseInt(versionCode.substring(0, 1)) + 2016; - int dayOfYear = Integer.parseInt(versionCode.substring(1, 4)); + @Override + public void onAuthenticated(@NotNull OAuthAccount oAuthAccount, @NotNull AuthType authType) { + + } - GregorianCalendar cal = (GregorianCalendar)GregorianCalendar.getInstance(); - cal.set(Calendar.YEAR, year); - cal.set(Calendar.DAY_OF_YEAR, dayOfYear); + @Override + public void onProfileUpdated(@NotNull Profile profile) { + updateProfile(profile); + } - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); - formatted = format.format(cal.getTime()); + @Override + public void onLoggedOut() { + post(() -> mBinding.fxaButton.setText(R.string.settings_fxa_account_sign_in)); + } - } catch (StringIndexOutOfBoundsException e) { - formatted = getContext().getString(R.string.settings_version_developer); + @Override + public void onAuthenticationProblems() { + post(() -> mBinding.fxaButton.setText(R.string.settings_fxa_account_reconnect)); } + }; + + private void updateProfile(Profile profile) { + if (profile != null) { + ThreadUtils.postToBackgroundThread(() -> { + try { + URL url = new URL(profile.getAvatar().getUrl()); + Drawable picture = Drawable.createFromStream(url.openStream(), "src"); + post(() -> mBinding.fxaButton.setImageDrawable(picture)); + + } catch (IOException e) { + e.printStackTrace(); - return formatted; + } + }); + + } else { + mBinding.fxaButton.setImageDrawable(getContext().getDrawable(R.drawable.ic_icon_settings_sign_in)); + } + } + + private void onDeveloperOptionsClick() { + showDeveloperOptionsDialog(); + } + + private void onLanguageOptionsClick() { + showLanguageOptionsDialog(); + } + + private void onDisplayOptionsClick() { + showDisplayOptionsDialog(); } public void showView(SettingsView aView) { @@ -325,12 +382,18 @@ public void showView(SettingsView aView) { this.addView(mCurrentView, params); mCurrentView.setDelegate(this); mCurrentView.onShown(); - mMainLayout.setVisibility(View.GONE); + mBinding.optionsLayout.setVisibility(View.GONE); + } else { - mMainLayout.setVisibility(View.VISIBLE); + mBinding.optionsLayout.setVisibility(View.VISIBLE); + updateCurrentAccountState(); } } + private void showPrivacyOptionsDialog() { + showView(new PrivacyOptionsView(getContext(), mWidgetManager)); + } + private void showDeveloperOptionsDialog() { showView(new DeveloperOptionsView(getContext(), mWidgetManager)); } @@ -353,6 +416,10 @@ private void showEnvironmentOptionsDialog() { showView(new EnvironmentOptionsView(getContext(), mWidgetManager)); } + private void showFXAOptionsDialog() { + showView(new FxAAccountOptionsView(getContext(), mWidgetManager)); + } + // WindowManagerDelegate.FocusChangeListener @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { @@ -363,11 +430,41 @@ public void onGlobalFocusChanged(View oldFocus, View newFocus) { } } + public void show(@ShowFlags int aShowFlags, @NonNull SettingDialog settingDialog) { + show(aShowFlags); + + switch (settingDialog) { + case LANGUAGE: + showLanguageOptionsDialog(); + break; + case DISPLAY: + showDisplayOptionsDialog(); + break; + case PRIVACY: + showPrivacyOptionsDialog(); + break; + case DEVELOPER: + showDeveloperOptionsDialog(); + break; + case FXA: + showFXAOptionsDialog(); + break; + case ENVIRONMENT: + showEnvironmentOptionsDialog(); + break; + case CONTROLLER: + showControllerOptionsDialog(); + break; + } + } + @Override public void show(@ShowFlags int aShowFlags) { super.show(aShowFlags); mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); + + updateCurrentAccountState(); } @Override @@ -446,4 +543,5 @@ private boolean isLanguagesSubView(View view) { return false; } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/utils/StringUtils.java b/app/src/common/shared/org/mozilla/vrbrowser/utils/StringUtils.java index 4a1dee008..229d1fce9 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/utils/StringUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/utils/StringUtils.java @@ -7,6 +7,11 @@ import androidx.annotation.NonNull; +import org.mozilla.vrbrowser.R; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.GregorianCalendar; import java.util.Locale; public class StringUtils { @@ -67,4 +72,46 @@ public static boolean contains(String[] aTarget, String aText) { return false; } + + /** + * The version code is composed like: yDDDHHmm + * * y = Double digit year, with 16 substracted: 2017 -> 17 -> 1 + * * DDD = Day of the year, pad with zeros if needed: September 6th -> 249 + * * HH = Hour in day (00-23) + * * mm = Minute in hour + * + * For September 6th, 2017, 9:41 am this will generate the versionCode: 12490941 (1-249-09-41). + * + * For local debug builds we use a fixed versionCode to not mess with the caching mechanism of the build + * system. The fixed local build number is 1. + * + * @param aVersionCode Application version code minus the leading architecture digit. + * @return String The converted date in the format yyyy-MM-dd + */ + public static String versionCodeToDate(final @NonNull Context context, final int aVersionCode) { + String versionCode = Integer.toString(aVersionCode); + + String formatted; + try { + int year = Integer.parseInt(versionCode.substring(0, 1)) + 2016; + int dayOfYear = Integer.parseInt(versionCode.substring(1, 4)); + + GregorianCalendar cal = (GregorianCalendar)GregorianCalendar.getInstance(); + cal.set(Calendar.YEAR, year); + cal.set(Calendar.DAY_OF_YEAR, dayOfYear); + + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + formatted = format.format(cal.getTime()); + + } catch (StringIndexOutOfBoundsException e) { + formatted = context.getString(R.string.settings_version_developer); + } + + return formatted; + } + + @NonNull + public static String capitalize(@NonNull String input) { + return input.substring(0, 1).toUpperCase() + input.substring(1); + } } diff --git a/app/src/main/res/color/library_panel_button_text_color.xml b/app/src/main/res/color/library_panel_button_text_color.xml index 7b0eb1784..daf8f8ebd 100644 --- a/app/src/main/res/color/library_panel_button_text_color.xml +++ b/app/src/main/res/color/library_panel_button_text_color.xml @@ -1,7 +1,7 @@ + diff --git a/app/src/main/res/color/library_panel_folder_title_text_color.xml b/app/src/main/res/color/library_panel_folder_title_text_color.xml new file mode 100644 index 000000000..167bea67d --- /dev/null +++ b/app/src/main/res/color/library_panel_folder_title_text_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/content_panel_button_background.xml b/app/src/main/res/drawable/content_panel_button_background.xml index a61ea98be..bb5a6bc66 100644 --- a/app/src/main/res/drawable/content_panel_button_background.xml +++ b/app/src/main/res/drawable/content_panel_button_background.xml @@ -2,6 +2,12 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_icon_settings_sign_in.xml b/app/src/main/res/drawable/ic_icon_settings_sign_in.xml new file mode 100644 index 000000000..3702585ac --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_settings_sign_in.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_icon_tray_tabs.xml b/app/src/main/res/drawable/ic_icon_tray_tabs.xml index 81b6f0d14..d5be66ce6 100644 --- a/app/src/main/res/drawable/ic_icon_tray_tabs.xml +++ b/app/src/main/res/drawable/ic_icon_tray_tabs.xml @@ -4,6 +4,6 @@ android:viewportWidth="200" android:viewportHeight="200"> diff --git a/app/src/main/res/layout/bookmark_item.xml b/app/src/main/res/layout/bookmark_item.xml index 25cdc679d..d1a57fbee 100644 --- a/app/src/main/res/layout/bookmark_item.xml +++ b/app/src/main/res/layout/bookmark_item.xml @@ -5,21 +5,20 @@ - + type="org.mozilla.vrbrowser.ui.adapters.Bookmark" /> @@ -27,6 +26,8 @@ android:layout_width="match_parent" android:layout_height="@dimen/library_item_row_height" app:layout_height="@{isNarrow ? @dimen/library_item_row_height_narrow : @dimen/library_item_row_height}" + app:leftMargin="@{item.level*100}" + android:layout_marginEnd="20dp" android:background="@color/void_color"> @@ -80,7 +81,8 @@ android:text="@{item.url}" android:textColor="@color/library_panel_description_color" android:textSize="@dimen/library_item_url_text_size" - tools:text="http://mozilla.org" /> + tools:text="http://mozilla.org" + android:visibility="visible"/> + android:gravity="center_vertical" + android:visibility="visible"> + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/bookmark_separator.xml b/app/src/main/res/layout/bookmark_separator.xml new file mode 100644 index 000000000..47ea7c6cc --- /dev/null +++ b/app/src/main/res/layout/bookmark_separator.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/bookmarks.xml b/app/src/main/res/layout/bookmarks.xml index cf36205ab..dee36fd90 100644 --- a/app/src/main/res/layout/bookmarks.xml +++ b/app/src/main/res/layout/bookmarks.xml @@ -4,6 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + + + + + + + + @@ -80,46 +101,42 @@ android:visibility="gone" app:visibleGone="@{isLoading}" /> - - - - - - + app:isLoading="@{isLoading}" + app:isEmpty="@{isEmpty}" + app:isSignedIn="@{isSignedIn}" + app:isSyncEnabled="@{isSyncEnabled}" + app:lastSync="@{lastSync}" + app:isSyncing="@{isSyncing}" + app:isNarrow="@{isNarrow}" + app:callback="@{callback}"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +