Skip to content
This repository has been archived by the owner on Jul 22, 2024. It is now read-only.

Commit

Permalink
Combined PR: FxA+Sync+Send Tab integration & Bookmarks navigation (#1417
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
Grisha Kruglov authored and keianhzo committed Oct 11, 2019
1 parent 4bf5938 commit 0820e73
Show file tree
Hide file tree
Showing 15 changed files with 451 additions and 20 deletions.
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -432,10 +432,14 @@ dependencies {
implementation deps.android_components.browser_search
implementation deps.android_components.browser_storage
implementation deps.android_components.browser_domains
implementation deps.android_components.service_accounts
implementation deps.android_components.ui_autocomplete
implementation deps.android_components.concept_fetch
implementation deps.android_components.lib_fetch

// TODO this should not be necessary at all, see Services.kt
implementation deps.work.runtime

// Kotlin dependency
implementation deps.kotlin.stdlib
implementation deps.kotlin.coroutines
Expand Down
38 changes: 38 additions & 0 deletions app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
import org.mozilla.vrbrowser.utils.ServoUtils;
import org.mozilla.vrbrowser.utils.SystemUtils;

import mozilla.components.concept.sync.AccountObserver;
import mozilla.components.concept.sync.OAuthAccount;
import mozilla.components.concept.sync.Profile;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
Expand Down Expand Up @@ -181,6 +185,23 @@ public void onGlobalFocusChanged(View oldFocus, View newFocus) {
}
};

private AccountObserver accountObserver = new AccountObserver() {
@Override
public void onLoggedOut() {}

@Override
public void onAuthenticated(@NonNull OAuthAccount account) {
// Check if we have any new device events (e.g. tabs).
account.deviceConstellation().refreshDeviceStateAsync();
}

@Override
public void onProfileUpdated(@NonNull Profile profile) {}

@Override
public void onAuthenticationProblems() {}
};

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(LocaleUtils.setLocale(base));
Expand Down Expand Up @@ -263,6 +284,12 @@ protected void onCreate(Bundle savedInstanceState) {
mConnectivityReceiver = new ConnectivityReceiver();
mPoorPerformanceWhiteList = new HashSet<>();
checkForCrash();

// Monitor FxA account state.
((VRBrowserApplication) this.getApplicationContext())
.getServices()
.getAccountManager()
.register(accountObserver);
}

protected void initializeWidgets() {
Expand Down Expand Up @@ -385,6 +412,17 @@ protected void onResume() {
}
handleConnectivityChange();
mConnectivityReceiver.register(this, () -> runOnUiThread(() -> handleConnectivityChange()));

// If we're signed-in, poll for any new device events (e.g. received tabs) on activity resume.
// There's no push support right now, so this helps with the perception of speedy tab delivery.
OAuthAccount account = ((VRBrowserApplication) this.getApplicationContext())
.getServices()
.getAccountManager()
.authenticatedAccount();
if (account != null) {
account.deviceConstellation().refreshDeviceStateAsync();
}

super.onResume();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
import android.content.res.Configuration;

import org.mozilla.vrbrowser.browser.Places;
import org.mozilla.vrbrowser.browser.Services;
import org.mozilla.vrbrowser.db.AppDatabase;
import org.mozilla.vrbrowser.telemetry.TelemetryWrapper;
import org.mozilla.vrbrowser.utils.LocaleUtils;

public class VRBrowserApplication extends Application {

private AppExecutors mAppExecutors;
private Services mServices;
private Places mPlaces;

@Override
Expand All @@ -24,6 +27,7 @@ public void onCreate() {

mAppExecutors = new AppExecutors();
mPlaces = new Places(this);
mServices = new Services(this, mPlaces);

TelemetryWrapper.init(this);
}
Expand All @@ -40,6 +44,18 @@ public void onConfigurationChanged(Configuration newConfig) {
LocaleUtils.setLocale(this);
}

public AppDatabase getDatabase() {
return AppDatabase.getInstance(this, mAppExecutors);
}

public DataRepository getRepository() {
return DataRepository.getInstance(getDatabase(), mAppExecutors);
}

public Services getServices() {
return mServices;
}

public Places getPlaces() {
return mPlaces;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,73 @@ package org.mozilla.vrbrowser.browser
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.lifecycle.ProcessLifecycleOwner
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.future.future
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.vrbrowser.VRBrowserApplication
import java.util.concurrent.CompletableFuture
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.service.fxa.sync.SyncStatusObserver
import org.mozilla.vrbrowser.R

const val DESKTOP_ROOT = "fake_desktop_root"

class BookmarksStore constructor(val context: Context) {
companion object {
private val coreRoots = listOf(
DESKTOP_ROOT,
BookmarkRoot.Mobile.id,
BookmarkRoot.Unfiled.id,
BookmarkRoot.Toolbar.id,
BookmarkRoot.Menu.id
)

@JvmStatic
fun allowDeletion(guid: String): Boolean {
return coreRoots.contains(guid)
}

/**
* User-friendly titles for various internal bookmark folders.
*/
fun rootTitles(context: Context): Map<String, String> {
return mapOf(
// "Virtual" desktop folder.
DESKTOP_ROOT to context.getString(R.string.bookmarks_desktop_folder_title),
// Our main root, in actuality the "mobile" root:
BookmarkRoot.Mobile.id to context.getString(R.string.bookmarks_title),
// What we consider the "desktop" roots:
BookmarkRoot.Menu.id to context.getString(R.string.bookmarks_desktop_menu_title),
BookmarkRoot.Toolbar.id to context.getString(R.string.bookmarks_desktop_toolbar_title),
BookmarkRoot.Unfiled.id to context.getString(R.string.bookmarks_desktop_unfiled_title)
)
}
}

private val listeners = ArrayList<BookmarkListener>()
private val storage = (context.applicationContext as VRBrowserApplication).places.bookmarks
private val titles = rootTitles(context)

// Bookmarks might have changed during sync, so notify our listeners.
private val syncStatusObserver = object : SyncStatusObserver {
override fun onStarted() {}

override fun onIdle() {
Log.d("BookmarksStore", "Detected that sync is finished, notifying listeners")
notifyListeners()
}

override fun onError(error: Exception?) {}
}

init {
(context.applicationContext as VRBrowserApplication).services.accountManager.registerForSyncEvents(
syncStatusObserver, ProcessLifecycleOwner.get(), false
)
}

interface BookmarkListener {
fun onBookmarksUpdated()
Expand All @@ -38,8 +95,37 @@ class BookmarksStore constructor(val context: Context) {
listeners.clear()
}

fun getBookmarks(): CompletableFuture<List<BookmarkNode>?> = GlobalScope.future {
storage.getTree(BookmarkRoot.Mobile.id)?.children?.toMutableList()
fun getBookmarks(guid: String): CompletableFuture<List<BookmarkNode>?> = 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 {
Expand Down
117 changes: 117 additions & 0 deletions app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.vrbrowser.browser

import android.content.Context
import android.net.Uri
import android.os.Build
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
import androidx.work.WorkManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.concept.sync.DeviceCapability
import mozilla.components.concept.sync.DeviceEvent
import mozilla.components.concept.sync.DeviceEventsObserver
import mozilla.components.concept.sync.DeviceType
import mozilla.components.service.fxa.DeviceConfig
import mozilla.components.service.fxa.ServerConfig
import mozilla.components.service.fxa.SyncConfig
import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.base.log.sink.AndroidLogSink
import org.mozilla.vrbrowser.browser.engine.SessionStore
import java.lang.IllegalStateException

class Services(context: Context, places: Places) {
companion object {
// TODO this is from a sample app, get a real client id before shipping.
const val CLIENT_ID = "3c49430b43dfba77"
const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID"
}

// This makes bookmarks storage accessible to background sync workers.
init {
// Make sure we get logs out of our android-components.
Log.addSink(AndroidLogSink())

GlobalSyncableStoreProvider.configureStore("bookmarks" to places.bookmarks)
GlobalSyncableStoreProvider.configureStore("history" to places.history)

// TODO this really shouldn't be necessary, since WorkManager auto-initializes itself, unless
// auto-initialization is disabled in the manifest file. We don't disable the initialization,
// but i'm seeing crashes locally because WorkManager isn't initialized correctly...
// Maybe this is a race of sorts? We're trying to access it before it had a chance to auto-initialize?
// It's not well-documented _when_ that auto-initialization is supposed to happen.

// For now, let's just manually initialize it here, and swallow failures (it's already initialized).
try {
WorkManager.initialize(
context,
Configuration.Builder().setMinimumLoggingLevel(android.util.Log.INFO).build()
)
} catch (e: IllegalStateException) {}
}

// Process received device events, only handling received tabs for now.
// They'll come from other FxA devices (e.g. Firefox Desktop).
private val deviceEventObserver = object : DeviceEventsObserver {
private val logTag = "DeviceEventsObserver"

override fun onEvents(events: List<DeviceEvent>) {
CoroutineScope(Dispatchers.Main).launch {
Logger(logTag).info("Received ${events.size} device event(s)")
events.filterIsInstance(DeviceEvent.TabReceived::class.java).forEach {
// Just load the first tab that was sent.
// TODO is there a notifications API of sorts here?
SessionStore.get().activeStore.loadUri(it.entries[0].url)
}
}
}
}

val accountManager = FxaAccountManager(
context = context,
serverConfig = ServerConfig.release(CLIENT_ID, REDIRECT_URL),
deviceConfig = DeviceConfig(
// This is a default name, and can be changed once user is logged in.
// E.g. accountManager.authenticatedAccount()?.deviceConstellation()?.setDeviceNameAsync("new name")
name = "Firefox Reality on ${Build.MANUFACTURER} ${Build.MODEL}",
// TODO need a new device type! "VR"
type = DeviceType.MOBILE,
capabilities = setOf(DeviceCapability.SEND_TAB)
),
// If background syncing is desired, pass in a 'syncPeriodInMinutes' parameter.
// As-is, sync will run on app startup.
syncConfig = SyncConfig(setOf("bookmarks", "history"))
).also {
it.registerForDeviceEvents(deviceEventObserver, ProcessLifecycleOwner.get(), true)
}

init {
CoroutineScope(Dispatchers.Main).launch {
accountManager.initAsync().await()
}
}

/**
* Call this for every loaded URL to enable FxA sign-in to finish. It's a bit of a hack, but oh well.
*/
fun interceptFxaUrl(uri: String) {
if (!uri.startsWith(REDIRECT_URL)) return
val parsedUri = Uri.parse(uri)

parsedUri.getQueryParameter("code")?.let { code ->
val state = parsedUri.getQueryParameter("state") as String

// Notify the state machine about our success.
accountManager.finishAuthenticationAsync(code, state)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.mozilla.geckoview.MediaElement;
import org.mozilla.geckoview.WebRequestError;
import org.mozilla.vrbrowser.R;
import org.mozilla.vrbrowser.VRBrowserApplication;
import org.mozilla.vrbrowser.browser.Media;
import org.mozilla.vrbrowser.browser.SessionChangeListener;
import org.mozilla.vrbrowser.browser.SettingsStore;
Expand Down Expand Up @@ -952,6 +953,8 @@ public void onCanGoForward(@NonNull GeckoSession aSession, boolean aCanGoForward

Log.d(LOGTAG, "onLoadRequest: " + uri);

((VRBrowserApplication) mContext.getApplicationContext()).getServices().interceptFxaUrl(uri);

String uriOverride = SessionUtils.checkYoutubeOverride(uri);
if (uriOverride != null) {
aSession.loadUri(uriOverride);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -79,7 +81,7 @@ public void setComparator(Comparator comparator) {

public CompletableFuture<List<SuggestionItem>> getBookmarkSuggestions(@NonNull List<SuggestionItem> items) {
CompletableFuture future = new CompletableFuture();
SessionStore.get().getBookmarkStore().getBookmarks().thenAcceptAsync((bookmarks) -> {
SessionStore.get().getBookmarkStore().getBookmarks(BookmarkRoot.Root.getId()).thenAcceptAsync((bookmarks) -> {
bookmarks.stream().
filter(b -> b.getUrl().toLowerCase().contains(mFilterText) ||
b.getTitle().toLowerCase().contains(mFilterText))
Expand Down
Loading

0 comments on commit 0820e73

Please sign in to comment.