diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /mobile/android/base/java/org/mozilla/gecko/overlays | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-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')
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; + } +} |