package org.fdroid.fdroid.nearby; import android.annotation.TargetApi; import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.graphics.LightingColorFilter; import android.net.Uri; import android.net.wifi.WifiManager; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.button.MaterialButton; import com.google.android.material.switchmaterial.SwitchMaterial; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; import org.fdroid.download.Downloader; import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.NfcHelper; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.nearby.peers.BluetoothPeer; import org.fdroid.fdroid.nearby.peers.Peer; import org.fdroid.fdroid.net.BluetoothDownloader; import org.fdroid.fdroid.net.DownloaderService; import org.fdroid.fdroid.qr.CameraCharacteristicsChecker; import org.fdroid.fdroid.views.main.MainActivity; import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.Timer; import java.util.TimerTask; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.SearchView; import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import cc.mvdan.accesspoint.WifiApControl; import io.reactivex.rxjava3.disposables.CompositeDisposable; import static org.fdroid.fdroid.views.main.MainActivity.ACTION_REQUEST_SWAP; /** * This is the core of the UI for the whole nearby swap experience. Each * screen is implemented as a {@link View} with the related logic in this * {@link android.app.Activity}. Long lived pieces work in {@link SwapService}. * All these pieces of the UX are tracked here: * *

* There are lots of async events in this system, and the user can also change * the views while things are working. The {@link ViewGroup} * {@link SwapWorkflowActivity#container} can have all its widgets removed and * replaced by a new view at any point. Therefore, any widget config that is * based on fetching it from {@code container} must check that the result is * not null before trying to config it. * * @see */ @SuppressWarnings("LineLength") public class SwapWorkflowActivity extends AppCompatActivity { private static final String TAG = "SwapWorkflowActivity"; /** * When connecting to a swap, we then go and initiate a connection with that * device and ask if it would like to swap with us. Upon receiving that request * and agreeing, we don't then want to be asked whether we want to swap back. * This flag protects against two devices continually going back and forth * among each other offering swaps. */ public static final String EXTRA_PREVENT_FURTHER_SWAP_REQUESTS = "preventFurtherSwap"; private ViewGroup container; private static final int REQUEST_BLUETOOTH_ENABLE_FOR_SWAP = 2; private static final int REQUEST_BLUETOOTH_DISCOVERABLE = 3; private static final int REQUEST_BLUETOOTH_ENABLE_FOR_SEND = 4; private static final int REQUEST_WRITE_SETTINGS_PERMISSION = 5; private MaterialToolbar toolbar; private SwapView currentView; private boolean hasPreparedLocalRepo; private boolean newIntent; private NewRepoConfig confirmSwapConfig; private LocalBroadcastManager localBroadcastManager; private WifiManager wifiManager; private WifiApControl wifiApControl; private BluetoothAdapter bluetoothAdapter; @LayoutRes private int currentSwapViewLayoutRes = R.layout.swap_start_swap; private final Stack backstack = new Stack<>(); private final CompositeDisposable compositeDisposable = new CompositeDisposable(); public static void requestSwap(Context context, String repo) { requestSwap(context, Uri.parse(repo)); } public static void requestSwap(Context context, Uri uri) { Intent intent = new Intent(MainActivity.ACTION_REQUEST_SWAP, uri, context, SwapWorkflowActivity.class); intent.putExtra(EXTRA_PREVENT_FURTHER_SWAP_REQUESTS, true); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } @NonNull private final ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder binder) { service = ((SwapService.Binder) binder).getService(); showRelevantView(); } @Override public void onServiceDisconnected(ComponentName className) { finish(); service = null; } }; @Nullable private SwapService service; @NonNull public SwapService getSwapService() { return service; } /** * Handle the back logic for the system back button. * * @see #inflateSwapView(int, boolean) */ @Override public void onBackPressed() { if (backstack.isEmpty()) { super.onBackPressed(); } else { int resId = backstack.pop(); inflateSwapView(resId, true); } } /** * Handle the back logic for the upper left back button in the toolbar. * This has a simpler, hard-coded back logic than the system back button. * * @see #onBackPressed() */ public void onToolbarBackPressed() { int nextStep = R.layout.swap_start_swap; switch (currentView.getLayoutResId()) { case R.layout.swap_confirm_receive: nextStep = backstack.peek(); break; case R.layout.swap_connecting: nextStep = R.layout.swap_select_apps; break; case R.layout.swap_join_wifi: nextStep = R.layout.swap_start_swap; break; case R.layout.swap_nfc: nextStep = R.layout.swap_join_wifi; break; case R.layout.swap_select_apps: if (!backstack.isEmpty() && backstack.peek() == R.layout.swap_start_swap) { nextStep = R.layout.swap_start_swap; } else if (getSwapService() != null && getSwapService().isConnectingWithPeer()) { nextStep = R.layout.swap_success; } else { nextStep = R.layout.swap_join_wifi; } break; case R.layout.swap_send_fdroid: nextStep = R.layout.swap_start_swap; break; case R.layout.swap_start_swap: if (getSwapService() != null && getSwapService().isConnectingWithPeer()) { nextStep = R.layout.swap_success; } else { SwapService.stop(this); finish(); return; } break; case R.layout.swap_success: nextStep = R.layout.swap_start_swap; break; case R.layout.swap_wifi_qr: if (!backstack.isEmpty() && backstack.peek() == R.layout.swap_start_swap) { nextStep = R.layout.swap_start_swap; } else { nextStep = R.layout.swap_join_wifi; } break; } currentSwapViewLayoutRes = nextStep; inflateSwapView(currentSwapViewLayoutRes); } @Override protected void onCreate(Bundle savedInstanceState) { FDroidApp fdroidApp = (FDroidApp) getApplication(); fdroidApp.setSecureWindow(this); fdroidApp.applyPureBlackBackgroundInDarkTheme(this); super.onCreate(savedInstanceState); currentView = new SwapView(this); // dummy placeholder to avoid NullPointerExceptions; if (!bindService(new Intent(this, SwapService.class), serviceConnection, BIND_ABOVE_CLIENT | BIND_IMPORTANT)) { Toast.makeText(this, "ERROR: cannot bind to SwapService!", Toast.LENGTH_LONG).show(); finish(); } setContentView(R.layout.swap_activity); toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); container = (ViewGroup) findViewById(R.id.container); backstack.clear(); localBroadcastManager = LocalBroadcastManager.getInstance(this); localBroadcastManager.registerReceiver(downloaderInterruptedReceiver, new IntentFilter(DownloaderService.ACTION_INTERRUPTED)); wifiManager = ContextCompat.getSystemService(getApplicationContext(), WifiManager.class); wifiApControl = WifiApControl.getInstance(this); bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); new SwapDebug().logStatus(); } @Override protected void onDestroy() { compositeDisposable.dispose(); localBroadcastManager.unregisterReceiver(downloaderInterruptedReceiver); unbindService(serviceConnection); super.onDestroy(); } @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.clear(); MenuInflater menuInflater = getMenuInflater(); switch (currentView.getLayoutResId()) { case R.layout.swap_select_apps: menuInflater.inflate(R.menu.swap_next_search, menu); if (getSwapService().isConnectingWithPeer()) { setUpNextButton(menu, R.string.next, R.drawable.ic_nearby); } else { setUpNextButton(menu, R.string.next, null); } setUpSearchView(menu); return true; case R.layout.swap_success: menuInflater.inflate(R.menu.swap_search, menu); setUpSearchView(menu); return true; case R.layout.swap_join_wifi: menuInflater.inflate(R.menu.swap_next, menu); setUpNextButton(menu, R.string.next, R.drawable.ic_arrow_forward); return true; case R.layout.swap_nfc: menuInflater.inflate(R.menu.swap_next, menu); setUpNextButton(menu, R.string.skip, R.drawable.ic_arrow_forward); return true; } return super.onPrepareOptionsMenu(menu); } private void setUpNextButton(Menu menu, @StringRes int titleResId, Integer drawableResId) { MenuItem next = menu.findItem(R.id.action_next); CharSequence title = getString(titleResId); next.setTitle(title); next.setTitleCondensed(title); if (drawableResId == null) { next.setVisible(false); } else { next.setVisible(true); next.setIcon(drawableResId); } next.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); next.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { sendNext(); return true; } }); } void sendNext() { int currentLayoutResId = currentView.getLayoutResId(); switch (currentLayoutResId) { case R.layout.swap_select_apps: onAppsSelected(); break; case R.layout.swap_join_wifi: inflateSwapView(R.layout.swap_select_apps); break; case R.layout.swap_nfc: inflateSwapView(R.layout.swap_wifi_qr); break; } } private void setUpSearchView(Menu menu) { MenuItem appsMenuItem = menu.findItem(R.id.action_apps); if (appsMenuItem != null) { appsMenuItem.setOnMenuItemClickListener(item -> { inflateSwapView(R.layout.swap_select_apps); return true; }); } SearchView searchView = new SearchView(this); MenuItem searchMenuItem = menu.findItem(R.id.action_search); searchMenuItem.setActionView(searchView); searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String newText) { String currentFilterString = currentView.getCurrentFilterString(); String newFilter = !TextUtils.isEmpty(newText) ? newText : null; if (currentFilterString == null && newFilter == null) { return true; } if (currentFilterString != null && currentFilterString.equals(newFilter)) { return true; } currentView.setCurrentFilterString(newFilter); if (currentView instanceof SelectAppsView) { getSupportLoaderManager().restartLoader(currentView.getLayoutResId(), null, (SelectAppsView) currentView); } else if (currentView instanceof SwapSuccessView) { getSupportLoaderManager().restartLoader(currentView.getLayoutResId(), null, (SwapSuccessView) currentView); } else { throw new IllegalStateException(currentView.getClass() + " does not have Loader!"); } return true; } @Override public boolean onQueryTextChange(String s) { return true; } }); } @Override protected void onResume() { super.onResume(); localBroadcastManager.registerReceiver(onWifiStateChanged, new IntentFilter(WifiStateChangeService.BROADCAST)); localBroadcastManager.registerReceiver(localRepoStatus, new IntentFilter(LocalRepoService.ACTION_STATUS)); localBroadcastManager.registerReceiver(repoUpdateReceiver, new IntentFilter(UpdateService.LOCAL_ACTION_STATUS)); localBroadcastManager.registerReceiver(bonjourFound, new IntentFilter(BonjourManager.ACTION_FOUND)); localBroadcastManager.registerReceiver(bonjourRemoved, new IntentFilter(BonjourManager.ACTION_REMOVED)); localBroadcastManager.registerReceiver(bonjourStatusReceiver, new IntentFilter(BonjourManager.ACTION_STATUS)); localBroadcastManager.registerReceiver(bluetoothFound, new IntentFilter(BluetoothManager.ACTION_FOUND)); localBroadcastManager.registerReceiver(bluetoothStatusReceiver, new IntentFilter(BluetoothManager.ACTION_STATUS)); registerReceiver(bluetoothScanModeChanged, new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)); checkIncomingIntent(); if (newIntent) { showRelevantView(); newIntent = false; } } @Override protected void onPause() { super.onPause(); unregisterReceiver(bluetoothScanModeChanged); localBroadcastManager.unregisterReceiver(onWifiStateChanged); localBroadcastManager.unregisterReceiver(localRepoStatus); localBroadcastManager.unregisterReceiver(repoUpdateReceiver); localBroadcastManager.unregisterReceiver(bonjourFound); localBroadcastManager.unregisterReceiver(bonjourRemoved); localBroadcastManager.unregisterReceiver(bonjourStatusReceiver); localBroadcastManager.unregisterReceiver(bluetoothFound); localBroadcastManager.unregisterReceiver(bluetoothStatusReceiver); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); newIntent = true; } /** * Check whether incoming {@link Intent} is a swap repo, and ensure that * it is a valid swap URL. The hostname can only be either an IP or * Bluetooth address. */ private void checkIncomingIntent() { Intent intent = getIntent(); if (!ACTION_REQUEST_SWAP.equals(intent.getAction())) { return; } Uri uri = intent.getData(); if (uri != null && !isSwapUrl(uri) && !BluetoothDownloader.isBluetoothUri(uri)) { String msg = getString(R.string.swap_toast_invalid_url, uri); Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); return; } confirmSwapConfig = new NewRepoConfig(this, intent); } private static boolean isSwapUrl(Uri uri) { return isSwapUrl(uri.getHost(), uri.getPort()); } private static boolean isSwapUrl(String host, int port) { return port > 1023 // only root can use <= 1023, so never a swap repo && host.matches("[0-9.]+") // host must be an IP address && FDroidApp.subnetInfo.isInRange(host); // on the same subnet as we are } public void promptToSelectWifiNetwork() { new AlertDialog.Builder(this) .setTitle(R.string.swap_join_same_wifi) .setMessage(R.string.swap_join_same_wifi_desc) .setNeutralButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Do nothing } }) .setPositiveButton(R.string.wifi, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { SwapService.putWifiEnabledBeforeSwap(wifiManager.isWifiEnabled()); wifiManager.setWifiEnabled(true); Intent intent = new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } }) .setNegativeButton(R.string.wifi_ap, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (Build.VERSION.SDK_INT >= 26) { showTetheringSettings(); } else if (Build.VERSION.SDK_INT >= 23 && !Settings.System.canWrite(getBaseContext())) { requestWriteSettingsPermission(); } else { setupWifiAP(); } } }) .create().show(); } private void setupWifiAP() { if (wifiApControl == null) { Log.e(TAG, "WiFi AP is null"); Toast.makeText(this, R.string.swap_toast_could_not_enable_hotspot, Toast.LENGTH_LONG).show(); return; } SwapService.putHotspotEnabledBeforeSwap(wifiApControl.isEnabled()); wifiManager.setWifiEnabled(false); if (wifiApControl.enable()) { Toast.makeText(this, R.string.swap_toast_hotspot_enabled, Toast.LENGTH_SHORT).show(); SwapService.putHotspotActivatedUserPreference(true); } else { Toast.makeText(this, R.string.swap_toast_could_not_enable_hotspot, Toast.LENGTH_LONG).show(); SwapService.putHotspotActivatedUserPreference(false); Log.e(TAG, "Could not enable WiFi AP."); } } /** * Handle events that trigger different swap views to be shown. */ private void showRelevantView() { if (confirmSwapConfig != null) { inflateSwapView(R.layout.swap_confirm_receive); setUpConfirmReceive(); confirmSwapConfig = null; return; } switch (currentSwapViewLayoutRes) { case R.layout.swap_start_swap: showIntro(); return; case R.layout.swap_nfc: if (!attemptToShowNfc()) { inflateSwapView(R.layout.swap_wifi_qr); return; } break; case R.layout.swap_connecting: // TODO: Properly decide what to do here (i.e. returning to the activity after it was connecting)... inflateSwapView(R.layout.swap_start_swap); return; } inflateSwapView(currentSwapViewLayoutRes); } public void inflateSwapView(@LayoutRes int viewRes) { inflateSwapView(viewRes, false); } /** * The {@link #backstack} for the global back button is managed mostly here. * The initial screen is never added to the {@code backstack} since the * empty state is used to detect that the system's backstack should be used. */ public void inflateSwapView(@LayoutRes int viewRes, boolean backPressed) { getSwapService().initTimer(); if (!backPressed) { switch (currentSwapViewLayoutRes) { case R.layout.swap_connecting: case R.layout.swap_confirm_receive: // do not add to backstack break; default: if (backstack.isEmpty()) { if (viewRes != R.layout.swap_start_swap) { backstack.push(currentSwapViewLayoutRes); } } else { if (backstack.peek() != currentSwapViewLayoutRes) { backstack.push(currentSwapViewLayoutRes); } } } } container.removeAllViews(); View view = ContextCompat.getSystemService(this, LayoutInflater.class) .inflate(viewRes, container, false); currentView = (SwapView) view; currentView.setLayoutResId(viewRes); currentSwapViewLayoutRes = viewRes; toolbar.setTitle(currentView.getToolbarTitle()); toolbar.setNavigationOnClickListener(v -> onToolbarBackPressed()); toolbar.setNavigationOnClickListener(v -> { switch (currentView.getLayoutResId()) { case R.layout.swap_start_swap: SwapService.stop(this); finish(); return; default: currentSwapViewLayoutRes = R.layout.swap_start_swap; } inflateSwapView(currentSwapViewLayoutRes); }); if (viewRes == R.layout.swap_start_swap) { toolbar.setNavigationIcon(R.drawable.ic_close); } else { toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material); } container.addView(view); supportInvalidateOptionsMenu(); switch (currentView.getLayoutResId()) { case R.layout.swap_send_fdroid: setUpFromWifi(); setUpUseBluetoothButton(); break; case R.layout.swap_wifi_qr: setUpFromWifi(); setUpQrScannerButton(); break; case R.layout.swap_nfc: setUpNfcView(); break; case R.layout.swap_select_apps: LocalRepoService.create(this, getSwapService().getAppsToSwap()); break; case R.layout.swap_connecting: setUpConnectingView(); break; case R.layout.swap_start_swap: setUpStartVisibility(); break; } } public void showIntro() { // If we were previously swapping with a specific client, forget that we were doing that, // as we are starting over now. getSwapService().swapWith(null); LocalRepoService.create(this); inflateSwapView(R.layout.swap_start_swap); } /** * On {@code android-26}, only apps with privileges can access * {@code WRITE_SETTINGS}. So this just shows the tethering settings * for the user to do it themselves. */ public void showTetheringSettings() { final Intent intent = new Intent(Intent.ACTION_MAIN, null); intent.addCategory(Intent.CATEGORY_LAUNCHER); final ComponentName cn = new ComponentName("com.android.settings", "com.android.settings.TetherSettings"); intent.setComponent(cn); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } @TargetApi(23) public void requestWriteSettingsPermission() { Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS, Uri.parse("package:" + getPackageName())); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivityForResult(intent, REQUEST_WRITE_SETTINGS_PERMISSION); } public void sendFDroid() { if (bluetoothAdapter == null || Build.VERSION.SDK_INT >= 23 // TODO make Bluetooth work with content:// URIs || (!bluetoothAdapter.isEnabled() && LocalHTTPDManager.isAlive())) { inflateSwapView(R.layout.swap_send_fdroid); } else { sendFDroidBluetooth(); } } /** * Send the F-Droid APK via Bluetooth. If Bluetooth has not been * enabled/turned on, then enabling device discoverability will * automatically enable Bluetooth. */ public void sendFDroidBluetooth() { if (bluetoothAdapter.isEnabled()) { sendFDroidApk(); } else { Intent discoverBt = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); discoverBt.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 120); startActivityForResult(discoverBt, REQUEST_BLUETOOTH_ENABLE_FOR_SEND); } } private void sendFDroidApk() { ((FDroidApp) getApplication()).sendViaBluetooth(this, AppCompatActivity.RESULT_OK, BuildConfig.APPLICATION_ID); } /** * TODO: Figure out whether they have changed since last time LocalRepoService * was run. If the local repo is running, then we can ask it what apps it is * swapping and compare with that. Otherwise, probably will need to scan the * file system. */ public void onAppsSelected() { if (hasPreparedLocalRepo) { onLocalRepoPrepared(); } else { LocalRepoService.create(this, getSwapService().getAppsToSwap()); currentSwapViewLayoutRes = R.layout.swap_connecting; inflateSwapView(R.layout.swap_connecting); } } /** * Once the LocalRepoService has finished preparing our repository index, we can * show the next screen to the user. This will be one of two things: *

    *
  1. If we directly selected a peer to swap with initially, we will skip straight to getting * the list of apps from that device.
  2. *
  3. Alternatively, if we didn't have a person to connect to, and instead clicked "Scan QR Code", * then we want to show a QR code or NFC dialog.
  4. *
*/ public void onLocalRepoPrepared() { // TODO ditch this, use a message from LocalRepoService. Maybe? hasPreparedLocalRepo = true; if (getSwapService().isConnectingWithPeer()) { startSwappingWithPeer(); } else if (!attemptToShowNfc()) { inflateSwapView(R.layout.swap_wifi_qr); } } private void startSwappingWithPeer() { getSwapService().connectToPeer(); inflateSwapView(R.layout.swap_connecting); } private boolean attemptToShowNfc() { // TODO: What if NFC is disabled? Hook up with NfcNotEnabledActivity? Or maybe only if they // click a relevant button? // Even if they opted to skip the message which says "Touch devices to swap", // we still want to actually enable the feature, so that they could touch // during the wifi qr code being shown too. boolean nfcMessageReady = NfcHelper.setPushMessage(this, Utils.getSharingUri(FDroidApp.repo)); // TODO move all swap-specific preferences to a SharedPreferences instance for SwapWorkflowActivity if (Preferences.get().showNfcDuringSwap() && nfcMessageReady) { inflateSwapView(R.layout.swap_nfc); return true; } return false; } public void swapWith(Peer peer) { getSwapService().swapWith(peer); inflateSwapView(R.layout.swap_select_apps); } /** * This is for when we initiate a swap by viewing the "Are you sure you want to swap with" view * This can arise either: * * As a result of scanning a QR code (in which case we likely already have a repo setup) or * * As a result of the other device selecting our device in the "start swap" screen, in which * case we are likely just sitting on the start swap screen also, and haven't configured * anything yet. */ public void swapWith(NewRepoConfig repoConfig) { Peer peer = repoConfig.toPeer(); if (currentSwapViewLayoutRes == R.layout.swap_start_swap || currentSwapViewLayoutRes == R.layout.swap_confirm_receive) { // This will force the "Select apps to swap" workflow to begin. swapWith(peer); } else { getSwapService().swapWith(peer); startSwappingWithPeer(); } } public void denySwap() { showIntro(); } /** * Attempts to open a QR code scanner, in the hope a user will then scan the QR code of another * device configured to swapp apps with us. Delegates to the zxing library to do so. */ public void initiateQrScan() { IntentIntegrator integrator = new IntentIntegrator(this); integrator.initiateScan(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); if (scanResult != null) { if (scanResult.getContents() != null) { NewRepoConfig repoConfig = new NewRepoConfig(this, scanResult.getContents()); if (repoConfig.isValidRepo()) { confirmSwapConfig = repoConfig; showRelevantView(); } else { Toast.makeText(this, R.string.swap_qr_isnt_for_swap, Toast.LENGTH_SHORT).show(); } } } else if (requestCode == REQUEST_WRITE_SETTINGS_PERMISSION) { if (Build.VERSION.SDK_INT >= 23 && Settings.System.canWrite(this)) { setupWifiAP(); } } else if (requestCode == REQUEST_BLUETOOTH_ENABLE_FOR_SWAP) { if (resultCode == RESULT_OK) { Utils.debugLog(TAG, "User enabled Bluetooth, will make sure we are discoverable."); ensureBluetoothDiscoverableThenStart(); } else { Utils.debugLog(TAG, "User chose not to enable Bluetooth, so doing nothing"); SwapService.putBluetoothVisibleUserPreference(false); } } else if (requestCode == REQUEST_BLUETOOTH_DISCOVERABLE) { if (resultCode != RESULT_CANCELED) { Utils.debugLog(TAG, "User made Bluetooth discoverable, will proceed to start bluetooth server."); BluetoothManager.start(this); } else { Utils.debugLog(TAG, "User chose not to make Bluetooth discoverable, so doing nothing"); SwapService.putBluetoothVisibleUserPreference(false); } } else if (requestCode == REQUEST_BLUETOOTH_ENABLE_FOR_SEND) { sendFDroidApk(); } } /** * The process for setting up bluetooth is as follows: * * Note that this is a little different than the usual process for bluetooth _clients_, which * involves pairing and connecting with other devices. */ public void startBluetoothSwap() { if (bluetoothAdapter != null) { if (bluetoothAdapter.isEnabled()) { Utils.debugLog(TAG, "Bluetooth enabled, will check if device is discoverable with device."); ensureBluetoothDiscoverableThenStart(); } else { Utils.debugLog(TAG, "Bluetooth disabled, asking user to enable it."); Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_BLUETOOTH_ENABLE_FOR_SWAP); } } } private void ensureBluetoothDiscoverableThenStart() { Utils.debugLog(TAG, "Ensuring Bluetooth is in discoverable mode."); if (bluetoothAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { Utils.debugLog(TAG, "Not currently in discoverable mode, so prompting user to enable."); Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 3600); // 1 hour startActivityForResult(intent, REQUEST_BLUETOOTH_DISCOVERABLE); } BluetoothManager.start(this); } private final BroadcastReceiver bluetoothScanModeChanged = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { SwitchMaterial bluetoothSwitch = container.findViewById(R.id.switch_bluetooth); TextView textBluetoothVisible = container.findViewById(R.id.bluetooth_visible); if (bluetoothSwitch == null || textBluetoothVisible == null || !BluetoothManager.ACTION_STATUS.equals(intent.getAction())) { return; } switch (intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1)) { case BluetoothAdapter.SCAN_MODE_NONE: textBluetoothVisible.setText(R.string.disabled); bluetoothSwitch.setEnabled(true); break; case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE: textBluetoothVisible.setText(R.string.swap_visible_bluetooth); bluetoothSwitch.setEnabled(true); break; case BluetoothAdapter.SCAN_MODE_CONNECTABLE: textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth); bluetoothSwitch.setEnabled(true); break; } } }; /** * Helper class to try and make sense of what the swap workflow is currently doing. * The more technologies are involved in the process (e.g. Bluetooth/Wifi/NFC/etc) * the harder it becomes to reason about and debug the whole thing. Thus,this class * will periodically dump the state to logcat so that it is easier to see when certain * protocols are enabled/disabled. *

* To view only this output from logcat: *

* adb logcat | grep 'Swap Status' *

* To exclude this output from logcat (it is very noisy): *

* adb logcat | grep -v 'Swap Status' */ class SwapDebug { public void logStatus() { if (true) return; // NOPMD String message = ""; if (service == null) { message = "No swap service"; } else { String bluetooth; bluetooth = "N/A"; if (bluetoothAdapter != null) { Map scanModes = new HashMap<>(3); scanModes.put(BluetoothAdapter.SCAN_MODE_CONNECTABLE, "CON"); scanModes.put(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, "CON_DISC"); scanModes.put(BluetoothAdapter.SCAN_MODE_NONE, "NONE"); bluetooth = "\"" + bluetoothAdapter.getName() + "\" - " + scanModes.get(bluetoothAdapter.getScanMode()); } } Date now = new Date(); Utils.debugLog("SWAP_STATUS", now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds() + " " + message); new Timer().schedule(new TimerTask() { @Override public void run() { new SwapDebug().logStatus(); } }, 1000 ); } } private final BroadcastReceiver onWifiStateChanged = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { setUpFromWifi(); TextView textWifiVisible = container.findViewById(R.id.wifi_visible); if (textWifiVisible == null) { return; } switch (intent.getIntExtra(WifiStateChangeService.EXTRA_STATUS, -1)) { case WifiManager.WIFI_STATE_ENABLING: textWifiVisible.setText(R.string.swap_setting_up_wifi); break; case WifiManager.WIFI_STATE_ENABLED: textWifiVisible.setText(R.string.swap_not_visible_wifi); break; case WifiManager.WIFI_STATE_DISABLING: case WifiManager.WIFI_STATE_DISABLED: textWifiVisible.setText(R.string.swap_stopping_wifi); break; case WifiManager.WIFI_STATE_UNKNOWN: break; } } }; private void setUpFromWifi() { String scheme = Preferences.get().isLocalRepoHttpsEnabled() ? "https://" : "http://"; // the fingerprint is not useful on the button label String buttonLabel = scheme + FDroidApp.ipAddressString + ":" + FDroidApp.port; TextView ipAddressView = container.findViewById(R.id.device_ip_address); if (ipAddressView != null) { ipAddressView.setText(buttonLabel); } String qrUriString = null; switch (currentView.getLayoutResId()) { case R.layout.swap_join_wifi: setUpJoinWifi(); return; case R.layout.swap_send_fdroid: qrUriString = buttonLabel; break; case R.layout.swap_wifi_qr: Uri sharingUri = Utils.getSharingUri(FDroidApp.repo); StringBuilder qrUrlBuilder = new StringBuilder(scheme); qrUrlBuilder.append(sharingUri.getHost()); if (sharingUri.getPort() != 80) { qrUrlBuilder.append(':'); qrUrlBuilder.append(sharingUri.getPort()); } qrUrlBuilder.append(sharingUri.getPath()); boolean first = true; Set names = sharingUri.getQueryParameterNames(); for (String name : names) { if (!"ssid".equals(name)) { if (first) { qrUrlBuilder.append('?'); first = false; } else { qrUrlBuilder.append('&'); } qrUrlBuilder.append(name.toUpperCase(Locale.ENGLISH)); qrUrlBuilder.append('='); qrUrlBuilder.append(sharingUri.getQueryParameter(name).toUpperCase(Locale.ENGLISH)); } } qrUriString = qrUrlBuilder.toString(); break; } ImageView qrImage = container.findViewById(R.id.wifi_qr_code); if (qrUriString != null && qrImage != null) { Utils.debugLog(TAG, "Encoded swap URI in QR Code: " + qrUriString); compositeDisposable.add(Utils.generateQrBitmap(this, qrUriString) .subscribe(qrBitmap -> { qrImage.setImageBitmap(qrBitmap); // Replace all blacks with the background blue. qrImage.setColorFilter(new LightingColorFilter(0xffffffff, ContextCompat.getColor(this, R.color.swap_blue))); final View qrWarningMessage = container.findViewById(R.id.warning_qr_scanner); if (qrWarningMessage != null) { if (CameraCharacteristicsChecker.getInstance(this).hasAutofocus()) { qrWarningMessage.setVisibility(View.GONE); } else { qrWarningMessage.setVisibility(View.VISIBLE); } } }) ); } } // TODO: Listen for "Connecting..." state and reflect that in the view too. private void setUpJoinWifi() { currentView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK)); } }); TextView descriptionView = container.findViewById(R.id.text_description); ImageView wifiIcon = container.findViewById(R.id.wifi_icon); TextView ssidView = container.findViewById(R.id.wifi_ssid); TextView tapView = container.findViewById(R.id.wifi_available_networks_prompt); if (descriptionView == null || wifiIcon == null || ssidView == null || tapView == null) { return; } if (TextUtils.isEmpty(FDroidApp.bssid) && !TextUtils.isEmpty(FDroidApp.ipAddressString)) { // empty bssid with an ipAddress means hotspot mode descriptionView.setText(R.string.swap_join_this_hotspot); wifiIcon.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_wifi_tethering)); ssidView.setText(R.string.swap_active_hotspot); tapView.setText(R.string.swap_switch_to_wifi); } else if (TextUtils.isEmpty(FDroidApp.ssid)) { // not connected to or setup with any wifi network descriptionView.setText(R.string.swap_join_same_wifi); wifiIcon.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_wifi)); ssidView.setText(R.string.swap_no_wifi_network); tapView.setText(R.string.swap_view_available_networks); } else { // connected to a regular wifi network descriptionView.setText(R.string.swap_join_same_wifi); wifiIcon.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_wifi)); ssidView.setText(FDroidApp.ssid); tapView.setText(R.string.swap_view_available_networks); } } private void setUpStartVisibility() { bluetoothStatusReceiver.onReceive(this, new Intent(BluetoothManager.ACTION_STATUS)); bonjourStatusReceiver.onReceive(this, new Intent(BonjourManager.ACTION_STATUS)); TextView viewWifiNetwork = findViewById(R.id.wifi_network); SwitchMaterial wifiSwitch = findViewById(R.id.switch_wifi); MaterialButton scanQrButton = findViewById(R.id.btn_scan_qr); MaterialButton appsButton = findViewById(R.id.btn_apps); if (viewWifiNetwork == null || wifiSwitch == null || scanQrButton == null || appsButton == null) { return; } viewWifiNetwork.setOnClickListener(v -> promptToSelectWifiNetwork()); wifiSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { Context context = getApplicationContext(); if (isChecked) { if (wifiApControl != null && wifiApControl.isEnabled()) { setupWifiAP(); } else { wifiManager.setWifiEnabled(true); } BonjourManager.start(context); } BonjourManager.setVisible(context, isChecked); SwapService.putWifiVisibleUserPreference(isChecked); } }); scanQrButton.setOnClickListener(v -> inflateSwapView(R.layout.swap_wifi_qr)); appsButton.setOnClickListener(v -> inflateSwapView(R.layout.swap_select_apps)); appsButton.setEllipsize(TextUtils.TruncateAt.END); if (SwapService.getWifiVisibleUserPreference()) { wifiSwitch.setChecked(true); } else { wifiSwitch.setChecked(false); } } private final BroadcastReceiver bonjourStatusReceiver = new BroadcastReceiver() { private volatile int bonjourStatus = BonjourManager.STATUS_STOPPED; @Override public void onReceive(Context context, Intent intent) { if (!BonjourManager.ACTION_STATUS.equals(intent.getAction())) { return; } bonjourStatus = intent.getIntExtra(BonjourManager.EXTRA_STATUS, bonjourStatus); TextView textWifiVisible = container.findViewById(R.id.wifi_visible); TextView peopleNearbyText = container.findViewById(R.id.text_people_nearby); ProgressBar peopleNearbyProgress = container.findViewById(R.id.searching_people_nearby); if (textWifiVisible == null || peopleNearbyText == null || peopleNearbyProgress == null) { return; } switch (bonjourStatus) { case BonjourManager.STATUS_STARTING: textWifiVisible.setText(R.string.swap_setting_up_wifi); peopleNearbyText.setText(R.string.swap_starting); peopleNearbyText.setVisibility(View.VISIBLE); peopleNearbyProgress.setVisibility(View.VISIBLE); break; case BonjourManager.STATUS_STARTED: textWifiVisible.setText(R.string.swap_not_visible_wifi); peopleNearbyText.setText(R.string.swap_scanning_for_peers); peopleNearbyText.setVisibility(View.VISIBLE); peopleNearbyProgress.setVisibility(View.VISIBLE); break; case BonjourManager.STATUS_VPN_CONFLICT: textWifiVisible.setText(R.string.swap_wifi_vpn_conflict); break; case BonjourManager.STATUS_NOT_VISIBLE: textWifiVisible.setText(R.string.swap_not_visible_wifi); peopleNearbyText.setText(R.string.swap_scanning_for_peers); peopleNearbyText.setVisibility(View.VISIBLE); peopleNearbyProgress.setVisibility(View.VISIBLE); break; case BonjourManager.STATUS_VISIBLE: if (wifiApControl != null && wifiApControl.isEnabled()) { textWifiVisible.setText(R.string.swap_visible_hotspot); } else { textWifiVisible.setText(R.string.swap_visible_wifi); } peopleNearbyText.setText(R.string.swap_scanning_for_peers); peopleNearbyText.setVisibility(View.VISIBLE); peopleNearbyProgress.setVisibility(View.VISIBLE); break; case BonjourManager.STATUS_STOPPING: textWifiVisible.setText(R.string.swap_stopping_wifi); if (!BluetoothManager.isAlive()) { peopleNearbyText.setText(R.string.swap_stopping); peopleNearbyText.setVisibility(View.VISIBLE); peopleNearbyProgress.setVisibility(View.VISIBLE); } break; case BonjourManager.STATUS_STOPPED: textWifiVisible.setText(R.string.swap_not_visible_wifi); if (!BluetoothManager.isAlive()) { peopleNearbyText.setVisibility(View.GONE); peopleNearbyProgress.setVisibility(View.GONE); } break; case BonjourManager.STATUS_ERROR: textWifiVisible.setText(R.string.swap_not_visible_wifi); peopleNearbyText.setText(intent.getStringExtra(Intent.EXTRA_TEXT)); peopleNearbyText.setVisibility(View.VISIBLE); peopleNearbyProgress.setVisibility(View.GONE); default: throw new IllegalArgumentException("Bad intent: " + intent); } } }; /** * Add any new Bonjour devices that were found, as long as they are not * already present. * * @see #bluetoothFound * @see ArrayAdapter#getPosition(Object) * @see java.util.List#indexOf(Object) */ private final BroadcastReceiver bonjourFound = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { ListView peopleNearbyList = container.findViewById(R.id.list_people_nearby); if (peopleNearbyList != null) { ArrayAdapter peopleNearbyAdapter = (ArrayAdapter) peopleNearbyList.getAdapter(); Peer peer = intent.getParcelableExtra(BonjourManager.EXTRA_BONJOUR_PEER); if (peopleNearbyAdapter.getPosition(peer) == -1) { peopleNearbyAdapter.add(peer); } } } }; private final BroadcastReceiver bonjourRemoved = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { ListView peopleNearbyList = container.findViewById(R.id.list_people_nearby); if (peopleNearbyList != null) { ArrayAdapter peopleNearbyAdapter = (ArrayAdapter) peopleNearbyList.getAdapter(); peopleNearbyAdapter.remove((Peer) intent.getParcelableExtra(BonjourManager.EXTRA_BONJOUR_PEER)); } } }; private final BroadcastReceiver bluetoothStatusReceiver = new BroadcastReceiver() { private volatile int bluetoothStatus = BluetoothManager.STATUS_STOPPED; @Override public void onReceive(Context context, Intent intent) { if (!BluetoothManager.ACTION_STATUS.equals(intent.getAction())) { return; } bluetoothStatus = intent.getIntExtra(BluetoothManager.EXTRA_STATUS, bluetoothStatus); SwitchMaterial bluetoothSwitch = container.findViewById(R.id.switch_bluetooth); TextView textBluetoothVisible = container.findViewById(R.id.bluetooth_visible); TextView textDeviceIdBluetooth = container.findViewById(R.id.device_id_bluetooth); TextView peopleNearbyText = container.findViewById(R.id.text_people_nearby); ProgressBar peopleNearbyProgress = container.findViewById(R.id.searching_people_nearby); if (bluetoothSwitch == null || textBluetoothVisible == null || textDeviceIdBluetooth == null || peopleNearbyText == null || peopleNearbyProgress == null) { return; } switch (bluetoothStatus) { case BluetoothManager.STATUS_STARTING: bluetoothSwitch.setEnabled(false); textBluetoothVisible.setText(R.string.swap_setting_up_bluetooth); textDeviceIdBluetooth.setVisibility(View.VISIBLE); peopleNearbyText.setText(R.string.swap_scanning_for_peers); peopleNearbyText.setVisibility(View.VISIBLE); peopleNearbyProgress.setVisibility(View.VISIBLE); break; case BluetoothManager.STATUS_STARTED: bluetoothSwitch.setEnabled(true); textBluetoothVisible.setText(R.string.swap_visible_bluetooth); textDeviceIdBluetooth.setVisibility(View.VISIBLE); peopleNearbyText.setText(R.string.swap_scanning_for_peers); peopleNearbyText.setVisibility(View.VISIBLE); peopleNearbyProgress.setVisibility(View.VISIBLE); break; case BluetoothManager.STATUS_STOPPING: bluetoothSwitch.setEnabled(false); textBluetoothVisible.setText(R.string.swap_stopping); textDeviceIdBluetooth.setVisibility(View.GONE); if (!BonjourManager.isAlive()) { peopleNearbyText.setText(R.string.swap_stopping); peopleNearbyText.setVisibility(View.VISIBLE); peopleNearbyProgress.setVisibility(View.VISIBLE); } break; case BluetoothManager.STATUS_STOPPED: bluetoothSwitch.setEnabled(true); textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth); textDeviceIdBluetooth.setVisibility(View.GONE); if (!BonjourManager.isAlive()) { peopleNearbyText.setVisibility(View.GONE); peopleNearbyProgress.setVisibility(View.GONE); } ListView peopleNearbyView = container.findViewById(R.id.list_people_nearby); if (peopleNearbyView == null) { break; } ArrayAdapter peopleNearbyAdapter = (ArrayAdapter) peopleNearbyView.getAdapter(); for (int i = 0; i < peopleNearbyAdapter.getCount(); i++) { Peer peer = (Peer) peopleNearbyAdapter.getItem(i); if (peer.getClass().equals(BluetoothPeer.class)) { Utils.debugLog(TAG, "Removing bluetooth peer: " + peer.getName()); peopleNearbyAdapter.remove(peer); } } break; case BluetoothManager.STATUS_ERROR: bluetoothSwitch.setEnabled(true); textBluetoothVisible.setText(intent.getStringExtra(Intent.EXTRA_TEXT)); textDeviceIdBluetooth.setVisibility(View.VISIBLE); break; default: throw new IllegalArgumentException("Bad intent: " + intent); } } }; /** * Add any new Bluetooth devices that were found, as long as they are not * already present. * * @see #bonjourFound * @see ArrayAdapter#getPosition(Object) * @see java.util.List#indexOf(Object) */ private final BroadcastReceiver bluetoothFound = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { ListView peopleNearbyList = container.findViewById(R.id.list_people_nearby); if (peopleNearbyList != null) { ArrayAdapter peopleNearbyAdapter = (ArrayAdapter) peopleNearbyList.getAdapter(); Peer peer = intent.getParcelableExtra(BluetoothManager.EXTRA_PEER); if (peopleNearbyAdapter.getPosition(peer) == -1) { peopleNearbyAdapter.add(peer); } } } }; private void setUpUseBluetoothButton() { Button useBluetooth = findViewById(R.id.btn_use_bluetooth); if (useBluetooth != null) { if (bluetoothAdapter == null) { useBluetooth.setVisibility(View.GONE); } else { useBluetooth.setVisibility(View.VISIBLE); } useBluetooth.setOnClickListener(v -> { showIntro(); sendFDroidBluetooth(); }); } } private void setUpQrScannerButton() { Button openQr = findViewById(R.id.btn_qr_scanner); if (openQr != null) { openQr.setOnClickListener(v -> initiateQrScan()); } } private void setUpConfirmReceive() { TextView descriptionTextView = findViewById(R.id.text_description); if (descriptionTextView != null) { descriptionTextView.setText(getString(R.string.swap_confirm_connect, confirmSwapConfig.getHost())); } Button confirmReceiveYes = container.findViewById(R.id.confirm_receive_yes); if (confirmReceiveYes != null) { confirmReceiveYes.setOnClickListener(v -> denySwap()); } Button confirmReceiveNo = container.findViewById(R.id.confirm_receive_no); if (confirmReceiveNo != null) { confirmReceiveNo.setOnClickListener(new View.OnClickListener() { private final NewRepoConfig config = confirmSwapConfig; @Override public void onClick(View v) { swapWith(config); } }); } } private void setUpNfcView() { CheckBox dontShowAgain = container.findViewById(R.id.checkbox_dont_show); if (dontShowAgain != null) { dontShowAgain.setOnCheckedChangeListener((buttonView, isChecked) -> Preferences.get().setShowNfcDuringSwap(!isChecked)); } } private void setUpConnectingProgressText(String message) { TextView progressText = container.findViewById(R.id.progress_text); if (progressText != null && message != null) { progressText.setVisibility(View.VISIBLE); progressText.setText(message); } } /** * Listens for feedback about a local repository being prepared, like APK * files copied to the LocalHTTPD webroot, the {@code index.html} generated, * etc. Icons will be copied to the webroot in the background and so are * not part of this process. */ private final BroadcastReceiver localRepoStatus = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { setUpConnectingProgressText(intent.getStringExtra(Intent.EXTRA_TEXT)); ProgressBar progressBar = container.findViewById(R.id.progress_bar); Button tryAgainButton = container.findViewById(R.id.try_again); if (progressBar == null || tryAgainButton == null) { return; } switch (intent.getIntExtra(LocalRepoService.EXTRA_STATUS, -1)) { case LocalRepoService.STATUS_PROGRESS: progressBar.setVisibility(View.VISIBLE); tryAgainButton.setVisibility(View.GONE); break; case LocalRepoService.STATUS_STARTED: progressBar.setVisibility(View.VISIBLE); tryAgainButton.setVisibility(View.GONE); onLocalRepoPrepared(); break; case LocalRepoService.STATUS_ERROR: progressBar.setVisibility(View.GONE); tryAgainButton.setVisibility(View.VISIBLE); break; default: throw new IllegalArgumentException("Bogus intent: " + intent); } } }; /** * Listens for feedback about a repo update process taking place. * Tracks an index.jar download and show the progress messages */ private final BroadcastReceiver repoUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String message = intent.getStringExtra(UpdateService.EXTRA_MESSAGE); if (message == null) { CharSequence[] repoErrors = intent.getCharSequenceArrayExtra(UpdateService.EXTRA_REPO_ERRORS); if (repoErrors != null) { StringBuilder msgBuilder = new StringBuilder(); for (CharSequence error : repoErrors) { if (msgBuilder.length() > 0) { msgBuilder.append(" + "); } msgBuilder.append(error); } message = msgBuilder.toString(); } } setUpConnectingProgressText(message); ProgressBar progressBar = container.findViewById(R.id.progress_bar); Button tryAgainButton = container.findViewById(R.id.try_again); if (progressBar == null || tryAgainButton == null) { return; } int status = intent.getIntExtra(UpdateService.EXTRA_STATUS_CODE, -1); if (status == UpdateService.STATUS_ERROR_GLOBAL || status == UpdateService.STATUS_ERROR_LOCAL || status == UpdateService.STATUS_ERROR_LOCAL_SMALL) { progressBar.setVisibility(View.GONE); tryAgainButton.setVisibility(View.VISIBLE); getSwapService().removeCurrentPeerFromActive(); return; } else { progressBar.setVisibility(View.VISIBLE); tryAgainButton.setVisibility(View.GONE); getSwapService().addCurrentPeerToActive(); } if (status == UpdateService.STATUS_COMPLETE_AND_SAME || status == UpdateService.STATUS_COMPLETE_WITH_CHANGES) { inflateSwapView(R.layout.swap_success); } } }; private final BroadcastReceiver downloaderInterruptedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Repo repo = RepoProvider.Helper.findByUrl(context, intent.getData(), null); if (repo != null && repo.isSwap) { setUpConnectingProgressText(intent.getStringExtra(DownloaderService.EXTRA_ERROR_MESSAGE)); } } }; private void setUpConnectingView() { TextView heading = container.findViewById(R.id.progress_text); heading.setText(R.string.swap_connecting); Button tryAgainButton = container.findViewById(R.id.try_again); if (tryAgainButton != null) { tryAgainButton.setOnClickListener(v -> onAppsSelected()); } } }