michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.sync.setup.activities; michael@0: michael@0: import java.util.ArrayList; michael@0: import java.util.Collection; michael@0: import java.util.List; michael@0: import java.util.Map; michael@0: import java.util.Map.Entry; michael@0: michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: import org.mozilla.gecko.fxa.FirefoxAccounts; michael@0: import org.mozilla.gecko.fxa.FxAccountConstants; michael@0: import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity; michael@0: import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity; michael@0: import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; michael@0: import org.mozilla.gecko.fxa.login.State.Action; michael@0: import org.mozilla.gecko.sync.CommandProcessor; michael@0: import org.mozilla.gecko.sync.CommandRunner; michael@0: import org.mozilla.gecko.sync.GlobalSession; michael@0: import org.mozilla.gecko.sync.SyncConfiguration; michael@0: import org.mozilla.gecko.sync.SyncConstants; michael@0: import org.mozilla.gecko.sync.repositories.NullCursorException; michael@0: import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; michael@0: import org.mozilla.gecko.sync.repositories.domain.ClientRecord; michael@0: import org.mozilla.gecko.sync.setup.SyncAccounts; michael@0: import org.mozilla.gecko.sync.setup.activities.LocaleAware.LocaleAwareActivity; michael@0: import org.mozilla.gecko.sync.stage.SyncClientsEngineStage; michael@0: import org.mozilla.gecko.sync.syncadapter.SyncAdapter; michael@0: michael@0: import android.accounts.Account; michael@0: import android.accounts.AccountManager; michael@0: import android.app.Activity; michael@0: import android.content.Context; michael@0: import android.content.Intent; michael@0: import android.content.SharedPreferences; michael@0: import android.os.AsyncTask; michael@0: import android.os.Bundle; michael@0: import android.view.View; michael@0: import android.widget.ListView; michael@0: import android.widget.TextView; michael@0: import android.widget.Toast; michael@0: michael@0: public class SendTabActivity extends LocaleAwareActivity { michael@0: private interface TabSender { michael@0: static final String[] CLIENTS_STAGE = new String[] { SyncClientsEngineStage.COLLECTION_NAME }; michael@0: michael@0: /** michael@0: * @return Return null if the account isn't correctly initialized. Return michael@0: * the account GUID otherwise. michael@0: */ michael@0: String getAccountGUID(); michael@0: michael@0: /** michael@0: * Sync this account, specifying only clients as the engine to sync. michael@0: */ michael@0: void syncClientsStage(); michael@0: } michael@0: michael@0: private static class FxAccountTabSender implements TabSender { michael@0: private final AndroidFxAccount fxAccount; michael@0: michael@0: public FxAccountTabSender(Context context, AndroidFxAccount fxAccount) { michael@0: this.fxAccount = fxAccount; michael@0: } michael@0: michael@0: @Override michael@0: public String getAccountGUID() { michael@0: try { michael@0: final SharedPreferences prefs = this.fxAccount.getSyncPrefs(); michael@0: return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null); michael@0: } catch (Exception e) { michael@0: Logger.warn(LOG_TAG, "Could not get Firefox Account parameters or preferences; aborting."); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void syncClientsStage() { michael@0: fxAccount.requestSync(FirefoxAccounts.FORCE, CLIENTS_STAGE, null); michael@0: } michael@0: } michael@0: michael@0: private static class Sync11TabSender implements TabSender { michael@0: private final Account account; michael@0: private final AccountManager accountManager; michael@0: private final Context context; michael@0: michael@0: private Sync11TabSender(Context context, Account syncAccount, AccountManager accountManager) { michael@0: this.context = context; michael@0: this.account = syncAccount; michael@0: this.accountManager = accountManager; michael@0: } michael@0: michael@0: @Override michael@0: public String getAccountGUID() { michael@0: try { michael@0: final SharedPreferences prefs = SyncAccounts.blockingPrefsFromDefaultProfileV0(this.context, this.accountManager, this.account); michael@0: return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null); michael@0: } catch (Exception e) { michael@0: Logger.warn(LOG_TAG, "Could not get Sync account parameters or preferences; aborting."); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void syncClientsStage() { michael@0: SyncAdapter.requestImmediateSync(this.account, CLIENTS_STAGE); michael@0: } michael@0: } michael@0: michael@0: public static final String LOG_TAG = "SendTabActivity"; michael@0: private ClientRecordArrayAdapter arrayAdapter; michael@0: michael@0: private TabSender tabSender; michael@0: private SendTabData sendTabData; michael@0: michael@0: @Override michael@0: public void onCreate(Bundle savedInstanceState) { michael@0: super.onCreate(savedInstanceState); michael@0: michael@0: try { michael@0: sendTabData = getSendTabData(getIntent()); michael@0: } catch (IllegalArgumentException e) { michael@0: notifyAndFinish(false); michael@0: return; michael@0: } michael@0: michael@0: setContentView(R.layout.sync_send_tab); michael@0: michael@0: final ListView listview = (ListView) findViewById(R.id.device_list); michael@0: listview.setItemsCanFocus(true); michael@0: listview.setTextFilterEnabled(true); michael@0: listview.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); michael@0: michael@0: arrayAdapter = new ClientRecordArrayAdapter(this, R.layout.sync_list_item); michael@0: listview.setAdapter(arrayAdapter); michael@0: michael@0: TextView textView = (TextView) findViewById(R.id.title); michael@0: textView.setText(sendTabData.title); michael@0: michael@0: textView = (TextView) findViewById(R.id.uri); michael@0: textView.setText(sendTabData.uri); michael@0: michael@0: enableSend(false); michael@0: michael@0: // will enableSend if appropriate. michael@0: updateClientList(); michael@0: } michael@0: michael@0: protected static SendTabData getSendTabData(Intent intent) throws IllegalArgumentException { michael@0: if (intent == null) { michael@0: Logger.warn(LOG_TAG, "intent was null; aborting without sending tab."); michael@0: throw new IllegalArgumentException(); michael@0: } michael@0: michael@0: Bundle extras = intent.getExtras(); michael@0: if (extras == null) { michael@0: Logger.warn(LOG_TAG, "extras was null; aborting without sending tab."); michael@0: throw new IllegalArgumentException(); michael@0: } michael@0: michael@0: SendTabData sendTabData = SendTabData.fromBundle(extras); michael@0: if (sendTabData == null) { michael@0: Logger.warn(LOG_TAG, "send tab data was null; aborting without sending tab."); michael@0: throw new IllegalArgumentException(); michael@0: } michael@0: michael@0: if (sendTabData.uri == null) { michael@0: Logger.warn(LOG_TAG, "uri was null; aborting without sending tab."); michael@0: throw new IllegalArgumentException(); michael@0: } michael@0: michael@0: if (sendTabData.title == null) { michael@0: Logger.warn(LOG_TAG, "title was null; ignoring and sending tab anyway."); michael@0: } michael@0: michael@0: return sendTabData; michael@0: } michael@0: michael@0: /** michael@0: * Ensure that the view's list of clients is backed by a recently populated michael@0: * array adapter. michael@0: */ michael@0: protected synchronized void updateClientList() { michael@0: // Fetching the client list hits the clients database, so we spin this onto michael@0: // a background task. michael@0: new AsyncTask>() { michael@0: michael@0: @Override michael@0: protected Collection doInBackground(Void... params) { michael@0: return getOtherClients(); michael@0: } michael@0: michael@0: @Override michael@0: protected void onPostExecute(final Collection clientArray) { michael@0: // We're allowed to update the UI from here. michael@0: michael@0: Logger.debug(LOG_TAG, "Got " + clientArray.size() + " clients."); michael@0: arrayAdapter.setClientRecordList(clientArray); michael@0: if (clientArray.size() == 1) { michael@0: arrayAdapter.checkItem(0, true); michael@0: } michael@0: michael@0: enableSend(arrayAdapter.getNumCheckedGUIDs() > 0); michael@0: } michael@0: }.execute(); michael@0: } michael@0: michael@0: @Override michael@0: public void onResume() { michael@0: ActivityUtils.prepareLogging(); michael@0: Logger.info(LOG_TAG, "Called SendTabActivity.onResume."); michael@0: super.onResume(); michael@0: michael@0: /* michael@0: * First, decide if we are able to send anything. michael@0: */ michael@0: final Context applicationContext = getApplicationContext(); michael@0: final AccountManager accountManager = AccountManager.get(applicationContext); michael@0: michael@0: final Account[] fxAccounts = accountManager.getAccountsByType(FxAccountConstants.ACCOUNT_TYPE); michael@0: if (fxAccounts.length > 0) { michael@0: final AndroidFxAccount fxAccount = new AndroidFxAccount(applicationContext, fxAccounts[0]); michael@0: if (fxAccount.getState().getNeededAction() != Action.None) { michael@0: // We have a Firefox Account, but it's definitely not able to send a tab michael@0: // right now. Redirect to the status activity. michael@0: Logger.warn(LOG_TAG, "Firefox Account named like " + fxAccount.getObfuscatedEmail() + michael@0: " needs action before it can send a tab; redirecting to status activity."); michael@0: redirectToNewTask(FxAccountStatusActivity.class, false); michael@0: return; michael@0: } michael@0: michael@0: this.tabSender = new FxAccountTabSender(applicationContext, fxAccount); michael@0: michael@0: Logger.info(LOG_TAG, "Allowing tab send for Firefox Account."); michael@0: registerDisplayURICommand(); michael@0: return; michael@0: } michael@0: michael@0: final Account[] syncAccounts = accountManager.getAccountsByType(SyncConstants.ACCOUNTTYPE_SYNC); michael@0: if (syncAccounts.length > 0) { michael@0: this.tabSender = new Sync11TabSender(applicationContext, syncAccounts[0], accountManager); michael@0: michael@0: Logger.info(LOG_TAG, "Allowing tab send for Sync account."); michael@0: registerDisplayURICommand(); michael@0: return; michael@0: } michael@0: michael@0: // Offer to set up a Firefox Account, and finish this activity. michael@0: redirectToNewTask(FxAccountGetStartedActivity.class, false); michael@0: } michael@0: michael@0: private static void registerDisplayURICommand() { michael@0: final CommandProcessor processor = CommandProcessor.getProcessor(); michael@0: processor.registerCommand("displayURI", new CommandRunner(3) { michael@0: @Override michael@0: public void executeCommand(final GlobalSession session, List args) { michael@0: CommandProcessor.displayURI(args, session.getContext()); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: public void sendClickHandler(View view) { michael@0: Logger.info(LOG_TAG, "Send was clicked."); michael@0: final List remoteClientGuids = arrayAdapter.getCheckedGUIDs(); michael@0: michael@0: if (remoteClientGuids == null) { michael@0: // Should never happen. michael@0: Logger.warn(LOG_TAG, "guids was null; aborting without sending tab."); michael@0: notifyAndFinish(false); michael@0: return; michael@0: } michael@0: michael@0: final TabSender sender = this.tabSender; michael@0: if (sender == null) { michael@0: // This should never happen. michael@0: Logger.warn(LOG_TAG, "tabSender was null; aborting without sending tab."); michael@0: notifyAndFinish(false); michael@0: return; michael@0: } michael@0: michael@0: // Fetching local client GUID hits the DB, and we want to update the UI michael@0: // afterward, so we perform the tab sending on another thread. michael@0: new AsyncTask() { michael@0: michael@0: @Override michael@0: protected Boolean doInBackground(Void... params) { michael@0: final CommandProcessor processor = CommandProcessor.getProcessor(); michael@0: michael@0: final String accountGUID = sender.getAccountGUID(); michael@0: Logger.debug(LOG_TAG, "Retrieved local account GUID '" + accountGUID + "'."); michael@0: if (accountGUID == null) { michael@0: return false; michael@0: } michael@0: michael@0: for (String remoteClientGuid : remoteClientGuids) { michael@0: processor.sendURIToClientForDisplay(sendTabData.uri, remoteClientGuid, sendTabData.title, accountGUID, getApplicationContext()); michael@0: } michael@0: michael@0: Logger.info(LOG_TAG, "Requesting immediate clients stage sync."); michael@0: sender.syncClientsStage(); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: protected void onPostExecute(final Boolean success) { michael@0: // We're allowed to update the UI from here. michael@0: notifyAndFinish(success.booleanValue()); michael@0: } michael@0: }.execute(); michael@0: } michael@0: michael@0: /** michael@0: * Notify the user about sent tabs status and then finish the activity. michael@0: *

michael@0: * "Success" is a bit of a misnomer: we wrote "displayURI" commands to the local michael@0: * command database, and they will be sent on next sync. There is no way to michael@0: * verify that the commands were successfully received by the intended remote michael@0: * client, so we lie and say they were sent. michael@0: * michael@0: * @param success true if tab was sent successfully; false otherwise. michael@0: */ michael@0: protected void notifyAndFinish(final boolean success) { michael@0: int textId; michael@0: if (success) { michael@0: textId = R.string.sync_text_tab_sent; michael@0: } else { michael@0: textId = R.string.sync_text_tab_not_sent; michael@0: } michael@0: michael@0: Toast.makeText(this, textId, Toast.LENGTH_LONG).show(); michael@0: finish(); michael@0: } michael@0: michael@0: public void enableSend(boolean shouldEnable) { michael@0: View sendButton = findViewById(R.id.send_button); michael@0: sendButton.setEnabled(shouldEnable); michael@0: sendButton.setClickable(shouldEnable); michael@0: } michael@0: michael@0: /** michael@0: * @return a map from GUID to client record, including our own. michael@0: */ michael@0: protected Map getAllClients() { michael@0: ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(this.getApplicationContext()); michael@0: try { michael@0: return db.fetchAllClients(); michael@0: } catch (NullCursorException e) { michael@0: Logger.warn(LOG_TAG, "NullCursorException while populating device list.", e); michael@0: return null; michael@0: } finally { michael@0: db.close(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @return a collection of client records, excluding our own. michael@0: */ michael@0: protected Collection getOtherClients() { michael@0: final Map all = getAllClients(); michael@0: if (all == null) { michael@0: return new ArrayList(0); michael@0: } michael@0: michael@0: if (this.tabSender == null) { michael@0: Logger.warn(LOG_TAG, "No tab sender when fetching other client IDs."); michael@0: return new ArrayList(0); michael@0: } michael@0: michael@0: final String ourGUID = this.tabSender.getAccountGUID(); michael@0: if (ourGUID == null) { michael@0: return all.values(); michael@0: } michael@0: michael@0: final ArrayList out = new ArrayList(all.size()); michael@0: for (Entry entry : all.entrySet()) { michael@0: if (ourGUID.equals(entry.getKey())) { michael@0: continue; michael@0: } michael@0: out.add(entry.getValue()); michael@0: } michael@0: return out; michael@0: } michael@0: michael@0: // Adapted from FxAccountAbstractActivity. michael@0: protected void redirectToNewTask(Class activityClass, boolean success) { michael@0: Intent intent = new Intent(this, activityClass); michael@0: // Per http://stackoverflow.com/a/8992365, this triggers a known bug with michael@0: // the soft keyboard not being shown for the started activity. Why, Android, why? michael@0: intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); michael@0: intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); michael@0: startActivity(intent); michael@0: notifyAndFinish(success); michael@0: } michael@0: }