pt 1 in reviving the android build (copied from pm 28a1, wish me luck)

This commit is contained in:
wuggy 2026-04-08 15:43:25 -07:00
commit d7788a6d6d
4249 changed files with 468189 additions and 0 deletions

View file

@ -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";
}

View file

@ -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);
}
}
});
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -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];
}
};
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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();
}

View file

@ -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;
}
}