summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods')
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/AddBookmark.java30
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java296
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/ShareMethod.java82
3 files changed, 408 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/AddBookmark.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/AddBookmark.java
new file mode 100644
index 000000000..71931e683
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/AddBookmark.java
@@ -0,0 +1,30 @@
+/* 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.gecko.overlays.service.sharemethods;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.LocalBrowserDB;
+import org.mozilla.gecko.overlays.service.ShareData;
+
+public class AddBookmark extends ShareMethod {
+ private static final String LOGTAG = "GeckoAddBookmark";
+
+ @Override
+ public Result handle(ShareData shareData) {
+ ContentResolver resolver = context.getContentResolver();
+
+ LocalBrowserDB browserDB = new LocalBrowserDB(GeckoProfile.DEFAULT_PROFILE);
+ browserDB.addBookmark(resolver, shareData.title, shareData.url);
+
+ return Result.SUCCESS;
+ }
+
+ public AddBookmark(Context context) {
+ super(context);
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java
new file mode 100644
index 000000000..5abcbd99f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java
@@ -0,0 +1,296 @@
+/* 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.gecko.overlays.service.sharemethods;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.RemoteClient;
+import org.mozilla.gecko.db.TabsAccessor;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.overlays.OverlayConstants;
+import org.mozilla.gecko.overlays.service.ShareData;
+import org.mozilla.gecko.sync.CommandProcessor;
+import org.mozilla.gecko.sync.CommandRunner;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.SyncConfiguration;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * ShareMethod implementation to handle Sync's "Send tab to device" mechanism.
+ * See OverlayConstants for documentation of OverlayIntentHandler service intent API (which is how
+ * this class is chiefly interacted with).
+ */
+public class SendTab extends ShareMethod {
+ private static final String LOGTAG = "GeckoSendTab";
+
+ // Key used in the extras Bundle in the share intent used for a send tab ShareMethod.
+ public static final String SEND_TAB_TARGET_DEVICES = "SEND_TAB_TARGET_DEVICES";
+
+ // Key used in broadcast intent from SendTab ShareMethod specifying available RemoteClients.
+ public static final String EXTRA_REMOTE_CLIENT_RECORDS = "RECORDS";
+
+ // The intent we should dispatch when the button for this ShareMethod is tapped, instead of
+ // taking the normal action (e.g., "Set up Sync!")
+ public static final String OVERRIDE_INTENT = "OVERRIDE_INTENT";
+
+ private Set<String> validGUIDs;
+
+ // A TabSender appropriate to the account type we're connected to.
+ private TabSender tabSender;
+
+ @Override
+ public Result handle(ShareData shareData) {
+ if (shareData.extra == null) {
+ Log.e(LOGTAG, "No target devices specified!");
+
+ // Retrying with an identical lack of devices ain't gonna fix it...
+ return Result.PERMANENT_FAILURE;
+ }
+
+ String[] targetGUIDs = ((Bundle) shareData.extra).getStringArray(SEND_TAB_TARGET_DEVICES);
+
+ // Ensure all target GUIDs are devices we actually know about.
+ if (!validGUIDs.containsAll(Arrays.asList(targetGUIDs))) {
+ // Find the set of invalid GUIDs to provide a nice error message.
+ Log.e(LOGTAG, "Not all provided GUIDs are real devices:");
+ for (String targetGUID : targetGUIDs) {
+ if (!validGUIDs.contains(targetGUID)) {
+ Log.e(LOGTAG, "Invalid GUID: " + targetGUID);
+ }
+ }
+
+ return Result.PERMANENT_FAILURE;
+ }
+
+ Log.i(LOGTAG, "Send tab handler invoked.");
+
+ final CommandProcessor processor = CommandProcessor.getProcessor();
+
+ final String accountGUID = tabSender.getAccountGUID();
+ Log.d(LOGTAG, "Retrieved local account GUID '" + accountGUID + "'.");
+
+ if (accountGUID == null) {
+ Log.e(LOGTAG, "Cannot determine account GUID");
+
+ // It's not completely out of the question that a background sync might come along and
+ // fix everything for us...
+ return Result.TRANSIENT_FAILURE;
+ }
+
+ // Queue up the share commands for each destination device.
+ // Remember that ShareMethod.handle is always run on the background thread, so the database
+ // access here is of no concern.
+ for (int i = 0; i < targetGUIDs.length; i++) {
+ processor.sendURIToClientForDisplay(shareData.url, targetGUIDs[i], shareData.title, accountGUID, context);
+ }
+
+ // Request an immediate sync to push these new commands to the network ASAP.
+ Log.i(LOGTAG, "Requesting immediate clients stage sync.");
+ tabSender.sync();
+
+ return Result.SUCCESS;
+ // ... Probably.
+ }
+
+ /**
+ * Get an Intent suitable for broadcasting the UI state of this ShareMethod.
+ * The caller shall populate the intent with the actual state.
+ */
+ private Intent getUIStateIntent() {
+ Intent uiStateIntent = new Intent(OverlayConstants.SHARE_METHOD_UI_EVENT);
+ uiStateIntent.putExtra(OverlayConstants.EXTRA_SHARE_METHOD, (Parcelable) Type.SEND_TAB);
+ return uiStateIntent;
+ }
+
+ /**
+ * Broadcast the given intent to any UIs that may be listening.
+ */
+ private void broadcastUIState(Intent uiStateIntent) {
+ LocalBroadcastManager.getInstance(context).sendBroadcast(uiStateIntent);
+ }
+
+ /**
+ * Load the state of the user's Firefox Sync accounts and broadcast it to any registered
+ * listeners. This will cause any UIs that may exist that depend on this information to update.
+ */
+ public SendTab(Context aContext) {
+ super(aContext);
+ // Initialise the UI state intent...
+
+ // Determine if the user has a new or old style sync account and load the available sync
+ // clients for it.
+ final AccountManager accountManager = AccountManager.get(context);
+ final Account[] fxAccounts = accountManager.getAccountsByType(FxAccountConstants.ACCOUNT_TYPE);
+
+ if (fxAccounts.length > 0) {
+ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, fxAccounts[0]);
+ if (fxAccount.getState().getNeededAction() != State.Action.None) {
+ // We have a Firefox Account, but it's definitely not able to send a tab
+ // right now. Redirect to the status activity.
+ Log.w(LOGTAG, "Firefox Account named like " + fxAccount.getObfuscatedEmail() +
+ " needs action before it can send a tab; redirecting to status activity.");
+
+ setOverrideIntentAction(FxAccountConstants.ACTION_FXA_STATUS);
+ return;
+ }
+
+ tabSender = new FxAccountTabSender(fxAccount);
+
+ updateClientList(tabSender);
+
+ Log.i(LOGTAG, "Allowing tab send for Firefox Account.");
+ registerDisplayURICommand();
+ return;
+ }
+
+ // Have registered UIs offer to set up a Firefox Account.
+ setOverrideIntentAction(FxAccountConstants.ACTION_FXA_GET_STARTED);
+ }
+
+ /**
+ * Load the list of Sync clients that are not this device using the given TabSender.
+ */
+ private void updateClientList(TabSender tabSender) {
+ Collection<RemoteClient> otherClients = getOtherClients(tabSender);
+
+ // Put the list of RemoteClients into the uiStateIntent and broadcast it.
+ RemoteClient[] records = new RemoteClient[otherClients.size()];
+ records = otherClients.toArray(records);
+
+ validGUIDs = new HashSet<>();
+
+ for (RemoteClient client : otherClients) {
+ validGUIDs.add(client.guid);
+ }
+
+ if (validGUIDs.isEmpty()) {
+ // Guess we'd better override. We have no clients.
+ // This does the broadcast for us.
+ setOverrideIntentAction(FxAccountConstants.ACTION_FXA_GET_STARTED);
+ return;
+ }
+
+ Intent uiStateIntent = getUIStateIntent();
+ uiStateIntent.putExtra(EXTRA_REMOTE_CLIENT_RECORDS, records);
+ broadcastUIState(uiStateIntent);
+ }
+
+ /**
+ * Record our intention to redirect the user to a different activity when they attempt to share
+ * with us, usually because we found something wrong with their Sync account (a need to login,
+ * register, etc.)
+ * This will be recorded in the OVERRIDE_INTENT field of the UI broadcast. Consumers should
+ * dispatch this intent instead of attempting to share with this ShareMethod whenever it is
+ * non-null.
+ *
+ * @param action to launch instead of invoking a share.
+ */
+ protected void setOverrideIntentAction(final String action) {
+ Intent intent = new Intent(action);
+ // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
+ // the soft keyboard not being shown for the started activity. Why, Android, why?
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ Intent uiStateIntent = getUIStateIntent();
+ uiStateIntent.putExtra(OVERRIDE_INTENT, intent);
+
+ broadcastUIState(uiStateIntent);
+ }
+
+ private static void registerDisplayURICommand() {
+ final CommandProcessor processor = CommandProcessor.getProcessor();
+ processor.registerCommand("displayURI", new CommandRunner(3) {
+ @Override
+ public void executeCommand(final GlobalSession session, List<String> args) {
+ CommandProcessor.displayURI(args, session.getContext());
+ }
+ });
+ }
+
+ /**
+ * @return A collection of unique remote clients sorted by most recently used.
+ */
+ protected Collection<RemoteClient> getOtherClients(final TabSender sender) {
+ if (sender == null) {
+ Log.w(LOGTAG, "No tab sender when fetching other client IDs.");
+ return Collections.emptyList();
+ }
+
+ final BrowserDB browserDB = BrowserDB.from(context);
+ final TabsAccessor tabsAccessor = browserDB.getTabsAccessor();
+ final Cursor remoteTabsCursor = tabsAccessor.getRemoteClientsByRecencyCursor(context);
+ try {
+ if (remoteTabsCursor.getCount() == 0) {
+ return Collections.emptyList();
+ }
+ return tabsAccessor.getClientsWithoutTabsByRecencyFromCursor(remoteTabsCursor);
+ } finally {
+ remoteTabsCursor.close();
+ }
+ }
+
+ /**
+ * Inteface for interacting with Sync accounts. Used to hide the difference in implementation
+ * between FXA and "old sync" accounts when sending tabs.
+ */
+ private interface TabSender {
+ public static final String[] STAGES_TO_SYNC = new String[] { "clients", "tabs" };
+
+ /**
+ * @return Return null if the account isn't correctly initialized. Return
+ * the account GUID otherwise.
+ */
+ String getAccountGUID();
+
+ /**
+ * Sync this account, specifying only clients and tabs as the engines to sync.
+ */
+ void sync();
+ }
+
+ private static class FxAccountTabSender implements TabSender {
+ private final AndroidFxAccount fxAccount;
+
+ public FxAccountTabSender(AndroidFxAccount fxa) {
+ fxAccount = fxa;
+ }
+
+ @Override
+ public String getAccountGUID() {
+ try {
+ final SharedPreferences prefs = fxAccount.getSyncPrefs();
+ return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Could not get Firefox Account parameters or preferences; aborting.");
+ return null;
+ }
+ }
+
+ @Override
+ public void sync() {
+ fxAccount.requestImmediateSync(STAGES_TO_SYNC, null);
+ }
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/ShareMethod.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/ShareMethod.java
new file mode 100644
index 000000000..768176d63
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/ShareMethod.java
@@ -0,0 +1,82 @@
+/*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.gecko.overlays.service.sharemethods;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+import org.mozilla.gecko.overlays.service.ShareData;
+
+/**
+ * Represents a method of sharing a URL/title. Add a bookmark? Send to a device? Add to reading list?
+ */
+public abstract class ShareMethod {
+ protected final Context context;
+
+ public ShareMethod(Context aContext) {
+ context = aContext;
+ }
+
+ /**
+ * Perform a share for the given title/URL combination. Called on the background thread by the
+ * handler service when a request is made. The "extra" parameter is provided should a ShareMethod
+ * desire to handle the share differently based on some additional parameters.
+ *
+ * @param title The page title for the page being shared. May be null if none can be found.
+ * @param url The URL of the page to be shared. Never null.
+ * @param extra A Parcelable of ShareMethod-specific parameters that may be provided by the
+ * caller. Generally null, but this field may be used to provide extra input to
+ * the ShareMethod (such as the device to share to in the case of SendTab).
+ * @return true if the attempt to share was a success. False in the event of an error.
+ */
+ public abstract Result handle(ShareData shareData);
+
+ /**
+ * Enum representing the possible results of performing a share.
+ */
+ public static enum Result {
+ // Victory!
+ SUCCESS,
+
+ // Failure, but retrying the same action again might lead to success.
+ TRANSIENT_FAILURE,
+
+ // Failure, and you're not going to succeed until you reinitialise the ShareMethod (ie.
+ // until you repeat the entire share action). Examples include broken Sync accounts, or
+ // Sync accounts with no valid target devices (so the only way to fix this is to add some
+ // and try again: pushing a retry button isn't sane).
+ PERMANENT_FAILURE
+ }
+
+ /**
+ * Enum representing types of ShareMethod. Parcelable so it may be efficiently used in Intents.
+ */
+ public static enum Type implements Parcelable {
+ ADD_BOOKMARK,
+ SEND_TAB;
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(final Parcel dest, final int flags) {
+ dest.writeInt(ordinal());
+ }
+
+ public static final Creator<Type> CREATOR = new Creator<Type>() {
+ @Override
+ public Type createFromParcel(final Parcel source) {
+ return Type.values()[source.readInt()];
+ }
+
+ @Override
+ public Type[] newArray(final int size) {
+ return new Type[size];
+ }
+ };
+ }
+}