summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/overlays/ui
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/ui
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/ui')
-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
5 files changed, 981 insertions, 0 deletions
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;
+ }
+}