summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/overlays
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /mobile/android/base/java/org/mozilla/gecko/overlays
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/overlays')
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/OverlayConstants.java68
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/service/OverlayActionService.java126
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/service/ShareData.java48
-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
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/ui/OverlayDialogButton.java128
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabDeviceListArrayAdapter.java185
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabList.java150
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabTargetSelectedListener.java25
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/ui/ShareDialog.java493
11 files changed, 1631 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/OverlayConstants.java b/mobile/android/base/java/org/mozilla/gecko/overlays/OverlayConstants.java
new file mode 100644
index 000000000..16f5560d3
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/OverlayConstants.java
@@ -0,0 +1,68 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.gecko.overlays;
+
+/**
+ * Constants used by the share handler service (and clients).
+ * The intent API used by the service is defined herein.
+ */
+public class OverlayConstants {
+ /*
+ * OverlayIntentHandler service intent actions.
+ */
+
+ /*
+ * Causes the service to broadcast an intent containing state necessary for proper display of
+ * a UI to select a target share method.
+ *
+ * Intent parameters:
+ *
+ * None.
+ */
+ public static final String ACTION_PREPARE_SHARE = "org.mozilla.gecko.overlays.ACTION_PREPARE_SHARE";
+
+ /*
+ * Action for sharing a page.
+ *
+ * Intent parameters:
+ *
+ * $EXTRA_URL: URL of page to share. (required)
+ * $EXTRA_SHARE_METHOD: Method(s) via which to share this url/title combination. Can be either a
+ * ShareType or a ShareType[]
+ * $EXTRA_TITLE: Title of page to share (optional)
+ * $EXTRA_PARAMETERS: Parcelable of extra data to pass to the ShareMethod (optional)
+ */
+ public static final String ACTION_SHARE = "org.mozilla.gecko.overlays.ACTION_SHARE";
+
+ /*
+ * OverlayIntentHandler service intent extra field keys.
+ */
+
+ // The URL/title of the page being shared
+ public static final String EXTRA_URL = "URL";
+ public static final String EXTRA_TITLE = "TITLE";
+
+ // The optional extra Parcelable parameters for a ShareMethod.
+ public static final String EXTRA_PARAMETERS = "EXTRA";
+
+ // The extra field key used for holding the ShareMethod.Type we wish to use for an operation.
+ public static final String EXTRA_SHARE_METHOD = "SHARE_METHOD";
+
+ /*
+ * ShareMethod UI event intent constants. Broadcast by ShareMethods using LocalBroadcastManager
+ * when state has changed that requires an update of any currently-displayed share UI.
+ */
+
+ /*
+ * Action for a ShareMethod UI event.
+ *
+ * Intent parameters:
+ *
+ * $EXTRA_SHARE_METHOD: The ShareType to which this event relates.
+ * ... ShareType-specific parameters as desired... (optional)
+ */
+ public static final String SHARE_METHOD_UI_EVENT = "org.mozilla.gecko.overlays.ACTION_SHARE_METHOD_UI_EVENT";
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/OverlayActionService.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/OverlayActionService.java
new file mode 100644
index 000000000..7182fcce7
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/service/OverlayActionService.java
@@ -0,0 +1,126 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.gecko.overlays.service;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+import org.mozilla.gecko.overlays.service.sharemethods.AddBookmark;
+import org.mozilla.gecko.overlays.service.sharemethods.SendTab;
+import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.util.EnumMap;
+import java.util.Map;
+
+import static org.mozilla.gecko.overlays.OverlayConstants.ACTION_PREPARE_SHARE;
+import static org.mozilla.gecko.overlays.OverlayConstants.ACTION_SHARE;
+
+/**
+ * A service to receive requests from overlays to perform actions.
+ * See OverlayConstants for details of the intent API supported by this service.
+ *
+ * Currently supported operations are:
+ *
+ * Add bookmark*
+ * Send tab (delegates to Sync's existing handler)
+ * Future: Load page in background.
+ *
+ * * Neither of these incur a page fetch on the service... yet. That will require headless Gecko,
+ * something we're yet to have. Refactoring Gecko as a service itself and restructing the rest of
+ * the app to talk to it seems like the way to go there.
+ */
+public class OverlayActionService extends Service {
+ private static final String LOGTAG = "GeckoOverlayService";
+
+ // Map used for selecting the appropriate helper object when handling a share.
+ final Map<ShareMethod.Type, ShareMethod> shareTypes = new EnumMap<>(ShareMethod.Type.class);
+
+ // Map relating Strings representing share types to the corresponding ShareMethods.
+ // Share methods are initialised (and shown in the UI) in the order they are given here.
+ // This map is used to look up the appropriate ShareMethod when handling a request, as well as
+ // for identifying which ShareMethod needs re-initialising in response to such an intent (which
+ // will be necessary in situations such as the deletion of Sync accounts).
+
+ // Not a bindable service.
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ return START_NOT_STICKY;
+ }
+
+ // Dispatch intent to appropriate method according to its action.
+ String action = intent.getAction();
+
+ switch (action) {
+ case ACTION_SHARE:
+ handleShare(intent);
+ break;
+ case ACTION_PREPARE_SHARE:
+ initShareMethods(getApplicationContext());
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported intent action: " + action);
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ /**
+ * Reinitialise all ShareMethods, causing them to broadcast any UI update events necessary.
+ */
+ private void initShareMethods(final Context context) {
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ shareTypes.clear();
+
+ shareTypes.put(ShareMethod.Type.ADD_BOOKMARK, new AddBookmark(context));
+ shareTypes.put(ShareMethod.Type.SEND_TAB, new SendTab(context));
+ }
+ });
+ }
+
+ public void handleShare(final Intent intent) {
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ ShareData shareData;
+ try {
+ shareData = ShareData.fromIntent(intent);
+ } catch (IllegalArgumentException e) {
+ Log.e(LOGTAG, "Error parsing share intent: ", e);
+ return;
+ }
+
+ ShareMethod shareMethod = shareTypes.get(shareData.shareMethodType);
+
+ final ShareMethod.Result result = shareMethod.handle(shareData);
+ // Dispatch the share to the targeted ShareMethod.
+ switch (result) {
+ case SUCCESS:
+ Log.d(LOGTAG, "Share was successful");
+ break;
+ case TRANSIENT_FAILURE:
+ // Fall-through
+ case PERMANENT_FAILURE:
+ Log.e(LOGTAG, "Share failed: " + result);
+ break;
+ default:
+ throw new IllegalStateException("Unknown share method result code: " + result);
+ }
+ }
+ });
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/ShareData.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/ShareData.java
new file mode 100644
index 000000000..df233d74a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/service/ShareData.java
@@ -0,0 +1,48 @@
+/* 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;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcelable;
+import org.mozilla.gecko.overlays.OverlayConstants;
+import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
+
+import static org.mozilla.gecko.overlays.OverlayConstants.EXTRA_SHARE_METHOD;
+
+/**
+ * Class to hold information related to a particular request to perform a share.
+ */
+public class ShareData {
+ private static final String LOGTAG = "GeckoShareRequest";
+
+ public final String url;
+ public final String title;
+ public final Parcelable extra;
+ public final ShareMethod.Type shareMethodType;
+
+ public ShareData(String url, String title, Parcelable extra, ShareMethod.Type shareMethodType) {
+ if (url == null) {
+ throw new IllegalArgumentException("Null url passed to ShareData!");
+ }
+
+ this.url = url;
+ this.title = title;
+ this.extra = extra;
+ this.shareMethodType = shareMethodType;
+ }
+
+ public static ShareData fromIntent(Intent intent) {
+ Bundle extras = intent.getExtras();
+
+ // Fish the parameters out of the Intent.
+ final String url = extras.getString(OverlayConstants.EXTRA_URL);
+ final String title = extras.getString(OverlayConstants.EXTRA_TITLE);
+ final Parcelable extra = extras.getParcelable(OverlayConstants.EXTRA_PARAMETERS);
+ ShareMethod.Type shareMethodType = (ShareMethod.Type) extras.get(EXTRA_SHARE_METHOD);
+
+ return new ShareData(url, title, extra, shareMethodType);
+ }
+}
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];
+ }
+ };
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/OverlayDialogButton.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/OverlayDialogButton.java
new file mode 100644
index 000000000..8b7bc872b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/OverlayDialogButton.java
@@ -0,0 +1,128 @@
+/* -*- 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.gecko.overlays.ui;
+
+import org.mozilla.gecko.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * A button in the share overlay, such as the "Add to Reading List" button.
+ * Has an associated icon and label, and two states: enabled and disabled.
+ *
+ * When disabled, tapping results in a "pop" animation causing the icon to pulse. When enabled,
+ * tapping calls the OnClickListener set by the consumer in the usual way.
+ */
+public class OverlayDialogButton extends LinearLayout {
+ private static final String LOGTAG = "GeckoOverlayDialogButton";
+
+ // We can't use super.isEnabled(), since we want to stay clickable in disabled state.
+ private boolean isEnabled = true;
+
+ private final ImageView iconView;
+ private final TextView labelView;
+
+ private String enabledText = "";
+ private String disabledText = "";
+
+ private OnClickListener enabledOnClickListener;
+
+ public OverlayDialogButton(Context context) {
+ this(context, null);
+ }
+
+ public OverlayDialogButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ setOrientation(LinearLayout.HORIZONTAL);
+
+ LayoutInflater.from(context).inflate(R.layout.overlay_share_button, this);
+
+ iconView = (ImageView) findViewById(R.id.overlaybtn_icon);
+ labelView = (TextView) findViewById(R.id.overlaybtn_label);
+
+ super.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+
+ if (isEnabled) {
+ if (enabledOnClickListener != null) {
+ enabledOnClickListener.onClick(v);
+ } else {
+ Log.e(LOGTAG, "enabledOnClickListener is null.");
+ }
+ } else {
+ Animation anim = AnimationUtils.loadAnimation(getContext(), R.anim.overlay_pop);
+ iconView.startAnimation(anim);
+ }
+ }
+ });
+
+ final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.OverlayDialogButton);
+
+ Drawable drawable = typedArray.getDrawable(R.styleable.OverlayDialogButton_drawable);
+ if (drawable != null) {
+ setDrawable(drawable);
+ }
+
+ String disabledText = typedArray.getString(R.styleable.OverlayDialogButton_disabledText);
+ if (disabledText != null) {
+ this.disabledText = disabledText;
+ }
+
+ String enabledText = typedArray.getString(R.styleable.OverlayDialogButton_enabledText);
+ if (enabledText != null) {
+ this.enabledText = enabledText;
+ }
+
+ typedArray.recycle();
+
+ setEnabled(true);
+ }
+
+ public void setDrawable(Drawable drawable) {
+ iconView.setImageDrawable(drawable);
+ }
+
+ public void setText(String text) {
+ labelView.setText(text);
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener listener) {
+ enabledOnClickListener = listener;
+ }
+
+ /**
+ * Set the enabledness state of this view. We don't call super.setEnabled, as we want to remain
+ * clickable even in the disabled state (but with a different click listener).
+ */
+ @Override
+ public void setEnabled(boolean enabled) {
+ isEnabled = enabled;
+ iconView.setEnabled(enabled);
+ labelView.setEnabled(enabled);
+
+ if (enabled) {
+ setText(enabledText);
+ } else {
+ setText(disabledText);
+ }
+ }
+
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabDeviceListArrayAdapter.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabDeviceListArrayAdapter.java
new file mode 100644
index 000000000..08e9c59f5
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabDeviceListArrayAdapter.java
@@ -0,0 +1,185 @@
+/* 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.ui;
+
+import java.util.Collection;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.RemoteClient;
+import org.mozilla.gecko.overlays.ui.SendTabList.State;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+public class SendTabDeviceListArrayAdapter extends ArrayAdapter<RemoteClient> {
+ @SuppressWarnings("unused")
+ private static final String LOGTAG = "GeckoSendTabAdapter";
+
+ private State currentState;
+
+ // String to display when in a "button-like" special state. Instead of using a
+ // RemoteClient we override the rendering using this string.
+ private String dummyRecordName;
+
+ private final SendTabTargetSelectedListener listener;
+
+ private Collection<RemoteClient> records;
+
+ // The AlertDialog to show in the event the record is pressed while in the SHOW_DEVICES state.
+ // This will show the user a prompt to select a device from a longer list of devices.
+ private AlertDialog dialog;
+
+ public SendTabDeviceListArrayAdapter(Context context, SendTabTargetSelectedListener aListener) {
+ super(context, R.layout.overlay_share_send_tab_item, R.id.overlaybtn_label);
+
+ listener = aListener;
+
+ // We do this manually and avoid multiple notifications when doing compound operations.
+ setNotifyOnChange(false);
+ }
+
+ /**
+ * Get an array of the contents of this adapter were it in the LIST state.
+ * Useful for determining the "real" contents of the adapter.
+ */
+ public RemoteClient[] toArray() {
+ return records.toArray(new RemoteClient[records.size()]);
+ }
+
+ public void setRemoteClientsList(Collection<RemoteClient> remoteClientsList) {
+ records = remoteClientsList;
+ updateRecordList();
+ }
+
+ /**
+ * Ensure the contents of the Adapter are synchronised with the `records` field. This may not
+ * be the case if records has recently changed, or if we have experienced a state change.
+ */
+ public void updateRecordList() {
+ if (currentState != State.LIST) {
+ return;
+ }
+
+ clear();
+
+ setNotifyOnChange(false); // So we don't notify for each add.
+ addAll(records);
+
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ final Context context = getContext();
+
+ // Reuse View objects if they exist.
+ OverlayDialogButton row = (OverlayDialogButton) convertView;
+ if (row == null) {
+ row = (OverlayDialogButton) View.inflate(context, R.layout.overlay_share_send_tab_item, null);
+ }
+
+ // The first view in the list has a unique style.
+ if (position == 0) {
+ row.setBackgroundResource(R.drawable.overlay_share_button_background_first);
+ } else {
+ row.setBackgroundResource(R.drawable.overlay_share_button_background);
+ }
+
+ if (currentState != State.LIST) {
+ // If we're in a special "Button-like" state, use the override string and a generic icon.
+ final Drawable sendTabIcon = context.getResources().getDrawable(R.drawable.shareplane);
+ row.setText(dummyRecordName);
+ row.setDrawable(sendTabIcon);
+ }
+
+ // If we're just a button to launch the dialog, set the listener and abort.
+ if (currentState == State.SHOW_DEVICES) {
+ row.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ dialog.show();
+ }
+ });
+
+ return row;
+ }
+
+ // The remaining states delegate to the SentTabTargetSelectedListener.
+ final RemoteClient remoteClient = getItem(position);
+ if (currentState == State.LIST) {
+ final Drawable clientIcon = context.getResources().getDrawable(getImage(remoteClient));
+ row.setText(remoteClient.name);
+ row.setDrawable(clientIcon);
+
+ final String listenerGUID = remoteClient.guid;
+
+ row.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ listener.onSendTabTargetSelected(listenerGUID);
+ }
+ });
+ } else {
+ row.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ listener.onSendTabActionSelected();
+ }
+ });
+ }
+
+ return row;
+ }
+
+ private static int getImage(RemoteClient record) {
+ if ("mobile".equals(record.deviceType)) {
+ return R.drawable.device_mobile;
+ }
+
+ return R.drawable.device_desktop;
+ }
+
+ public void switchState(State newState) {
+ if (currentState == newState) {
+ return;
+ }
+
+ currentState = newState;
+
+ switch (newState) {
+ case LIST:
+ updateRecordList();
+ break;
+ case NONE:
+ showDummyRecord(getContext().getResources().getString(R.string.overlay_share_send_tab_btn_label));
+ break;
+ case SHOW_DEVICES:
+ showDummyRecord(getContext().getResources().getString(R.string.overlay_share_send_other));
+ break;
+ default:
+ throw new IllegalStateException("Unexpected state transition: " + newState);
+ }
+ }
+
+ /**
+ * Set the dummy override string to the given value and clear the list.
+ */
+ private void showDummyRecord(String name) {
+ dummyRecordName = name;
+ clear();
+ add(null);
+ notifyDataSetChanged();
+ }
+
+ public void setDialog(AlertDialog aDialog) {
+ dialog = aDialog;
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabList.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabList.java
new file mode 100644
index 000000000..4fc6caaa9
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabList.java
@@ -0,0 +1,150 @@
+/* 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.ui;
+
+import static org.mozilla.gecko.overlays.ui.SendTabList.State.LOADING;
+import static org.mozilla.gecko.overlays.ui.SendTabList.State.SHOW_DEVICES;
+
+import java.util.Arrays;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.db.RemoteClient;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.util.AttributeSet;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+/**
+ * The SendTab button has a few different states depending on the available devices (and whether
+ * we've loaded them yet...)
+ *
+ * Initially, the view resembles a disabled button. (the LOADING state)
+ * Once state is loaded from Sync's database, we know how many devices the user may send their tab
+ * to.
+ *
+ * If there are no targets, the user was found to not have a Sync account, or their Sync account is
+ * in a state that prevents it from being able to send a tab, we enter the NONE state and display
+ * a generic button which launches an appropriate activity to fix the situation when tapped (such
+ * as the set up Sync wizard).
+ *
+ * If the number of targets does not MAX_INLINE_SYNC_TARGETS, we present a button for each of them.
+ * (the LIST state)
+ *
+ * Otherwise, we enter the SHOW_DEVICES state, in which we display a "Send to other devices" button
+ * that takes the user to a menu for selecting a target device from their complete list of many
+ * devices.
+ */
+public class SendTabList extends ListView {
+ @SuppressWarnings("unused")
+ private static final String LOGTAG = "GeckoSendTabList";
+
+ // The maximum number of target devices to show in the main list. Further devices are available
+ // from a secondary menu.
+ public static final int MAXIMUM_INLINE_ELEMENTS = R.integer.number_of_inline_share_devices;
+
+ private SendTabDeviceListArrayAdapter clientListAdapter;
+
+ // Listener to fire when a share target is selected (either directly or via the prompt)
+ private SendTabTargetSelectedListener listener;
+
+ private final State currentState = LOADING;
+
+ /**
+ * Enum defining the states this view may occupy.
+ */
+ public enum State {
+ // State when no sync targets exist (a generic "Send to Firefox Sync" button which launches
+ // an activity to set it up)
+ NONE,
+
+ // As NONE, but disabled. Initial state. Used until we get information from Sync about what
+ // we really want.
+ LOADING,
+
+ // A list of devices to share to.
+ LIST,
+
+ // A single button prompting the user to select a device to share to.
+ SHOW_DEVICES
+ }
+
+ public SendTabList(Context context) {
+ super(context);
+ }
+
+ public SendTabList(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ if (!(adapter instanceof SendTabDeviceListArrayAdapter)) {
+ throw new IllegalArgumentException("adapter must be a SendTabDeviceListArrayAdapter instance");
+ }
+
+ clientListAdapter = (SendTabDeviceListArrayAdapter) adapter;
+ super.setAdapter(adapter);
+ }
+
+ public void setSendTabTargetSelectedListener(SendTabTargetSelectedListener aListener) {
+ listener = aListener;
+ }
+
+ public void switchState(State state) {
+ if (state == currentState) {
+ return;
+ }
+
+ clientListAdapter.switchState(state);
+ if (state == SHOW_DEVICES) {
+ clientListAdapter.setDialog(getDialog());
+ }
+ }
+
+ public void setSyncClients(final RemoteClient[] c) {
+ final RemoteClient[] clients = c == null ? new RemoteClient[0] : c;
+
+ clientListAdapter.setRemoteClientsList(Arrays.asList(clients));
+ }
+
+ /**
+ * Get an AlertDialog listing all devices, allowing the user to select the one they want.
+ * Used when more than MAXIMUM_INLINE_ELEMENTS devices are found (to avoid displaying them all
+ * inline and looking crazy).
+ */
+ public AlertDialog getDialog() {
+ final Context context = getContext();
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+
+ final RemoteClient[] records = clientListAdapter.toArray();
+ final String[] dialogElements = new String[records.length];
+
+ for (int i = 0; i < records.length; i++) {
+ dialogElements[i] = records[i].name;
+ }
+
+ builder.setTitle(R.string.overlay_share_select_device)
+ .setItems(dialogElements, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int index) {
+ listener.onSendTabTargetSelected(records[index].guid);
+ }
+ })
+ .setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.SHARE_OVERLAY, "device_selection_cancel");
+ }
+ });
+
+ return builder.create();
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabTargetSelectedListener.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabTargetSelectedListener.java
new file mode 100644
index 000000000..79da526da
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabTargetSelectedListener.java
@@ -0,0 +1,25 @@
+/*
+ * 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.ui;
+
+/**
+ * Interface for classes that wish to listen for the selection of an element from a SendTabList.
+ */
+public interface SendTabTargetSelectedListener {
+ /**
+ * Called when a row in the SendTabList is clicked.
+ *
+ * @param targetGUID The GUID of the ClientRecord the element represents (if any, otherwise null)
+ */
+ public void onSendTabTargetSelected(String targetGUID);
+
+ /**
+ * Called when the overall Send Tab item is clicked.
+ *
+ * This implies that the clients list was unavailable.
+ */
+ public void onSendTabActionSelected();
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/ShareDialog.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/ShareDialog.java
new file mode 100644
index 000000000..156fdda2a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/ShareDialog.java
@@ -0,0 +1,493 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.gecko.overlays.ui;
+
+import java.net.URISyntaxException;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.db.LocalBrowserDB;
+import org.mozilla.gecko.db.RemoteClient;
+import org.mozilla.gecko.overlays.OverlayConstants;
+import org.mozilla.gecko.overlays.service.OverlayActionService;
+import org.mozilla.gecko.overlays.service.sharemethods.SendTab;
+import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
+import org.mozilla.gecko.sync.setup.activities.WebURLFinder;
+import org.mozilla.gecko.util.IntentUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.UIAsyncTask;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v4.content.LocalBroadcastManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.animation.AnimationSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.TextView;
+import android.widget.Toast;
+
+/**
+ * A transparent activity that displays the share overlay.
+ */
+public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabTargetSelectedListener {
+
+ private enum State {
+ DEFAULT,
+ DEVICES_ONLY // Only display the device list.
+ }
+
+ private static final String LOGTAG = "GeckoShareDialog";
+
+ /** Flag to indicate that we should always show the device list; specific to this release channel. **/
+ public static final String INTENT_EXTRA_DEVICES_ONLY =
+ AppConstants.ANDROID_PACKAGE_NAME + ".intent.extra.DEVICES_ONLY";
+
+ /** The maximum number of devices we'll show in the dialog when in State.DEFAULT. **/
+ private static final int MAXIMUM_INLINE_DEVICES = 2;
+
+ private State state;
+
+ private SendTabList sendTabList;
+ private OverlayDialogButton bookmarkButton;
+
+ // The bookmark button drawable set from XML - we need this to reset state.
+ private Drawable bookmarkButtonDrawable;
+
+ private String url;
+ private String title;
+
+ // The override intent specified by SendTab (if any). See SendTab.java.
+ private Intent sendTabOverrideIntent;
+
+ // Flag set during animation to prevent animation multiple-start.
+ private boolean isAnimating;
+
+ // BroadcastReceiver to receive callbacks from ShareMethods which are changing state.
+ private final BroadcastReceiver uiEventListener = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ShareMethod.Type originShareMethod = intent.getParcelableExtra(OverlayConstants.EXTRA_SHARE_METHOD);
+ switch (originShareMethod) {
+ case SEND_TAB:
+ handleSendTabUIEvent(intent);
+ break;
+ default:
+ throw new IllegalArgumentException("UIEvent broadcast from ShareMethod that isn't thought to support such broadcasts.");
+ }
+ }
+ };
+
+ /**
+ * Called when a UI event broadcast is received from the SendTab ShareMethod.
+ */
+ protected void handleSendTabUIEvent(Intent intent) {
+ sendTabOverrideIntent = intent.getParcelableExtra(SendTab.OVERRIDE_INTENT);
+
+ RemoteClient[] remoteClientRecords = (RemoteClient[]) intent.getParcelableArrayExtra(SendTab.EXTRA_REMOTE_CLIENT_RECORDS);
+
+ // Escape hatch: we don't show the option to open this dialog in this state so this should
+ // never be run. However, due to potential inconsistencies in synced client state
+ // (e.g. bug 1122302 comment 47), we might fail.
+ if (state == State.DEVICES_ONLY &&
+ (remoteClientRecords == null || remoteClientRecords.length == 0)) {
+ Log.e(LOGTAG, "In state: " + State.DEVICES_ONLY + " and received 0 synced clients. Finishing...");
+ Toast.makeText(this, getResources().getText(R.string.overlay_no_synced_devices), Toast.LENGTH_SHORT)
+ .show();
+ finish();
+ return;
+ }
+
+ sendTabList.setSyncClients(remoteClientRecords);
+
+ if (state == State.DEVICES_ONLY ||
+ remoteClientRecords == null ||
+ remoteClientRecords.length <= MAXIMUM_INLINE_DEVICES) {
+ // Show the list of devices in-line.
+ sendTabList.switchState(SendTabList.State.LIST);
+
+ // The first item in the list has a unique style. If there are no items
+ // in the list, the next button appears to be the first item in the list.
+ //
+ // Note: a more thorough implementation would add this
+ // (and other non-ListView buttons) into a custom ListView.
+ if (remoteClientRecords == null || remoteClientRecords.length == 0) {
+ bookmarkButton.setBackgroundResource(
+ R.drawable.overlay_share_button_background_first);
+ }
+ return;
+ }
+
+ // Just show a button to launch the list of devices to choose from.
+ sendTabList.switchState(SendTabList.State.SHOW_DEVICES);
+ }
+
+ @Override
+ protected void onDestroy() {
+ // Remove the listener when the activity is destroyed: we no longer care.
+ // Note: The activity can be destroyed without onDestroy being called. However, this occurs
+ // only when the application is killed, something which also kills the registered receiver
+ // list, and the service, and everything else: so we don't care.
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(uiEventListener);
+
+ super.onDestroy();
+ }
+
+ /**
+ * Show a toast indicating we were started with no URL, and then stop.
+ */
+ private void abortDueToNoURL() {
+ Log.e(LOGTAG, "Unable to process shared intent. No URL found!");
+
+ // Display toast notifying the user of failure (most likely a developer who screwed up
+ // trying to send a share intent).
+ Toast toast = Toast.makeText(this, getResources().getText(R.string.overlay_share_no_url), Toast.LENGTH_SHORT);
+ toast.show();
+ finish();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.overlay_share_dialog);
+
+ LocalBroadcastManager.getInstance(this).registerReceiver(uiEventListener,
+ new IntentFilter(OverlayConstants.SHARE_METHOD_UI_EVENT));
+
+ // Send tab.
+ sendTabList = (SendTabList) findViewById(R.id.overlay_send_tab_btn);
+
+ // Register ourselves as both the listener and the context for the Adapter.
+ final SendTabDeviceListArrayAdapter adapter = new SendTabDeviceListArrayAdapter(this, this);
+ sendTabList.setAdapter(adapter);
+ sendTabList.setSendTabTargetSelectedListener(this);
+
+ bookmarkButton = (OverlayDialogButton) findViewById(R.id.overlay_share_bookmark_btn);
+
+ bookmarkButtonDrawable = bookmarkButton.getBackground();
+
+ // Bookmark button
+ bookmarkButton = (OverlayDialogButton) findViewById(R.id.overlay_share_bookmark_btn);
+ bookmarkButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ addBookmark();
+ }
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ final Intent intent = getIntent();
+
+ state = intent.getBooleanExtra(INTENT_EXTRA_DEVICES_ONLY, false) ?
+ State.DEVICES_ONLY : State.DEFAULT;
+
+ // If the Activity is being reused, we need to reset the state. Ideally, we create a
+ // new instance for each call, but Android L breaks this (bug 1137928).
+ sendTabList.switchState(SendTabList.State.LOADING);
+ bookmarkButton.setBackgroundDrawable(bookmarkButtonDrawable);
+
+ // The URL is usually hiding somewhere in the extra text. Extract it.
+ final String extraText = IntentUtils.getStringExtraSafe(intent, Intent.EXTRA_TEXT);
+ if (TextUtils.isEmpty(extraText)) {
+ abortDueToNoURL();
+ return;
+ }
+
+ final String pageUrl = new WebURLFinder(extraText).bestWebURL();
+ if (TextUtils.isEmpty(pageUrl)) {
+ abortDueToNoURL();
+ return;
+ }
+
+ // Have the service start any initialisation work that's necessary for us to show the correct
+ // UI. The results of such work will come in via the BroadcastListener.
+ Intent serviceStartupIntent = new Intent(this, OverlayActionService.class);
+ serviceStartupIntent.setAction(OverlayConstants.ACTION_PREPARE_SHARE);
+ startService(serviceStartupIntent);
+
+ // Start the slide-up animation.
+ getWindow().setWindowAnimations(0);
+ final Animation anim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_up);
+ findViewById(R.id.sharedialog).startAnimation(anim);
+
+ // If provided, we use the subject text to give us something nice to display.
+ // If not, we wing it with the URL.
+
+ // TODO: Consider polling Fennec databases to find better information to display.
+ final String subjectText = intent.getStringExtra(Intent.EXTRA_SUBJECT);
+
+ final String telemetryExtras = "title=" + (subjectText != null);
+ if (subjectText != null) {
+ ((TextView) findViewById(R.id.title)).setText(subjectText);
+ }
+
+ Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.SHARE_OVERLAY, telemetryExtras);
+
+ title = subjectText;
+ url = pageUrl;
+
+ // Set the subtitle text on the view and cause it to marquee if it's too long (which it will
+ // be, since it's a URL).
+ final TextView subtitleView = (TextView) findViewById(R.id.subtitle);
+ subtitleView.setText(pageUrl);
+ subtitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
+ subtitleView.setSingleLine(true);
+ subtitleView.setMarqueeRepeatLimit(5);
+ subtitleView.setSelected(true);
+
+ final View titleView = findViewById(R.id.title);
+
+ if (state == State.DEVICES_ONLY) {
+ bookmarkButton.setVisibility(View.GONE);
+
+ titleView.setOnClickListener(null);
+ subtitleView.setOnClickListener(null);
+ return;
+ }
+
+ bookmarkButton.setVisibility(View.VISIBLE);
+
+ // Configure buttons.
+ final View.OnClickListener launchBrowser = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ShareDialog.this.launchBrowser();
+ }
+ };
+
+ titleView.setOnClickListener(launchBrowser);
+ subtitleView.setOnClickListener(launchBrowser);
+
+ final LocalBrowserDB browserDB = new LocalBrowserDB(getCurrentProfile());
+ setButtonState(url, browserDB);
+ }
+
+ @Override
+ protected void onNewIntent(final Intent intent) {
+ super.onNewIntent(intent);
+
+ // The intent returned by getIntent is not updated automatically.
+ setIntent(intent);
+ }
+
+ /**
+ * Sets the state of the bookmark/reading list buttons: they are disabled if the given URL is
+ * already in the corresponding list.
+ */
+ private void setButtonState(final String pageURL, final LocalBrowserDB browserDB) {
+ new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
+ // Flags to hold the result
+ boolean isBookmark;
+
+ @Override
+ protected Void doInBackground() {
+ final ContentResolver contentResolver = getApplicationContext().getContentResolver();
+
+ isBookmark = browserDB.isBookmark(contentResolver, pageURL);
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ findViewById(R.id.overlay_share_bookmark_btn).setEnabled(!isBookmark);
+ }
+ }.execute();
+ }
+
+ /**
+ * Helper method to get an overlay service intent populated with the data held in this dialog.
+ */
+ private Intent getServiceIntent(ShareMethod.Type method) {
+ final Intent serviceIntent = new Intent(this, OverlayActionService.class);
+ serviceIntent.setAction(OverlayConstants.ACTION_SHARE);
+
+ serviceIntent.putExtra(OverlayConstants.EXTRA_SHARE_METHOD, (Parcelable) method);
+ serviceIntent.putExtra(OverlayConstants.EXTRA_URL, url);
+ serviceIntent.putExtra(OverlayConstants.EXTRA_TITLE, title);
+
+ return serviceIntent;
+ }
+
+ @Override
+ public void finish() {
+ finish(true);
+ }
+
+ private void finish(final boolean shouldOverrideAnimations) {
+ super.finish();
+ if (shouldOverrideAnimations) {
+ // Don't perform an activity-dismiss animation.
+ overridePendingTransition(0, 0);
+ }
+ }
+
+ /*
+ * Button handlers. Send intents to the background service responsible for processing requests
+ * on Fennec in the background. (a nice extensible mechanism for "doing stuff without properly
+ * launching Fennec").
+ */
+
+ @Override
+ public void onSendTabActionSelected() {
+ // This requires an override intent.
+ if (sendTabOverrideIntent == null) {
+ throw new IllegalStateException("sendTabOverrideIntent must not be null");
+ }
+
+ startActivity(sendTabOverrideIntent);
+ finish();
+ }
+
+ @Override
+ public void onSendTabTargetSelected(String targetGUID) {
+ // targetGUID being null with no override intent should be an impossible state.
+ if (targetGUID == null) {
+ throw new IllegalStateException("targetGUID must not be null");
+ }
+
+ Intent serviceIntent = getServiceIntent(ShareMethod.Type.SEND_TAB);
+
+ // Currently, only one extra parameter is necessary (the GUID of the target device).
+ Bundle extraParameters = new Bundle();
+
+ // Future: Handle multiple-selection. Bug 1061297.
+ extraParameters.putStringArray(SendTab.SEND_TAB_TARGET_DEVICES, new String[] { targetGUID });
+
+ serviceIntent.putExtra(OverlayConstants.EXTRA_PARAMETERS, extraParameters);
+
+ startService(serviceIntent);
+ animateOut(true);
+
+ Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.SHARE_OVERLAY, "sendtab");
+ }
+
+ public void addBookmark() {
+ startService(getServiceIntent(ShareMethod.Type.ADD_BOOKMARK));
+ animateOut(true);
+
+ Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.SHARE_OVERLAY, "bookmark");
+ }
+
+ public void launchBrowser() {
+ try {
+ // This can launch in the guest profile. Sorry.
+ final Intent i = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
+ i.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
+ startActivity(i);
+ } catch (URISyntaxException e) {
+ // Nothing much we can do.
+ } finally {
+ // Since we're changing apps, users expect the default app switch animations.
+ finish(false);
+ }
+ }
+
+ private String getCurrentProfile() {
+ return GeckoProfile.DEFAULT_PROFILE;
+ }
+
+ /**
+ * Slide the overlay down off the screen, display
+ * a check (if given), and finish the activity.
+ */
+ private void animateOut(final boolean shouldDisplayConfirmation) {
+ if (isAnimating) {
+ return;
+ }
+
+ isAnimating = true;
+ final Animation slideOutAnim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_down);
+
+ final Animation animationToFinishActivity;
+ if (!shouldDisplayConfirmation) {
+ animationToFinishActivity = slideOutAnim;
+ } else {
+ final View check = findViewById(R.id.check);
+ check.setVisibility(View.VISIBLE);
+ final Animation checkEntryAnim = AnimationUtils.loadAnimation(this, R.anim.overlay_check_entry);
+ final Animation checkExitAnim = AnimationUtils.loadAnimation(this, R.anim.overlay_check_exit);
+ checkExitAnim.setStartOffset(checkEntryAnim.getDuration() + 500);
+
+ final AnimationSet checkAnimationSet = new AnimationSet(this, null);
+ checkAnimationSet.addAnimation(checkEntryAnim);
+ checkAnimationSet.addAnimation(checkExitAnim);
+
+ check.startAnimation(checkAnimationSet);
+ animationToFinishActivity = checkExitAnim;
+ }
+
+ findViewById(R.id.sharedialog).startAnimation(slideOutAnim);
+ animationToFinishActivity.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) { /* Unused. */ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ finish();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) { /* Unused. */ }
+ });
+
+ // Allows the user to dismiss the animation early.
+ setFullscreenFinishOnClickListener();
+ }
+
+ /**
+ * Sets a fullscreen {@link #finish()} click listener. We do this rather than attaching an
+ * onClickListener to the root View because in that case, we need to remove all of the
+ * existing listeners, which is less robust.
+ */
+ private void setFullscreenFinishOnClickListener() {
+ final View clickTarget = findViewById(R.id.fullscreen_click_target);
+ clickTarget.setVisibility(View.VISIBLE);
+ clickTarget.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ finish();
+ }
+ });
+ }
+
+ /**
+ * Close the dialog if back is pressed.
+ */
+ @Override
+ public void onBackPressed() {
+ animateOut(false);
+ Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.SHARE_OVERLAY);
+ }
+
+ /**
+ * Close the dialog if the anything that isn't a button is tapped.
+ */
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ animateOut(false);
+ Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.SHARE_OVERLAY);
+ return true;
+ }
+}