mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-09 01:08:39 +09:00
Issue #1053 - Drop support Android and remove Fennec - Part 1a: Remove mobile/android
This commit is contained in:
parent
85045bf6be
commit
c9b411d6cd
3891 changed files with 0 additions and 428084 deletions
|
|
@ -1,68 +0,0 @@
|
|||
/* -*- 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";
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
/* -*- 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
/* 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
/* 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,296 +0,0 @@
|
|||
/* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
/*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];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
/* -*- 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
/* 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
/* 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
|
|
@ -1,493 +0,0 @@
|
|||
/* -*- 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue