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/ui | |
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/ui')
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; + } +} |