fdroidclient / F-Droid / src / org / fdroid / fdroid / views / swap / SwapWorkflowActivity.java @ 9848816d
History | View | Annotate | Download (32.1 KB)
1 |
package org.fdroid.fdroid.views.swap; |
---|---|
2 |
|
3 |
import android.app.Activity; |
4 |
import android.content.ComponentName; |
5 |
import android.bluetooth.BluetoothAdapter; |
6 |
import android.content.Context; |
7 |
import android.content.DialogInterface; |
8 |
import android.content.Intent; |
9 |
import android.content.ServiceConnection; |
10 |
import android.net.Uri; |
11 |
import android.net.wifi.WifiManager; |
12 |
import android.os.AsyncTask; |
13 |
import android.os.Bundle; |
14 |
import android.os.IBinder; |
15 |
import android.support.annotation.ColorRes; |
16 |
import android.support.annotation.LayoutRes; |
17 |
import android.support.annotation.NonNull; |
18 |
import android.support.annotation.Nullable; |
19 |
import android.support.v4.content.LocalBroadcastManager; |
20 |
import android.support.v7.app.AlertDialog; |
21 |
import android.support.v7.app.AppCompatActivity; |
22 |
import android.support.v7.widget.Toolbar; |
23 |
import android.util.Log; |
24 |
import android.view.LayoutInflater; |
25 |
import android.view.Menu; |
26 |
import android.view.MenuInflater; |
27 |
import android.view.View; |
28 |
import android.view.ViewGroup; |
29 |
import android.widget.Toast; |
30 |
|
31 |
import com.google.zxing.integration.android.IntentIntegrator; |
32 |
import com.google.zxing.integration.android.IntentResult; |
33 |
|
34 |
import org.fdroid.fdroid.FDroidApp; |
35 |
import org.fdroid.fdroid.NfcHelper; |
36 |
import org.fdroid.fdroid.Preferences; |
37 |
import org.fdroid.fdroid.ProgressListener; |
38 |
import org.fdroid.fdroid.R; |
39 |
import org.fdroid.fdroid.Utils; |
40 |
import org.fdroid.fdroid.data.Apk; |
41 |
import org.fdroid.fdroid.data.ApkProvider; |
42 |
import org.fdroid.fdroid.data.App; |
43 |
import org.fdroid.fdroid.data.NewRepoConfig; |
44 |
import org.fdroid.fdroid.installer.Installer; |
45 |
import org.fdroid.fdroid.localrepo.LocalRepoManager; |
46 |
import org.fdroid.fdroid.localrepo.SwapService; |
47 |
import org.fdroid.fdroid.localrepo.peers.Peer; |
48 |
import org.fdroid.fdroid.net.ApkDownloader; |
49 |
|
50 |
import java.io.File; |
51 |
import java.util.Arrays; |
52 |
import java.util.Date; |
53 |
import java.util.HashMap; |
54 |
import java.util.HashSet; |
55 |
import java.util.Map; |
56 |
import java.util.Set; |
57 |
import java.util.Timer; |
58 |
import java.util.TimerTask; |
59 |
|
60 |
import cc.mvdan.accesspoint.WifiApControl; |
61 |
|
62 |
/**
|
63 |
* This activity will do its best to show the most relevant screen about swapping to the user.
|
64 |
* The problem comes when there are two competing goals - 1) Show the user a list of apps from another
|
65 |
* device to download and install, and 2) Prepare your own list of apps to share.
|
66 |
*/
|
67 |
public class SwapWorkflowActivity extends AppCompatActivity { |
68 |
|
69 |
/**
|
70 |
* When connecting to a swap, we then go and initiate a connection with that
|
71 |
* device and ask if it would like to swap with us. Upon receiving that request
|
72 |
* and agreeing, we don't then want to be asked whether we want to swap back.
|
73 |
* This flag protects against two devices continually going back and forth
|
74 |
* among each other offering swaps.
|
75 |
*/
|
76 |
public static final String EXTRA_PREVENT_FURTHER_SWAP_REQUESTS = "preventFurtherSwap"; |
77 |
public static final String EXTRA_CONFIRM = "EXTRA_CONFIRM"; |
78 |
public static final String EXTRA_REPO_ID = "repoId"; |
79 |
|
80 |
/**
|
81 |
* Ensure that we don't try to handle specific intents more than once in onResume()
|
82 |
* (e.g. the "Do you want to swap back with ..." intent).
|
83 |
*/
|
84 |
public static final String EXTRA_HANDLED = "handled"; |
85 |
|
86 |
private ViewGroup container;
|
87 |
|
88 |
/**
|
89 |
* A UI component (subclass of {@link View}) which forms part of the swap workflow.
|
90 |
* There is a one to one mapping between an {@link org.fdroid.fdroid.views.swap.SwapWorkflowActivity.InnerView}
|
91 |
* and a {@link SwapService.SwapStep}, and these views know what
|
92 |
* the previous view before them should be.
|
93 |
*/
|
94 |
public interface InnerView { |
95 |
/** @return True if the menu should be shown. */
|
96 |
boolean buildMenu(Menu menu, @NonNull MenuInflater inflater); |
97 |
|
98 |
/** @return The step that this view represents. */
|
99 |
@SwapService.SwapStep int getStep(); |
100 |
|
101 |
@SwapService.SwapStep int getPreviousStep(); |
102 |
|
103 |
@ColorRes int getToolbarColour(); |
104 |
|
105 |
String getToolbarTitle();
|
106 |
} |
107 |
|
108 |
private static final String TAG = "SwapWorkflowActivity"; |
109 |
|
110 |
private static final int CONNECT_TO_SWAP = 1; |
111 |
private static final int REQUEST_BLUETOOTH_ENABLE_FOR_SWAP = 2; |
112 |
private static final int REQUEST_BLUETOOTH_DISCOVERABLE = 3; |
113 |
private static final int REQUEST_BLUETOOTH_ENABLE_FOR_SEND = 4; |
114 |
|
115 |
private Toolbar toolbar;
|
116 |
private InnerView currentView;
|
117 |
private boolean hasPreparedLocalRepo = false; |
118 |
private PrepareSwapRepo updateSwappableAppsTask = null; |
119 |
private NewRepoConfig confirmSwapConfig = null; |
120 |
|
121 |
@NonNull
|
122 |
private final ServiceConnection serviceConnection = new ServiceConnection() { |
123 |
@Override
|
124 |
public void onServiceConnected(ComponentName className, IBinder binder) { |
125 |
Log.d(TAG, "Swap service connected. Will hold onto it so we can talk to it regularly.");
|
126 |
service = ((SwapService.Binder)binder).getService(); |
127 |
showRelevantView(); |
128 |
} |
129 |
|
130 |
// TODO: What causes this? Do we need to stop swapping explicitly when this is invoked?
|
131 |
@Override
|
132 |
public void onServiceDisconnected(ComponentName className) { |
133 |
Log.d(TAG, "Swap service disconnected");
|
134 |
service = null;
|
135 |
// TODO: What to do about the UI in this instance?
|
136 |
} |
137 |
}; |
138 |
|
139 |
@Nullable
|
140 |
private SwapService service = null; |
141 |
|
142 |
@NonNull
|
143 |
public SwapService getService() {
|
144 |
if (service == null) { |
145 |
// *Slightly* more informative than a null-pointer error that would otherwise happen.
|
146 |
throw new IllegalStateException("Trying to access swap service before it was initialized."); |
147 |
} |
148 |
return service;
|
149 |
} |
150 |
|
151 |
@Override
|
152 |
public void onBackPressed() { |
153 |
if (currentView.getStep() == SwapService.STEP_INTRO) {
|
154 |
if (service != null) { |
155 |
service.disableAllSwapping(); |
156 |
} |
157 |
finish(); |
158 |
} else {
|
159 |
int nextStep = currentView.getPreviousStep();
|
160 |
getService().setStep(nextStep); |
161 |
showRelevantView(); |
162 |
} |
163 |
} |
164 |
|
165 |
@Override
|
166 |
protected void onCreate(Bundle savedInstanceState) { |
167 |
super.onCreate(savedInstanceState);
|
168 |
|
169 |
// The server should not be doing anything or occupying any (noticeable) resources
|
170 |
// until we actually ask it to enable swapping. Therefore, we will start it nice and
|
171 |
// early so we don't have to wait until it is connected later.
|
172 |
Intent service = new Intent(this, SwapService.class); |
173 |
if (bindService(service, serviceConnection, Context.BIND_AUTO_CREATE)) { |
174 |
startService(service); |
175 |
} |
176 |
|
177 |
setContentView(R.layout.swap_activity); |
178 |
|
179 |
toolbar = (Toolbar) findViewById(R.id.toolbar); |
180 |
toolbar.setTitleTextAppearance(getApplicationContext(), R.style.SwapTheme_Wizard_Text_Toolbar); |
181 |
setSupportActionBar(toolbar); |
182 |
|
183 |
container = (ViewGroup) findViewById(R.id.fragment_container); |
184 |
|
185 |
new SwapDebug().logStatus();
|
186 |
} |
187 |
|
188 |
@Override
|
189 |
protected void onDestroy() { |
190 |
unbindService(serviceConnection); |
191 |
super.onDestroy();
|
192 |
} |
193 |
|
194 |
@Override
|
195 |
public boolean onPrepareOptionsMenu(Menu menu) { |
196 |
menu.clear(); |
197 |
boolean parent = super.onPrepareOptionsMenu(menu); |
198 |
boolean inner = currentView != null && currentView.buildMenu(menu, getMenuInflater()); |
199 |
return parent || inner;
|
200 |
} |
201 |
|
202 |
@Override
|
203 |
protected void onResume() { |
204 |
super.onResume();
|
205 |
|
206 |
checkIncomingIntent(); |
207 |
showRelevantView(); |
208 |
} |
209 |
|
210 |
private void checkIncomingIntent() { |
211 |
Intent intent = getIntent(); |
212 |
if (intent.getBooleanExtra(EXTRA_CONFIRM, false) && !intent.getBooleanExtra(EXTRA_HANDLED, false)) { |
213 |
// Storing config in this variable will ensure that when showRelevantView() is next
|
214 |
// run, it will show the connect swap view (if the service is available).
|
215 |
intent.putExtra(EXTRA_HANDLED, true);
|
216 |
confirmSwapConfig = new NewRepoConfig(this, intent); |
217 |
} |
218 |
} |
219 |
|
220 |
public void promptToSelectWifiNetwork() { |
221 |
//
|
222 |
// On Android >= 5.0, the neutral button is the one by itself off to the left of a dialog
|
223 |
// (not the negative button). Thus, the layout of this dialogs buttons should be:
|
224 |
//
|
225 |
// | |
|
226 |
// +---------------------------------+
|
227 |
// | Cancel Hotspot WiFi |
|
228 |
// +---------------------------------+
|
229 |
//
|
230 |
// TODO: Investigate if this should be set dynamically for earlier APIs.
|
231 |
//
|
232 |
new AlertDialog.Builder(this) |
233 |
.setTitle(R.string.swap_join_same_wifi) |
234 |
.setMessage(R.string.swap_join_same_wifi_desc) |
235 |
.setNeutralButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
236 |
@Override
|
237 |
public void onClick(DialogInterface dialog, int which) { |
238 |
// Do nothing
|
239 |
} |
240 |
} |
241 |
).setPositiveButton(R.string.wifi, new DialogInterface.OnClickListener() {
|
242 |
@Override
|
243 |
public void onClick(DialogInterface dialog, int which) { |
244 |
startActivity(new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK));
|
245 |
} |
246 |
} |
247 |
).setNegativeButton(R.string.wifi_ap, new DialogInterface.OnClickListener() {
|
248 |
@Override
|
249 |
public void onClick(DialogInterface dialog, int which) { |
250 |
promptToSetupWifiAP(); |
251 |
} |
252 |
} |
253 |
).create().show(); |
254 |
} |
255 |
|
256 |
private void promptToSetupWifiAP() { |
257 |
WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
258 |
WifiApControl ap = WifiApControl.getInstance(this);
|
259 |
wifiManager.setWifiEnabled(false);
|
260 |
if (!ap.enable()) {
|
261 |
Log.e(TAG, "Could not enable WiFi AP.");
|
262 |
// TODO: Feedback to user?
|
263 |
} else {
|
264 |
Log.d(TAG, "WiFi AP enabled.");
|
265 |
// TODO: Seems to be broken some times...
|
266 |
} |
267 |
} |
268 |
|
269 |
private void showRelevantView() { |
270 |
showRelevantView(false);
|
271 |
} |
272 |
|
273 |
private void showRelevantView(boolean forceReload) { |
274 |
|
275 |
if (service == null) { |
276 |
showInitialLoading(); |
277 |
return;
|
278 |
} |
279 |
|
280 |
// This is separate from the switch statement below, because it is usually populated
|
281 |
// during onResume, when there is a high probability of not having a swap service
|
282 |
// available. Thus, we were unable to set the state of the swap service appropriately.
|
283 |
if (confirmSwapConfig != null) { |
284 |
showConfirmSwap(confirmSwapConfig); |
285 |
confirmSwapConfig = null;
|
286 |
return;
|
287 |
} |
288 |
|
289 |
if (!forceReload) {
|
290 |
if (container.getVisibility() == View.GONE || currentView != null && currentView.getStep() == service.getStep()) { |
291 |
// Already showing the correct step, so don't bother changing anything.
|
292 |
return;
|
293 |
} |
294 |
} |
295 |
|
296 |
switch(service.getStep()) {
|
297 |
case SwapService.STEP_INTRO:
|
298 |
showIntro(); |
299 |
break;
|
300 |
case SwapService.STEP_SELECT_APPS:
|
301 |
showSelectApps(); |
302 |
break;
|
303 |
case SwapService.STEP_SHOW_NFC:
|
304 |
showNfc(); |
305 |
break;
|
306 |
case SwapService.STEP_JOIN_WIFI:
|
307 |
showJoinWifi(); |
308 |
break;
|
309 |
case SwapService.STEP_WIFI_QR:
|
310 |
showWifiQr(); |
311 |
break;
|
312 |
case SwapService.STEP_SUCCESS:
|
313 |
showSwapConnected(); |
314 |
break;
|
315 |
case SwapService.STEP_CONNECTING:
|
316 |
// TODO: Properly decide what to do here (i.e. returning to the activity after it was connecting)...
|
317 |
inflateInnerView(R.layout.swap_blank); |
318 |
break;
|
319 |
} |
320 |
} |
321 |
|
322 |
public SwapService getState() {
|
323 |
return service;
|
324 |
} |
325 |
|
326 |
private void showNfc() { |
327 |
if (!attemptToShowNfc()) {
|
328 |
showWifiQr(); |
329 |
} |
330 |
} |
331 |
|
332 |
private InnerView inflateInnerView(@LayoutRes int viewRes) { |
333 |
container.removeAllViews(); |
334 |
View view = ((LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(viewRes, container, false); |
335 |
currentView = (InnerView)view; |
336 |
|
337 |
// Don't actually set the step to STEP_INITIAL_LOADING, as we are going to use this view
|
338 |
// purely as a placeholder for _whatever view is meant to be shown_.
|
339 |
if (currentView.getStep() != SwapService.STEP_INITIAL_LOADING) {
|
340 |
if (service == null) { |
341 |
throw new IllegalStateException("We are not in the STEP_INITIAL_LOADING state, but the service is not ready."); |
342 |
} |
343 |
service.setStep(currentView.getStep()); |
344 |
} |
345 |
|
346 |
toolbar.setBackgroundColor(getResources().getColor(currentView.getToolbarColour())); |
347 |
toolbar.setTitle(currentView.getToolbarTitle()); |
348 |
toolbar.setNavigationIcon(R.drawable.ic_close_white); |
349 |
toolbar.setNavigationOnClickListener(new View.OnClickListener() { |
350 |
@Override
|
351 |
public void onClick(View v) { |
352 |
onToolbarCancel(); |
353 |
} |
354 |
}); |
355 |
container.addView(view); |
356 |
supportInvalidateOptionsMenu(); |
357 |
|
358 |
return currentView;
|
359 |
} |
360 |
|
361 |
private void onToolbarCancel() { |
362 |
getService().disableAllSwapping(); |
363 |
finish(); |
364 |
} |
365 |
|
366 |
private void showInitialLoading() { |
367 |
inflateInnerView(R.layout.swap_initial_loading); |
368 |
} |
369 |
|
370 |
public void showIntro() { |
371 |
// If we were previously swapping with a specific client, forget that we were doing that,
|
372 |
// as we are starting over now.
|
373 |
getService().swapWith(null);
|
374 |
|
375 |
if (!getService().isEnabled()) {
|
376 |
prepareInitialRepo(); |
377 |
} |
378 |
getService().scanForPeers(); |
379 |
inflateInnerView(R.layout.swap_blank); |
380 |
} |
381 |
|
382 |
private void showConfirmSwap(@NonNull NewRepoConfig config) { |
383 |
((ConfirmReceive)inflateInnerView(R.layout.swap_confirm_receive)).setup(config); |
384 |
} |
385 |
|
386 |
public void startQrWorkflow() { |
387 |
if (!getService().isEnabled()) {
|
388 |
new AlertDialog.Builder(this) |
389 |
.setTitle(R.string.swap_not_enabled) |
390 |
.setMessage(R.string.swap_not_enabled_description) |
391 |
.setCancelable(true)
|
392 |
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
393 |
@Override
|
394 |
public void onClick(DialogInterface dialog, int which) { |
395 |
// Do nothing. The dialog will get dismissed anyway, which is all we ever wanted...
|
396 |
} |
397 |
}) |
398 |
.create().show(); |
399 |
} else {
|
400 |
showSelectApps(); |
401 |
} |
402 |
} |
403 |
|
404 |
public void showSelectApps() { |
405 |
inflateInnerView(R.layout.swap_select_apps); |
406 |
} |
407 |
|
408 |
public void sendFDroid() { |
409 |
// If Bluetooth has not been enabled/turned on, then enabling device discoverability
|
410 |
// will automatically enable Bluetooth.
|
411 |
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); |
412 |
if (adapter != null) { |
413 |
if (adapter.getState() != BluetoothAdapter.STATE_ON) {
|
414 |
Intent discoverBt = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
|
415 |
discoverBt.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 120);
|
416 |
startActivityForResult(discoverBt, REQUEST_BLUETOOTH_ENABLE_FOR_SEND); |
417 |
} else {
|
418 |
sendFDroidApk(); |
419 |
} |
420 |
} else {
|
421 |
new AlertDialog.Builder(this) |
422 |
.setTitle(R.string.bluetooth_unavailable) |
423 |
.setMessage(R.string.swap_cant_send_no_bluetooth) |
424 |
.setNegativeButton( |
425 |
R.string.cancel, |
426 |
new DialogInterface.OnClickListener() {
|
427 |
@Override
|
428 |
public void onClick(DialogInterface dialog, int which) {} |
429 |
} |
430 |
).create().show(); |
431 |
} |
432 |
} |
433 |
|
434 |
private void sendFDroidApk() { |
435 |
((FDroidApp) getApplication()).sendViaBluetooth(this, Activity.RESULT_OK, "org.fdroid.fdroid"); |
436 |
} |
437 |
|
438 |
// TODO: Figure out whether they have changed since last time UpdateAsyncTask was run.
|
439 |
// If the local repo is running, then we can ask it what apps it is swapping and compare with that.
|
440 |
// Otherwise, probably will need to scan the file system.
|
441 |
public void onAppsSelected() { |
442 |
if (updateSwappableAppsTask == null && !hasPreparedLocalRepo) { |
443 |
updateSwappableAppsTask = new PrepareSwapRepo(getService().getAppsToSwap());
|
444 |
updateSwappableAppsTask.execute(); |
445 |
getService().setStep(SwapService.STEP_CONNECTING); |
446 |
inflateInnerView(R.layout.swap_connecting); |
447 |
} else {
|
448 |
onLocalRepoPrepared(); |
449 |
} |
450 |
} |
451 |
|
452 |
private void prepareInitialRepo() { |
453 |
// TODO: Make it so that this and updateSwappableAppsTask (the _real_ swap repo task)
|
454 |
// don't stomp on eachothers toes. The other one should wait for this to finish, or cancel
|
455 |
// this, but this should never take precedence over the other.
|
456 |
// TODO: Also don't allow this to run multiple times (e.g. if a user keeps navigating back
|
457 |
// to the main screen.
|
458 |
Log.d(TAG, "Preparing initial repo with only F-Droid, until we have allowed the user to configure their own repo.");
|
459 |
new PrepareInitialSwapRepo().execute();
|
460 |
} |
461 |
|
462 |
/**
|
463 |
* Once the UpdateAsyncTask has finished preparing our repository index, we can
|
464 |
* show the next screen to the user. This will be one of two things:
|
465 |
* * If we directly selected a peer to swap with initially, we will skip straight to getting
|
466 |
* the list of apps from that device.
|
467 |
* * Alternatively, if we didn't have a person to connect to, and instead clicked "Scan QR Code",
|
468 |
* then we want to show a QR code or NFC dialog.
|
469 |
*/
|
470 |
public void onLocalRepoPrepared() { |
471 |
updateSwappableAppsTask = null;
|
472 |
hasPreparedLocalRepo = true;
|
473 |
if (getService().isConnectingWithPeer()) {
|
474 |
startSwappingWithPeer(); |
475 |
} else if (!attemptToShowNfc()) { |
476 |
showWifiQr(); |
477 |
} |
478 |
} |
479 |
|
480 |
private void startSwappingWithPeer() { |
481 |
getService().connectToPeer(); |
482 |
inflateInnerView(R.layout.swap_connecting); |
483 |
} |
484 |
|
485 |
private void showJoinWifi() { |
486 |
inflateInnerView(R.layout.swap_join_wifi); |
487 |
} |
488 |
|
489 |
public void showWifiQr() { |
490 |
inflateInnerView(R.layout.swap_wifi_qr); |
491 |
} |
492 |
|
493 |
public void showSwapConnected() { |
494 |
inflateInnerView(R.layout.swap_success); |
495 |
} |
496 |
|
497 |
private boolean attemptToShowNfc() { |
498 |
// TODO: What if NFC is disabled? Hook up with NfcNotEnabledActivity? Or maybe only if they
|
499 |
// click a relevant button?
|
500 |
|
501 |
// Even if they opted to skip the message which says "Touch devices to swap",
|
502 |
// we still want to actually enable the feature, so that they could touch
|
503 |
// during the wifi qr code being shown too.
|
504 |
boolean nfcMessageReady = NfcHelper.setPushMessage(this, Utils.getSharingUri(FDroidApp.repo)); |
505 |
|
506 |
if (Preferences.get().showNfcDuringSwap() && nfcMessageReady) { |
507 |
inflateInnerView(R.layout.swap_nfc); |
508 |
return true; |
509 |
} |
510 |
return false; |
511 |
} |
512 |
|
513 |
public void swapWith(Peer peer) { |
514 |
getService().stopScanningForPeers(); |
515 |
getService().swapWith(peer); |
516 |
showSelectApps(); |
517 |
} |
518 |
|
519 |
/**
|
520 |
* This is for when we initiate a swap by viewing the "Are you sure you want to swap with" view
|
521 |
* This can arise either:
|
522 |
* * As a result of scanning a QR code (in which case we likely already have a repo setup) or
|
523 |
* * As a result of the other device selecting our device in the "start swap" screen, in which
|
524 |
* case we are likely just sitting on the start swap screen also, and haven't configured
|
525 |
* anything yet.
|
526 |
*/
|
527 |
public void swapWith(NewRepoConfig repoConfig) { |
528 |
Peer peer = repoConfig.toPeer(); |
529 |
if (getService().getStep() == SwapService.STEP_INTRO || getService().getStep() == SwapService.STEP_CONFIRM_SWAP) {
|
530 |
// This will force the "Select apps to swap" workflow to begin.
|
531 |
// TODO: Find a better way to decide whether we need to select the apps. Not sure if we
|
532 |
// can or cannot be in STEP_INTRO with a full blown repo ready to swap.
|
533 |
swapWith(peer); |
534 |
} else {
|
535 |
getService().swapWith(repoConfig.toPeer()); |
536 |
startSwappingWithPeer(); |
537 |
} |
538 |
} |
539 |
|
540 |
public void denySwap() { |
541 |
showIntro(); |
542 |
} |
543 |
|
544 |
/**
|
545 |
* Attempts to open a QR code scanner, in the hope a user will then scan the QR code of another
|
546 |
* device configured to swapp apps with us. Delegates to the zxing library to do so.
|
547 |
*/
|
548 |
public void initiateQrScan() { |
549 |
IntentIntegrator integrator = new IntentIntegrator(this); |
550 |
integrator.initiateScan(); |
551 |
} |
552 |
|
553 |
@Override
|
554 |
public void onActivityResult(int requestCode, int resultCode, Intent intent) { |
555 |
IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); |
556 |
if (scanResult != null) { |
557 |
if (scanResult.getContents() != null) { |
558 |
NewRepoConfig repoConfig = new NewRepoConfig(this, scanResult.getContents()); |
559 |
if (repoConfig.isValidRepo()) {
|
560 |
confirmSwapConfig = repoConfig; |
561 |
showRelevantView(); |
562 |
} else {
|
563 |
Toast.makeText(this, R.string.swap_qr_isnt_for_swap, Toast.LENGTH_SHORT).show();
|
564 |
} |
565 |
} |
566 |
} else if (requestCode == CONNECT_TO_SWAP && resultCode == Activity.RESULT_OK) { |
567 |
finish(); |
568 |
} else if (requestCode == REQUEST_BLUETOOTH_ENABLE_FOR_SWAP) { |
569 |
|
570 |
if (resultCode == RESULT_OK) {
|
571 |
Log.d(TAG, "User enabled Bluetooth, will make sure we are discoverable.");
|
572 |
ensureBluetoothDiscoverableThenStart(); |
573 |
} else {
|
574 |
// Didn't enable bluetooth
|
575 |
Log.d(TAG, "User chose not to enable Bluetooth, so doing nothing (i.e. sticking with wifi).");
|
576 |
} |
577 |
|
578 |
} else if (requestCode == REQUEST_BLUETOOTH_DISCOVERABLE) { |
579 |
|
580 |
if (resultCode != RESULT_CANCELED) {
|
581 |
Log.d(TAG, "User made Bluetooth discoverable, will proceed to start bluetooth server.");
|
582 |
getState().getBluetoothSwap().startInBackground(); |
583 |
} else {
|
584 |
Log.d(TAG, "User chose not to make Bluetooth discoverable, so doing nothing (i.e. sticking with wifi).");
|
585 |
} |
586 |
|
587 |
} else if (requestCode == REQUEST_BLUETOOTH_ENABLE_FOR_SEND) { |
588 |
sendFDroidApk(); |
589 |
} |
590 |
} |
591 |
|
592 |
/**
|
593 |
* The process for setting up bluetooth is as follows:
|
594 |
* * Assume we have bluetooth available (otherwise the button which allowed us to start
|
595 |
* the bluetooth process should not have been available).
|
596 |
* * Ask user to enable (if not enabled yet).
|
597 |
* * Start bluetooth server socket.
|
598 |
* * Enable bluetooth discoverability, so that people can connect to our server socket.
|
599 |
*
|
600 |
* Note that this is a little different than the usual process for bluetooth _clients_, which
|
601 |
* involves pairing and connecting with other devices.
|
602 |
*/
|
603 |
public void startBluetoothSwap() { |
604 |
|
605 |
Log.d(TAG, "Initiating Bluetooth swap, will ensure the Bluetooth devices is enabled and discoverable before starting server.");
|
606 |
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); |
607 |
|
608 |
if (adapter != null) |
609 |
if (adapter.isEnabled()) {
|
610 |
Log.d(TAG, "Bluetooth enabled, will check if device is discoverable with device.");
|
611 |
ensureBluetoothDiscoverableThenStart(); |
612 |
} else {
|
613 |
Log.d(TAG, "Bluetooth disabled, asking user to enable it.");
|
614 |
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
|
615 |
startActivityForResult(enableBtIntent, REQUEST_BLUETOOTH_ENABLE_FOR_SWAP); |
616 |
} |
617 |
} |
618 |
|
619 |
private void ensureBluetoothDiscoverableThenStart() { |
620 |
Log.d(TAG, "Ensuring Bluetooth is in discoverable mode.");
|
621 |
if (BluetoothAdapter.getDefaultAdapter().getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
|
622 |
|
623 |
// TODO: Listen for BluetoothAdapter.ACTION_SCAN_MODE_CHANGED and respond if discovery
|
624 |
// is cancelled prematurely.
|
625 |
|
626 |
Log.d(TAG, "Not currently in discoverable mode, so prompting user to enable.");
|
627 |
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
|
628 |
intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); // TODO: What about when this expires? What if user manually disables discovery? |
629 |
startActivityForResult(intent, REQUEST_BLUETOOTH_DISCOVERABLE); |
630 |
} |
631 |
|
632 |
if (service == null) { |
633 |
throw new IllegalStateException("Can't start Bluetooth swap because service is null for some strange reason."); |
634 |
} |
635 |
|
636 |
service.getBluetoothSwap().startInBackground(); |
637 |
} |
638 |
|
639 |
class PrepareInitialSwapRepo extends PrepareSwapRepo { |
640 |
public PrepareInitialSwapRepo() {
|
641 |
super(new HashSet<>(Arrays.asList(new String[] { "org.fdroid.fdroid" }))); |
642 |
} |
643 |
} |
644 |
|
645 |
class PrepareSwapRepo extends AsyncTask<Void, Void, Void> { |
646 |
|
647 |
public static final String ACTION = "PrepareSwapRepo.Action"; |
648 |
public static final String EXTRA_MESSAGE = "PrepareSwapRepo.Status.Message"; |
649 |
public static final String EXTRA_TYPE = "PrepareSwapRepo.Action.Type"; |
650 |
public static final int TYPE_STATUS = 0; |
651 |
public static final int TYPE_COMPLETE = 1; |
652 |
public static final int TYPE_ERROR = 2; |
653 |
|
654 |
@SuppressWarnings("UnusedDeclaration") |
655 |
private static final String TAG = "UpdateAsyncTask"; |
656 |
|
657 |
@NonNull
|
658 |
protected final Set<String> selectedApps; |
659 |
|
660 |
@NonNull
|
661 |
protected final Uri sharingUri; |
662 |
|
663 |
@NonNull
|
664 |
protected final Context context; |
665 |
|
666 |
public PrepareSwapRepo(@NonNull Set<String> apps) { |
667 |
context = SwapWorkflowActivity.this; |
668 |
selectedApps = apps; |
669 |
sharingUri = Utils.getSharingUri(FDroidApp.repo); |
670 |
} |
671 |
|
672 |
private void broadcast(int type) { |
673 |
broadcast(type, null);
|
674 |
} |
675 |
|
676 |
private void broadcast(int type, String message) { |
677 |
Intent intent = new Intent(ACTION);
|
678 |
intent.putExtra(EXTRA_TYPE, type); |
679 |
if (message != null) { |
680 |
Log.d(TAG, "Preparing swap: " + message);
|
681 |
intent.putExtra(EXTRA_MESSAGE, message); |
682 |
} |
683 |
LocalBroadcastManager.getInstance(SwapWorkflowActivity.this).sendBroadcast(intent); |
684 |
} |
685 |
|
686 |
@Override
|
687 |
protected Void doInBackground(Void... params) { |
688 |
try {
|
689 |
final LocalRepoManager lrm = LocalRepoManager.get(context);
|
690 |
broadcast(TYPE_STATUS, getString(R.string.deleting_repo)); |
691 |
lrm.deleteRepo(); |
692 |
for (String app : selectedApps) { |
693 |
broadcast(TYPE_STATUS, String.format(getString(R.string.adding_apks_format), app));
|
694 |
lrm.addApp(context, app); |
695 |
} |
696 |
lrm.writeIndexPage(sharingUri.toString()); |
697 |
broadcast(TYPE_STATUS, getString(R.string.writing_index_jar)); |
698 |
lrm.writeIndexJar(); |
699 |
broadcast(TYPE_STATUS, getString(R.string.linking_apks)); |
700 |
lrm.copyApksToRepo(); |
701 |
broadcast(TYPE_STATUS, getString(R.string.copying_icons)); |
702 |
// run the icon copy without progress, its not a blocker
|
703 |
// TODO: Fix lint error about this being run from a worker thread, says it should be
|
704 |
// run on a main thread.
|
705 |
new AsyncTask<Void, Void, Void>() { |
706 |
|
707 |
@Override
|
708 |
protected Void doInBackground(Void... params) { |
709 |
lrm.copyIconsToRepo(); |
710 |
return null; |
711 |
} |
712 |
}.execute(); |
713 |
|
714 |
broadcast(TYPE_COMPLETE); |
715 |
} catch (Exception e) { |
716 |
broadcast(TYPE_ERROR); |
717 |
e.printStackTrace(); |
718 |
} |
719 |
return null; |
720 |
} |
721 |
} |
722 |
|
723 |
/**
|
724 |
* Helper class to try and make sense of what the swap workflow is currently doing.
|
725 |
* The more technologies are involved in the process (e.g. Bluetooth/Wifi/NFC/etc)
|
726 |
* the harder it becomes to reason about and debug the whole thing. Thus,this class
|
727 |
* will periodically dump the state to logcat so that it is easier to see when certain
|
728 |
* protocols are enabled/disabled.
|
729 |
*
|
730 |
* To view only this output from logcat:
|
731 |
*
|
732 |
* adb logcat | grep 'Swap Status'
|
733 |
*
|
734 |
* To exclude this output from logcat (it is very noisy):
|
735 |
*
|
736 |
* adb logcat | grep -v 'Swap Status'
|
737 |
*
|
738 |
*/
|
739 |
class SwapDebug { |
740 |
|
741 |
public void logStatus() { |
742 |
|
743 |
if (true) return; |
744 |
|
745 |
String message = ""; |
746 |
if (service == null) { |
747 |
message = "No swap service";
|
748 |
} else {
|
749 |
{ |
750 |
String bluetooth = service.getBluetoothSwap().isConnected() ? "Y" : " N"; |
751 |
String wifi = service.getWifiSwap().isConnected() ? "Y" : " N"; |
752 |
String mdns = service.getWifiSwap().getBonjour().isConnected() ? "Y" : " N"; |
753 |
message += "Swap { BT: " + bluetooth + ", WiFi: " + wifi + ", mDNS: " + mdns + "}, "; |
754 |
} |
755 |
|
756 |
{ |
757 |
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); |
758 |
String bluetooth = "N/A"; |
759 |
if (adapter != null) { |
760 |
Map<Integer, String> scanModes = new HashMap<>(3); |
761 |
scanModes.put(BluetoothAdapter.SCAN_MODE_CONNECTABLE, "CON");
|
762 |
scanModes.put(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, "CON_DISC");
|
763 |
scanModes.put(BluetoothAdapter.SCAN_MODE_NONE, "NONE");
|
764 |
bluetooth = "\"" + adapter.getName() + "\" - " + scanModes.get(adapter.getScanMode()); |
765 |
} |
766 |
|
767 |
String wifi = service.getBonjourFinder().isScanning() ? "Y" : " N"; |
768 |
message += "Find { BT: " + bluetooth + ", WiFi: " + wifi + "}"; |
769 |
} |
770 |
} |
771 |
|
772 |
Date now = new Date(); |
773 |
Log.d("Swap Status", now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds() + " " + message); |
774 |
|
775 |
new Timer().schedule(new TimerTask() { |
776 |
@Override
|
777 |
public void run() { |
778 |
new SwapDebug().logStatus();
|
779 |
} |
780 |
}, |
781 |
1000
|
782 |
); |
783 |
} |
784 |
} |
785 |
|
786 |
public void install(@NonNull final App app) { |
787 |
final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.suggestedVercode); |
788 |
final ApkDownloader downloader = new ApkDownloader(this, app, apkToInstall, apkToInstall.repoAddress); |
789 |
downloader.setProgressListener(new ProgressListener() {
|
790 |
@Override
|
791 |
public void onProgress(Event event) { |
792 |
switch (event.type) {
|
793 |
case ApkDownloader.EVENT_APK_DOWNLOAD_COMPLETE:
|
794 |
handleDownloadComplete(downloader.localFile()); |
795 |
break;
|
796 |
case ApkDownloader.EVENT_ERROR:
|
797 |
break;
|
798 |
} |
799 |
} |
800 |
}); |
801 |
downloader.download(); |
802 |
} |
803 |
|
804 |
private void handleDownloadComplete(File apkFile) { |
805 |
|
806 |
try {
|
807 |
Installer.getActivityInstaller(SwapWorkflowActivity.this, new Installer.InstallerCallback() {
|
808 |
@Override
|
809 |
public void onSuccess(int operation) { |
810 |
// TODO: Don't reload the view weely-neely, but rather get the view to listen
|
811 |
// for broadcasts that say the install was complete.
|
812 |
showRelevantView(true);
|
813 |
} |
814 |
|
815 |
@Override
|
816 |
public void onError(int operation, int errorCode) { |
817 |
// TODO: Boo!
|
818 |
} |
819 |
}).installPackage(apkFile); |
820 |
} catch (Installer.AndroidNotCompatibleException e) {
|
821 |
// TODO: Handle exception properly
|
822 |
} |
823 |
} |
824 |
|
825 |
} |