|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.sync.setup.activities; |
|
6 |
|
7 import java.util.ArrayList; |
|
8 import java.util.Collection; |
|
9 import java.util.List; |
|
10 import java.util.Map; |
|
11 import java.util.Map.Entry; |
|
12 |
|
13 import org.mozilla.gecko.R; |
|
14 import org.mozilla.gecko.background.common.log.Logger; |
|
15 import org.mozilla.gecko.fxa.FirefoxAccounts; |
|
16 import org.mozilla.gecko.fxa.FxAccountConstants; |
|
17 import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity; |
|
18 import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity; |
|
19 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; |
|
20 import org.mozilla.gecko.fxa.login.State.Action; |
|
21 import org.mozilla.gecko.sync.CommandProcessor; |
|
22 import org.mozilla.gecko.sync.CommandRunner; |
|
23 import org.mozilla.gecko.sync.GlobalSession; |
|
24 import org.mozilla.gecko.sync.SyncConfiguration; |
|
25 import org.mozilla.gecko.sync.SyncConstants; |
|
26 import org.mozilla.gecko.sync.repositories.NullCursorException; |
|
27 import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; |
|
28 import org.mozilla.gecko.sync.repositories.domain.ClientRecord; |
|
29 import org.mozilla.gecko.sync.setup.SyncAccounts; |
|
30 import org.mozilla.gecko.sync.setup.activities.LocaleAware.LocaleAwareActivity; |
|
31 import org.mozilla.gecko.sync.stage.SyncClientsEngineStage; |
|
32 import org.mozilla.gecko.sync.syncadapter.SyncAdapter; |
|
33 |
|
34 import android.accounts.Account; |
|
35 import android.accounts.AccountManager; |
|
36 import android.app.Activity; |
|
37 import android.content.Context; |
|
38 import android.content.Intent; |
|
39 import android.content.SharedPreferences; |
|
40 import android.os.AsyncTask; |
|
41 import android.os.Bundle; |
|
42 import android.view.View; |
|
43 import android.widget.ListView; |
|
44 import android.widget.TextView; |
|
45 import android.widget.Toast; |
|
46 |
|
47 public class SendTabActivity extends LocaleAwareActivity { |
|
48 private interface TabSender { |
|
49 static final String[] CLIENTS_STAGE = new String[] { SyncClientsEngineStage.COLLECTION_NAME }; |
|
50 |
|
51 /** |
|
52 * @return Return null if the account isn't correctly initialized. Return |
|
53 * the account GUID otherwise. |
|
54 */ |
|
55 String getAccountGUID(); |
|
56 |
|
57 /** |
|
58 * Sync this account, specifying only clients as the engine to sync. |
|
59 */ |
|
60 void syncClientsStage(); |
|
61 } |
|
62 |
|
63 private static class FxAccountTabSender implements TabSender { |
|
64 private final AndroidFxAccount fxAccount; |
|
65 |
|
66 public FxAccountTabSender(Context context, AndroidFxAccount fxAccount) { |
|
67 this.fxAccount = fxAccount; |
|
68 } |
|
69 |
|
70 @Override |
|
71 public String getAccountGUID() { |
|
72 try { |
|
73 final SharedPreferences prefs = this.fxAccount.getSyncPrefs(); |
|
74 return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null); |
|
75 } catch (Exception e) { |
|
76 Logger.warn(LOG_TAG, "Could not get Firefox Account parameters or preferences; aborting."); |
|
77 return null; |
|
78 } |
|
79 } |
|
80 |
|
81 @Override |
|
82 public void syncClientsStage() { |
|
83 fxAccount.requestSync(FirefoxAccounts.FORCE, CLIENTS_STAGE, null); |
|
84 } |
|
85 } |
|
86 |
|
87 private static class Sync11TabSender implements TabSender { |
|
88 private final Account account; |
|
89 private final AccountManager accountManager; |
|
90 private final Context context; |
|
91 |
|
92 private Sync11TabSender(Context context, Account syncAccount, AccountManager accountManager) { |
|
93 this.context = context; |
|
94 this.account = syncAccount; |
|
95 this.accountManager = accountManager; |
|
96 } |
|
97 |
|
98 @Override |
|
99 public String getAccountGUID() { |
|
100 try { |
|
101 final SharedPreferences prefs = SyncAccounts.blockingPrefsFromDefaultProfileV0(this.context, this.accountManager, this.account); |
|
102 return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null); |
|
103 } catch (Exception e) { |
|
104 Logger.warn(LOG_TAG, "Could not get Sync account parameters or preferences; aborting."); |
|
105 return null; |
|
106 } |
|
107 } |
|
108 |
|
109 @Override |
|
110 public void syncClientsStage() { |
|
111 SyncAdapter.requestImmediateSync(this.account, CLIENTS_STAGE); |
|
112 } |
|
113 } |
|
114 |
|
115 public static final String LOG_TAG = "SendTabActivity"; |
|
116 private ClientRecordArrayAdapter arrayAdapter; |
|
117 |
|
118 private TabSender tabSender; |
|
119 private SendTabData sendTabData; |
|
120 |
|
121 @Override |
|
122 public void onCreate(Bundle savedInstanceState) { |
|
123 super.onCreate(savedInstanceState); |
|
124 |
|
125 try { |
|
126 sendTabData = getSendTabData(getIntent()); |
|
127 } catch (IllegalArgumentException e) { |
|
128 notifyAndFinish(false); |
|
129 return; |
|
130 } |
|
131 |
|
132 setContentView(R.layout.sync_send_tab); |
|
133 |
|
134 final ListView listview = (ListView) findViewById(R.id.device_list); |
|
135 listview.setItemsCanFocus(true); |
|
136 listview.setTextFilterEnabled(true); |
|
137 listview.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); |
|
138 |
|
139 arrayAdapter = new ClientRecordArrayAdapter(this, R.layout.sync_list_item); |
|
140 listview.setAdapter(arrayAdapter); |
|
141 |
|
142 TextView textView = (TextView) findViewById(R.id.title); |
|
143 textView.setText(sendTabData.title); |
|
144 |
|
145 textView = (TextView) findViewById(R.id.uri); |
|
146 textView.setText(sendTabData.uri); |
|
147 |
|
148 enableSend(false); |
|
149 |
|
150 // will enableSend if appropriate. |
|
151 updateClientList(); |
|
152 } |
|
153 |
|
154 protected static SendTabData getSendTabData(Intent intent) throws IllegalArgumentException { |
|
155 if (intent == null) { |
|
156 Logger.warn(LOG_TAG, "intent was null; aborting without sending tab."); |
|
157 throw new IllegalArgumentException(); |
|
158 } |
|
159 |
|
160 Bundle extras = intent.getExtras(); |
|
161 if (extras == null) { |
|
162 Logger.warn(LOG_TAG, "extras was null; aborting without sending tab."); |
|
163 throw new IllegalArgumentException(); |
|
164 } |
|
165 |
|
166 SendTabData sendTabData = SendTabData.fromBundle(extras); |
|
167 if (sendTabData == null) { |
|
168 Logger.warn(LOG_TAG, "send tab data was null; aborting without sending tab."); |
|
169 throw new IllegalArgumentException(); |
|
170 } |
|
171 |
|
172 if (sendTabData.uri == null) { |
|
173 Logger.warn(LOG_TAG, "uri was null; aborting without sending tab."); |
|
174 throw new IllegalArgumentException(); |
|
175 } |
|
176 |
|
177 if (sendTabData.title == null) { |
|
178 Logger.warn(LOG_TAG, "title was null; ignoring and sending tab anyway."); |
|
179 } |
|
180 |
|
181 return sendTabData; |
|
182 } |
|
183 |
|
184 /** |
|
185 * Ensure that the view's list of clients is backed by a recently populated |
|
186 * array adapter. |
|
187 */ |
|
188 protected synchronized void updateClientList() { |
|
189 // Fetching the client list hits the clients database, so we spin this onto |
|
190 // a background task. |
|
191 new AsyncTask<Void, Void, Collection<ClientRecord>>() { |
|
192 |
|
193 @Override |
|
194 protected Collection<ClientRecord> doInBackground(Void... params) { |
|
195 return getOtherClients(); |
|
196 } |
|
197 |
|
198 @Override |
|
199 protected void onPostExecute(final Collection<ClientRecord> clientArray) { |
|
200 // We're allowed to update the UI from here. |
|
201 |
|
202 Logger.debug(LOG_TAG, "Got " + clientArray.size() + " clients."); |
|
203 arrayAdapter.setClientRecordList(clientArray); |
|
204 if (clientArray.size() == 1) { |
|
205 arrayAdapter.checkItem(0, true); |
|
206 } |
|
207 |
|
208 enableSend(arrayAdapter.getNumCheckedGUIDs() > 0); |
|
209 } |
|
210 }.execute(); |
|
211 } |
|
212 |
|
213 @Override |
|
214 public void onResume() { |
|
215 ActivityUtils.prepareLogging(); |
|
216 Logger.info(LOG_TAG, "Called SendTabActivity.onResume."); |
|
217 super.onResume(); |
|
218 |
|
219 /* |
|
220 * First, decide if we are able to send anything. |
|
221 */ |
|
222 final Context applicationContext = getApplicationContext(); |
|
223 final AccountManager accountManager = AccountManager.get(applicationContext); |
|
224 |
|
225 final Account[] fxAccounts = accountManager.getAccountsByType(FxAccountConstants.ACCOUNT_TYPE); |
|
226 if (fxAccounts.length > 0) { |
|
227 final AndroidFxAccount fxAccount = new AndroidFxAccount(applicationContext, fxAccounts[0]); |
|
228 if (fxAccount.getState().getNeededAction() != Action.None) { |
|
229 // We have a Firefox Account, but it's definitely not able to send a tab |
|
230 // right now. Redirect to the status activity. |
|
231 Logger.warn(LOG_TAG, "Firefox Account named like " + fxAccount.getObfuscatedEmail() + |
|
232 " needs action before it can send a tab; redirecting to status activity."); |
|
233 redirectToNewTask(FxAccountStatusActivity.class, false); |
|
234 return; |
|
235 } |
|
236 |
|
237 this.tabSender = new FxAccountTabSender(applicationContext, fxAccount); |
|
238 |
|
239 Logger.info(LOG_TAG, "Allowing tab send for Firefox Account."); |
|
240 registerDisplayURICommand(); |
|
241 return; |
|
242 } |
|
243 |
|
244 final Account[] syncAccounts = accountManager.getAccountsByType(SyncConstants.ACCOUNTTYPE_SYNC); |
|
245 if (syncAccounts.length > 0) { |
|
246 this.tabSender = new Sync11TabSender(applicationContext, syncAccounts[0], accountManager); |
|
247 |
|
248 Logger.info(LOG_TAG, "Allowing tab send for Sync account."); |
|
249 registerDisplayURICommand(); |
|
250 return; |
|
251 } |
|
252 |
|
253 // Offer to set up a Firefox Account, and finish this activity. |
|
254 redirectToNewTask(FxAccountGetStartedActivity.class, false); |
|
255 } |
|
256 |
|
257 private static void registerDisplayURICommand() { |
|
258 final CommandProcessor processor = CommandProcessor.getProcessor(); |
|
259 processor.registerCommand("displayURI", new CommandRunner(3) { |
|
260 @Override |
|
261 public void executeCommand(final GlobalSession session, List<String> args) { |
|
262 CommandProcessor.displayURI(args, session.getContext()); |
|
263 } |
|
264 }); |
|
265 } |
|
266 |
|
267 public void sendClickHandler(View view) { |
|
268 Logger.info(LOG_TAG, "Send was clicked."); |
|
269 final List<String> remoteClientGuids = arrayAdapter.getCheckedGUIDs(); |
|
270 |
|
271 if (remoteClientGuids == null) { |
|
272 // Should never happen. |
|
273 Logger.warn(LOG_TAG, "guids was null; aborting without sending tab."); |
|
274 notifyAndFinish(false); |
|
275 return; |
|
276 } |
|
277 |
|
278 final TabSender sender = this.tabSender; |
|
279 if (sender == null) { |
|
280 // This should never happen. |
|
281 Logger.warn(LOG_TAG, "tabSender was null; aborting without sending tab."); |
|
282 notifyAndFinish(false); |
|
283 return; |
|
284 } |
|
285 |
|
286 // Fetching local client GUID hits the DB, and we want to update the UI |
|
287 // afterward, so we perform the tab sending on another thread. |
|
288 new AsyncTask<Void, Void, Boolean>() { |
|
289 |
|
290 @Override |
|
291 protected Boolean doInBackground(Void... params) { |
|
292 final CommandProcessor processor = CommandProcessor.getProcessor(); |
|
293 |
|
294 final String accountGUID = sender.getAccountGUID(); |
|
295 Logger.debug(LOG_TAG, "Retrieved local account GUID '" + accountGUID + "'."); |
|
296 if (accountGUID == null) { |
|
297 return false; |
|
298 } |
|
299 |
|
300 for (String remoteClientGuid : remoteClientGuids) { |
|
301 processor.sendURIToClientForDisplay(sendTabData.uri, remoteClientGuid, sendTabData.title, accountGUID, getApplicationContext()); |
|
302 } |
|
303 |
|
304 Logger.info(LOG_TAG, "Requesting immediate clients stage sync."); |
|
305 sender.syncClientsStage(); |
|
306 |
|
307 return true; |
|
308 } |
|
309 |
|
310 @Override |
|
311 protected void onPostExecute(final Boolean success) { |
|
312 // We're allowed to update the UI from here. |
|
313 notifyAndFinish(success.booleanValue()); |
|
314 } |
|
315 }.execute(); |
|
316 } |
|
317 |
|
318 /** |
|
319 * Notify the user about sent tabs status and then finish the activity. |
|
320 * <p> |
|
321 * "Success" is a bit of a misnomer: we wrote "displayURI" commands to the local |
|
322 * command database, and they will be sent on next sync. There is no way to |
|
323 * verify that the commands were successfully received by the intended remote |
|
324 * client, so we lie and say they were sent. |
|
325 * |
|
326 * @param success true if tab was sent successfully; false otherwise. |
|
327 */ |
|
328 protected void notifyAndFinish(final boolean success) { |
|
329 int textId; |
|
330 if (success) { |
|
331 textId = R.string.sync_text_tab_sent; |
|
332 } else { |
|
333 textId = R.string.sync_text_tab_not_sent; |
|
334 } |
|
335 |
|
336 Toast.makeText(this, textId, Toast.LENGTH_LONG).show(); |
|
337 finish(); |
|
338 } |
|
339 |
|
340 public void enableSend(boolean shouldEnable) { |
|
341 View sendButton = findViewById(R.id.send_button); |
|
342 sendButton.setEnabled(shouldEnable); |
|
343 sendButton.setClickable(shouldEnable); |
|
344 } |
|
345 |
|
346 /** |
|
347 * @return a map from GUID to client record, including our own. |
|
348 */ |
|
349 protected Map<String, ClientRecord> getAllClients() { |
|
350 ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(this.getApplicationContext()); |
|
351 try { |
|
352 return db.fetchAllClients(); |
|
353 } catch (NullCursorException e) { |
|
354 Logger.warn(LOG_TAG, "NullCursorException while populating device list.", e); |
|
355 return null; |
|
356 } finally { |
|
357 db.close(); |
|
358 } |
|
359 } |
|
360 |
|
361 /** |
|
362 * @return a collection of client records, excluding our own. |
|
363 */ |
|
364 protected Collection<ClientRecord> getOtherClients() { |
|
365 final Map<String, ClientRecord> all = getAllClients(); |
|
366 if (all == null) { |
|
367 return new ArrayList<ClientRecord>(0); |
|
368 } |
|
369 |
|
370 if (this.tabSender == null) { |
|
371 Logger.warn(LOG_TAG, "No tab sender when fetching other client IDs."); |
|
372 return new ArrayList<ClientRecord>(0); |
|
373 } |
|
374 |
|
375 final String ourGUID = this.tabSender.getAccountGUID(); |
|
376 if (ourGUID == null) { |
|
377 return all.values(); |
|
378 } |
|
379 |
|
380 final ArrayList<ClientRecord> out = new ArrayList<ClientRecord>(all.size()); |
|
381 for (Entry<String, ClientRecord> entry : all.entrySet()) { |
|
382 if (ourGUID.equals(entry.getKey())) { |
|
383 continue; |
|
384 } |
|
385 out.add(entry.getValue()); |
|
386 } |
|
387 return out; |
|
388 } |
|
389 |
|
390 // Adapted from FxAccountAbstractActivity. |
|
391 protected void redirectToNewTask(Class<? extends Activity> activityClass, boolean success) { |
|
392 Intent intent = new Intent(this, activityClass); |
|
393 // Per http://stackoverflow.com/a/8992365, this triggers a known bug with |
|
394 // the soft keyboard not being shown for the started activity. Why, Android, why? |
|
395 intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); |
|
396 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
|
397 startActivity(intent); |
|
398 notifyAndFinish(success); |
|
399 } |
|
400 } |