fdroid-client/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java

1523 lines
64 KiB
Java

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:
* <ul>
* <li>which WiFi network to use</li>
* <li>whether to advertise via Bluetooth or WiFi+Bonjour</li>
* <li>connect to another device's swap</li>
* <li>choose which apps to share</li>
* <li>ask if the other device would like to swap with us</li>
* <li>help connect via QR Code or NFC</li>
* </ul>
* <p>
* 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 <a href="https://developer.squareup.com/blog/advocating-against-android-fragments/"></a>
*/
@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<Integer> 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:
* <ol>
* <li>If we directly selected a peer to swap with initially, we will skip straight to getting
* the list of apps from that device.</li>
* <li>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.</li>
* </ol>
*/
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:
* <ul>
* <li>Assume we have bluetooth available (otherwise the button which allowed us to start
* the bluetooth process should not have been available)</li>
* <li>Ask user to enable (if not enabled yet)</li>
* <li>Start bluetooth server socket</li>
* <li>Enable bluetooth discoverability, so that people can connect to our server socket.</li>
* </ul>
* 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.
* <p>
* To view only this output from logcat:
* <p>
* adb logcat | grep 'Swap Status'
* <p>
* To exclude this output from logcat (it is very noisy):
* <p>
* 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<Integer, String> 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<String> 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<Peer> peopleNearbyAdapter = (ArrayAdapter<Peer>) 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<Peer> peopleNearbyAdapter = (ArrayAdapter<Peer>) 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<Peer> peopleNearbyAdapter = (ArrayAdapter<Peer>) 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());
}
}
}