package org.fdroid.fdroid.views.swap;
import android.app.Activity;
import android.app.PendingIntent;
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.ServiceConnection;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.ColorRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.NfcHelper;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.NewRepoConfig;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.localrepo.LocalRepoManager;
import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.localrepo.peers.Peer;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import cc.mvdan.accesspoint.WifiApControl;
/**
* This activity will do its best to show the most relevant screen about swapping to the user.
* The problem comes when there are two competing goals - 1) Show the user a list of apps from another
* device to download and install, and 2) Prepare your own list of apps to share.
*/
public class SwapWorkflowActivity extends AppCompatActivity {
/**
* 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";
public static final String EXTRA_CONFIRM = "EXTRA_CONFIRM";
/**
* Ensure that we don't try to handle specific intents more than once in onResume()
* (e.g. the "Do you want to swap back with ..." intent).
*/
public static final String EXTRA_SWAP_INTENT_HANDLED = "swapIntentHandled";
private ViewGroup container;
/**
* A UI component (subclass of {@link View}) which forms part of the swap workflow.
* There is a one to one mapping between an {@link org.fdroid.fdroid.views.swap.SwapWorkflowActivity.InnerView}
* and a {@link SwapService.SwapStep}, and these views know what
* the previous view before them should be.
*/
public interface InnerView {
/** @return True if the menu should be shown. */
boolean buildMenu(Menu menu, @NonNull MenuInflater inflater);
/** @return The step that this view represents. */
@SwapService.SwapStep int getStep();
@SwapService.SwapStep int getPreviousStep();
@ColorRes int getToolbarColour();
String getToolbarTitle();
}
private static final String TAG = "SwapWorkflowActivity";
private static final int CONNECT_TO_SWAP = 1;
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 Toolbar toolbar;
private InnerView currentView;
private boolean hasPreparedLocalRepo;
private PrepareSwapRepo updateSwappableAppsTask;
private NewRepoConfig confirmSwapConfig;
private LocalBroadcastManager localBroadcastManager;
@NonNull
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder binder) {
Utils.debugLog(TAG, "Swap service connected. Will hold onto it so we can talk to it regularly.");
service = ((SwapService.Binder) binder).getService();
showRelevantView();
}
// TODO: What causes this? Do we need to stop swapping explicitly when this is invoked?
@Override
public void onServiceDisconnected(ComponentName className) {
Utils.debugLog(TAG, "Swap service disconnected");
service = null;
// TODO: What to do about the UI in this instance?
}
};
@Nullable
private SwapService service;
@NonNull
public SwapService getService() {
if (service == null) {
// *Slightly* more informative than a null-pointer error that would otherwise happen.
throw new IllegalStateException("Trying to access swap service before it was initialized.");
}
return service;
}
@Override
public void onBackPressed() {
if (currentView.getStep() == SwapService.STEP_INTRO) {
SwapService.stop(this);
finish();
} else {
int nextStep = currentView.getPreviousStep();
getService().setStep(nextStep);
showRelevantView();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// The server should not be doing anything or occupying any (noticeable) resources
// until we actually ask it to enable swapping. Therefore, we will start it nice and
// early so we don't have to wait until it is connected later.
Intent service = new Intent(this, SwapService.class);
if (bindService(service, serviceConnection, Context.BIND_AUTO_CREATE)) {
startService(service);
}
setContentView(R.layout.swap_activity);
toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar.setTitleTextAppearance(getApplicationContext(), R.style.SwapTheme_Wizard_Text_Toolbar);
setSupportActionBar(toolbar);
container = (ViewGroup) findViewById(R.id.fragment_container);
localBroadcastManager = LocalBroadcastManager.getInstance(this);
new SwapDebug().logStatus();
}
@Override
protected void onDestroy() {
unbindService(serviceConnection);
super.onDestroy();
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.clear();
boolean parent = super.onPrepareOptionsMenu(menu);
boolean inner = currentView != null && currentView.buildMenu(menu, getMenuInflater());
return parent || inner;
}
@Override
protected void onResume() {
super.onResume();
checkIncomingIntent();
showRelevantView();
}
private void checkIncomingIntent() {
Intent intent = getIntent();
if (intent.getBooleanExtra(EXTRA_CONFIRM, false) && !intent.getBooleanExtra(EXTRA_SWAP_INTENT_HANDLED, false)) {
// Storing config in this variable will ensure that when showRelevantView() is next
// run, it will show the connect swap view (if the service is available).
intent.putExtra(EXTRA_SWAP_INTENT_HANDLED, true);
confirmSwapConfig = new NewRepoConfig(this, intent);
}
}
public void promptToSelectWifiNetwork() {
//
// On Android >= 5.0, the neutral button is the one by itself off to the left of a dialog
// (not the negative button). Thus, the layout of this dialogs buttons should be:
//
// | |
// +---------------------------------+
// | Cancel Hotspot WiFi |
// +---------------------------------+
//
// TODO: Investigate if this should be set dynamically for earlier APIs.
//
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) {
startActivity(new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK));
}
}
).setNegativeButton(R.string.wifi_ap, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
promptToSetupWifiAP();
}
}
).create().show();
}
private void promptToSetupWifiAP() {
WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
WifiApControl ap = WifiApControl.getInstance(this);
wifiManager.setWifiEnabled(false);
if (!ap.enable()) {
Log.e(TAG, "Could not enable WiFi AP.");
// TODO: Feedback to user?
} else {
Utils.debugLog(TAG, "WiFi AP enabled.");
// TODO: Seems to be broken some times...
}
}
private void showRelevantView() {
showRelevantView(false);
}
private void showRelevantView(boolean forceReload) {
if (service == null) {
showInitialLoading();
return;
}
// This is separate from the switch statement below, because it is usually populated
// during onResume, when there is a high probability of not having a swap service
// available. Thus, we were unable to set the state of the swap service appropriately.
if (confirmSwapConfig != null) {
showConfirmSwap(confirmSwapConfig);
confirmSwapConfig = null;
return;
}
if (!forceReload && (container.getVisibility() == View.GONE || currentView != null && currentView.getStep() == service.getStep())) {
// Already showing the correct step, so don't bother changing anything.
return;
}
switch (service.getStep()) {
case SwapService.STEP_INTRO:
showIntro();
break;
case SwapService.STEP_SELECT_APPS:
showSelectApps();
break;
case SwapService.STEP_SHOW_NFC:
showNfc();
break;
case SwapService.STEP_JOIN_WIFI:
showJoinWifi();
break;
case SwapService.STEP_WIFI_QR:
showWifiQr();
break;
case SwapService.STEP_SUCCESS:
showSwapConnected();
break;
case SwapService.STEP_CONNECTING:
// TODO: Properly decide what to do here (i.e. returning to the activity after it was connecting)...
inflateInnerView(R.layout.swap_blank);
break;
}
}
public SwapService getState() {
return service;
}
private void showNfc() {
if (!attemptToShowNfc()) {
showWifiQr();
}
}
private InnerView inflateInnerView(@LayoutRes int viewRes) {
container.removeAllViews();
View view = ((LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(viewRes, container, false);
currentView = (InnerView) view;
// Don't actually set the step to STEP_INITIAL_LOADING, as we are going to use this view
// purely as a placeholder for _whatever view is meant to be shown_.
if (currentView.getStep() != SwapService.STEP_INITIAL_LOADING) {
if (service == null) {
throw new IllegalStateException("We are not in the STEP_INITIAL_LOADING state, but the service is not ready.");
}
service.setStep(currentView.getStep());
}
toolbar.setBackgroundColor(getResources().getColor(currentView.getToolbarColour()));
toolbar.setTitle(currentView.getToolbarTitle());
toolbar.setNavigationIcon(R.drawable.ic_close_white);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onToolbarCancel();
}
});
container.addView(view);
supportInvalidateOptionsMenu();
return currentView;
}
private void onToolbarCancel() {
SwapService.stop(this);
finish();
}
private void showInitialLoading() {
inflateInnerView(R.layout.swap_initial_loading);
}
public void showIntro() {
// If we were previously swapping with a specific client, forget that we were doing that,
// as we are starting over now.
getService().swapWith(null);
if (!getService().isEnabled()) {
if (!LocalRepoManager.get(this).getIndexJar().exists()) {
Utils.debugLog(TAG, "Preparing initial repo with only F-Droid, until we have allowed the user to configure their own repo.");
new PrepareInitialSwapRepo().execute();
}
}
inflateInnerView(R.layout.swap_blank);
}
private void showConfirmSwap(@NonNull NewRepoConfig config) {
((ConfirmReceive) inflateInnerView(R.layout.swap_confirm_receive)).setup(config);
}
public void startQrWorkflow() {
if (!getService().isEnabled()) {
new AlertDialog.Builder(this)
.setTitle(R.string.swap_not_enabled)
.setMessage(R.string.swap_not_enabled_description)
.setCancelable(true)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Do nothing. The dialog will get dismissed anyway, which is all we ever wanted...
}
})
.create().show();
} else {
showSelectApps();
}
}
public void showSelectApps() {
inflateInnerView(R.layout.swap_select_apps);
}
public void sendFDroid() {
// If Bluetooth has not been enabled/turned on, then enabling device discoverability
// will automatically enable Bluetooth.
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter != null) {
if (adapter.getState() != BluetoothAdapter.STATE_ON) {
Intent discoverBt = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverBt.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 120);
startActivityForResult(discoverBt, REQUEST_BLUETOOTH_ENABLE_FOR_SEND);
} else {
sendFDroidApk();
}
} else {
new AlertDialog.Builder(this)
.setTitle(R.string.bluetooth_unavailable)
.setMessage(R.string.swap_cant_send_no_bluetooth)
.setNegativeButton(
R.string.cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) { }
}
).create().show();
}
}
private void sendFDroidApk() {
((FDroidApp) getApplication()).sendViaBluetooth(this, Activity.RESULT_OK, "org.fdroid.fdroid");
}
// TODO: Figure out whether they have changed since last time UpdateAsyncTask 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 (updateSwappableAppsTask == null && !hasPreparedLocalRepo) {
updateSwappableAppsTask = new PrepareSwapRepo(getService().getAppsToSwap());
updateSwappableAppsTask.execute();
getService().setStep(SwapService.STEP_CONNECTING);
inflateInnerView(R.layout.swap_connecting);
} else {
onLocalRepoPrepared();
}
}
/**
* Once the UpdateAsyncTask has finished preparing our repository index, we can
* show the next screen to the user. This will be one of two things:
* * If we directly selected a peer to swap with initially, we will skip straight to getting
* the list of apps from that device.
* * 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.
*/
public void onLocalRepoPrepared() {
updateSwappableAppsTask = null;
hasPreparedLocalRepo = true;
if (getService().isConnectingWithPeer()) {
startSwappingWithPeer();
} else if (!attemptToShowNfc()) {
showWifiQr();
}
}
private void startSwappingWithPeer() {
getService().connectToPeer();
inflateInnerView(R.layout.swap_connecting);
}
private void showJoinWifi() {
inflateInnerView(R.layout.swap_join_wifi);
}
public void showWifiQr() {
inflateInnerView(R.layout.swap_wifi_qr);
}
public void showSwapConnected() {
inflateInnerView(R.layout.swap_success);
}
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));
if (Preferences.get().showNfcDuringSwap() && nfcMessageReady) {
inflateInnerView(R.layout.swap_nfc);
return true;
}
return false;
}
public void swapWith(Peer peer) {
getService().swapWith(peer);
showSelectApps();
}
/**
* 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 (getService().getStep() == SwapService.STEP_INTRO || getService().getStep() == SwapService.STEP_CONFIRM_SWAP) {
// This will force the "Select apps to swap" workflow to begin.
// TODO: Find a better way to decide whether we need to select the apps. Not sure if we
// can or cannot be in STEP_INTRO with a full blown repo ready to swap.
swapWith(peer);
} else {
getService().swapWith(repoConfig.toPeer());
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) {
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 == CONNECT_TO_SWAP && resultCode == Activity.RESULT_OK) {
finish();
} 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 {
// Didn't enable bluetooth
Utils.debugLog(TAG, "User chose not to enable Bluetooth, so doing nothing (i.e. sticking with wifi).");
}
} else if (requestCode == REQUEST_BLUETOOTH_DISCOVERABLE) {
if (resultCode != RESULT_CANCELED) {
Utils.debugLog(TAG, "User made Bluetooth discoverable, will proceed to start bluetooth server.");
getState().getBluetoothSwap().startInBackground();
} else {
Utils.debugLog(TAG, "User chose not to make Bluetooth discoverable, so doing nothing (i.e. sticking with wifi).");
}
} else if (requestCode == REQUEST_BLUETOOTH_ENABLE_FOR_SEND) {
sendFDroidApk();
}
}
/**
* The process for setting up bluetooth is as follows:
* * Assume we have bluetooth available (otherwise the button which allowed us to start
* the bluetooth process should not have been available).
* * Ask user to enable (if not enabled yet).
* * Start bluetooth server socket.
* * Enable bluetooth discoverability, so that people can connect to our server socket.
*
* 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() {
Utils.debugLog(TAG, "Initiating Bluetooth swap, will ensure the Bluetooth devices is enabled and discoverable before starting server.");
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter != null) {
if (adapter.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.getDefaultAdapter().getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
// TODO: Listen for BluetoothAdapter.ACTION_SCAN_MODE_CHANGED and respond if discovery
// is cancelled prematurely.
// 3600 is new maximum! TODO: What about when this expires? What if user manually disables discovery?
final int discoverableTimeout = 3600;
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, discoverableTimeout);
startActivityForResult(intent, REQUEST_BLUETOOTH_DISCOVERABLE);
}
if (service == null) {
throw new IllegalStateException("Can't start Bluetooth swap because service is null for some strange reason.");
}
service.getBluetoothSwap().startInBackground();
}
class PrepareInitialSwapRepo extends PrepareSwapRepo {
PrepareInitialSwapRepo() {
super(new HashSet<>(Arrays.asList(new String[] {"org.fdroid.fdroid"})));
}
}
class PrepareSwapRepo extends AsyncTask<Void, Void, Void> {
public static final String ACTION = "PrepareSwapRepo.Action";
public static final String EXTRA_MESSAGE = "PrepareSwapRepo.Status.Message";
public static final String EXTRA_TYPE = "PrepareSwapRepo.Action.Type";
public static final int TYPE_STATUS = 0;
public static final int TYPE_COMPLETE = 1;
public static final int TYPE_ERROR = 2;
@NonNull
protected final Set<String> selectedApps;
@NonNull
protected final Uri sharingUri;
@NonNull
protected final Context context;
PrepareSwapRepo(@NonNull Set<String> apps) {
context = SwapWorkflowActivity.this;
selectedApps = apps;
sharingUri = Utils.getSharingUri(FDroidApp.repo);
}
private void broadcast(int type) {
broadcast(type, null);
}
private void broadcast(int type, String message) {
Intent intent = new Intent(ACTION);
intent.putExtra(EXTRA_TYPE, type);
if (message != null) {
Utils.debugLog(TAG, "Preparing swap: " + message);
intent.putExtra(EXTRA_MESSAGE, message);
}
LocalBroadcastManager.getInstance(SwapWorkflowActivity.this).sendBroadcast(intent);
}
@Override
protected Void doInBackground(Void... params) {
try {
final LocalRepoManager lrm = LocalRepoManager.get(context);
broadcast(TYPE_STATUS, getString(R.string.deleting_repo));
lrm.deleteRepo();
for (String app : selectedApps) {
broadcast(TYPE_STATUS, String.format(getString(R.string.adding_apks_format), app));
lrm.addApp(context, app);
}
lrm.writeIndexPage(sharingUri.toString());
broadcast(TYPE_STATUS, getString(R.string.writing_index_jar));
lrm.writeIndexJar();
broadcast(TYPE_STATUS, getString(R.string.linking_apks));
lrm.copyApksToRepo();
broadcast(TYPE_STATUS, getString(R.string.copying_icons));
// run the icon copy without progress, its not a blocker
new Thread() {
@Override
public void run() {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
lrm.copyIconsToRepo();
}
}.start();
broadcast(TYPE_COMPLETE);
} catch (Exception e) {
broadcast(TYPE_ERROR);
Log.e(TAG, "", e);
}
return null;
}
}
/**
* 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 = service.getBluetoothSwap().isConnected() ? "Y" : " N";
String wifi = service.getWifiSwap().isConnected() ? "Y" : " N";
String mdns = service.getWifiSwap().getBonjour().isConnected() ? "Y" : " N";
message += "Swap { BT: " + bluetooth + ", WiFi: " + wifi + ", mDNS: " + mdns + "}, ";
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
bluetooth = "N/A";
if (adapter != 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 = "\"" + adapter.getName() + "\" - " + scanModes.get(adapter.getScanMode());
}
message += "Find { BT: " + bluetooth + ", WiFi: " + wifi + "}";
}
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
);
}
}
public void install(@NonNull final App app) {
final Apk apk = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode);
Uri downloadUri = Uri.parse(apk.getUrl());
localBroadcastManager.registerReceiver(installReceiver,
Installer.getInstallIntentFilter(downloadUri));
InstallManagerService.queue(this, app, apk);
}
private final BroadcastReceiver installReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Installer.ACTION_INSTALL_STARTED:
break;
case Installer.ACTION_INSTALL_COMPLETE:
localBroadcastManager.unregisterReceiver(this);
showRelevantView(true);
break;
case Installer.ACTION_INSTALL_INTERRUPTED:
localBroadcastManager.unregisterReceiver(this);
// TODO: handle errors!
break;
case Installer.ACTION_INSTALL_USER_INTERACTION:
PendingIntent installPendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
installPendingIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "PI canceled", e);
}
break;
default:
throw new RuntimeException("intent action not handled!");
}
}
};
}