1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/sync/setup/activities/SendTabActivity.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,400 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.sync.setup.activities; 1.9 + 1.10 +import java.util.ArrayList; 1.11 +import java.util.Collection; 1.12 +import java.util.List; 1.13 +import java.util.Map; 1.14 +import java.util.Map.Entry; 1.15 + 1.16 +import org.mozilla.gecko.R; 1.17 +import org.mozilla.gecko.background.common.log.Logger; 1.18 +import org.mozilla.gecko.fxa.FirefoxAccounts; 1.19 +import org.mozilla.gecko.fxa.FxAccountConstants; 1.20 +import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity; 1.21 +import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity; 1.22 +import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; 1.23 +import org.mozilla.gecko.fxa.login.State.Action; 1.24 +import org.mozilla.gecko.sync.CommandProcessor; 1.25 +import org.mozilla.gecko.sync.CommandRunner; 1.26 +import org.mozilla.gecko.sync.GlobalSession; 1.27 +import org.mozilla.gecko.sync.SyncConfiguration; 1.28 +import org.mozilla.gecko.sync.SyncConstants; 1.29 +import org.mozilla.gecko.sync.repositories.NullCursorException; 1.30 +import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; 1.31 +import org.mozilla.gecko.sync.repositories.domain.ClientRecord; 1.32 +import org.mozilla.gecko.sync.setup.SyncAccounts; 1.33 +import org.mozilla.gecko.sync.setup.activities.LocaleAware.LocaleAwareActivity; 1.34 +import org.mozilla.gecko.sync.stage.SyncClientsEngineStage; 1.35 +import org.mozilla.gecko.sync.syncadapter.SyncAdapter; 1.36 + 1.37 +import android.accounts.Account; 1.38 +import android.accounts.AccountManager; 1.39 +import android.app.Activity; 1.40 +import android.content.Context; 1.41 +import android.content.Intent; 1.42 +import android.content.SharedPreferences; 1.43 +import android.os.AsyncTask; 1.44 +import android.os.Bundle; 1.45 +import android.view.View; 1.46 +import android.widget.ListView; 1.47 +import android.widget.TextView; 1.48 +import android.widget.Toast; 1.49 + 1.50 +public class SendTabActivity extends LocaleAwareActivity { 1.51 + private interface TabSender { 1.52 + static final String[] CLIENTS_STAGE = new String[] { SyncClientsEngineStage.COLLECTION_NAME }; 1.53 + 1.54 + /** 1.55 + * @return Return null if the account isn't correctly initialized. Return 1.56 + * the account GUID otherwise. 1.57 + */ 1.58 + String getAccountGUID(); 1.59 + 1.60 + /** 1.61 + * Sync this account, specifying only clients as the engine to sync. 1.62 + */ 1.63 + void syncClientsStage(); 1.64 + } 1.65 + 1.66 + private static class FxAccountTabSender implements TabSender { 1.67 + private final AndroidFxAccount fxAccount; 1.68 + 1.69 + public FxAccountTabSender(Context context, AndroidFxAccount fxAccount) { 1.70 + this.fxAccount = fxAccount; 1.71 + } 1.72 + 1.73 + @Override 1.74 + public String getAccountGUID() { 1.75 + try { 1.76 + final SharedPreferences prefs = this.fxAccount.getSyncPrefs(); 1.77 + return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null); 1.78 + } catch (Exception e) { 1.79 + Logger.warn(LOG_TAG, "Could not get Firefox Account parameters or preferences; aborting."); 1.80 + return null; 1.81 + } 1.82 + } 1.83 + 1.84 + @Override 1.85 + public void syncClientsStage() { 1.86 + fxAccount.requestSync(FirefoxAccounts.FORCE, CLIENTS_STAGE, null); 1.87 + } 1.88 + } 1.89 + 1.90 + private static class Sync11TabSender implements TabSender { 1.91 + private final Account account; 1.92 + private final AccountManager accountManager; 1.93 + private final Context context; 1.94 + 1.95 + private Sync11TabSender(Context context, Account syncAccount, AccountManager accountManager) { 1.96 + this.context = context; 1.97 + this.account = syncAccount; 1.98 + this.accountManager = accountManager; 1.99 + } 1.100 + 1.101 + @Override 1.102 + public String getAccountGUID() { 1.103 + try { 1.104 + final SharedPreferences prefs = SyncAccounts.blockingPrefsFromDefaultProfileV0(this.context, this.accountManager, this.account); 1.105 + return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null); 1.106 + } catch (Exception e) { 1.107 + Logger.warn(LOG_TAG, "Could not get Sync account parameters or preferences; aborting."); 1.108 + return null; 1.109 + } 1.110 + } 1.111 + 1.112 + @Override 1.113 + public void syncClientsStage() { 1.114 + SyncAdapter.requestImmediateSync(this.account, CLIENTS_STAGE); 1.115 + } 1.116 + } 1.117 + 1.118 + public static final String LOG_TAG = "SendTabActivity"; 1.119 + private ClientRecordArrayAdapter arrayAdapter; 1.120 + 1.121 + private TabSender tabSender; 1.122 + private SendTabData sendTabData; 1.123 + 1.124 + @Override 1.125 + public void onCreate(Bundle savedInstanceState) { 1.126 + super.onCreate(savedInstanceState); 1.127 + 1.128 + try { 1.129 + sendTabData = getSendTabData(getIntent()); 1.130 + } catch (IllegalArgumentException e) { 1.131 + notifyAndFinish(false); 1.132 + return; 1.133 + } 1.134 + 1.135 + setContentView(R.layout.sync_send_tab); 1.136 + 1.137 + final ListView listview = (ListView) findViewById(R.id.device_list); 1.138 + listview.setItemsCanFocus(true); 1.139 + listview.setTextFilterEnabled(true); 1.140 + listview.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); 1.141 + 1.142 + arrayAdapter = new ClientRecordArrayAdapter(this, R.layout.sync_list_item); 1.143 + listview.setAdapter(arrayAdapter); 1.144 + 1.145 + TextView textView = (TextView) findViewById(R.id.title); 1.146 + textView.setText(sendTabData.title); 1.147 + 1.148 + textView = (TextView) findViewById(R.id.uri); 1.149 + textView.setText(sendTabData.uri); 1.150 + 1.151 + enableSend(false); 1.152 + 1.153 + // will enableSend if appropriate. 1.154 + updateClientList(); 1.155 + } 1.156 + 1.157 + protected static SendTabData getSendTabData(Intent intent) throws IllegalArgumentException { 1.158 + if (intent == null) { 1.159 + Logger.warn(LOG_TAG, "intent was null; aborting without sending tab."); 1.160 + throw new IllegalArgumentException(); 1.161 + } 1.162 + 1.163 + Bundle extras = intent.getExtras(); 1.164 + if (extras == null) { 1.165 + Logger.warn(LOG_TAG, "extras was null; aborting without sending tab."); 1.166 + throw new IllegalArgumentException(); 1.167 + } 1.168 + 1.169 + SendTabData sendTabData = SendTabData.fromBundle(extras); 1.170 + if (sendTabData == null) { 1.171 + Logger.warn(LOG_TAG, "send tab data was null; aborting without sending tab."); 1.172 + throw new IllegalArgumentException(); 1.173 + } 1.174 + 1.175 + if (sendTabData.uri == null) { 1.176 + Logger.warn(LOG_TAG, "uri was null; aborting without sending tab."); 1.177 + throw new IllegalArgumentException(); 1.178 + } 1.179 + 1.180 + if (sendTabData.title == null) { 1.181 + Logger.warn(LOG_TAG, "title was null; ignoring and sending tab anyway."); 1.182 + } 1.183 + 1.184 + return sendTabData; 1.185 + } 1.186 + 1.187 + /** 1.188 + * Ensure that the view's list of clients is backed by a recently populated 1.189 + * array adapter. 1.190 + */ 1.191 + protected synchronized void updateClientList() { 1.192 + // Fetching the client list hits the clients database, so we spin this onto 1.193 + // a background task. 1.194 + new AsyncTask<Void, Void, Collection<ClientRecord>>() { 1.195 + 1.196 + @Override 1.197 + protected Collection<ClientRecord> doInBackground(Void... params) { 1.198 + return getOtherClients(); 1.199 + } 1.200 + 1.201 + @Override 1.202 + protected void onPostExecute(final Collection<ClientRecord> clientArray) { 1.203 + // We're allowed to update the UI from here. 1.204 + 1.205 + Logger.debug(LOG_TAG, "Got " + clientArray.size() + " clients."); 1.206 + arrayAdapter.setClientRecordList(clientArray); 1.207 + if (clientArray.size() == 1) { 1.208 + arrayAdapter.checkItem(0, true); 1.209 + } 1.210 + 1.211 + enableSend(arrayAdapter.getNumCheckedGUIDs() > 0); 1.212 + } 1.213 + }.execute(); 1.214 + } 1.215 + 1.216 + @Override 1.217 + public void onResume() { 1.218 + ActivityUtils.prepareLogging(); 1.219 + Logger.info(LOG_TAG, "Called SendTabActivity.onResume."); 1.220 + super.onResume(); 1.221 + 1.222 + /* 1.223 + * First, decide if we are able to send anything. 1.224 + */ 1.225 + final Context applicationContext = getApplicationContext(); 1.226 + final AccountManager accountManager = AccountManager.get(applicationContext); 1.227 + 1.228 + final Account[] fxAccounts = accountManager.getAccountsByType(FxAccountConstants.ACCOUNT_TYPE); 1.229 + if (fxAccounts.length > 0) { 1.230 + final AndroidFxAccount fxAccount = new AndroidFxAccount(applicationContext, fxAccounts[0]); 1.231 + if (fxAccount.getState().getNeededAction() != Action.None) { 1.232 + // We have a Firefox Account, but it's definitely not able to send a tab 1.233 + // right now. Redirect to the status activity. 1.234 + Logger.warn(LOG_TAG, "Firefox Account named like " + fxAccount.getObfuscatedEmail() + 1.235 + " needs action before it can send a tab; redirecting to status activity."); 1.236 + redirectToNewTask(FxAccountStatusActivity.class, false); 1.237 + return; 1.238 + } 1.239 + 1.240 + this.tabSender = new FxAccountTabSender(applicationContext, fxAccount); 1.241 + 1.242 + Logger.info(LOG_TAG, "Allowing tab send for Firefox Account."); 1.243 + registerDisplayURICommand(); 1.244 + return; 1.245 + } 1.246 + 1.247 + final Account[] syncAccounts = accountManager.getAccountsByType(SyncConstants.ACCOUNTTYPE_SYNC); 1.248 + if (syncAccounts.length > 0) { 1.249 + this.tabSender = new Sync11TabSender(applicationContext, syncAccounts[0], accountManager); 1.250 + 1.251 + Logger.info(LOG_TAG, "Allowing tab send for Sync account."); 1.252 + registerDisplayURICommand(); 1.253 + return; 1.254 + } 1.255 + 1.256 + // Offer to set up a Firefox Account, and finish this activity. 1.257 + redirectToNewTask(FxAccountGetStartedActivity.class, false); 1.258 + } 1.259 + 1.260 + private static void registerDisplayURICommand() { 1.261 + final CommandProcessor processor = CommandProcessor.getProcessor(); 1.262 + processor.registerCommand("displayURI", new CommandRunner(3) { 1.263 + @Override 1.264 + public void executeCommand(final GlobalSession session, List<String> args) { 1.265 + CommandProcessor.displayURI(args, session.getContext()); 1.266 + } 1.267 + }); 1.268 + } 1.269 + 1.270 + public void sendClickHandler(View view) { 1.271 + Logger.info(LOG_TAG, "Send was clicked."); 1.272 + final List<String> remoteClientGuids = arrayAdapter.getCheckedGUIDs(); 1.273 + 1.274 + if (remoteClientGuids == null) { 1.275 + // Should never happen. 1.276 + Logger.warn(LOG_TAG, "guids was null; aborting without sending tab."); 1.277 + notifyAndFinish(false); 1.278 + return; 1.279 + } 1.280 + 1.281 + final TabSender sender = this.tabSender; 1.282 + if (sender == null) { 1.283 + // This should never happen. 1.284 + Logger.warn(LOG_TAG, "tabSender was null; aborting without sending tab."); 1.285 + notifyAndFinish(false); 1.286 + return; 1.287 + } 1.288 + 1.289 + // Fetching local client GUID hits the DB, and we want to update the UI 1.290 + // afterward, so we perform the tab sending on another thread. 1.291 + new AsyncTask<Void, Void, Boolean>() { 1.292 + 1.293 + @Override 1.294 + protected Boolean doInBackground(Void... params) { 1.295 + final CommandProcessor processor = CommandProcessor.getProcessor(); 1.296 + 1.297 + final String accountGUID = sender.getAccountGUID(); 1.298 + Logger.debug(LOG_TAG, "Retrieved local account GUID '" + accountGUID + "'."); 1.299 + if (accountGUID == null) { 1.300 + return false; 1.301 + } 1.302 + 1.303 + for (String remoteClientGuid : remoteClientGuids) { 1.304 + processor.sendURIToClientForDisplay(sendTabData.uri, remoteClientGuid, sendTabData.title, accountGUID, getApplicationContext()); 1.305 + } 1.306 + 1.307 + Logger.info(LOG_TAG, "Requesting immediate clients stage sync."); 1.308 + sender.syncClientsStage(); 1.309 + 1.310 + return true; 1.311 + } 1.312 + 1.313 + @Override 1.314 + protected void onPostExecute(final Boolean success) { 1.315 + // We're allowed to update the UI from here. 1.316 + notifyAndFinish(success.booleanValue()); 1.317 + } 1.318 + }.execute(); 1.319 + } 1.320 + 1.321 + /** 1.322 + * Notify the user about sent tabs status and then finish the activity. 1.323 + * <p> 1.324 + * "Success" is a bit of a misnomer: we wrote "displayURI" commands to the local 1.325 + * command database, and they will be sent on next sync. There is no way to 1.326 + * verify that the commands were successfully received by the intended remote 1.327 + * client, so we lie and say they were sent. 1.328 + * 1.329 + * @param success true if tab was sent successfully; false otherwise. 1.330 + */ 1.331 + protected void notifyAndFinish(final boolean success) { 1.332 + int textId; 1.333 + if (success) { 1.334 + textId = R.string.sync_text_tab_sent; 1.335 + } else { 1.336 + textId = R.string.sync_text_tab_not_sent; 1.337 + } 1.338 + 1.339 + Toast.makeText(this, textId, Toast.LENGTH_LONG).show(); 1.340 + finish(); 1.341 + } 1.342 + 1.343 + public void enableSend(boolean shouldEnable) { 1.344 + View sendButton = findViewById(R.id.send_button); 1.345 + sendButton.setEnabled(shouldEnable); 1.346 + sendButton.setClickable(shouldEnable); 1.347 + } 1.348 + 1.349 + /** 1.350 + * @return a map from GUID to client record, including our own. 1.351 + */ 1.352 + protected Map<String, ClientRecord> getAllClients() { 1.353 + ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(this.getApplicationContext()); 1.354 + try { 1.355 + return db.fetchAllClients(); 1.356 + } catch (NullCursorException e) { 1.357 + Logger.warn(LOG_TAG, "NullCursorException while populating device list.", e); 1.358 + return null; 1.359 + } finally { 1.360 + db.close(); 1.361 + } 1.362 + } 1.363 + 1.364 + /** 1.365 + * @return a collection of client records, excluding our own. 1.366 + */ 1.367 + protected Collection<ClientRecord> getOtherClients() { 1.368 + final Map<String, ClientRecord> all = getAllClients(); 1.369 + if (all == null) { 1.370 + return new ArrayList<ClientRecord>(0); 1.371 + } 1.372 + 1.373 + if (this.tabSender == null) { 1.374 + Logger.warn(LOG_TAG, "No tab sender when fetching other client IDs."); 1.375 + return new ArrayList<ClientRecord>(0); 1.376 + } 1.377 + 1.378 + final String ourGUID = this.tabSender.getAccountGUID(); 1.379 + if (ourGUID == null) { 1.380 + return all.values(); 1.381 + } 1.382 + 1.383 + final ArrayList<ClientRecord> out = new ArrayList<ClientRecord>(all.size()); 1.384 + for (Entry<String, ClientRecord> entry : all.entrySet()) { 1.385 + if (ourGUID.equals(entry.getKey())) { 1.386 + continue; 1.387 + } 1.388 + out.add(entry.getValue()); 1.389 + } 1.390 + return out; 1.391 + } 1.392 + 1.393 + // Adapted from FxAccountAbstractActivity. 1.394 + protected void redirectToNewTask(Class<? extends Activity> activityClass, boolean success) { 1.395 + Intent intent = new Intent(this, activityClass); 1.396 + // Per http://stackoverflow.com/a/8992365, this triggers a known bug with 1.397 + // the soft keyboard not being shown for the started activity. Why, Android, why? 1.398 + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); 1.399 + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1.400 + startActivity(intent); 1.401 + notifyAndFinish(success); 1.402 + } 1.403 +}