public class DiscoverDeviceActivity extends RequiresWifiScansActivity implements WifiListFragment.Client<ScanResultNetwork>, ConnectToApFragment.Client { private static final int MAX_NUM_DISCOVER_PROCESS_ATTEMPTS = 5; private static final long CONNECT_TO_DEVICE_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20); private static final TLog log = TLog.get(DiscoverDeviceActivity.class); private WifiManager wifiManager; private ParticleCloud sparkCloud; private DiscoverProcessWorker discoverProcessWorker; private SoftAPConfigRemover softAPConfigRemover; private WifiListFragment wifiListFragment; private ProgressDialog connectToApSpinnerDialog; private AsyncTask<Void, Void, SetupStepException> connectToApTask; private boolean isResumed = false; private int discoverProcessAttempts = 0; // FIXME: UGH. Figure out a way to pass this info along without making it // into class-wide mutable state. private String currentSSID; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_discover_device); softAPConfigRemover = new SoftAPConfigRemover(this); softAPConfigRemover.removeAllSoftApConfigs(); softAPConfigRemover.reenableWifiNetworks(); DeviceSetupState.previouslyConnectedWifiNetwork = WiFi.getCurrentlyConnectedSSID(this); wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); sparkCloud = ParticleCloud.get(this); wifiListFragment = Ui.findFrag(this, R.id.wifi_list_fragment); ConnectToApFragment.ensureAttached(this); resetWorker(); Ui.setText( this, R.id.wifi_list_header, Phrase.from(this, R.string.wifi_list_header_text) .put("device_name", getString(R.string.device_name)) .format()); Ui.setText( this, R.id.msg_device_not_listed, Phrase.from(this, R.string.msg_device_not_listed) .put("device_name", getString(R.string.device_name)) // .put("setup_button_identifier", // getString(R.string.mode_button_name)) // .put("indicator_light", getString(R.string.indicator_light)) // .put("indicator_light_setup_color_name", // getString(R.string.listen_mode_led_color_name)) .format()); Ui.setTextFromHtml(this, R.id.action_troubleshooting, R.string.troubleshooting) .setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { Uri uri = Uri.parse(v.getContext().getString(R.string.troubleshooting_uri)); startActivity(WebViewActivity.buildIntent(v.getContext(), uri)); } }); if (sparkCloud.getAccessToken() == null) { Ui.setText( this, R.id.logged_in_as, Phrase.from(this, R.string.you_are_logged_in_as) .put("username", sparkCloud.getLoggedInUsername()) .format()); Ui.findView(this, R.id.action_log_out) .setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { sparkCloud.logOut(); log.i("logged out, username is: " + sparkCloud.getLoggedInUsername()); startActivity(new Intent(DiscoverDeviceActivity.this, LoginActivity.class)); finish(); } }); } else { Ui.findView(this, R.id.action_log_out).setVisibility(View.INVISIBLE); } Ui.findView(this, R.id.action_cancel) .setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { finish(); } }); } @Override protected void onStart() { super.onStart(); if (!wifiManager.isWifiEnabled()) { onWifiDisabled(); } } @Override protected void onResume() { super.onResume(); isResumed = true; } @Override protected void onPause() { super.onPause(); isResumed = false; } private void resetWorker() { discoverProcessWorker = new DiscoverProcessWorker(CommandClient.newClientUsingDefaultSocketAddress()); } // FIXME: do we even want to do this...? private void onWifiDisabled() { log.d("Wi-Fi disabled; prompting user"); new AlertDialog.Builder(this) .setTitle(R.string.wifi_required) .setPositiveButton( R.string.enable_wifi, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); log.i("Enabling Wi-Fi at the user's request."); wifiManager.setWifiEnabled(true); wifiListFragment.scanAsync(); } }) .setNegativeButton( R.string.exit_setup, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); finish(); } }) .show(); } @Override public void onNetworkSelected(ScanResultNetwork selectedNetwork) { WifiConfiguration wifiConfig = ConnectToApFragment.buildUnsecuredConfig(selectedNetwork.getSsid(), false); currentSSID = selectedNetwork.getSsid(); connectToSoftAp(wifiConfig); } private void connectToSoftAp(WifiConfiguration config) { discoverProcessAttempts++; softAPConfigRemover.onSoftApConfigured(config.SSID); ConnectToApFragment.get(this).connectToAP(config, CONNECT_TO_DEVICE_TIMEOUT_MILLIS); showProgressDialog(); } @Override public Loader<Set<ScanResultNetwork>> createLoader(int id, Bundle args) { return new WifiScanResultLoader(this); } @Override public void onLoadFinished() { // no-op } @Override public String getListEmptyText() { return Phrase.from(this, R.string.empty_soft_ap_list_text) .put("device_name", getString(R.string.device_name)) .format() .toString(); } @Override public int getAggroLoadingTimeMillis() { return 5000; } @Override public void onApConnectionSuccessful(WifiConfiguration config) { startConnectWorker(); } @Override public void onApConnectionFailed(WifiConfiguration config) { hideProgressDialog(); if (!canStartProcessAgain()) { onMaxAttemptsReached(); } else { connectToSoftAp(config); } } private void showProgressDialog() { wifiListFragment.stopAggroLoading(); String msg = Phrase.from(this, R.string.connecting_to_soft_ap) .put("device_name", getString(R.string.device_name)) .format() .toString(); connectToApSpinnerDialog = new ProgressDialog(this); connectToApSpinnerDialog.setMessage(msg); connectToApSpinnerDialog.setCancelable(false); connectToApSpinnerDialog.setIndeterminate(true); connectToApSpinnerDialog.show(); } private void hideProgressDialog() { wifiListFragment.startAggroLoading(); if (connectToApSpinnerDialog != null) { if (!isFinishing()) { connectToApSpinnerDialog.dismiss(); } connectToApSpinnerDialog = null; } } private void startConnectWorker() { // first, make sure we haven't actually been called twice... if (connectToApTask != null) { log.d("Already running connect worker " + connectToApTask + ", refusing to start another"); return; } wifiListFragment.stopAggroLoading(); // FIXME: verify first that we're still connected to the intended network if (!canStartProcessAgain()) { hideProgressDialog(); onMaxAttemptsReached(); return; } discoverProcessAttempts++; // Kind of lame; this just has doInBackground() return null on success, or if an // exception was thrown, it passes that along instead to indicate failure. connectToApTask = new AsyncTask<Void, Void, SetupStepException>() { @Override protected SetupStepException doInBackground(Void... voids) { try { // including this sleep because without it, // we seem to attempt a socket connection too early, // and it makes the process time out log.d("Waiting a couple seconds before trying the socket connection..."); EZ.threadSleep(2000); discoverProcessWorker.doTheThing( new InterfaceBindingSocketFactory(DiscoverDeviceActivity.this, currentSSID)); return null; } catch (SetupStepException e) { log.d("Setup exception thrown: ", e); return e; } } @Override protected void onPostExecute(SetupStepException error) { connectToApTask = null; if (error == null) { // no exceptions thrown, huzzah hideProgressDialog(); startActivity(new Intent(DiscoverDeviceActivity.this, SelectNetworkActivity.class)); finish(); } else if (error instanceof DeviceAlreadyClaimed) { hideProgressDialog(); onDeviceClaimedByOtherUser(); } else { // nope, do it all over again. // FIXME: this might be a good time to display some feedback... startConnectWorker(); } } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean canStartProcessAgain() { return discoverProcessAttempts < MAX_NUM_DISCOVER_PROCESS_ATTEMPTS; } private void onMaxAttemptsReached() { if (!isResumed) { finish(); return; } String errorMsg = Phrase.from(this, R.string.unable_to_connect_to_soft_ap) .put("device_name", getString(R.string.device_name)) .format() .toString(); new AlertDialog.Builder(this) .setTitle(R.string.error) .setMessage(errorMsg) .setPositiveButton( R.string.ok, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); startActivity(new Intent(DiscoverDeviceActivity.this, GetReadyActivity.class)); finish(); } }) .show(); } private void onDeviceClaimedByOtherUser() { String dialogMsg = getString( R.string.dialog_title_owned_by_another_user, getString(R.string.device_name), sparkCloud.getLoggedInUsername()); new Builder(this) .setTitle(getString(R.string.change_owner_question)) .setMessage(dialogMsg) .setPositiveButton( getString(R.string.change_owner), new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); log.i("Changing owner to " + sparkCloud.getLoggedInUsername()); // // FIXME: state mutation from another class. Not pretty. // // Fix this by breaking DiscoverProcessWorker down into // Steps resetWorker(); discoverProcessWorker.needToClaimDevice = true; discoverProcessWorker.gotOwnershipInfo = true; discoverProcessWorker.isDetectedDeviceClaimed = false; DeviceSetupState.deviceNeedsToBeClaimed = true; showProgressDialog(); startConnectWorker(); } }) .setNegativeButton( R.string.cancel, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); startActivity(new Intent(DiscoverDeviceActivity.this, GetReadyActivity.class)); finish(); } }) .show(); } // FIXME: Even before it's done, I am pretty sure this will need // to go through a round of "solve et coagula" before it's // really right, at least maintenance-wise. // FIXME: this naming is no longer really applicable. static class DiscoverProcessWorker { private final CommandClient client; private String detectedDeviceID; private volatile boolean isDetectedDeviceClaimed; private volatile boolean gotOwnershipInfo; private volatile boolean needToClaimDevice; DiscoverProcessWorker(CommandClient client) { this.client = client; } // FIXME: all this should probably become a list of commands to run in a queue, // each with shortcut conditions for when they've already been fulfilled, instead of // this if-else/try-catch ladder. public void doTheThing(InterfaceBindingSocketFactory socketFactory) throws SetupStepException { // 1. get device ID if (!truthy(detectedDeviceID)) { try { DeviceIdCommand.Response response = client.sendCommandAndReturnResponse( new DeviceIdCommand(), DeviceIdCommand.Response.class, socketFactory); detectedDeviceID = response.deviceIdHex.toLowerCase(); DeviceSetupState.deviceToBeSetUpId = detectedDeviceID; isDetectedDeviceClaimed = truthy(response.isClaimed); } catch (IOException e) { throw new SetupStepException("Process died while trying to get the device ID", e); } } // 2. Get public key if (DeviceSetupState.publicKey == null) { try { DeviceSetupState.publicKey = getPublicKey(socketFactory); } catch (Crypto.CryptoException e) { throw new SetupStepException("Unable to get public key: ", e); } catch (IOException e) { throw new SetupStepException("Error while fetching public key: ", e); } } // 3. check ownership // // all cases: // (1) device not claimed `c=0` — device should also not be in list from API => mobile // app assumes user is claiming // (2) device claimed `c=1` and already in list from API => mobile app does not ask // user about taking ownership because device already belongs to this user // (3) device claimed `c=1` and NOT in the list from the API => mobile app asks whether // use would like to take ownership if (!gotOwnershipInfo) { needToClaimDevice = false; // device was never claimed before - so we need to claim it anyways if (!isDetectedDeviceClaimed) { setClaimCode(socketFactory); needToClaimDevice = true; } else { boolean deviceClaimedByUser = false; for (String deviceId : DeviceSetupState.claimedDeviceIds) { if (deviceId.equalsIgnoreCase(detectedDeviceID)) { deviceClaimedByUser = true; break; } } gotOwnershipInfo = true; if (isDetectedDeviceClaimed && !deviceClaimedByUser) { // This device is already claimed by someone else. Ask the user if we should // change ownership to the current logged in user, and if so, set the claim code. throw new DeviceAlreadyClaimed("Device already claimed by another user"); } else { // Success: no exception thrown, this part of the process is complete. // Let the caller continue on with the setup process. return; } } } else { if (needToClaimDevice) { setClaimCode(socketFactory); } // Success: no exception thrown, the part of the process is complete. Let the caller // continue on with the setup process. return; } } private void setClaimCode(InterfaceBindingSocketFactory socketFactory) throws SetupStepException { try { log.d("Setting claim code using code: " + DeviceSetupState.claimCode); SetCommand.Response response = client.sendCommandAndReturnResponse( new SetCommand("cc", StringUtils.remove(DeviceSetupState.claimCode, "\\")), SetCommand.Response.class, socketFactory); if (truthy(response.responseCode)) { // a non-zero response indicates an error, ala UNIX return codes throw new SetupStepException( "Received non-zero return code from set command: " + response.responseCode); } log.d("Successfully set claim code"); } catch (IOException e) { throw new SetupStepException(e); } } private PublicKey getPublicKey(InterfaceBindingSocketFactory socketFactory) throws Crypto.CryptoException, IOException { PublicKeyCommand.Response response = this.client.sendCommandAndReturnResponse( new PublicKeyCommand(), PublicKeyCommand.Response.class, socketFactory); return Crypto.readPublicKeyFromHexEncodedDerString(response.publicKey); } } // FIXME: remove this if we break down the worker above into Steps // no data to pass along with this at the moment, I just want to specify // that this isn't an error which should necessarily count against retries. static class DeviceAlreadyClaimed extends SetupStepException { public DeviceAlreadyClaimed(String msg, Throwable throwable) { super(msg, throwable); } public DeviceAlreadyClaimed(String msg) { super(msg); } public DeviceAlreadyClaimed(Throwable throwable) { super(throwable); } } }
public class WifiScanResultLoader extends AsyncTaskLoader<Set<ScanResultNetwork>> { private static final TLog log = TLog.get(WifiScanResultLoader.class); private final WifiManager wifiManager; private final WifiScannedBroadcastReceiver receiver = new WifiScannedBroadcastReceiver(); private volatile int loadCount = 0; public WifiScanResultLoader(Context context) { super(context); wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); } @Override protected void onStartLoading() { super.onStartLoading(); getContext().registerReceiver(receiver, receiver.buildIntentFilter()); forceLoad(); } @Override protected void onStopLoading() { getContext().unregisterReceiver(receiver); cancelLoad(); } @Override public Set<ScanResultNetwork> loadInBackground() { List<ScanResult> scanResults = wifiManager.getScanResults(); log.d("Latest (unfiltered) scan results: " + scanResults); if (scanResults == null) { scanResults = Collections.emptyList(); log.wtf("wifiManager.getScanResults() returned null??"); } if (loadCount % 3 == 0) { wifiManager.startScan(); } loadCount++; return FluentIterable.from(scanResults) .filter(ssidStartsWithProductName) .transform(toWifiNetwork) .toSet(); } private class WifiScannedBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { log.d("Received WifiManager.SCAN_RESULTS_AVAILABLE_ACTION broadcast"); forceLoad(); } IntentFilter buildIntentFilter() { return new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); } } private Predicate<ScanResult> ssidStartsWithProductName = new Predicate<ScanResult>() { final String softApPrefix = getPrefix(); @Override public boolean apply(ScanResult input) { return input.SSID != null && input.SSID.toLowerCase().startsWith(softApPrefix); } String getPrefix() { return (getContext().getString(R.string.network_name_prefix) + "-").toLowerCase(); } }; private static Function<ScanResult, ScanResultNetwork> toWifiNetwork = new Function<ScanResult, ScanResultNetwork>() { @Override public ScanResultNetwork apply(ScanResult input) { return new ScanResultNetwork(input); } }; }