├── .gitignore ├── .idea ├── caches │ └── build_file_checksums.ser ├── codeStyles │ └── Project.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── CHANGELOG ├── LICENSE ├── README.md ├── add_to_app_manifest.xml ├── bintray_upload_v1.gradle ├── build.gradle ├── devicesetup ├── .gitignore ├── build.gradle ├── lint.xml ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── particle │ │ └── sdk │ │ └── library │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── fonts │ │ ├── roboto_light.ttf │ │ ├── roboto_medium.ttf │ │ ├── roboto_regular.ttf │ │ └── roboto_thin_italic.ttf │ ├── java │ └── io │ │ └── particle │ │ └── android │ │ └── sdk │ │ ├── accountsetup │ │ ├── CreateAccountActivity.java │ │ ├── LoginActivity.java │ │ ├── PasswordResetActivity.java │ │ └── TwoFactorActivity.java │ │ ├── devicesetup │ │ ├── ApConnector.java │ │ ├── ParticleDeviceSetupLibrary.java │ │ ├── SetupCompleteIntentBuilder.java │ │ ├── SetupProcessException.java │ │ ├── SetupResult.java │ │ ├── SimpleReceiver.java │ │ ├── commands │ │ │ ├── Command.java │ │ │ ├── CommandClient.java │ │ │ ├── CommandClientFactory.java │ │ │ ├── CommandClientUtils.java │ │ │ ├── ConfigureApCommand.java │ │ │ ├── ConnectAPCommand.java │ │ │ ├── DeviceIdCommand.java │ │ │ ├── NetworkBindingSocketFactory.java │ │ │ ├── NoArgsCommand.java │ │ │ ├── PublicKeyCommand.java │ │ │ ├── ScanApCommand.java │ │ │ ├── SetCommand.java │ │ │ ├── VersionCommand.java │ │ │ └── data │ │ │ │ └── WifiSecurity.java │ │ ├── loaders │ │ │ ├── ScanApCommandLoader.java │ │ │ └── WifiScanResultLoader.java │ │ ├── model │ │ │ ├── ScanAPCommandResult.java │ │ │ ├── ScanResultNetwork.java │ │ │ └── WifiNetwork.java │ │ ├── setupsteps │ │ │ ├── CheckIfDeviceClaimedStep.java │ │ │ ├── ConfigureAPStep.java │ │ │ ├── ConnectDeviceToNetworkStep.java │ │ │ ├── EnsureSoftApNotVisible.java │ │ │ ├── SetupStep.java │ │ │ ├── SetupStepApReconnector.java │ │ │ ├── SetupStepException.java │ │ │ ├── SetupStepsFactory.java │ │ │ ├── SetupStepsRunnerTask.java │ │ │ ├── StepConfig.java │ │ │ ├── StepProgress.java │ │ │ ├── WaitForCloudConnectivityStep.java │ │ │ └── WaitForDisconnectionFromDeviceStep.java │ │ └── ui │ │ │ ├── ConnectToApFragment.java │ │ │ ├── ConnectingActivity.java │ │ │ ├── ConnectingProcessWorkerTask.java │ │ │ ├── DeviceSetupState.java │ │ │ ├── DiscoverDeviceActivity.java │ │ │ ├── DiscoverProcessWorker.java │ │ │ ├── GetReadyActivity.java │ │ │ ├── ManualNetworkEntryActivity.java │ │ │ ├── PasswordEntryActivity.java │ │ │ ├── PermissionsFragment.java │ │ │ ├── RequiresWifiScansActivity.java │ │ │ ├── SelectNetworkActivity.java │ │ │ ├── SuccessActivity.java │ │ │ └── WifiListFragment.java │ │ ├── di │ │ ├── ActivityInjectorComponent.java │ │ ├── ApModule.java │ │ ├── ApplicationComponent.java │ │ ├── ApplicationModule.java │ │ ├── CloudModule.java │ │ └── PerActivity.java │ │ ├── ui │ │ ├── BaseActivity.java │ │ └── NextActivitySelector.java │ │ └── utils │ │ ├── BetterAsyncTaskLoader.java │ │ ├── CoreNameGenerator.java │ │ ├── Crypto.java │ │ ├── ParticleDeviceSetupInternalStringUtils.java │ │ ├── SEGAnalytics.java │ │ ├── SSID.java │ │ ├── SoftAPConfigRemover.java │ │ ├── WifiFacade.java │ │ ├── WorkerFragment.java │ │ └── ui │ │ ├── ParticleUi.java │ │ ├── SoftKeyboardVisibilityDetectingLinearLayout.java │ │ ├── Toaster.java │ │ ├── Ui.java │ │ └── WebViewActivity.java │ └── res │ ├── drawable-hdpi │ └── ic_clear_black_24dp.png │ ├── drawable-mdpi │ └── ic_clear_black_24dp.png │ ├── drawable-xhdpi │ ├── checkmark.png │ └── ic_clear_black_24dp.png │ ├── drawable-xxhdpi │ ├── fail.png │ ├── ic_clear_black_24dp.png │ ├── particle_vertical_blue.png │ ├── photon_vector.png │ ├── photon_vector_small.png │ ├── success.png │ ├── the_wifi.png │ └── trianglifybackground.png │ ├── drawable-xxxhdpi │ ├── ic_clear_black_24dp.png │ ├── lock.png │ ├── particle_horizontal_blue.png │ └── particle_horizontal_head.png │ ├── drawable │ ├── button_text_color_selector.xml │ ├── link_text_selector.xml │ ├── progress_indicator_graphic.png │ └── progress_spinner.xml │ ├── layout-xlarge │ └── activity_connecting.xml │ ├── layout │ ├── activity_connecting.xml │ ├── activity_create_account.xml │ ├── activity_discover_device.xml │ ├── activity_get_ready.xml │ ├── activity_manual_network_entry.xml │ ├── activity_password_entry.xml │ ├── activity_password_reset.xml │ ├── activity_select_network.xml │ ├── activity_success.xml │ ├── activity_two_factor.xml │ ├── activity_web_view.xml │ ├── brand_image_header.xml │ ├── particle_activity_login.xml │ └── row_wifi_scan_result.xml │ ├── values-sw820dp │ └── dimens.xml │ ├── values-v19 │ ├── dimens.xml │ └── themes.xml │ ├── values-v21 │ └── themes.xml │ ├── values-xlarge │ └── dimens_font_size.xml │ └── values │ ├── colors.xml │ ├── customization.xml │ ├── devicesetup_styles.xml │ ├── dimens.xml │ ├── dimens_font_size.xml │ ├── ids.xml │ ├── strings.xml │ ├── strings_activity_login.xml │ ├── styles.xml │ ├── success_failure_messages.xml │ └── themes.xml ├── exampleapp ├── build.gradle ├── lint.xml ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── io │ │ └── particle │ │ └── devicesetup │ │ └── exampleapp │ │ ├── ExampleSetupCompleteIntentBuilder.java │ │ └── MainActivity.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pom_generator_v1.gradle ├── settings.gradle └── testapp ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── io │ └── particle │ └── devicesetup │ └── testapp │ ├── ApplicationTest.java │ ├── CustomAndroidTestRunner.java │ ├── CustomApplication.java │ ├── EspressoDaggerMockRule.java │ └── accountsetup │ └── SetupFlowTest.java ├── main ├── AndroidManifest.xml ├── java │ └── io │ │ └── particle │ │ └── devicesetup │ │ └── testapp │ │ └── MainActivity.java └── res │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml └── test └── java └── io └── particle └── devicesetup └── testapp └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | /*/build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | # this file shouldn't be anywhere under the root of this repo in the first place, but 23 | # better safe than sorry, in case of copy/paste accidents, etc. 24 | bintray_user_auth_secrets.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | 33 | # IntelliJ project files that shouldn't be checked in 34 | /.idea/workspace.xml 35 | /.idea/tasks.xml 36 | /.idea/gradle.xml 37 | /.idea/libraries 38 | *.iml 39 | .idea/inspectionProfiles/Project_Default.xml 40 | .idea/inspectionProfiles/profiles_settings.xml 41 | .idea/qaplug_profiles.xml 42 | 43 | # OS X noise 44 | .DS_Store 45 | 46 | -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Particle Device Setup library 2 | 3 | ## MOVED! 4 | 5 | The Device Setup Library has moved to [the new Particle Android repository!](https://github.com/particle-iot/particle-android) This repository is now deprecated and will not be updated. 6 | 7 | -------------------------------------------------------------------------------- /add_to_app_manifest.xml: -------------------------------------------------------------------------------- 1 | 7 | 13 | 19 | 25 | 31 | 36 | 41 | 47 | 53 | 59 | 65 | -------------------------------------------------------------------------------- /bintray_upload_v1.gradle: -------------------------------------------------------------------------------- 1 | // lifted from https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle 2 | // and copied into this repo to maintain a hermetic build 3 | apply plugin: 'com.jfrog.bintray' 4 | 5 | version = libraryVersion 6 | 7 | task sourcesJar(type: Jar) { 8 | from android.sourceSets.main.java.srcDirs 9 | classifier = 'sources' 10 | } 11 | 12 | task javadoc(type: Javadoc) { 13 | source = android.sourceSets.main.java.srcDirs 14 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 15 | } 16 | 17 | task javadocJar(type: Jar, dependsOn: javadoc) { 18 | classifier = 'javadoc' 19 | from javadoc.destinationDir 20 | } 21 | 22 | artifacts { 23 | archives javadocJar 24 | archives sourcesJar 25 | } 26 | 27 | // FIXME: this feels hackish, but it works for now, and it shouldn't 28 | // have any side effects* for anyone building the lib locally, 29 | // so #SHIPIT 30 | // 31 | // * My apologies if this turns out not to be true. Patches welcome! 32 | Properties authDataProps = new Properties() 33 | try { 34 | authDataProps.load(project.rootProject.file('../bintray_user_auth_secrets.properties').newDataInputStream()) 35 | } catch (Exception ignore) { 36 | // do nothing; this is the default state for everyone who isn't publishing 37 | // the lib. 38 | } 39 | 40 | bintray { 41 | user = authDataProps.getProperty("bintray.user") 42 | key = authDataProps.getProperty("bintray.apikey") 43 | 44 | configurations = ['archives'] 45 | pkg { 46 | userOrg = bintrayOrg 47 | repo = bintrayRepo 48 | name = bintrayName 49 | desc = libraryDescription 50 | websiteUrl = siteUrl 51 | vcsUrl = gitUrl 52 | licenses = allLicenses 53 | publish = true 54 | publicDownloadNumbers = false 55 | version { 56 | // desc = libraryDescription 57 | gpg { 58 | sign = false // Determines whether to GPG sign the files. The default is false 59 | // passphrase = properties.getProperty("bintray.gpg.password") // Optional. The passphrase for GPG signing' 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.1.4' 11 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' 12 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' 13 | classpath 'com.jakewharton:butterknife-gradle-plugin:8.4.0' 14 | } 15 | } 16 | 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /devicesetup/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /devicesetup/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.jakewharton.butterknife' 3 | 4 | // This is the library version used when deploying the artifact 5 | version = '0.6.3' 6 | 7 | ext { 8 | bintrayRepo = 'android' 9 | bintrayName = 'devicesetup' 10 | bintrayOrg = 'particle' 11 | 12 | publishedGroupId = 'io.particle' 13 | libraryName = 'Particle (formerly Spark) Android Device Setup library' 14 | artifact = 'devicesetup' 15 | 16 | libraryDescription = "The Particle Device Setup library provides everything you need to " + 17 | "offer your users a simple initial setup process for Particle-powered devices. This " + 18 | "includes all the necessary device communication code, an easily customizable UI, and " + 19 | "a simple developer API." 20 | 21 | siteUrl = 'https://github.com/spark/spark-setup-android' 22 | gitUrl = 'https://github.com/spark/spark-setup-android.git' 23 | 24 | libraryVersion = project.version 25 | 26 | developerId = 'idok' 27 | developerName = 'Ido Kleinman' 28 | developerEmail = 'ido@particle.io' 29 | 30 | licenseName = 'The Apache Software License, Version 2.0' 31 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 32 | allLicenses = ["Apache-2.0"] 33 | } 34 | 35 | 36 | android { 37 | compileSdkVersion 27 38 | buildToolsVersion '27.0.3' 39 | 40 | dexOptions { 41 | javaMaxHeapSize "2g" 42 | } 43 | 44 | defaultConfig { 45 | minSdkVersion 15 46 | targetSdkVersion 27 47 | versionCode 1 48 | versionName "1.0" 49 | } 50 | 51 | compileOptions { 52 | sourceCompatibility JavaVersion.VERSION_1_8 53 | targetCompatibility JavaVersion.VERSION_1_8 54 | } 55 | 56 | buildTypes { 57 | release { 58 | minifyEnabled false 59 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 60 | } 61 | } 62 | 63 | lintOptions { 64 | abortOnError true 65 | } 66 | 67 | } 68 | 69 | // TESTING ONLY: to build against a locally built version of the cloud SDK, uncomment these 70 | // lines, and the "compile(name:'cloudsdk', ext:'aar')" line below under dependencies. 71 | // (If you don't know what this means or why we (the SDK maintainers at Particle) would want to 72 | // do this, then you can safely ignore all this and keep it commented out. :) 73 | //repositories { 74 | // flatDir { 75 | // dirs 'libs' 76 | // } 77 | //} 78 | 79 | 80 | dependencies { 81 | api fileTree(include: ['*.jar'], dir: 'libs') 82 | 83 | api 'io.particle:cloudsdk:0.5.1' 84 | 85 | api 'com.squareup.phrase:phrase:1.0.3' 86 | api 'uk.co.chrisjenx:calligraphy:2.3.0' 87 | api 'com.segment.analytics.android:analytics:4.3.1' 88 | api 'com.madgag.spongycastle:core:1.58.0.0' 89 | 90 | api 'com.google.dagger:dagger:2.15' 91 | annotationProcessor 'com.google.dagger:dagger-compiler:2.15' 92 | api 'com.jakewharton:butterknife:8.8.1' 93 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' 94 | 95 | api 'com.android.support:appcompat-v7:27.1.1' 96 | api 'com.android.support:recyclerview-v7:27.1.1' 97 | 98 | // TESTING ONLY (see other TESTING comments further up) 99 | // compile(name: 'cloudsdk', ext: 'aar') 100 | } 101 | 102 | apply from: '../pom_generator_v1.gradle' 103 | apply from: '../bintray_upload_v1.gradle' 104 | 105 | // disable insane, build-breaking doclint tool in Java 8 106 | if (JavaVersion.current().isJava8Compatible()) { 107 | tasks.withType(Javadoc) { 108 | //noinspection SpellCheckingInspection 109 | options.addStringOption('Xdoclint:none', '-quiet') 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /devicesetup/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /devicesetup/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/jensck/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /devicesetup/src/androidTest/java/io/particle/sdk/library/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package io.particle.sdk.library; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /devicesetup/src/main/assets/fonts/roboto_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/assets/fonts/roboto_light.ttf -------------------------------------------------------------------------------- /devicesetup/src/main/assets/fonts/roboto_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/assets/fonts/roboto_medium.ttf -------------------------------------------------------------------------------- /devicesetup/src/main/assets/fonts/roboto_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/assets/fonts/roboto_regular.ttf -------------------------------------------------------------------------------- /devicesetup/src/main/assets/fonts/roboto_thin_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/assets/fonts/roboto_thin_italic.ttf -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/SetupCompleteIntentBuilder.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.support.annotation.Nullable; 6 | 7 | public interface SetupCompleteIntentBuilder { 8 | Intent buildIntent(Context ctx, @Nullable SetupResult result); 9 | } 10 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/SetupProcessException.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup; 2 | 3 | 4 | import io.particle.android.sdk.devicesetup.setupsteps.SetupStep; 5 | 6 | public class SetupProcessException extends Exception { 7 | 8 | public final SetupStep failedStep; 9 | 10 | public SetupProcessException(String msg, SetupStep failedStep) { 11 | super(msg); 12 | this.failedStep = failedStep; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/SetupResult.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | public class SetupResult implements Parcelable { 7 | private final boolean wasSuccessful; 8 | private final String configuredDeviceId; 9 | 10 | public SetupResult(boolean wasSuccessful, String configuredDeviceId) { 11 | this.wasSuccessful = wasSuccessful; 12 | this.configuredDeviceId = configuredDeviceId; 13 | } 14 | 15 | public boolean wasSuccessful() { 16 | return wasSuccessful; 17 | } 18 | 19 | public String getConfiguredDeviceId() { 20 | return configuredDeviceId; 21 | } 22 | 23 | @Override 24 | public int describeContents() { 25 | return 0; 26 | } 27 | 28 | @Override 29 | public void writeToParcel(Parcel dest, int flags) { 30 | dest.writeInt(wasSuccessful ? 1 : 0); 31 | dest.writeString(configuredDeviceId); 32 | } 33 | 34 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 35 | @Override 36 | public SetupResult createFromParcel(Parcel source) { 37 | return new SetupResult(source); 38 | } 39 | 40 | @Override 41 | public SetupResult[] newArray(int size) { 42 | return new SetupResult[size]; 43 | } 44 | }; 45 | 46 | private SetupResult(Parcel source) { 47 | wasSuccessful = source.readInt() == 1; 48 | configuredDeviceId = source.readString(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/SimpleReceiver.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup; 2 | 3 | 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.IntentFilter; 8 | 9 | 10 | public class SimpleReceiver extends BroadcastReceiver { 11 | 12 | public interface LambdafiableBroadcastReceiver { 13 | void onReceive(Context context, Intent intent); 14 | } 15 | 16 | 17 | public static SimpleReceiver newReceiver(Context ctx, IntentFilter intentFilter, 18 | LambdafiableBroadcastReceiver receiver) { 19 | return new SimpleReceiver(ctx, intentFilter, receiver); 20 | } 21 | 22 | 23 | public static SimpleReceiver newRegisteredReceiver(Context ctx, IntentFilter intentFilter, 24 | LambdafiableBroadcastReceiver receiver) { 25 | SimpleReceiver sr = new SimpleReceiver(ctx, intentFilter, receiver); 26 | sr.register(); 27 | return sr; 28 | } 29 | 30 | 31 | private final Context appContext; 32 | private final IntentFilter intentFilter; 33 | private final LambdafiableBroadcastReceiver receiver; 34 | 35 | private boolean registered = false; 36 | 37 | private SimpleReceiver(Context ctx, IntentFilter intentFilter, 38 | LambdafiableBroadcastReceiver receiver) { 39 | this.appContext = ctx.getApplicationContext(); 40 | this.intentFilter = intentFilter; 41 | this.receiver = receiver; 42 | } 43 | 44 | @Override 45 | public void onReceive(Context context, Intent intent) { 46 | receiver.onReceive(context, intent); 47 | } 48 | 49 | public void register() { 50 | if (registered) { 51 | return; 52 | } 53 | appContext.registerReceiver(this, intentFilter); 54 | registered = true; 55 | } 56 | 57 | public void unregister() { 58 | if (!registered) { 59 | return; 60 | } 61 | appContext.unregisterReceiver(this); 62 | registered = false; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/commands/Command.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.commands; 2 | 3 | import com.google.gson.Gson; 4 | 5 | 6 | public abstract class Command { 7 | 8 | public abstract String getCommandName(); 9 | 10 | // override if you want a different implementation 11 | public String argsAsJsonString(Gson gson) { 12 | return gson.toJson(this); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/commands/CommandClient.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.commands; 2 | 3 | import android.support.annotation.CheckResult; 4 | 5 | import com.google.gson.Gson; 6 | 7 | import java.io.IOException; 8 | import java.net.Socket; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import javax.net.SocketFactory; 12 | 13 | import io.particle.android.sdk.utils.EZ; 14 | import io.particle.android.sdk.utils.TLog; 15 | import okio.BufferedSink; 16 | import okio.BufferedSource; 17 | import okio.Okio; 18 | 19 | import static io.particle.android.sdk.utils.Py.truthy; 20 | 21 | 22 | public class CommandClient { 23 | static final int DEFAULT_TIMEOUT_SECONDS = 10; 24 | 25 | private static final TLog log = TLog.get(CommandClient.class); 26 | private static final Gson gson = new Gson(); 27 | 28 | private final String ipAddress; 29 | private final int port; 30 | private final SocketFactory socketFactory; 31 | 32 | CommandClient(String ipAddress, int port, SocketFactory socketFactory) { 33 | this.ipAddress = ipAddress; 34 | this.port = port; 35 | this.socketFactory = socketFactory; 36 | } 37 | 38 | public void sendCommand(Command command) throws IOException { 39 | sendAndMaybeReceive(command, Void.class); 40 | } 41 | 42 | @CheckResult 43 | public T sendCommand(Command command, Class responseType) throws IOException { 44 | return sendAndMaybeReceive(command, responseType); 45 | } 46 | 47 | 48 | private T sendAndMaybeReceive(Command command, Class responseType) throws IOException { 49 | log.i("Preparing to send command '" + command.getCommandName() + "'"); 50 | String commandData = buildCommandData(command); 51 | 52 | BufferedSink buffer = null; 53 | try { 54 | // send command 55 | Socket socket = socketFactory.createSocket(ipAddress, port); 56 | buffer = wrapSocket(socket, DEFAULT_TIMEOUT_SECONDS); 57 | log.d("Writing command data"); 58 | buffer.writeUtf8(commandData); 59 | buffer.flush(); 60 | 61 | // if no response defined, just exit early. 62 | if (responseType.equals(Void.class)) { 63 | log.d("Done."); 64 | return null; 65 | } 66 | 67 | return readResponse(socket, responseType, DEFAULT_TIMEOUT_SECONDS); 68 | 69 | } finally { 70 | EZ.closeThisThingOrMaybeDont(buffer); 71 | } 72 | } 73 | 74 | private BufferedSink wrapSocket(Socket socket, int timeoutValueInSeconds) throws IOException { 75 | BufferedSink sink = Okio.buffer(Okio.sink(socket)); 76 | sink.timeout().timeout(timeoutValueInSeconds, TimeUnit.SECONDS); 77 | return sink; 78 | } 79 | 80 | private String buildCommandData(Command command) { 81 | StringBuilder commandData = new StringBuilder() 82 | .append(command.getCommandName()) 83 | .append("\n"); 84 | 85 | String commandArgs = command.argsAsJsonString(gson); 86 | if (truthy(commandArgs)) { 87 | commandData.append(commandArgs.length()); 88 | commandData.append("\n\n"); 89 | commandData.append(commandArgs); 90 | } else { 91 | commandData.append("0\n\n"); 92 | } 93 | 94 | String built = commandData.toString(); 95 | log.i("*** BUILT COMMAND DATA: '" + CommandClientUtils.escapeJava(built) + "'"); 96 | return built; 97 | } 98 | 99 | private T readResponse(Socket socket, Class responseType, int timeoutValueInSeconds) 100 | throws IOException { 101 | BufferedSource buffer = Okio.buffer(Okio.source(socket)); 102 | buffer.timeout().timeout(timeoutValueInSeconds, TimeUnit.SECONDS); 103 | 104 | log.d("Reading response data..."); 105 | String line; 106 | do { 107 | // read (and throw away, for now) any headers 108 | line = buffer.readUtf8LineStrict(); 109 | } while (truthy(line)); 110 | 111 | String responseData = buffer.readUtf8(); 112 | log.d("Command response (raw): " + CommandClientUtils.escapeJava(responseData)); 113 | T tee = gson.fromJson(responseData, responseType); 114 | log.d("Command response: " + tee); 115 | EZ.closeThisThingOrMaybeDont(buffer); 116 | return tee; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/commands/CommandClientFactory.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.commands; 2 | 3 | import io.particle.android.sdk.utils.SSID; 4 | import io.particle.android.sdk.utils.WifiFacade; 5 | 6 | import static io.particle.android.sdk.devicesetup.commands.CommandClient.DEFAULT_TIMEOUT_SECONDS; 7 | 8 | public class CommandClientFactory { 9 | 10 | public CommandClient newClient(WifiFacade wifiFacade, SSID softApSSID, String ipAddress, int port) { 11 | return new CommandClient(ipAddress, port, 12 | new NetworkBindingSocketFactory(wifiFacade, softApSSID, DEFAULT_TIMEOUT_SECONDS * 1000)); 13 | } 14 | 15 | // FIXME: set these defaults in a resource file? 16 | public CommandClient newClientUsingDefaultsForDevices(WifiFacade wifiFacade, SSID softApSSID) { 17 | return newClient(wifiFacade, softApSSID, "192.168.0.1", 5609); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/commands/ConfigureApCommand.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.commands; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import io.particle.android.sdk.devicesetup.commands.data.WifiSecurity; 6 | 7 | import static io.particle.android.sdk.utils.Py.all; 8 | import static io.particle.android.sdk.utils.Py.truthy; 9 | 10 | 11 | /** 12 | * Configure the access point details to connect to when connect-ap is called. The AP doesn't have 13 | * to be in the list from scan-ap, allowing manual entry of hidden networks. 14 | */ 15 | public class ConfigureApCommand extends Command { 16 | 17 | public final Integer idx; 18 | 19 | public final String ssid; 20 | 21 | @SerializedName("pwd") 22 | public final String encryptedPasswordHex; 23 | 24 | @SerializedName("sec") 25 | public final Integer wifiSecurityType; 26 | 27 | @SerializedName("ch") 28 | public final Integer channel; 29 | 30 | public static Builder newBuilder() { 31 | return new Builder(); 32 | } 33 | 34 | @Override 35 | public String getCommandName() { 36 | return "configure-ap"; 37 | } 38 | 39 | // private constructor -- use .newBuilder() instead. 40 | private ConfigureApCommand(int idx, String ssid, String encryptedPasswordHex, 41 | WifiSecurity wifiSecurityType, int channel) { 42 | this.idx = idx; 43 | this.ssid = ssid; 44 | this.encryptedPasswordHex = encryptedPasswordHex; 45 | this.wifiSecurityType = wifiSecurityType.asInt(); 46 | this.channel = channel; 47 | } 48 | 49 | 50 | public static class Response { 51 | 52 | @SerializedName("r") 53 | public final Integer responseCode; // 0 == OK, non-zero == problem with index/data 54 | 55 | public Response(Integer responseCode) { 56 | this.responseCode = responseCode; 57 | } 58 | 59 | // FIXME: do this for the other ones with just the "responseCode" field 60 | public boolean isOk() { 61 | return responseCode == 0; 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return "Response{" + 67 | "responseCode=" + responseCode + 68 | '}'; 69 | } 70 | } 71 | 72 | 73 | public static class Builder { 74 | private Integer idx; 75 | private String ssid; 76 | private String encryptedPasswordHex; 77 | private WifiSecurity securityType; 78 | private Integer channel; 79 | 80 | public Builder setIdx(int idx) { 81 | this.idx = idx; 82 | return this; 83 | } 84 | 85 | public Builder setSsid(String ssid) { 86 | this.ssid = ssid; 87 | return this; 88 | } 89 | 90 | public Builder setEncryptedPasswordHex(String encryptedPasswordHex) { 91 | this.encryptedPasswordHex = encryptedPasswordHex; 92 | return this; 93 | } 94 | 95 | public Builder setSecurityType(WifiSecurity securityType) { 96 | this.securityType = securityType; 97 | return this; 98 | } 99 | 100 | public Builder setChannel(int channel) { 101 | this.channel = channel; 102 | return this; 103 | } 104 | 105 | public ConfigureApCommand build() { 106 | if (!all(ssid, securityType) 107 | || (truthy(encryptedPasswordHex) && securityType == WifiSecurity.OPEN)) { 108 | throw new IllegalArgumentException( 109 | "One or more required arguments was not set on ConfigureApCommand"); 110 | } 111 | if (idx == null) { 112 | idx = 0; 113 | } 114 | return new ConfigureApCommand(idx, ssid, encryptedPasswordHex, securityType, channel); 115 | } 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/commands/ConnectAPCommand.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.commands; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | /** 6 | * Connects to an AP previously configured with configure-ap. This disconnects the soft-ap after 7 | * the response code has been sent. Note that the response code doesn't indicate successful 8 | * connection to the AP, but only that the command was acknowledged and the AP will be 9 | * connected to after the result is sent to the client. 10 | *

11 | * If the AP connection is unsuccessful, the soft-AP will be reinstated so the user can enter 12 | * new credentials/try again. 13 | */ 14 | public class ConnectAPCommand extends Command { 15 | 16 | @SerializedName("idx") 17 | public final int index; 18 | 19 | public ConnectAPCommand(int index) { 20 | this.index = index; 21 | } 22 | 23 | @Override 24 | public String getCommandName() { 25 | return "connect-ap"; 26 | } 27 | 28 | 29 | public static class Response { 30 | 31 | @SerializedName("r") 32 | public final int responseCode; // 0 == OK, non-zero == problem with index/data 33 | 34 | public Response(int responseCode) { 35 | this.responseCode = responseCode; 36 | } 37 | 38 | public boolean isOK() { 39 | return responseCode == 0; 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return "Response{" + 45 | "responseCode=" + responseCode + 46 | '}'; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/commands/DeviceIdCommand.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.commands; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | /** 6 | * Retrieves the unique device ID as a 24-digit hex string 7 | */ 8 | public class DeviceIdCommand extends NoArgsCommand { 9 | 10 | 11 | @Override 12 | public String getCommandName() { 13 | return "device-id"; 14 | } 15 | 16 | 17 | public static class Response { 18 | 19 | @SerializedName("id") 20 | public final String deviceIdHex; 21 | 22 | @SerializedName("c") 23 | public final int isClaimed; 24 | 25 | 26 | public Response(String deviceIdHex, int isClaimed) { 27 | this.deviceIdHex = deviceIdHex; 28 | this.isClaimed = isClaimed; 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return "Response{" + 34 | "deviceIdHex='" + deviceIdHex + '\'' + 35 | ", isClaimed=" + isClaimed + 36 | '}'; 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/commands/NetworkBindingSocketFactory.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.commands; 2 | 3 | import android.annotation.TargetApi; 4 | import android.net.Network; 5 | import android.os.Build.VERSION; 6 | import android.os.Build.VERSION_CODES; 7 | 8 | import java.io.IOException; 9 | import java.net.InetAddress; 10 | import java.net.InetSocketAddress; 11 | import java.net.Socket; 12 | import java.net.UnknownHostException; 13 | 14 | import javax.net.SocketFactory; 15 | 16 | import io.particle.android.sdk.utils.SSID; 17 | import io.particle.android.sdk.utils.TLog; 18 | import io.particle.android.sdk.utils.WifiFacade; 19 | 20 | /** 21 | * Factory for Sockets which binds communication to a particular {@link android.net.Network} 22 | */ 23 | public class NetworkBindingSocketFactory extends SocketFactory { 24 | 25 | private static final TLog log = TLog.get(NetworkBindingSocketFactory.class); 26 | 27 | private final WifiFacade wifiFacade; 28 | private final SSID softAPSSID; 29 | // used as connection timeout and read timeout 30 | private final int timeoutMillis; 31 | 32 | public NetworkBindingSocketFactory(WifiFacade wifiFacade, SSID softAPSSID, int timeoutMillis) { 33 | this.wifiFacade = wifiFacade; 34 | this.softAPSSID = softAPSSID; 35 | this.timeoutMillis = timeoutMillis; 36 | } 37 | 38 | @Override 39 | public Socket createSocket() throws IOException { 40 | return buildSocket(); 41 | } 42 | 43 | @Override 44 | public Socket createSocket(String host, int port) throws IOException, UnknownHostException { 45 | Socket socket = buildSocket(); 46 | socket.connect(new InetSocketAddress(host, port), timeoutMillis); 47 | return socket; 48 | } 49 | 50 | @Override 51 | public Socket createSocket(String host, int port, InetAddress localHost, int localPort) 52 | throws IOException, UnknownHostException { 53 | throw new UnsupportedOperationException( 54 | "Specifying a localHost or localPort arg is not supported."); 55 | } 56 | 57 | @Override 58 | public Socket createSocket(InetAddress host, int port) throws IOException { 59 | Socket socket = buildSocket(); 60 | socket.connect(new InetSocketAddress(host, port), timeoutMillis); 61 | return socket; 62 | } 63 | 64 | @Override 65 | public Socket createSocket(InetAddress address, int port, InetAddress localAddress, 66 | int localPort) throws IOException { 67 | throw new UnsupportedOperationException( 68 | "Specifying a localHost or localPort arg is not supported."); 69 | } 70 | 71 | 72 | private Socket buildSocket() throws IOException { 73 | Socket socket = new Socket(); 74 | socket.setSoTimeout(timeoutMillis); 75 | 76 | if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 77 | bindSocketToSoftAp(socket); 78 | } 79 | 80 | return socket; 81 | } 82 | 83 | @TargetApi(VERSION_CODES.LOLLIPOP) 84 | private void bindSocketToSoftAp(Socket socket) throws IOException { 85 | Network softAp = wifiFacade.getNetworkObjectForCurrentWifiConnection(); 86 | 87 | if (softAp == null) { 88 | // If this ever fails, fail VERY LOUDLY to make sure we hear about it... 89 | // FIXME: report this error via analytics 90 | throw new SocketBindingException("Could not find Network for SSID " + softAPSSID); 91 | } 92 | 93 | softAp.bindSocket(socket); 94 | } 95 | 96 | 97 | private static class SocketBindingException extends IOException { 98 | 99 | SocketBindingException(String msg) { 100 | super(msg); 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/commands/NoArgsCommand.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.commands; 2 | 3 | import com.google.gson.Gson; 4 | 5 | /** 6 | * Convenience class for commands with no argument data 7 | */ 8 | public abstract class NoArgsCommand extends Command { 9 | 10 | @Override 11 | public String argsAsJsonString(Gson gson) { 12 | // this command has no argument data 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/commands/PublicKeyCommand.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.commands; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class PublicKeyCommand extends NoArgsCommand { 6 | 7 | @Override 8 | public String getCommandName() { 9 | return "public-key"; 10 | } 11 | 12 | 13 | public static class Response { 14 | 15 | @SerializedName("r") 16 | public final int responseCode; 17 | 18 | // Hex-encoded public key, in DER format 19 | @SerializedName("b") 20 | public final String publicKey; 21 | 22 | public Response(int responseCode, String publicKey) { 23 | this.responseCode = responseCode; 24 | this.publicKey = publicKey; 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/commands/ScanApCommand.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.commands; 2 | 3 | 4 | import com.google.gson.annotations.SerializedName; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | public class ScanApCommand extends NoArgsCommand { 10 | 11 | @Override 12 | public String getCommandName() { 13 | return "scan-ap"; 14 | } 15 | 16 | 17 | public static class Response { 18 | 19 | // using an array here instead of a generic 20 | // collection makes Gson usage simpler 21 | public final Scan[] scans; 22 | 23 | public Response(Scan[] scans) { 24 | this.scans = scans; 25 | } 26 | 27 | public List getScans() { 28 | return Arrays.asList(scans); 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return "Response{" + 34 | "scans=" + Arrays.toString(scans) + 35 | '}'; 36 | } 37 | } 38 | 39 | 40 | public static class Scan { 41 | 42 | public final String ssid; 43 | 44 | @SerializedName("sec") 45 | public final Integer wifiSecurityType; 46 | 47 | @SerializedName("ch") 48 | public final Integer channel; 49 | 50 | public Scan(String ssid, Integer wifiSecurityType, Integer channel) { 51 | this.ssid = ssid; 52 | this.wifiSecurityType = wifiSecurityType; 53 | this.channel = channel; 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return "Scan{" + 59 | "ssid='" + ssid + '\'' + 60 | ", wifiSecurityType=" + wifiSecurityType + 61 | ", channel=" + channel + 62 | '}'; 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/commands/SetCommand.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.commands; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import io.particle.android.sdk.utils.Preconditions; 6 | 7 | 8 | public class SetCommand extends Command { 9 | 10 | @SerializedName("k") 11 | public final String key; 12 | 13 | @SerializedName("v") 14 | public final String value; 15 | 16 | public SetCommand(String key, String value) { 17 | Preconditions.checkNotNull(key, "Key cannot be null"); 18 | Preconditions.checkNotNull(value, "Value cannot be null"); 19 | this.key = key; 20 | this.value = value; 21 | } 22 | 23 | @Override 24 | public String getCommandName() { 25 | return "set"; 26 | } 27 | 28 | 29 | public static class Response { 30 | 31 | @SerializedName("r") 32 | public final int responseCode; 33 | 34 | public Response(int responseCode) { 35 | this.responseCode = responseCode; 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/commands/VersionCommand.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.commands; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | 6 | public class VersionCommand extends NoArgsCommand { 7 | 8 | @Override 9 | public String getCommandName() { 10 | return "version"; 11 | } 12 | 13 | 14 | public static class Response { 15 | 16 | @SerializedName("v") 17 | public final int version; 18 | 19 | public Response(int version) { 20 | this.version = version; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return "Response{" + 26 | "version=" + version + 27 | '}'; 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/commands/data/WifiSecurity.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.commands.data; 2 | 3 | 4 | import android.util.SparseArray; 5 | 6 | import io.particle.android.sdk.utils.Preconditions; 7 | 8 | 9 | public enum WifiSecurity { 10 | 11 | OPEN(0), // Unsecured 12 | WEP_PSK(1), // WEP Security with open authentication 13 | WEP_SHARED(0x8001), // WEP Security with shared authentication 14 | WPA_TKIP_PSK(0x00200002), // WPA Security with TKIP 15 | WPA_AES_PSK(0x00200004), // WPA Security with AES 16 | WPA_MIXED_PSK(0x00200006), // WPA Security with AES & TKIP 17 | WPA2_AES_PSK(0x00400004), // WPA2 Security with AES 18 | WPA2_TKIP_PSK(0x00400002), // WPA2 Security with TKIP 19 | WPA2_MIXED_PSK(0x00400006); // WPA2 Security with AES & TKIP 20 | 21 | 22 | static final SparseArray fromIntMap; 23 | 24 | static { 25 | fromIntMap = new SparseArray<>(); 26 | fromIntMap.put(OPEN.asInt(), OPEN); 27 | fromIntMap.put(WEP_PSK.asInt(), WEP_PSK); 28 | fromIntMap.put(WEP_SHARED.asInt(), WEP_SHARED); 29 | fromIntMap.put(WPA_TKIP_PSK.asInt(), WPA_TKIP_PSK); 30 | fromIntMap.put(WPA_MIXED_PSK.asInt(), WPA_MIXED_PSK); 31 | fromIntMap.put(WPA_AES_PSK.asInt(), WPA_AES_PSK); 32 | fromIntMap.put(WPA2_AES_PSK.asInt(), WPA2_AES_PSK); 33 | fromIntMap.put(WPA2_TKIP_PSK.asInt(), WPA2_TKIP_PSK); 34 | fromIntMap.put(WPA2_MIXED_PSK.asInt(), WPA2_MIXED_PSK); 35 | } 36 | 37 | private final int intValue; 38 | 39 | WifiSecurity(int intValue) { 40 | this.intValue = intValue; 41 | } 42 | 43 | private static final int ENTERPRISE_ENABLED_MASK = 0x02000000; 44 | 45 | // FIXME: accommodate this better 46 | public static boolean isEnterpriseNetwork(int value) { 47 | return (ENTERPRISE_ENABLED_MASK & value) != 0; 48 | } 49 | 50 | public static WifiSecurity fromInteger(Integer value) { 51 | Preconditions.checkNotNull(value); 52 | fromIntMap.indexOfKey(value); 53 | Preconditions.checkArgument( 54 | fromIntMap.indexOfKey(value) >= 0, 55 | "Value not found in map: " + value); 56 | 57 | return fromIntMap.get(value); 58 | } 59 | 60 | public int asInt() { 61 | return intValue; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/loaders/ScanApCommandLoader.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.loaders; 2 | 3 | import android.content.Context; 4 | 5 | import java.io.IOException; 6 | import java.util.Set; 7 | 8 | import javax.annotation.ParametersAreNonnullByDefault; 9 | 10 | import io.particle.android.sdk.devicesetup.commands.CommandClient; 11 | import io.particle.android.sdk.devicesetup.commands.ScanApCommand; 12 | import io.particle.android.sdk.devicesetup.model.ScanAPCommandResult; 13 | import io.particle.android.sdk.utils.BetterAsyncTaskLoader; 14 | import io.particle.android.sdk.utils.Funcy; 15 | import io.particle.android.sdk.utils.TLog; 16 | 17 | import static io.particle.android.sdk.utils.Py.set; 18 | 19 | 20 | /** 21 | * Returns the results of the "scan-ap" command from the device. 22 | *

23 | * Will return null if an exception is thrown when trying to send the command 24 | * and receive a reply from the device. 25 | */ 26 | @ParametersAreNonnullByDefault 27 | public class ScanApCommandLoader extends BetterAsyncTaskLoader> { 28 | 29 | private static final TLog log = TLog.get(ScanApCommandLoader.class); 30 | 31 | private final CommandClient commandClient; 32 | private final Set accumulatedResults = set(); 33 | 34 | public ScanApCommandLoader(Context context, CommandClient client) { 35 | super(context); 36 | commandClient = client; 37 | } 38 | 39 | @Override 40 | public boolean hasContent() { 41 | return !accumulatedResults.isEmpty(); 42 | } 43 | 44 | @Override 45 | public Set getLoadedContent() { 46 | return accumulatedResults; 47 | } 48 | 49 | @Override 50 | protected void onStartLoading() { 51 | super.onStartLoading(); 52 | forceLoad(); 53 | } 54 | 55 | @Override 56 | protected void onStopLoading() { 57 | cancelLoad(); 58 | } 59 | 60 | @Override 61 | public Set loadInBackground() { 62 | try { 63 | ScanApCommand.Response response = commandClient.sendCommand(new ScanApCommand(), 64 | ScanApCommand.Response.class); 65 | accumulatedResults.addAll(Funcy.transformList(response.getScans(), ScanAPCommandResult::new)); 66 | log.d("Latest accumulated scan results: " + accumulatedResults); 67 | return set(accumulatedResults); 68 | 69 | } catch (IOException e) { 70 | log.e("Error running scan-ap command: ", e); 71 | return null; 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/loaders/WifiScanResultLoader.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.loaders; 2 | 3 | import android.content.Context; 4 | import android.content.IntentFilter; 5 | import android.net.wifi.ScanResult; 6 | import android.net.wifi.WifiManager; 7 | 8 | import java.util.List; 9 | import java.util.Locale; 10 | import java.util.Set; 11 | 12 | import io.particle.android.sdk.devicesetup.R; 13 | import io.particle.android.sdk.devicesetup.SimpleReceiver; 14 | import io.particle.android.sdk.devicesetup.model.ScanResultNetwork; 15 | import io.particle.android.sdk.utils.BetterAsyncTaskLoader; 16 | import io.particle.android.sdk.utils.Funcy; 17 | import io.particle.android.sdk.utils.Funcy.Predicate; 18 | import io.particle.android.sdk.utils.TLog; 19 | import io.particle.android.sdk.utils.WifiFacade; 20 | 21 | import static io.particle.android.sdk.utils.Py.set; 22 | import static io.particle.android.sdk.utils.Py.truthy; 23 | 24 | 25 | public class WifiScanResultLoader extends BetterAsyncTaskLoader> { 26 | 27 | private static final TLog log = TLog.get(WifiScanResultLoader.class); 28 | 29 | 30 | private final WifiFacade wifiFacade; 31 | private final SimpleReceiver receiver; 32 | private volatile Set mostRecentResult; 33 | private volatile int loadCount = 0; 34 | 35 | public WifiScanResultLoader(Context context, WifiFacade wifiFacade) { 36 | super(context); 37 | Context appCtx = context.getApplicationContext(); 38 | receiver = SimpleReceiver.newReceiver( 39 | appCtx, new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION), 40 | (ctx, intent) -> { 41 | log.d("Received WifiManager.SCAN_RESULTS_AVAILABLE_ACTION broadcast"); 42 | forceLoad(); 43 | }); 44 | this.wifiFacade = wifiFacade; 45 | } 46 | 47 | @Override 48 | public boolean hasContent() { 49 | return mostRecentResult != null; 50 | } 51 | 52 | @Override 53 | public Set getLoadedContent() { 54 | return mostRecentResult; 55 | } 56 | 57 | @Override 58 | protected void onStartLoading() { 59 | super.onStartLoading(); 60 | receiver.register(); 61 | forceLoad(); 62 | } 63 | 64 | @Override 65 | protected void onStopLoading() { 66 | receiver.unregister(); 67 | cancelLoad(); 68 | } 69 | 70 | @Override 71 | public Set loadInBackground() { 72 | List scanResults = wifiFacade.getScanResults(); 73 | log.d("Latest (unfiltered) scan results: " + scanResults); 74 | 75 | if (loadCount % 3 == 0) { 76 | wifiFacade.startScan(); 77 | } 78 | 79 | loadCount++; 80 | // filter the list, transform the matched results, then wrap those in a Set 81 | mostRecentResult = set(Funcy.transformList( 82 | scanResults, ssidStartsWithProductName, ScanResultNetwork::new)); 83 | 84 | if (mostRecentResult.isEmpty()) { 85 | log.i("No SSID scan results returned after filtering by product name. " + 86 | "Do you need to change the 'network_name_prefix' resource?"); 87 | } 88 | 89 | return mostRecentResult; 90 | } 91 | 92 | 93 | private final Predicate ssidStartsWithProductName = input -> { 94 | if (!truthy(input.SSID)) { 95 | return false; 96 | } 97 | String softApPrefix = (getContext().getString(R.string.network_name_prefix) + "-").toLowerCase(Locale.ROOT); 98 | return input.SSID.toLowerCase(Locale.ROOT).startsWith(softApPrefix); 99 | }; 100 | 101 | } 102 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/model/ScanAPCommandResult.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.model; 2 | 3 | import io.particle.android.sdk.devicesetup.commands.ScanApCommand; 4 | import io.particle.android.sdk.devicesetup.commands.data.WifiSecurity; 5 | import io.particle.android.sdk.utils.SSID; 6 | 7 | 8 | // FIXME: this naming is not ideal. 9 | public class ScanAPCommandResult implements WifiNetwork { 10 | 11 | public final ScanApCommand.Scan scan; 12 | public final SSID ssid; 13 | 14 | public ScanAPCommandResult(ScanApCommand.Scan scan) { 15 | this.scan = scan; 16 | ssid = SSID.from(scan.ssid); 17 | } 18 | 19 | @Override 20 | public SSID getSsid() { 21 | return ssid; 22 | } 23 | 24 | @Override 25 | public boolean isSecured() { 26 | return scan.wifiSecurityType != WifiSecurity.OPEN.asInt(); 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "ScanAPCommandResult{" + 32 | "scan=" + scan + 33 | '}'; 34 | } 35 | 36 | @Override 37 | public boolean equals(Object o) { 38 | if (this == o) return true; 39 | if (o == null || getClass() != o.getClass()) return false; 40 | 41 | ScanAPCommandResult that = (ScanAPCommandResult) o; 42 | 43 | return getSsid() != null ? getSsid().equals(that.getSsid()) : that.getSsid() == null; 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | return getSsid() != null ? getSsid().hashCode() : 0; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/model/ScanResultNetwork.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.model; 2 | 3 | import android.net.wifi.ScanResult; 4 | 5 | import java.util.Set; 6 | 7 | import io.particle.android.sdk.utils.SSID; 8 | 9 | import static io.particle.android.sdk.utils.Py.set; 10 | 11 | 12 | // FIXME: this naming... is not ideal. 13 | public class ScanResultNetwork implements WifiNetwork { 14 | 15 | private static final Set wifiSecurityTypes = set("WEP", "PSK", "EAP"); 16 | 17 | 18 | private final ScanResult scanResult; 19 | private final SSID ssid; 20 | 21 | public ScanResultNetwork(ScanResult scanResult) { 22 | this.scanResult = scanResult; 23 | ssid = SSID.from(scanResult.SSID); 24 | } 25 | 26 | @Override 27 | public SSID getSsid() { 28 | return ssid; 29 | } 30 | 31 | @Override 32 | public boolean isSecured() { 33 | // 34 | // this seems like a bad joke of an "API", but this is basically what 35 | // Android does internally (see: http://goo.gl/GCRIKi) 36 | for (String securityType : wifiSecurityTypes) { 37 | if (scanResult.capabilities.contains(securityType)) { 38 | return true; 39 | } 40 | } 41 | return false; 42 | } 43 | 44 | @Override 45 | public boolean equals(Object o) { 46 | if (this == o) return true; 47 | if (o == null || getClass() != o.getClass()) return false; 48 | 49 | ScanResultNetwork that = (ScanResultNetwork) o; 50 | 51 | return getSsid() != null ? getSsid().equals(that.getSsid()) : that.getSsid() == null; 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return getSsid() != null ? getSsid().hashCode() : 0; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/model/WifiNetwork.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.model; 2 | 3 | import io.particle.android.sdk.utils.SSID; 4 | 5 | 6 | public interface WifiNetwork { 7 | 8 | SSID getSsid(); 9 | 10 | boolean isSecured(); 11 | } 12 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/setupsteps/CheckIfDeviceClaimedStep.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.setupsteps; 2 | 3 | 4 | import java.util.List; 5 | 6 | import io.particle.android.sdk.cloud.ParticleCloud; 7 | import io.particle.android.sdk.cloud.exceptions.ParticleCloudException; 8 | import io.particle.android.sdk.cloud.ParticleDevice; 9 | 10 | 11 | public class CheckIfDeviceClaimedStep extends SetupStep { 12 | 13 | private final ParticleCloud sparkCloud; 14 | private final String deviceBeingConfiguredId; 15 | private boolean needToClaimDevice = true; 16 | 17 | CheckIfDeviceClaimedStep(StepConfig stepConfig, ParticleCloud sparkCloud, 18 | String deviceBeingConfiguredId) { 19 | super(stepConfig); 20 | this.sparkCloud = sparkCloud; 21 | this.deviceBeingConfiguredId = deviceBeingConfiguredId; 22 | } 23 | 24 | @Override 25 | protected void onRunStep() throws SetupStepException { 26 | List devices; 27 | try { 28 | devices = sparkCloud.getDevices(); 29 | } catch (ParticleCloudException e) { 30 | throw new SetupStepException(e); 31 | } 32 | 33 | log.d("Got devices back from the cloud..."); 34 | for (ParticleDevice device : devices) { 35 | if (deviceBeingConfiguredId.equalsIgnoreCase(device.getID())) { 36 | log.d("Success, device " + device.getID() + " claimed!"); 37 | needToClaimDevice = false; 38 | return; 39 | } 40 | } 41 | 42 | // device not found in the loop 43 | throw new SetupStepException("Device " + deviceBeingConfiguredId + " still not claimed."); 44 | } 45 | 46 | @Override 47 | public boolean isStepFulfilled() { 48 | return !needToClaimDevice; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/setupsteps/ConfigureAPStep.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.setupsteps; 2 | 3 | 4 | import java.io.IOException; 5 | import java.security.PublicKey; 6 | 7 | import io.particle.android.sdk.devicesetup.commands.CommandClient; 8 | import io.particle.android.sdk.devicesetup.commands.ConfigureApCommand; 9 | import io.particle.android.sdk.devicesetup.commands.ScanApCommand; 10 | import io.particle.android.sdk.devicesetup.commands.data.WifiSecurity; 11 | import io.particle.android.sdk.utils.Crypto; 12 | 13 | 14 | public class ConfigureAPStep extends SetupStep { 15 | 16 | private final CommandClient commandClient; 17 | private final SetupStepApReconnector workerThreadApConnector; 18 | private final ScanApCommand.Scan networkToConnectTo; 19 | private final String networkSecretPlaintext; 20 | private final PublicKey publicKey; 21 | 22 | private volatile boolean commandSent = false; 23 | 24 | ConfigureAPStep(StepConfig stepConfig, CommandClient commandClient, 25 | SetupStepApReconnector workerThreadApConnector, 26 | ScanApCommand.Scan networkToConnectTo, String networkSecretPlaintext, 27 | PublicKey publicKey) { 28 | super(stepConfig); 29 | this.commandClient = commandClient; 30 | this.workerThreadApConnector = workerThreadApConnector; 31 | this.networkToConnectTo = networkToConnectTo; 32 | this.networkSecretPlaintext = networkSecretPlaintext; 33 | this.publicKey = publicKey; 34 | } 35 | 36 | protected void onRunStep() throws SetupStepException { 37 | WifiSecurity wifiSecurity = WifiSecurity.fromInteger(networkToConnectTo.wifiSecurityType); 38 | ConfigureApCommand.Builder builder = ConfigureApCommand.newBuilder() 39 | .setSsid(networkToConnectTo.ssid) 40 | .setSecurityType(wifiSecurity) 41 | .setChannel(networkToConnectTo.channel) 42 | .setIdx(0); 43 | if (wifiSecurity != WifiSecurity.OPEN) { 44 | try { 45 | builder.setEncryptedPasswordHex( 46 | Crypto.encryptAndEncodeToHex(networkSecretPlaintext, publicKey)); 47 | } catch (Crypto.CryptoException e) { 48 | // FIXME: try to throw a more specific exception here. 49 | // Don't throw SetupException here -- if this is failing, it's not 50 | // going to get any better by the running this SetupStep again, and 51 | // it can really only fail if the surrounding app code is doing something 52 | // wrong. To wit: you *want* the app to crash here (or at least 53 | // throw out a dialog saying "horrible thing happened! horrible error 54 | // code: ..." and then return to a safe "default" activity. 55 | throw new RuntimeException("Error encrypting network credentials", e); 56 | } 57 | } 58 | ConfigureApCommand command = builder.build(); 59 | 60 | try { 61 | log.d("Ensuring connection to AP"); 62 | workerThreadApConnector.ensureConnectionToSoftAp(); 63 | 64 | ConfigureApCommand.Response response = commandClient.sendCommand( 65 | command, ConfigureApCommand.Response.class); 66 | if (!response.isOk()) { 67 | throw new SetupStepException("Error response code " + response.responseCode + 68 | " while configuring device"); 69 | } 70 | log.d("Configure AP command returned: " + response.responseCode); 71 | commandSent = true; 72 | 73 | } catch (IOException e) { 74 | throw new SetupStepException(e); 75 | } 76 | } 77 | 78 | public boolean isStepFulfilled() { 79 | return commandSent; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/setupsteps/ConnectDeviceToNetworkStep.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.setupsteps; 2 | 3 | import java.io.IOException; 4 | 5 | import io.particle.android.sdk.devicesetup.commands.CommandClient; 6 | import io.particle.android.sdk.devicesetup.commands.ConnectAPCommand; 7 | 8 | 9 | public class ConnectDeviceToNetworkStep extends SetupStep { 10 | 11 | private final CommandClient commandClient; 12 | private final SetupStepApReconnector workerThreadApConnector; 13 | 14 | private volatile boolean commandSent = false; 15 | 16 | ConnectDeviceToNetworkStep(StepConfig stepConfig, CommandClient commandClient, 17 | SetupStepApReconnector workerThreadApConnector) { 18 | super(stepConfig); 19 | this.commandClient = commandClient; 20 | this.workerThreadApConnector = workerThreadApConnector; 21 | } 22 | 23 | @Override 24 | protected void onRunStep() throws SetupStepException { 25 | try { 26 | log.d("Ensuring connection to AP"); 27 | workerThreadApConnector.ensureConnectionToSoftAp(); 28 | 29 | log.d("Sending connect-ap command"); 30 | ConnectAPCommand.Response response = commandClient.sendCommand( 31 | // FIXME: is hard-coding zero here correct? If so, document why 32 | new ConnectAPCommand(0), ConnectAPCommand.Response.class); 33 | if (!response.isOK()) { 34 | throw new SetupStepException("ConnectAPCommand returned non-zero response code: " + 35 | response.responseCode); 36 | } 37 | 38 | commandSent = true; 39 | 40 | } catch (IOException e) { 41 | throw new SetupStepException(e); 42 | } 43 | } 44 | 45 | @Override 46 | public boolean isStepFulfilled() { 47 | return commandSent; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/setupsteps/EnsureSoftApNotVisible.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.setupsteps; 2 | 3 | import java.util.List; 4 | 5 | import io.particle.android.sdk.devicesetup.SetupProcessException; 6 | import io.particle.android.sdk.utils.EZ; 7 | import io.particle.android.sdk.utils.Funcy; 8 | import io.particle.android.sdk.utils.Funcy.Predicate; 9 | import io.particle.android.sdk.utils.Preconditions; 10 | import io.particle.android.sdk.utils.SSID; 11 | import io.particle.android.sdk.utils.WifiFacade; 12 | 13 | import static io.particle.android.sdk.utils.Py.list; 14 | 15 | 16 | public class EnsureSoftApNotVisible extends SetupStep { 17 | 18 | private final WifiFacade wifiFacade; 19 | private final SSID softApName; 20 | private final Predicate matchesSoftApSSID; 21 | 22 | private boolean wasFulfilledOnce = false; 23 | 24 | EnsureSoftApNotVisible(StepConfig stepConfig, SSID softApSSID, WifiFacade wifiFacade) { 25 | super(stepConfig); 26 | Preconditions.checkNotNull(softApSSID, "softApSSID cannot be null."); 27 | this.wifiFacade = wifiFacade; 28 | this.softApName = softApSSID; 29 | this.matchesSoftApSSID = softApName::equals; 30 | } 31 | 32 | @Override 33 | public boolean isStepFulfilled() { 34 | return wasFulfilledOnce && !isSoftApVisible(); 35 | } 36 | 37 | @Override 38 | protected void onRunStep() throws SetupStepException, SetupProcessException { 39 | if (!wasFulfilledOnce) { 40 | onStepNeverYetFulfilled(); 41 | 42 | } else { 43 | onStepPreviouslyFulfilled(); 44 | } 45 | } 46 | 47 | // Before the soft AP disappears for the FIRST time, be lenient in allowing for retries if 48 | // it fails to disappear, since we don't have a good idea of why it's failing, so just throw 49 | // SetupStepException. (But see onStepPreviouslyFulfilled()) 50 | private void onStepNeverYetFulfilled() throws SetupStepException { 51 | for (int i = 0; i < 16; i++) { 52 | if (!isSoftApVisible()) { 53 | // it's gone! 54 | wasFulfilledOnce = true; 55 | return; 56 | } 57 | 58 | if (i % 6 == 0) { 59 | wifiFacade.startScan(); 60 | } 61 | 62 | EZ.threadSleep(250); 63 | } 64 | throw new SetupStepException("Wi-Fi credentials appear to be incorrect or an error has occurred"); 65 | } 66 | 67 | // If this step was previously fulfilled, i.e.: the soft AP was gone, and now it's visible again, 68 | // this almost certainly means the device was given invalid Wi-Fi credentials, so we should 69 | // fail the whole setup process immediately. 70 | private void onStepPreviouslyFulfilled() throws SetupProcessException { 71 | if (isSoftApVisible()) { 72 | throw new SetupProcessException( 73 | "Soft AP visible again; Wi-Fi credentials may be incorrect", this); 74 | } 75 | } 76 | 77 | private boolean isSoftApVisible() { 78 | List scansPlusConnectedSsid = list(); 79 | 80 | SSID currentlyConnected = wifiFacade.getCurrentlyConnectedSSID(); 81 | if (currentlyConnected != null) { 82 | scansPlusConnectedSsid.add(currentlyConnected); 83 | } 84 | 85 | scansPlusConnectedSsid.addAll( 86 | Funcy.transformList(wifiFacade.getScanResults(), 87 | Funcy.notNull(), 88 | SSID::from) 89 | ); 90 | 91 | log.d("scansPlusConnectedSsid: " + scansPlusConnectedSsid); 92 | log.d("Soft AP we're looking for: " + softApName); 93 | 94 | SSID firstMatch = Funcy.findFirstMatch(scansPlusConnectedSsid, matchesSoftApSSID); 95 | log.d("Matching SSID result: '" + firstMatch + "'"); 96 | return firstMatch != null; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/setupsteps/SetupStep.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.setupsteps; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.support.annotation.AnyThread; 5 | import android.support.annotation.WorkerThread; 6 | 7 | import io.particle.android.sdk.devicesetup.SetupProcessException; 8 | import io.particle.android.sdk.utils.TLog; 9 | 10 | 11 | @WorkerThread 12 | public abstract class SetupStep { 13 | 14 | protected final TLog log; 15 | private final StepConfig stepConfig; 16 | private volatile int numAttempts; 17 | 18 | 19 | public SetupStep(StepConfig stepConfig) { 20 | log = TLog.get(this.getClass()); 21 | this.stepConfig = stepConfig; 22 | } 23 | 24 | protected abstract void onRunStep() throws SetupStepException, SetupProcessException; 25 | 26 | public abstract boolean isStepFulfilled(); 27 | 28 | final void runStep() throws SetupStepException, SetupProcessException { 29 | if (isStepFulfilled()) { 30 | getLog().i("Step " + getStepName() + " already fulfilled, skipping..."); 31 | return; 32 | } 33 | if (numAttempts > stepConfig.maxAttempts) { 34 | @SuppressLint("DefaultLocale") 35 | String msg = String.format("Exceeded limit of %d retries for step %s", 36 | stepConfig.maxAttempts, getStepName()); 37 | throw new SetupProcessException(msg, this); 38 | } else { 39 | getLog().i("Running step " + getStepName()); 40 | numAttempts++; 41 | onRunStep(); 42 | } 43 | } 44 | 45 | public TLog getLog() { 46 | return log; 47 | } 48 | 49 | @AnyThread 50 | public StepConfig getStepConfig() { 51 | return this.stepConfig; 52 | } 53 | 54 | protected void resetAttemptsCount() { 55 | numAttempts = 0; 56 | } 57 | 58 | private String getStepName() { 59 | return this.getClass().getSimpleName(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/setupsteps/SetupStepApReconnector.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.setupsteps; 2 | 3 | 4 | import android.net.wifi.WifiConfiguration; 5 | import android.os.Handler; 6 | 7 | import java.io.IOException; 8 | import java.util.concurrent.CountDownLatch; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.atomic.AtomicBoolean; 11 | 12 | import io.particle.android.sdk.devicesetup.ApConnector; 13 | import io.particle.android.sdk.devicesetup.ApConnector.Client; 14 | import io.particle.android.sdk.utils.SSID; 15 | import io.particle.android.sdk.utils.WifiFacade; 16 | 17 | 18 | public class SetupStepApReconnector { 19 | 20 | private final WifiFacade wifiFacade; 21 | private final ApConnector apConnector; 22 | private final Handler mainThreadHandler; 23 | private final SSID softApSSID; 24 | private final WifiConfiguration config; 25 | 26 | public SetupStepApReconnector(WifiFacade wifiFacade, ApConnector apConnector, 27 | Handler mainThreadHandler, SSID softApSSID) { 28 | this.wifiFacade = wifiFacade; 29 | this.apConnector = apConnector; 30 | this.mainThreadHandler = mainThreadHandler; 31 | this.softApSSID = softApSSID; 32 | this.config = ApConnector.buildUnsecuredConfig(softApSSID); 33 | } 34 | 35 | void ensureConnectionToSoftAp() throws IOException { 36 | if (isConnectedToSoftAp()) { 37 | return; 38 | } 39 | 40 | final CountDownLatch latch = new CountDownLatch(1); 41 | final AtomicBoolean gotConnected = new AtomicBoolean(false); 42 | 43 | mainThread(() -> 44 | apConnector.connectToAP(config, new Client() { 45 | @Override 46 | public void onApConnectionSuccessful(WifiConfiguration config) { 47 | gotConnected.set(true); 48 | latch.countDown(); 49 | } 50 | 51 | @Override 52 | public void onApConnectionFailed(WifiConfiguration config) { 53 | latch.countDown(); 54 | } 55 | })); 56 | 57 | // 50ms is an arbitrary number; just give the ApConnector time to do its work and allow for 58 | // a slight delay for overhead, etc. 59 | awaitCountdown(latch, ApConnector.CONNECT_TO_DEVICE_TIMEOUT_MILLIS + 50); 60 | 61 | // throw if it didn't work, otherwise assume success 62 | if (!gotConnected.get()) { 63 | throw new IOException("ApConnector did not finish in time; could not reconnect to soft AP"); 64 | } 65 | } 66 | 67 | private boolean isConnectedToSoftAp() { 68 | return softApSSID.equals(wifiFacade.getCurrentlyConnectedSSID()); 69 | } 70 | 71 | private CountDownLatch mainThread(final Runnable runnable) { 72 | final CountDownLatch latch = new CountDownLatch(1); 73 | mainThreadHandler.post(() -> { 74 | runnable.run(); 75 | latch.countDown(); 76 | }); 77 | return latch; 78 | } 79 | 80 | private boolean awaitCountdown(CountDownLatch latch, long awaitMs) { 81 | try { 82 | return latch.await(awaitMs, TimeUnit.MILLISECONDS); 83 | } catch (InterruptedException e) { 84 | e.printStackTrace(); 85 | return false; 86 | } 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/setupsteps/SetupStepException.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.setupsteps; 2 | 3 | 4 | public class SetupStepException extends Exception { 5 | 6 | public SetupStepException(String msg, Throwable throwable) { 7 | super(msg, throwable); 8 | } 9 | 10 | public SetupStepException(String msg) { 11 | super(msg); 12 | } 13 | 14 | public SetupStepException(Throwable throwable) { 15 | super(throwable); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/setupsteps/SetupStepsRunnerTask.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.setupsteps; 2 | 3 | import android.os.AsyncTask; 4 | 5 | import java.util.List; 6 | 7 | import io.particle.android.sdk.devicesetup.SetupProcessException; 8 | import io.particle.android.sdk.utils.EZ; 9 | import io.particle.android.sdk.utils.TLog; 10 | 11 | 12 | public abstract class SetupStepsRunnerTask extends 13 | AsyncTask { 14 | 15 | private final TLog log = TLog.get(getClass()); 16 | 17 | private final List steps; 18 | private final int maxOverallAttempts; 19 | 20 | public SetupStepsRunnerTask(List steps, int maxOverallAttempts) { 21 | this.steps = steps; 22 | this.maxOverallAttempts = maxOverallAttempts; 23 | } 24 | 25 | @Override 26 | public SetupProcessException doInBackground(Void... voids) { 27 | int attempts = 0; 28 | // We should never hit this limit, but just in case, we want to 29 | // avoid an infinite loop 30 | while (attempts < maxOverallAttempts) { 31 | attempts++; 32 | try { 33 | runSteps(); 34 | // we got all the way through the steps, break out of the loop! 35 | return null; 36 | 37 | } catch (SetupStepException e) { 38 | log.w("Setup step failed: " + e.getMessage()); 39 | 40 | } catch (SetupProcessException e) { 41 | return e; 42 | } 43 | } 44 | 45 | return new SetupProcessException("(Unknown setup error)", null); 46 | } 47 | 48 | private void runSteps() throws SetupStepException, SetupProcessException { 49 | for (SetupStep step : steps) { 50 | 51 | throwIfCancelled(); 52 | 53 | publishProgress(new StepProgress( 54 | step.getStepConfig().getStepId(), 55 | StepProgress.STARTING)); 56 | 57 | try { 58 | EZ.threadSleep(1000); 59 | throwIfCancelled(); 60 | 61 | step.runStep(); 62 | 63 | } catch (SetupStepException e) { 64 | // give it a moment before trying again. 65 | EZ.threadSleep(2000); 66 | throw e; 67 | } 68 | 69 | publishProgress(new StepProgress( 70 | step.getStepConfig().getStepId(), 71 | StepProgress.SUCCEEDED)); 72 | } 73 | } 74 | 75 | private void throwIfCancelled() { 76 | // FIXME: while it's good that we handle being cancelled, this doesn't seem like 77 | // an ideal way to do it... 78 | if (isCancelled()) { 79 | throw new RuntimeException("Task was cancelled"); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/setupsteps/StepConfig.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.setupsteps; 2 | 3 | import io.particle.android.sdk.utils.Preconditions; 4 | 5 | 6 | public class StepConfig { 7 | 8 | final int maxAttempts; 9 | private final int stepId; 10 | public final int resultCode; 11 | 12 | private StepConfig(int maxAttempts, int stepId, int resultCode) { 13 | this.maxAttempts = maxAttempts; 14 | this.stepId = stepId; 15 | this.resultCode = resultCode; 16 | } 17 | 18 | public int getStepId() { 19 | return stepId; 20 | } 21 | 22 | static Builder newBuilder() { 23 | return new Builder(); 24 | } 25 | 26 | public static class Builder { 27 | 28 | private int maxAttempts; 29 | private int stepId; 30 | private int resultCode; 31 | 32 | Builder setMaxAttempts(int maxAttempts) { 33 | this.maxAttempts = maxAttempts; 34 | return this; 35 | } 36 | 37 | Builder setStepId(int stepId) { 38 | this.stepId = stepId; 39 | return this; 40 | } 41 | 42 | Builder setResultCode(int resultCode) { 43 | this.resultCode = resultCode; 44 | return this; 45 | } 46 | 47 | public StepConfig build() { 48 | Preconditions.checkArgument(maxAttempts > 0, "Max attempts must be > 0"); 49 | Preconditions.checkArgument(stepId != 0, "Step ID cannot be unset or set to 0"); 50 | Preconditions.checkArgument(resultCode != 0, "Result code cannot be unset or set to 0"); 51 | return new StepConfig(maxAttempts, stepId, resultCode); 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/setupsteps/StepProgress.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.setupsteps; 2 | 3 | 4 | public class StepProgress { 5 | 6 | public static final int STARTING = 1; 7 | static final int SUCCEEDED = 2; 8 | 9 | public final int stepId; 10 | public final int status; 11 | 12 | StepProgress(int stepId, int status) { 13 | this.status = status; 14 | this.stepId = stepId; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/setupsteps/WaitForCloudConnectivityStep.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.setupsteps; 2 | 3 | 4 | import android.content.Context; 5 | import android.net.ConnectivityManager; 6 | import android.net.NetworkInfo; 7 | 8 | import io.particle.android.sdk.utils.EZ; 9 | 10 | 11 | public class WaitForCloudConnectivityStep extends SetupStep { 12 | 13 | private static final int MAX_RETRIES_REACHABILITY = 1; 14 | 15 | private final Context ctx; 16 | 17 | WaitForCloudConnectivityStep(StepConfig stepConfig, Context ctx) { 18 | super(stepConfig); 19 | this.ctx = ctx; 20 | } 21 | 22 | @Override 23 | protected void onRunStep() throws SetupStepException { 24 | // Wait for just a couple seconds for a WifiFacade connection if possible, in case we 25 | // flip from the soft AP, to mobile data, and then to WifiFacade in rapid succession. 26 | EZ.threadSleep(2000); 27 | int reachabilityRetries = 0; 28 | boolean isAPIHostReachable = checkIsApiHostAvailable(); 29 | while (!isAPIHostReachable && reachabilityRetries <= MAX_RETRIES_REACHABILITY) { 30 | EZ.threadSleep(2000); 31 | isAPIHostReachable = checkIsApiHostAvailable(); 32 | log.d("Checked for reachability " + reachabilityRetries + " times"); 33 | reachabilityRetries++; 34 | } 35 | if (!isAPIHostReachable) { 36 | throw new SetupStepException("Unable to reach API host"); 37 | } 38 | } 39 | 40 | @Override 41 | public boolean isStepFulfilled() { 42 | return checkIsApiHostAvailable(); 43 | } 44 | 45 | private boolean checkIsApiHostAvailable() { 46 | ConnectivityManager cm = (ConnectivityManager) ctx.getSystemService( 47 | Context.CONNECTIVITY_SERVICE); 48 | NetworkInfo activeNetworkInfo = null; 49 | if (cm != null) { 50 | activeNetworkInfo = cm.getActiveNetworkInfo(); 51 | } 52 | if (activeNetworkInfo == null || !activeNetworkInfo.isConnected()) { 53 | return false; 54 | } 55 | 56 | // FIXME: why is this commented out? See what iOS does here now. 57 | // try { 58 | // cloud.getDevices(); 59 | // } catch (Exception e) { 60 | // log.e("error checking availability: ", e); 61 | // // FIXME: 62 | // return false; 63 | // // At this stage we're technically OK with other types of errors 64 | // if (set(Kind.NETWORK, Kind.UNEXPECTED).contains(e.getKind())) { 65 | // return false; 66 | // } 67 | // } 68 | 69 | return true; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/setupsteps/WaitForDisconnectionFromDeviceStep.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.setupsteps; 2 | 3 | import io.particle.android.sdk.devicesetup.SetupProcessException; 4 | import io.particle.android.sdk.devicesetup.ui.DeviceSetupState; 5 | import io.particle.android.sdk.utils.EZ; 6 | import io.particle.android.sdk.utils.Preconditions; 7 | import io.particle.android.sdk.utils.SSID; 8 | import io.particle.android.sdk.utils.WifiFacade; 9 | 10 | 11 | public class WaitForDisconnectionFromDeviceStep extends SetupStep { 12 | 13 | private final SSID softApName; 14 | private final WifiFacade wifiFacade; 15 | 16 | private boolean wasDisconnected = false; 17 | 18 | WaitForDisconnectionFromDeviceStep(StepConfig stepConfig, SSID softApSSID, WifiFacade wifiFacade) { 19 | super(stepConfig); 20 | Preconditions.checkNotNull(softApSSID, "softApSSID cannot be null."); 21 | this.softApName = softApSSID; 22 | this.wifiFacade = wifiFacade; 23 | } 24 | 25 | @Override 26 | public boolean isStepFulfilled() { 27 | return wasDisconnected; 28 | } 29 | 30 | @Override 31 | protected void onRunStep() throws SetupStepException, SetupProcessException { 32 | for (int i = 0; i <= 5; i++) { 33 | if (isConnectedToSoftAp()) { 34 | // wait and try again 35 | EZ.threadSleep(200); 36 | } else { 37 | EZ.threadSleep(1000); 38 | // success, no longer connected. 39 | wasDisconnected = true; 40 | if (EZ.isUsingOlderWifiStack()) { 41 | // for some reason Lollipop doesn't need this?? 42 | reenablePreviousWifi(); 43 | } 44 | return; 45 | } 46 | } 47 | 48 | // Still connected after the above completed: fail 49 | throw new SetupStepException("Not disconnected from soft AP"); 50 | } 51 | 52 | private void reenablePreviousWifi() { 53 | SSID prevSSID = DeviceSetupState.previouslyConnectedWifiNetwork; 54 | wifiFacade.reenableNetwork(prevSSID); 55 | wifiFacade.reassociate(); 56 | } 57 | 58 | private boolean isConnectedToSoftAp() { 59 | SSID currentlyConnectedSSID = wifiFacade.getCurrentlyConnectedSSID(); 60 | log.d("Currently connected SSID: " + currentlyConnectedSSID); 61 | return softApName.equals(currentlyConnectedSSID); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/ui/ConnectToApFragment.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.ui; 2 | 3 | import android.content.Context; 4 | import android.net.wifi.WifiConfiguration; 5 | import android.os.Bundle; 6 | import android.support.v4.app.FragmentActivity; 7 | 8 | import javax.inject.Inject; 9 | 10 | import io.particle.android.sdk.devicesetup.ApConnector; 11 | import io.particle.android.sdk.devicesetup.ApConnector.Client; 12 | import io.particle.android.sdk.devicesetup.ParticleDeviceSetupLibrary; 13 | import io.particle.android.sdk.di.ApModule; 14 | import io.particle.android.sdk.utils.EZ; 15 | import io.particle.android.sdk.utils.SSID; 16 | import io.particle.android.sdk.utils.WifiFacade; 17 | import io.particle.android.sdk.utils.WorkerFragment; 18 | import io.particle.android.sdk.utils.ui.Ui; 19 | 20 | 21 | // reconsider if this even needs to be a fragment at all 22 | public class ConnectToApFragment extends WorkerFragment { 23 | 24 | public static final String TAG = WorkerFragment.buildFragmentTag(ConnectToApFragment.class); 25 | 26 | 27 | public static ConnectToApFragment get(FragmentActivity activity) { 28 | return Ui.findFrag(activity, TAG); 29 | } 30 | 31 | public static ConnectToApFragment ensureAttached(FragmentActivity activity) { 32 | ConnectToApFragment frag = get(activity); 33 | if (frag == null) { 34 | frag = new ConnectToApFragment(); 35 | WorkerFragment.addFragment(activity, frag, TAG); 36 | } 37 | return frag; 38 | } 39 | 40 | @Inject protected ApConnector apConnector; 41 | @Inject protected WifiFacade wifiFacade; 42 | private Client apConnectorClient; 43 | 44 | @Override 45 | public void onAttach(Context context) { 46 | super.onAttach(context); 47 | apConnectorClient = EZ.getCallbacksOrThrow(this, Client.class); 48 | } 49 | 50 | @Override 51 | public void onCreate(Bundle savedInstanceState) { 52 | super.onCreate(savedInstanceState); 53 | ParticleDeviceSetupLibrary.getInstance().getApplicationComponent().activityComponentBuilder() 54 | .apModule(new ApModule()).build().inject(this); 55 | } 56 | 57 | @Override 58 | public void onStop() { 59 | super.onStop(); 60 | apConnector.stop(); 61 | } 62 | 63 | /** 64 | * Connect this Android device to the specified AP. 65 | * 66 | * @param config the WifiConfiguration defining which AP to connect to 67 | * @return the SSID that was connected prior to calling this method. Will be null if 68 | * there was no network connected, or if already connected to the target network. 69 | */ 70 | public SSID connectToAP(final WifiConfiguration config) { 71 | return apConnector.connectToAP(config, apConnectorClient); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/ui/DeviceSetupState.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.ui; 2 | 3 | 4 | import java.security.PublicKey; 5 | import java.util.Set; 6 | import java.util.concurrent.ConcurrentSkipListSet; 7 | 8 | import io.particle.android.sdk.utils.SSID; 9 | 10 | // FIXME: Statically defined, global, mutable state... refactor this thing into oblivion soon. 11 | public class DeviceSetupState { 12 | 13 | static final Set claimedDeviceIds = new ConcurrentSkipListSet<>(); 14 | public static volatile SSID previouslyConnectedWifiNetwork; 15 | static volatile String claimCode; 16 | static volatile PublicKey publicKey; 17 | static volatile String deviceToBeSetUpId; 18 | 19 | static void reset() { 20 | claimCode = null; 21 | claimedDeviceIds.clear(); 22 | publicKey = null; 23 | deviceToBeSetUpId = null; 24 | previouslyConnectedWifiNetwork = null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/ui/ManualNetworkEntryActivity.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.ui; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v4.app.LoaderManager; 7 | import android.support.v4.content.Loader; 8 | import android.view.View; 9 | import android.widget.CheckBox; 10 | 11 | import java.util.Set; 12 | 13 | import javax.inject.Inject; 14 | 15 | import butterknife.ButterKnife; 16 | import butterknife.OnCheckedChanged; 17 | import io.particle.android.sdk.devicesetup.ParticleDeviceSetupLibrary; 18 | import io.particle.android.sdk.devicesetup.R; 19 | import io.particle.android.sdk.devicesetup.R2; 20 | import io.particle.android.sdk.devicesetup.commands.CommandClientFactory; 21 | import io.particle.android.sdk.devicesetup.commands.ScanApCommand; 22 | import io.particle.android.sdk.devicesetup.commands.data.WifiSecurity; 23 | import io.particle.android.sdk.devicesetup.loaders.ScanApCommandLoader; 24 | import io.particle.android.sdk.devicesetup.model.ScanAPCommandResult; 25 | import io.particle.android.sdk.di.ApModule; 26 | import io.particle.android.sdk.ui.BaseActivity; 27 | import io.particle.android.sdk.utils.SEGAnalytics; 28 | import io.particle.android.sdk.utils.SSID; 29 | import io.particle.android.sdk.utils.WifiFacade; 30 | import io.particle.android.sdk.utils.ui.ParticleUi; 31 | import io.particle.android.sdk.utils.ui.Ui; 32 | 33 | 34 | public class ManualNetworkEntryActivity extends BaseActivity 35 | implements LoaderManager.LoaderCallbacks> { 36 | 37 | 38 | public static Intent buildIntent(Context ctx, SSID softApSSID) { 39 | return new Intent(ctx, ManualNetworkEntryActivity.class) 40 | .putExtra(EXTRA_SOFT_AP, softApSSID); 41 | } 42 | 43 | 44 | private static final String EXTRA_SOFT_AP = "EXTRA_SOFT_AP"; 45 | 46 | 47 | @Inject protected WifiFacade wifiFacade; 48 | @Inject protected CommandClientFactory commandClientFactory; 49 | private SSID softApSSID; 50 | protected Integer wifiSecurityType = WifiSecurity.WPA2_AES_PSK.asInt(); 51 | 52 | @OnCheckedChanged(R2.id.network_requires_password) 53 | protected void onSecureCheckedChange(boolean isChecked) { 54 | if (isChecked) { 55 | SEGAnalytics.track("Device Setup: Selected secured network"); 56 | wifiSecurityType = WifiSecurity.WPA2_AES_PSK.asInt(); 57 | } else { 58 | SEGAnalytics.track("Device Setup: Selected open network"); 59 | wifiSecurityType = WifiSecurity.OPEN.asInt(); 60 | } 61 | } 62 | 63 | @Override 64 | protected void onCreate(Bundle savedInstanceState) { 65 | super.onCreate(savedInstanceState); 66 | ParticleDeviceSetupLibrary.getInstance().getApplicationComponent().activityComponentBuilder() 67 | .apModule(new ApModule()).build().inject(this); 68 | SEGAnalytics.screen("Device Setup: Manual network entry screen"); 69 | softApSSID = getIntent().getParcelableExtra(EXTRA_SOFT_AP); 70 | 71 | setContentView(R.layout.activity_manual_network_entry); 72 | ButterKnife.bind(this); 73 | ParticleUi.enableBrandLogoInverseVisibilityAgainstSoftKeyboard(this); 74 | } 75 | 76 | public void onConnectClicked(View view) { 77 | String ssid = Ui.getText(this, R.id.network_name, true); 78 | ScanApCommand.Scan scan = new ScanApCommand.Scan(ssid, wifiSecurityType, 0); 79 | 80 | CheckBox requiresPassword = Ui.findView(this, R.id.network_requires_password); 81 | if (requiresPassword.isChecked()) { 82 | startActivity(PasswordEntryActivity.buildIntent(this, softApSSID, scan)); 83 | } else { 84 | startActivity(ConnectingActivity.buildIntent(this, softApSSID, scan)); 85 | } 86 | } 87 | 88 | public void onCancelClicked(View view) { 89 | finish(); 90 | } 91 | 92 | // FIXME: loader not currently used, see note in onLoadFinished() 93 | @Override 94 | public Loader> onCreateLoader(int id, Bundle args) { 95 | return new ScanApCommandLoader(this, commandClientFactory.newClientUsingDefaultsForDevices(wifiFacade, softApSSID)); 96 | } 97 | 98 | @Override 99 | public void onLoadFinished(Loader> loader, Set data) { 100 | // FIXME: perform process described here?: 101 | // https://github.com/spark/mobile-sdk-ios/issues/56 102 | } 103 | 104 | @Override 105 | public void onLoaderReset(Loader> loader) { 106 | // no-op 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/devicesetup/ui/RequiresWifiScansActivity.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.devicesetup.ui; 2 | 3 | import android.Manifest.permission; 4 | import android.annotation.SuppressLint; 5 | import android.util.Log; 6 | 7 | import io.particle.android.sdk.ui.BaseActivity; 8 | 9 | // FIXME: doing this via Activities feels sketchy. Find a better way when refactoring 10 | // to use fragments (or similar) 11 | @SuppressLint("Registered") 12 | public class RequiresWifiScansActivity extends BaseActivity { 13 | 14 | @Override 15 | protected void onStart() { 16 | super.onStart(); 17 | if (!PermissionsFragment.hasPermission(this, permission.ACCESS_COARSE_LOCATION)) { 18 | Log.d("RequiresWifiScans", "Location permission appears to have been revoked, finishing activity..."); 19 | finish(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/di/ActivityInjectorComponent.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.di; 2 | 3 | import android.support.annotation.RestrictTo; 4 | 5 | import dagger.Subcomponent; 6 | import io.particle.android.sdk.accountsetup.LoginActivity; 7 | import io.particle.android.sdk.accountsetup.PasswordResetActivity; 8 | import io.particle.android.sdk.accountsetup.TwoFactorActivity; 9 | import io.particle.android.sdk.devicesetup.ui.ConnectToApFragment; 10 | import io.particle.android.sdk.devicesetup.ui.ConnectingActivity; 11 | import io.particle.android.sdk.devicesetup.ui.ConnectingProcessWorkerTask; 12 | import io.particle.android.sdk.devicesetup.ui.DiscoverDeviceActivity; 13 | import io.particle.android.sdk.devicesetup.ui.GetReadyActivity; 14 | import io.particle.android.sdk.devicesetup.ui.ManualNetworkEntryActivity; 15 | import io.particle.android.sdk.devicesetup.ui.PasswordEntryActivity; 16 | import io.particle.android.sdk.devicesetup.ui.SelectNetworkActivity; 17 | import io.particle.android.sdk.devicesetup.ui.SuccessActivity; 18 | 19 | @PerActivity 20 | @Subcomponent(modules = {ApModule.class}) 21 | @RestrictTo({RestrictTo.Scope.LIBRARY}) 22 | public interface ActivityInjectorComponent { 23 | void inject(GetReadyActivity activity); 24 | 25 | void inject(LoginActivity loginActivity); 26 | 27 | void inject(PasswordResetActivity passwordResetActivity); 28 | 29 | void inject(SuccessActivity successActivity); 30 | 31 | void inject(DiscoverDeviceActivity discoverDeviceActivity); 32 | 33 | void inject(ConnectingActivity connectingActivity); 34 | 35 | void inject(PasswordEntryActivity passwordEntryActivity); 36 | 37 | void inject(ManualNetworkEntryActivity manualNetworkEntryActivity); 38 | 39 | void inject(SelectNetworkActivity selectNetworkActivity); 40 | 41 | void inject(ConnectToApFragment connectToApFragment); 42 | 43 | void inject(ConnectingProcessWorkerTask connectingProcessWorkerTask); 44 | 45 | void inject(TwoFactorActivity twoFactorActivity); 46 | 47 | @Subcomponent.Builder 48 | interface Builder { 49 | Builder apModule(ApModule module); 50 | 51 | ActivityInjectorComponent build(); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/di/ApModule.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.di; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.RestrictTo; 5 | 6 | import dagger.Module; 7 | import dagger.Provides; 8 | import io.particle.android.sdk.devicesetup.ApConnector; 9 | import io.particle.android.sdk.devicesetup.commands.CommandClientFactory; 10 | import io.particle.android.sdk.devicesetup.setupsteps.SetupStepsFactory; 11 | import io.particle.android.sdk.devicesetup.ui.DiscoverProcessWorker; 12 | import io.particle.android.sdk.utils.SoftAPConfigRemover; 13 | import io.particle.android.sdk.utils.WifiFacade; 14 | 15 | @Module 16 | @RestrictTo({RestrictTo.Scope.LIBRARY}) 17 | public class ApModule { 18 | 19 | @Provides 20 | protected SoftAPConfigRemover providesSoftApConfigRemover(Context context, WifiFacade wifiFacade) { 21 | return new SoftAPConfigRemover(context, wifiFacade); 22 | } 23 | 24 | @Provides 25 | protected WifiFacade providesWifiFacade(Context context) { 26 | return WifiFacade.get(context); 27 | } 28 | 29 | @Provides 30 | protected DiscoverProcessWorker providesDiscoverProcessWorker() { 31 | return new DiscoverProcessWorker(); 32 | } 33 | 34 | @Provides 35 | protected CommandClientFactory providesCommandClientFactory() { 36 | return new CommandClientFactory(); 37 | } 38 | 39 | @Provides 40 | protected SetupStepsFactory providesSetupStepsFactory() { 41 | return new SetupStepsFactory(); 42 | } 43 | 44 | @Provides 45 | protected ApConnector providesApConnector(Context context, SoftAPConfigRemover softAPConfigRemover, 46 | WifiFacade wifiFacade) { 47 | return new ApConnector(context, softAPConfigRemover, wifiFacade); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/di/ApplicationComponent.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.di; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.support.annotation.RestrictTo; 6 | 7 | import com.google.gson.Gson; 8 | 9 | import javax.inject.Singleton; 10 | 11 | import dagger.Component; 12 | import io.particle.android.sdk.cloud.ParticleCloud; 13 | 14 | @Singleton 15 | @Component(modules = {ApplicationModule.class, CloudModule.class}) 16 | @RestrictTo({RestrictTo.Scope.LIBRARY}) 17 | public interface ApplicationComponent { 18 | ActivityInjectorComponent.Builder activityComponentBuilder(); 19 | 20 | Application getApplication(); 21 | 22 | Context getContext(); 23 | 24 | ParticleCloud getParticleCloud(); 25 | 26 | Gson getGson(); 27 | } 28 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/di/ApplicationModule.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.di; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.support.annotation.RestrictTo; 6 | 7 | import javax.inject.Singleton; 8 | 9 | import dagger.Module; 10 | import dagger.Provides; 11 | 12 | @Module 13 | @RestrictTo({RestrictTo.Scope.LIBRARY}) 14 | public class ApplicationModule { 15 | private Application application; 16 | 17 | @RestrictTo(RestrictTo.Scope.LIBRARY) 18 | public ApplicationModule(Application application) { 19 | this.application = application; 20 | } 21 | 22 | @Singleton 23 | @Provides 24 | protected Application providesApplication() { 25 | return application; 26 | } 27 | 28 | @Singleton 29 | @Provides 30 | protected Context providesContext() { 31 | return application; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/di/CloudModule.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.di; 2 | 3 | import android.support.annotation.RestrictTo; 4 | 5 | import com.google.gson.Gson; 6 | 7 | import javax.inject.Singleton; 8 | 9 | import dagger.Module; 10 | import dagger.Provides; 11 | import io.particle.android.sdk.cloud.ParticleCloud; 12 | import io.particle.android.sdk.cloud.ParticleCloudSDK; 13 | 14 | @Module 15 | @RestrictTo({RestrictTo.Scope.LIBRARY}) 16 | public class CloudModule { 17 | 18 | @Singleton 19 | @Provides 20 | protected ParticleCloud providesParticleCloud() { 21 | return ParticleCloudSDK.getCloud(); 22 | } 23 | 24 | @Singleton 25 | @Provides 26 | protected Gson providesGson() { 27 | return new Gson(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/di/PerActivity.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.di; 2 | 3 | import android.support.annotation.RestrictTo; 4 | 5 | import java.lang.annotation.Documented; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | 9 | import javax.inject.Scope; 10 | 11 | @Scope 12 | @Documented 13 | @Retention(value = RetentionPolicy.RUNTIME) 14 | @RestrictTo({RestrictTo.Scope.LIBRARY}) 15 | public @interface PerActivity { 16 | } -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/ui/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.ui; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.os.Bundle; 6 | import android.support.annotation.RestrictTo; 7 | import android.support.v7.app.AppCompatActivity; 8 | 9 | import io.particle.android.sdk.cloud.SDKGlobals; 10 | import io.particle.android.sdk.devicesetup.R; 11 | import io.particle.android.sdk.utils.SEGAnalytics; 12 | import uk.co.chrisjenx.calligraphy.CalligraphyConfig; 13 | import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper; 14 | 15 | /** 16 | * This class exists solely to avoid requiring SDK users to have to define 17 | * anything in an Application subclass. By (ab)using this custom Activity, 18 | * we can at least be sure that the custom fonts in the device setup screens 19 | * work correctly without any special instructions. 20 | */ 21 | // this is a base activity, it shouldn't be registered. 22 | @SuppressLint("Registered") 23 | public class BaseActivity extends AppCompatActivity { 24 | @RestrictTo(RestrictTo.Scope.LIBRARY) 25 | public static boolean setupOnly = false; 26 | private static boolean customFontInitDone = false; 27 | 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | SEGAnalytics.initialize(getApplicationContext()); 32 | } 33 | 34 | @Override 35 | protected void attachBaseContext(Context newBase) { 36 | if (!customFontInitDone) { 37 | // FIXME: make actually customizable via resources 38 | // (see file extension string formatting nonsense) 39 | CalligraphyConfig.initDefault( 40 | new CalligraphyConfig.Builder() 41 | .setDefaultFontPath(newBase.getString(R.string.normal_text_font_name)) 42 | .setFontAttrId(R.attr.fontPath) 43 | .build()); 44 | customFontInitDone = true; 45 | } 46 | super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase)); 47 | SDKGlobals.init(this); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/ui/NextActivitySelector.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.ui; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | 6 | import io.particle.android.sdk.accountsetup.CreateAccountActivity; 7 | import io.particle.android.sdk.accountsetup.LoginActivity; 8 | import io.particle.android.sdk.cloud.ParticleCloud; 9 | import io.particle.android.sdk.devicesetup.ParticleDeviceSetupLibrary; 10 | import io.particle.android.sdk.devicesetup.SetupCompleteIntentBuilder; 11 | import io.particle.android.sdk.devicesetup.SetupResult; 12 | import io.particle.android.sdk.persistance.SensitiveDataStorage; 13 | import io.particle.android.sdk.utils.Preconditions; 14 | import io.particle.android.sdk.utils.TLog; 15 | 16 | import static io.particle.android.sdk.utils.Py.any; 17 | import static io.particle.android.sdk.utils.Py.truthy; 18 | 19 | /** 20 | * Selects the next Activity in the workflow, up to the "GetReady" screen or main UI. 21 | */ 22 | public class NextActivitySelector { 23 | 24 | private static final TLog log = TLog.get(NextActivitySelector.class); 25 | 26 | 27 | public static Intent getNextActivityIntent(Context ctx, 28 | ParticleCloud particleCloud, 29 | SensitiveDataStorage credStorage, 30 | SetupResult setupResult) { 31 | NextActivitySelector selector = new NextActivitySelector(particleCloud, credStorage, 32 | ParticleDeviceSetupLibrary.getInstance().getSetupCompleteIntentBuilder()); 33 | 34 | return selector.buildIntentForNextActivity(ctx, setupResult); 35 | } 36 | 37 | 38 | private final ParticleCloud cloud; 39 | private final SensitiveDataStorage credStorage; 40 | private final SetupCompleteIntentBuilder setupCompleteIntentBuilder; 41 | 42 | private NextActivitySelector(ParticleCloud cloud, 43 | SensitiveDataStorage credStorage, 44 | SetupCompleteIntentBuilder setupCompleteIntentBuilder) { 45 | Preconditions.checkNotNull(setupCompleteIntentBuilder, "SetupCompleteIntentBuilder instance is null"); 46 | 47 | this.cloud = cloud; 48 | this.credStorage = credStorage; 49 | this.setupCompleteIntentBuilder = setupCompleteIntentBuilder; 50 | } 51 | 52 | Intent buildIntentForNextActivity(Context ctx, SetupResult result) { 53 | if (!hasUserBeenLoggedInBefore() && !BaseActivity.setupOnly) { 54 | log.d("User has not been logged in before"); 55 | return new Intent(ctx, CreateAccountActivity.class); 56 | } 57 | 58 | if (!isOAuthTokenPresent() && !BaseActivity.setupOnly) { 59 | log.d("No auth token present"); 60 | return new Intent(ctx, LoginActivity.class); 61 | } 62 | 63 | log.d("Building setup complete activity..."); 64 | Intent successActivity = setupCompleteIntentBuilder.buildIntent(ctx, result); 65 | 66 | log.d("Returning setup complete activity"); 67 | return successActivity; 68 | } 69 | 70 | boolean hasUserBeenLoggedInBefore() { 71 | return any(credStorage.getUser(), credStorage.getToken()); 72 | } 73 | 74 | boolean isOAuthTokenPresent() { 75 | return truthy(cloud.getAccessToken()); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/utils/BetterAsyncTaskLoader.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.utils; 2 | 3 | import android.content.Context; 4 | import android.support.v4.content.AsyncTaskLoader; 5 | 6 | 7 | public abstract class BetterAsyncTaskLoader extends AsyncTaskLoader { 8 | 9 | 10 | public abstract boolean hasContent(); 11 | 12 | public abstract T getLoadedContent(); 13 | 14 | 15 | public BetterAsyncTaskLoader(Context context) { 16 | super(context); 17 | } 18 | 19 | @Override 20 | protected void onStartLoading() { 21 | // How is this not on AsyncTaskLoader already? 22 | if (hasContent()) { 23 | deliverResult(getLoadedContent()); 24 | } 25 | if (takeContentChanged() || !hasContent()) { 26 | forceLoad(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/utils/CoreNameGenerator.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.utils; 2 | 3 | import java.util.Random; 4 | import java.util.Set; 5 | 6 | 7 | public class CoreNameGenerator { 8 | 9 | private static final Random random = new Random(); 10 | 11 | private static final String[] TROCHEES = new String[]{"aardvark", "bacon", "badger", "banjo", 12 | "bobcat", "boomer", "captain", "chicken", "cowboy", "maker", "splendid", "useful", 13 | "dentist", "doctor", "dozen", "easter", "ferret", "gerbil", "hacker", "hamster", 14 | "sparkling", "hobbit", "hoosier", "hunter", "jester", "jetpack", "kitty", "laser", "lawyer", 15 | "mighty", "monkey", "morphing", "mutant", "narwhal", "ninja", "normal", "penguin", 16 | "pirate", "pizza", "plumber", "power", "puppy", "ranger", "raptor", "robot", "scraper", 17 | "spark", "station", "tasty", "trochee", "turkey", "turtle", "vampire", "wombat", 18 | "zombie"}; 19 | 20 | 21 | public static String generateUniqueName(Set existingNames) { 22 | String uniqueName = null; 23 | while (uniqueName == null) { 24 | String part1 = getRandomName(); 25 | String part2 = getRandomName(); 26 | String candidate = part1 + "_" + part2; 27 | if (!existingNames.contains(candidate) && !part1.equals(part2)) { 28 | uniqueName = candidate; 29 | } 30 | } 31 | return uniqueName; 32 | } 33 | 34 | private static String getRandomName() { 35 | int randomIndex = random.nextInt(TROCHEES.length); 36 | return TROCHEES[randomIndex]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/utils/ParticleDeviceSetupInternalStringUtils.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.utils; 2 | 3 | /** 4 | * Methods copied from Apache commons-lang, so that I don't have to incur the 4k+ method overhead 5 | * of commons-lang itself. 6 | */ 7 | public class ParticleDeviceSetupInternalStringUtils { 8 | 9 | private static final int INDEX_NOT_FOUND = -1; 10 | 11 | 12 | public static String remove(final String str, final String remove) { 13 | if (isEmpty(str) || isEmpty(remove)) { 14 | return str; 15 | } 16 | return replace(str, remove, "", -1); 17 | } 18 | 19 | 20 | public static String removeStart(final String str, final String remove) { 21 | if (isEmpty(str) || isEmpty(remove)) { 22 | return str; 23 | } 24 | if (str.startsWith(remove)){ 25 | return str.substring(remove.length()); 26 | } 27 | return str; 28 | } 29 | 30 | 31 | public static String removeEnd(final String str, final String remove) { 32 | if (isEmpty(str) || isEmpty(remove)) { 33 | return str; 34 | } 35 | if (str.endsWith(remove)) { 36 | return str.substring(0, str.length() - remove.length()); 37 | } 38 | return str; 39 | } 40 | 41 | 42 | private static String replace(final String text, final String searchString, 43 | final String replacement, int max) { 44 | if (isEmpty(text) || isEmpty(searchString) || replacement == null || max == 0) { 45 | return text; 46 | } 47 | int start = 0; 48 | int end = text.indexOf(searchString, start); 49 | if (end == INDEX_NOT_FOUND) { 50 | return text; 51 | } 52 | final int replLength = searchString.length(); 53 | int increase = replacement.length() - replLength; 54 | increase = increase < 0 ? 0 : increase; 55 | increase *= max < 0 ? 16 : max > 64 ? 64 : max; 56 | final StringBuilder buf = new StringBuilder(text.length() + increase); 57 | while (end != INDEX_NOT_FOUND) { 58 | buf.append(text.substring(start, end)).append(replacement); 59 | start = end + replLength; 60 | if (--max == 0) { 61 | break; 62 | } 63 | end = text.indexOf(searchString, start); 64 | } 65 | buf.append(text.substring(start)); 66 | return buf.toString(); 67 | } 68 | 69 | private static boolean isEmpty(final CharSequence cs) { 70 | return cs == null || cs.length() == 0; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/utils/SEGAnalytics.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.utils; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.support.annotation.RestrictTo; 6 | 7 | import com.segment.analytics.Analytics; 8 | import com.segment.analytics.Properties; 9 | 10 | 11 | /** 12 | * Created by Julius. 13 | */ 14 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 15 | public class SEGAnalytics { 16 | public static String analyticsKey = ""; 17 | public static boolean analyticsOptOut = true; 18 | @SuppressLint("StaticFieldLeak") private static Context context; 19 | 20 | public static void initialize(Context context) { 21 | SEGAnalytics.context = context.getApplicationContext(); 22 | try { 23 | Analytics.with(context); 24 | } catch (IllegalArgumentException exception) { 25 | if (!analyticsKey.isEmpty()) { 26 | Analytics analytics = new Analytics.Builder(context, analyticsKey).build(); 27 | analytics.optOut(analyticsOptOut); 28 | Analytics.setSingletonInstance(analytics); 29 | } 30 | } 31 | } 32 | 33 | public static void track(String track) { 34 | if (!analyticsKey.isEmpty()) { 35 | Analytics.with(context).track(track); 36 | } 37 | } 38 | 39 | public static void screen(String screen) { 40 | if (!analyticsKey.isEmpty()) { 41 | Analytics.with(context).track(screen); 42 | } 43 | } 44 | 45 | public static void track(String track, Properties analyticProperties) { 46 | if (!analyticsKey.isEmpty()) { 47 | Analytics.with(context).track(track, analyticProperties); 48 | } 49 | } 50 | 51 | public static void identify(String email) { 52 | if (!analyticsKey.isEmpty()) { 53 | Analytics.with(context).identify(email); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/utils/SSID.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.utils; 2 | 3 | import android.net.wifi.ScanResult; 4 | import android.net.wifi.WifiConfiguration; 5 | import android.net.wifi.WifiInfo; 6 | import android.os.Parcel; 7 | import android.os.Parcelable; 8 | import android.support.annotation.NonNull; 9 | 10 | import java.util.Locale; 11 | 12 | 13 | /** 14 | * Simple value wrapper for SSID strings. Eliminates case comparison issues and the quoting 15 | * nonsense introduced by {@link android.net.wifi.WifiConfiguration#SSID} (and potentially elsewhere) 16 | */ 17 | public class SSID implements Comparable, Parcelable { 18 | 19 | public static SSID from(@NonNull String rawSsidString) { 20 | Preconditions.checkNotNull(rawSsidString); 21 | return new SSID(deQuotifySsid(rawSsidString)); 22 | } 23 | 24 | public static SSID from(WifiInfo wifiInfo) { 25 | return from(wifiInfo.getSSID()); 26 | } 27 | 28 | public static SSID from(WifiConfiguration wifiConfiguration) { 29 | return from(wifiConfiguration.SSID); 30 | } 31 | 32 | public static SSID from(ScanResult scanResult) { 33 | return SSID.from(scanResult.SSID); 34 | } 35 | 36 | 37 | private final String ssidString; 38 | 39 | private SSID(String ssidString) { 40 | this.ssidString = ssidString; 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | return ssidString; 46 | } 47 | 48 | public String inQuotes() { 49 | return "\"" + ssidString + "\""; 50 | } 51 | 52 | @Override 53 | public boolean equals(Object o) { 54 | if (this == o) return true; 55 | if (o == null || getClass() != o.getClass()) return false; 56 | 57 | SSID ssid = (SSID) o; 58 | 59 | return ssidString.equalsIgnoreCase(ssid.ssidString); 60 | } 61 | 62 | @Override 63 | public int hashCode() { 64 | return ssidString.toLowerCase(Locale.ROOT).hashCode(); 65 | } 66 | 67 | @Override 68 | public int compareTo(@NonNull SSID o) { 69 | return ssidString.compareToIgnoreCase(o.ssidString); 70 | } 71 | 72 | private static String deQuotifySsid(String SSID) { 73 | String quoteMark = "\""; 74 | SSID = ParticleDeviceSetupInternalStringUtils.removeStart(SSID, quoteMark); 75 | SSID = ParticleDeviceSetupInternalStringUtils.removeEnd(SSID, quoteMark); 76 | return SSID; 77 | } 78 | 79 | 80 | //region Parcelable noise 81 | @Override 82 | public int describeContents() { 83 | return 0; 84 | } 85 | 86 | @Override 87 | public void writeToParcel(Parcel dest, int flags) { 88 | dest.writeString(ssidString); 89 | } 90 | 91 | public static final Creator CREATOR = new Creator() { 92 | @Override 93 | public SSID createFromParcel(Parcel in) { 94 | return new SSID(in.readString()); 95 | } 96 | 97 | @Override 98 | public SSID[] newArray(int size) { 99 | return new SSID[size]; 100 | } 101 | }; 102 | //endregion 103 | } 104 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/utils/SoftAPConfigRemover.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.utils; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.SharedPreferences; 6 | 7 | import java.util.Set; 8 | 9 | import static io.particle.android.sdk.utils.Funcy.transformSet; 10 | import static io.particle.android.sdk.utils.Py.set; 11 | 12 | 13 | public class SoftAPConfigRemover { 14 | 15 | private static final TLog log = TLog.get(SoftAPConfigRemover.class); 16 | 17 | 18 | private static final String 19 | PREFS_SOFT_AP_NETWORK_REMOVER = "PREFS_SOFT_AP_NETWORK_REMOVER", 20 | KEY_SOFT_AP_SSIDS = "KEY_SOFT_AP_SSIDS", 21 | KEY_DISABLED_WIFI_SSIDS = "KEY_DISABLED_WIFI_SSIDS"; 22 | 23 | 24 | private final SharedPreferences prefs; 25 | private final WifiFacade wifiFacade; 26 | 27 | public SoftAPConfigRemover(Context context, WifiFacade wifiFacade) { 28 | this.wifiFacade = wifiFacade; 29 | Context ctx = context.getApplicationContext(); 30 | prefs = ctx.getSharedPreferences(PREFS_SOFT_AP_NETWORK_REMOVER, Context.MODE_PRIVATE); 31 | } 32 | 33 | public void onSoftApConfigured(SSID newSsid) { 34 | // make a defensive copy of what we get back 35 | Set ssids = set(loadSSIDsWithKey(KEY_SOFT_AP_SSIDS)); 36 | ssids.add(newSsid); 37 | saveWithKey(KEY_SOFT_AP_SSIDS, ssids); 38 | } 39 | 40 | public void removeAllSoftApConfigs() { 41 | for (SSID ssid : loadSSIDsWithKey(KEY_SOFT_AP_SSIDS)) { 42 | wifiFacade.removeNetwork(ssid); 43 | } 44 | saveWithKey(KEY_SOFT_AP_SSIDS, set()); 45 | } 46 | 47 | public void onWifiNetworkDisabled(SSID ssid) { 48 | log.v("onWifiNetworkDisabled() " + ssid); 49 | Set ssids = set(loadSSIDsWithKey(KEY_DISABLED_WIFI_SSIDS)); 50 | ssids.add(ssid); 51 | saveWithKey(KEY_DISABLED_WIFI_SSIDS, ssids); 52 | } 53 | 54 | public void reenableWifiNetworks() { 55 | log.v("reenableWifiNetworks()"); 56 | for (SSID ssid : loadSSIDsWithKey(KEY_DISABLED_WIFI_SSIDS)) { 57 | wifiFacade.reenableNetwork(ssid); 58 | } 59 | saveWithKey(KEY_DISABLED_WIFI_SSIDS, set()); 60 | } 61 | 62 | 63 | private Set loadSSIDsWithKey(String key) { 64 | return Funcy.transformSet(prefs.getStringSet(key, set()), SSID::from); 65 | } 66 | 67 | @SuppressLint("CommitPrefEdits") 68 | private void saveWithKey(String key, Set ssids) { 69 | Set asStrings = transformSet(ssids, SSID::toString); 70 | prefs.edit() 71 | .putStringSet(key, asStrings) 72 | .apply(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/utils/WorkerFragment.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.utils; 2 | 3 | 4 | import android.content.Context; 5 | import android.os.Bundle; 6 | import android.support.annotation.NonNull; 7 | import android.support.v4.app.Fragment; 8 | import android.support.v4.app.FragmentActivity; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | 13 | /** 14 | * A simple {@link Fragment} subclass used as a marker for "worker" fragments, while bundling in the 15 | * fundamental behavior that makes them worker fragments. 16 | */ 17 | public class WorkerFragment extends Fragment { 18 | 19 | // produce a standardized, unique tag 20 | public static String buildFragmentTag(Class fragClass) { 21 | // return a unique name 22 | return "FRAG_" + fragClass.getCanonicalName(); 23 | } 24 | 25 | // Syntactic sugar for simply adding a WorkerFragment 26 | public static void addFragment(FragmentActivity activity, Fragment frag, String tag) { 27 | activity.getSupportFragmentManager() 28 | .beginTransaction() 29 | .add(frag, tag) 30 | .commit(); 31 | } 32 | 33 | 34 | @Override 35 | public void onAttach(Context context) { 36 | super.onAttach(context); 37 | this.setRetainInstance(true); 38 | } 39 | 40 | @Override 41 | public final View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, 42 | Bundle savedInstanceState) { 43 | return null; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/utils/ui/ParticleUi.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.utils.ui; 2 | 3 | import android.support.v4.app.FragmentActivity; 4 | import android.view.View; 5 | 6 | import io.particle.android.sdk.devicesetup.R; 7 | 8 | 9 | public class ParticleUi { 10 | 11 | // since it's specific to the SDK UI, this method assumes that the id of the 12 | // progress spinner in the button layout is "R.id.button_progress_indicator" 13 | public static void showParticleButtonProgress(FragmentActivity activity, int buttonId, 14 | final boolean show) { 15 | Ui.fadeViewVisibility(activity, R.id.button_progress_indicator, show); 16 | Ui.findView(activity, buttonId).setEnabled(!show); 17 | } 18 | 19 | 20 | public static void enableBrandLogoInverseVisibilityAgainstSoftKeyboard(FragmentActivity activity) { 21 | SoftKeyboardVisibilityDetectingLinearLayout detectingLayout; 22 | detectingLayout = Ui.findView(activity, R.id.keyboard_change_detector_layout); 23 | detectingLayout.setOnSoftKeyboardVisibilityChangeListener(new BrandImageHeaderHider(activity)); 24 | } 25 | 26 | 27 | public static class BrandImageHeaderHider 28 | implements SoftKeyboardVisibilityDetectingLinearLayout.SoftKeyboardVisibilityChangeListener { 29 | 30 | final View logoView; 31 | 32 | public BrandImageHeaderHider(FragmentActivity activity) { 33 | logoView = Ui.findView(activity, R.id.brand_image_header); 34 | } 35 | 36 | @Override 37 | public void onSoftKeyboardShown() { 38 | logoView.setVisibility(View.GONE); 39 | } 40 | 41 | @Override 42 | public void onSoftKeyboardHidden() { 43 | logoView.setVisibility(View.VISIBLE); 44 | } 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/utils/ui/SoftKeyboardVisibilityDetectingLinearLayout.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.utils.ui; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.KeyEvent; 6 | import android.widget.LinearLayout; 7 | 8 | /** 9 | * How can Android have gone this long without this ability being built-in...? 10 | * 11 | * Derived from: https://gist.github.com/mrleolink/8823150 12 | * 13 | */ 14 | public class SoftKeyboardVisibilityDetectingLinearLayout extends LinearLayout { 15 | 16 | 17 | public interface SoftKeyboardVisibilityChangeListener { 18 | 19 | void onSoftKeyboardShown(); 20 | 21 | void onSoftKeyboardHidden(); 22 | 23 | } 24 | 25 | 26 | private boolean isKeyboardShown; 27 | private SoftKeyboardVisibilityChangeListener listener; 28 | 29 | public SoftKeyboardVisibilityDetectingLinearLayout(Context context) { 30 | super(context); 31 | } 32 | 33 | public SoftKeyboardVisibilityDetectingLinearLayout(Context context, AttributeSet attrs) { 34 | super(context, attrs); 35 | } 36 | 37 | public SoftKeyboardVisibilityDetectingLinearLayout(Context context, AttributeSet attrs, int defStyle) { 38 | super(context, attrs, defStyle); 39 | } 40 | 41 | 42 | @Override 43 | public boolean dispatchKeyEventPreIme(KeyEvent event) { 44 | if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { 45 | // Keyboard is hidden 46 | if (isKeyboardShown) { 47 | isKeyboardShown = false; 48 | listener.onSoftKeyboardHidden(); 49 | } 50 | } 51 | return super.dispatchKeyEventPreIme(event); 52 | } 53 | 54 | @Override 55 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 56 | final int proposedheight = MeasureSpec.getSize(heightMeasureSpec); 57 | final int actualHeight = getHeight(); 58 | if (actualHeight > proposedheight) { 59 | // Keyboard is shown 60 | if (!isKeyboardShown) { 61 | isKeyboardShown = true; 62 | listener.onSoftKeyboardShown(); 63 | } 64 | } else { 65 | // Keyboard is hidden <<< this doesn't work sometimes, so I don't use it 66 | } 67 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 68 | } 69 | 70 | public void setOnSoftKeyboardVisibilityChangeListener(SoftKeyboardVisibilityChangeListener listener) { 71 | this.listener = listener; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/utils/ui/Toaster.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.utils.ui; 2 | 3 | import android.content.Context; 4 | import android.support.v4.app.Fragment; 5 | import android.view.View; 6 | import android.widget.Toast; 7 | 8 | 9 | public class Toaster { 10 | 11 | public static void s(Context ctx, String msg) { 12 | Toast.makeText(ctx, msg, Toast.LENGTH_SHORT).show(); 13 | } 14 | 15 | public static void l(Context ctx, String msg) { 16 | Toast.makeText(ctx, msg, Toast.LENGTH_LONG).show(); 17 | } 18 | 19 | public static void s(Context ctx, String msg, int gravity) { 20 | Toast t = Toast.makeText(ctx, msg, Toast.LENGTH_SHORT); 21 | t.setGravity(gravity, 0, 0); 22 | t.show(); 23 | } 24 | 25 | public static void l(Context ctx, String msg, int gravity) { 26 | Toast t = Toast.makeText(ctx, msg, Toast.LENGTH_LONG); 27 | t.setGravity(gravity, 0, 0); 28 | t.show(); 29 | } 30 | 31 | public static void s(Fragment frag, String msg) { 32 | s(frag.getActivity(), msg); 33 | } 34 | 35 | public static void l(Fragment frag, String msg) { 36 | l(frag.getActivity(), msg); 37 | } 38 | 39 | public static void s(View view, String msg) { 40 | s(view.getContext(), msg); 41 | } 42 | 43 | public static void l(View view, String msg) { 44 | l(view.getContext(), msg); 45 | } 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /devicesetup/src/main/java/io/particle/android/sdk/utils/ui/WebViewActivity.java: -------------------------------------------------------------------------------- 1 | package io.particle.android.sdk.utils.ui; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.os.Bundle; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.support.v7.widget.Toolbar; 10 | import android.webkit.WebSettings; 11 | import android.webkit.WebView; 12 | import android.webkit.WebViewClient; 13 | 14 | import io.particle.android.sdk.devicesetup.R; 15 | import io.particle.android.sdk.utils.SEGAnalytics; 16 | 17 | 18 | public class WebViewActivity extends AppCompatActivity { 19 | 20 | 21 | private static final String EXTRA_CONTENT_URI = "EXTRA_CONTENT_URI"; 22 | private static final String EXTRA_PAGE_TITLE = "EXTRA_PAGE_TITLE"; 23 | 24 | 25 | public static Intent buildIntent(Context ctx, Uri uri) { 26 | return new Intent(ctx, WebViewActivity.class) 27 | .putExtra(EXTRA_CONTENT_URI, uri); 28 | } 29 | 30 | 31 | public static Intent buildIntent(Context ctx, Uri uri, CharSequence pageTitle) { 32 | return buildIntent(ctx, uri) 33 | .putExtra(EXTRA_PAGE_TITLE, pageTitle.toString()); 34 | } 35 | 36 | 37 | @SuppressLint("SetJavaScriptEnabled") 38 | @Override 39 | protected void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | setContentView(R.layout.activity_web_view); 42 | SEGAnalytics.track("Device Setup: Webview Screen"); 43 | Toolbar toolbar = Ui.findView(this, R.id.toolbar); 44 | toolbar.setNavigationIcon( 45 | Ui.getTintedDrawable(this, R.drawable.ic_clear_black_24dp, R.color.element_tint_color)); 46 | 47 | toolbar.setNavigationOnClickListener(view -> finish()); 48 | 49 | if (getIntent().hasExtra(EXTRA_PAGE_TITLE)) { 50 | toolbar.setTitle(getIntent().getStringExtra(EXTRA_PAGE_TITLE)); 51 | } 52 | 53 | WebView webView = Ui.findView(this, R.id.web_content); 54 | 55 | webView.setWebViewClient(new WebViewClient() { 56 | @Override 57 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 58 | // handle redirects in the same view 59 | view.loadUrl(url); 60 | // return false to indicate that we do not want to leave the webview 61 | return false; // then it is not handled by default action 62 | } 63 | }); 64 | 65 | WebSettings webSettings = webView.getSettings(); 66 | // this has to be enabled or else some pages don't render *at all.* 67 | webSettings.setJavaScriptEnabled(true); 68 | 69 | Uri uri = getIntent().getParcelableExtra(EXTRA_CONTENT_URI); 70 | webView.loadUrl(uri.toString()); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-hdpi/ic_clear_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-hdpi/ic_clear_black_24dp.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-mdpi/ic_clear_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-mdpi/ic_clear_black_24dp.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xhdpi/checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xhdpi/checkmark.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xhdpi/ic_clear_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xhdpi/ic_clear_black_24dp.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xxhdpi/fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xxhdpi/fail.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xxhdpi/ic_clear_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xxhdpi/ic_clear_black_24dp.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xxhdpi/particle_vertical_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xxhdpi/particle_vertical_blue.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xxhdpi/photon_vector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xxhdpi/photon_vector.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xxhdpi/photon_vector_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xxhdpi/photon_vector_small.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xxhdpi/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xxhdpi/success.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xxhdpi/the_wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xxhdpi/the_wifi.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xxhdpi/trianglifybackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xxhdpi/trianglifybackground.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xxxhdpi/ic_clear_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xxxhdpi/ic_clear_black_24dp.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xxxhdpi/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xxxhdpi/lock.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xxxhdpi/particle_horizontal_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xxxhdpi/particle_horizontal_blue.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable-xxxhdpi/particle_horizontal_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable-xxxhdpi/particle_horizontal_head.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable/button_text_color_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable/link_text_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable/progress_indicator_graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/spark-setup-android/60290536c1be65b78ac8ef0d475999eb468a24c0/devicesetup/src/main/res/drawable/progress_indicator_graphic.png -------------------------------------------------------------------------------- /devicesetup/src/main/res/drawable/progress_spinner.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /devicesetup/src/main/res/layout/activity_discover_device.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 21 | 22 | 30 | 31 | 37 | 38 | 45 | 46 | 57 | 58 | 67 | 68 | 77 | 78 |