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

Combined PR: FxA+Sync+Send Tab integration & Bookmarks navigation #1417

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -437,10 +437,14 @@ dependencies {
implementation deps.android_components.browser_search
implementation deps.android_components.browser_storage
implementation deps.android_components.browser_domains
implementation deps.android_components.service_accounts
implementation deps.android_components.ui_autocomplete
implementation deps.android_components.concept_fetch
implementation deps.android_components.lib_fetch

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

// Kotlin dependency
implementation deps.kotlin.stdlib
implementation deps.kotlin.coroutines
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 @@ -180,6 +184,23 @@ public void onGlobalFocusChanged(View oldFocus, View newFocus) {
}
};

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

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

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

@Override
public void onAuthenticationProblems() {}
};

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(LocaleUtils.setLocale(base));
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,13 +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 @@ -25,6 +27,7 @@ public void onCreate() {

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

TelemetryWrapper.init(this);
}
Expand All @@ -49,6 +52,10 @@ 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 @@ -941,6 +942,8 @@ public void onCanGoForward(@NonNull GeckoSession aSession, boolean aCanGoForward

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

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

String uriOverride = SessionUtils.checkYoutubeOverride(uri);
if (uriOverride != null) {
aSession.loadUri(uriOverride);
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