diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..94d8dfcc --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,41 @@ +version: 2.1 + +orbs: + android: circleci/android@1.0.3 + +jobs: + static-analysis: + executor: + name: android/android-machine + steps: + - checkout + - run: + name: Run lint + command: ./gradlew lintDebug + - run: + name: Run Kotlinter + command: ./gradlew lintKotlin + - run: + name: Run detekt + command: ./gradlew detekt + - run: + name: Run CPD + command: ./gradlew cpdCheck + + build: + executor: + name: android/android-machine + steps: + - checkout + - run: + name: Assemble debug build + command: ./gradlew assembleDebug + - run: + name: Assemble release build + command: ./gradlew assembleRelease + +workflows: + ci: + jobs: + - static-analysis + - build diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ac908a3e..2decd659 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @isuPatches \ No newline at end of file +* @isuPatches diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index f5687b64..7ef3465f 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -4,9 +4,9 @@ about: Create a report to help us improve --- -Thank you for creating an issue to improve this library! Please check Please check [the current issues](https://github.com/isuPatches/WiseFy/issues) to make sure that the improvement isn't already being worked on. +Thank you for creating an issue to improve this library! Please check Please check [the current issues](https://github.com/isuPatches/android-wisefy/issues) to make sure that the improvement isn't already being worked on. -It will be _EXTREMELY_ helpful if you also take a look at the [sample app](/wisefysample) to see how it behaves and if you can reproduce there. +It will be _EXTREMELY_ helpful if you also take a look at the [sample app](/app) to see how it behaves and if you can reproduce there. **Description** A clear and concise description of the bug. @@ -19,8 +19,8 @@ If applicable, add screenshots to help explain. **To Reproduce** Steps to reproduce the behavior: -1. Create WiseFy instance -2. Call '...' method with '...' as params (please include if it's the synchronous or asynchronous API) +1. Create Wisefy instance +2. Call '...' method with '...' as params (please include if it's the synchronous, asynchronous, or ktx API) 3. Witness unexpected behavior **Expected behavior** @@ -38,4 +38,4 @@ A description of what actually happened - Reproducible with sample app?: **Logs** -Please include logs from WiseFy if at all possible. They can be found with `logging(true)` set on the WiseFy instance and by filtering on 'WiseFy' +Please include logs from Wisefy if at all possible. They can be found with `logging(true)` set on the Wisefy instance and by filtering on 'Wisefy' diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 0fa5221e..ce46e2d4 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -4,7 +4,7 @@ about: Suggest an idea for this project --- -Thank you for creating an issue to improve this library! Please check Please check [the current issues](https://github.com/isuPatches/WiseFy/issues) to make sure that the improvement isn't already being worked on. +Thank you for creating an issue to improve this library! Please check Please check [the current issues](https://github.com/isuPatches/android-wisefy/issues) to make sure that the improvement isn't already being worked on. **Description** A clear and concise high-level description of the request. (f.e. As a developer I want ___ so that ___) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..4e9569d6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,44 @@ +I'd love to have your help, but please don't skimp on greatness! + +Please check the [Issues](https://github.com/isupatches/android-wisefy/issues) and the [Main Board](https://github.com/isuPatches/android-wisefy/projects/1) to make sure what you want is not already being worked on. + +* Please create an issue and use the [Main Board](https://github.com/isuPatches/android-wisefy/projects/1) to denote its status + - Please tag the issue with the appropriate labels as well (i.e. status, bug, TODO, etc.) + - Each issue should have clear details (i.e. steps to reproduce if it's a bug or acceptance criteria if a new feature) +* Please branch off develop to do any new work + - Branches should be prefixed with issue-#/ and a short description, example: issue-53/adding-some-feature +* Please make sure all static code analysis checks pass +* Please write tests +* Please make sure current tests pass +* Please help maintain the KDocs and write new ones +* Please help maintain the markdown docs (if necessary) +* Once satisfied with your work, please submit a Pull Request and I'll be happy to review it! All PRs need to be reviewed by a code owner. + +For merging: + +* PRs will be squashed and merged after a rebase + - The commit should have the issue-# in its message + - [Signed commits](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification/signing-commits) are required + - The branch must be up-to-date before merging + - Conventional commits should be used to denote and differentiate between breaking changes, fixes, chores/tasks, and new features + +Some architecture/code guidelines: + - Follow the patterns and naming conventions present to ensure consistency + - The query class is prefixed with function name (f.e. the function `getCurrentNetwork` uses the query class `GetCurrentNetworkQuery`) + - The request class is prefixed with function name (f.e. the function `addNetwork` uses the request class `AddNetworkRequest`) + - The result class is prefixed with function name (f.e. the function `removeNetwork` uses the result class `RemoveNetworkResult`) + - The `ktx` extension functions have the suffix `Async` (f.e. the extension for `getNetworkConnectionStatus` is `getNetworkConnectionStatusAsync`) + - Keep in-mind the folder and organization structure defined in the [README.md](/README.md) + - Synchronous, async, and ktx options are supported for each feature (unless there is an exceptional case) + - Adapters are the only expected location for assertions + - For `ktx` extension functions, the library deliberately catches any exception and throws a [WisefyException](/wisefy/core/src/main/java/com/isupatches/android/wisefy/core/exceptions/WisefyException.kt) + - Async operations use coroutines internally and: + - Execute on the background thread + - Return results to the main thread + - Are locked with a mutex if there are potential conflicts with out async functions (f.e. enableWifi, disableWifi, and isWifiEnabled) + +For releasing: + + - The CHANGELOG.md will be generated once per release + - The release task will be run to regenerate all of the KDocs + - The version codes and version names will be bumped in a release PR diff --git a/LICENSE.md b/LICENSE.md index f3abe5c6..c046c60c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2017 Patches Klinefelter + Copyright 2022 Patches Barrett Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index aaeea7de..f57878f9 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,194 @@ ## Wisefy + + +A Wifi configuration and util library built in Kotlin for Android. + +> Developed by Patches 04/24/2016 - present +> Logo/icon created by mansya (2018) +> +> Supports Android SDK levels 23-33 + +[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-WiseFy-brightgreen.svg?style=flat)](https://android-arsenal.com/details/1/6011) [![Android Weekly](https://img.shields.io/badge/Android%20Weekly-%23230-blue.svg)](http://androidweekly.net/issues/issue-230) + +- [Installation](#installation) +- [KTX Artifact](#ktx-artifact) +- [5.0 Rewrite](#50-rewrite) + - [Highlights](#highlights) + - [New Structure](#new-structure) + - [Packaging & Naming Conventions](#packaging-and-naming-conventions) + - [Deprecations](#deprecations) + - [Known Android Q Problems](#known-android-q-problems) +- [Documentation](#documentation) + - [Reset](#reset) + - [Links](#links) +- [FAQ](#faq) +- [For Contributors](#for-contributors) +- [License](#license) + ## Installation There is a new package for 5.0. Please use: ```kotlin -implementation("com.isupatches.android:wisefy:5.0.0-RC2") +implementation("com.isupatches.android:wisefy:5.0.0-RC3") ``` +This will include all of the Wisefy sub-artifacts through `api` dependencies. + +There are also new more modular artifacts published so that individual pieces of Wisefy can be imported directly: + +> `com.isupatches.android.wisefy:core:` will be a requirement for the other Wisefy artifacts. + +- `com.isupatches.android.wisefy:accesspoints:` +- `com.isupatches.android.wisefy:addnetwork:` +- `com.isupatches.android.wisefy:core:` +- `com.isupatches.android.wisefy:networkconnection:` +- `com.isupatches.android.wisefy:networkinfo:` +- `com.isupatches.android.wisefy:removenetwork:` +- `com.isupatches.android.wisefy:savednetworks:` +- `com.isupatches.android.wisefy:signal:` +- `com.isupatches.android.wisefy:wifi:` + +Here are the descriptions of what functionality each artifact provides: + +- `:accesspoints` For getting and searching for nearby networks +- `:addnetwork` For adding a Wifi network +- `:core` For base Wisefy functionality +- `:networkconnection` For connecting and disconnecting from networks +- `:networkinfo` For information about the device's current network and current connectivity status +- `:removenetwork` For removing a Wifi network +- `:savednetworks` For getting and searching for saved networks +- `:signal` For calculating a signal level and comparing signal levels +- `:wifi` For enabling and disabling Wifi + +## KTX Artifact + +There is a new artifact for 5.0 that provides Kotlin extension functions. Please use: + +```kotlin +implementation("com.isupatches.android:wisefy-ktx:") +``` + +if you want to try it out. All functions in this package have the suffix `Async` and are `suspend` functions. + ## 5.0 Rewrite The 5.0 version of WiseFy works to rectify the problems that caused it to be overly challenging as a single developer -to keep up with the ever-changing Wifi APIs for new Android OS's, especially with a lot of functionality becoming -privatized. +to keep up with the ever-changing Wifi APIs for new Android operating systems, especially with a lot of functionality +becoming privatized. It also strives to simplify the API and removes redundancy within the APIs that caused additional +overhead for maintenance. I hope you enjoy the rewrite and please create an issue if you see anything odd or have questions! ### Highlights -- Android P, Android Q, and Android R are now supported +- Android Q, Android R, Android S, and Android T are now supported +- Compiled with Java 11 - Rewritten with extensibility and future Android OS's in-mind - * Future versions of the Android OS will be easier to support with the new delegate system + * Future versions of the Android OS will be easier to support with the new delegate/adapter system * Improved modularity where APIs for OS versions are contained in their own API / API implementation files -- Async operations updated to internally leverage Coroutines and new exception handler -- Returns are now WiseFy classes or primitives opposed to a class from the OS, making it easier to add new variants - in the future through sealed classes +- Async operations updated to internally leverage Coroutines and a new exception handler +- Returns are now Wisefy classes opposed to a class from the OS, making it easier to add new variants in the future + through sealed classes - Updated names for callbacks - Kotlin first mentality (but willing to support Java as first class too!) -- WPA3 networks now supported -- wisefysample renamed to app +- WPA3 networks are now supported +- `wisefysample` renamed to `app` +- BSSID support is now added +- More modular artifacts are available now + - Less bloat if there are things you're not going to use + - Able to iterate and update portions of the library without the overhead of the entire project +- `gradle.lockfile`s and Gradle ranges are enabled for more control over versioning + - [Locking dependency versions](https://docs.gradle.org/current/userguide/dependency_locking.html) + - [Declaring Versions and Ranges](https://docs.gradle.org/current/userguide/single_versions.html) +- Sample app re-written + - Down with SharedPreferences, onward to DataStore + - Written in Compose complete with previews + - Niceties like progress bars added + - Better input validation -### Work Remaining Before Official Release +### New Structure -- Documentation added -- Unit and instrumentation tests added back +- Wisefy public API -> Wisefy implementation -> Delegate to determine which adapter to use -> + Adapter converts request for usage by the Android OS -> Android OS APIs are used and the adapter returns information + as a Wisefy result -### New Structure +### Packaging and Naming Conventions + +Types of classes: + +- `Query` - Indicates a read only operation where nothing is modified or written +- `Request` - Indicates an action where some state is modified or a value is written +- `Result` - Indicates the return from an action or query +- `Data` - Indicates a complex return for a result that includes several different properties, fields, and functions -- WiseFy API -> WiseFy -> Internal util -> Delegate -> OS API and OS API implementation +Types of property values: + +- `value` - A field within a `Result` object for a `Data` class (a Wisefy based class) +- `rawValue` - A field for a OS level class or object (not a Wisefy based class) within a `Data` object + +Suffixes: + +- Api (top-level of a feature package) -> Contains the definitions of synchronous APIs available for the feature +- ApiAsync (top-level of a feature package) -> Contains the definitions of asynchronous APIs available for the feature +- Delegate -> Determines which adapter to use based on the Android device's SDK level +- Adapter -> Middleware that converts requests and responses between the API and Delegate layers +- Api (within os package context) -> Houses the definitions of Android OS level APIs that Adapter classes may use +- ApiImpl (within os package context) -> Houses the implementation for executing Android OS level APIs + +Package structure for each section is as follows: + +- Feature (accesspoints, addnetwork, etc.) -> Location of Wisefy Specific delegate -> supporting sub-directories + +*Supporting sub-directories can include* + +- callbacks (public) -> Location of callback interfaces for returns from async calls +- entities (public) -> Location of data classes for queries, requests, responses, and data +- os (internal) - Purely on organizational directory + - adapters (internal) -> Location of the classes that convert requests and responses between the Delegate and Android + OS level APIs + - converters (internal) -> Location of helpers to convert one data class to another + - apis (internal) -> Location of the API interfaces to talk to the Android OS + - impls (internal) -> Location of the implementation for an API that talks to the Android OS ### Deprecations - disableWifi() and disableWifi(callbacks: DisableWifiCallbacks?) +- disconnectFromCurrentNetwork() and disconnectFromCurrentNetwork(callbacks: DisconnectFromCurrentNetworkCallbacks?) - enableWifi() and enableWifi(callbacks: EnableWifiCallbacks?) - calculateBars(rssiLevel: Int, targetNumberOfBars: Int) ### Known Android Q Problems -- Saving a network doesn't seem possible. A notification is presented if not connected to the suggestion, but even the -appearance of the notification seems flakey +- Adding, removing, and getting saved network functionality is not present. + Android Q / SDK 29 was in a weird half-baked state between the old WifiManager APIs and the new WifiSuggestion APIs...Android 30 seems to have full support, but Android 29 really drops the ball for saved network functionality. + +## Documentation + +### Reset + +To keep the documentation clean and because of the amount of architectural changes for the 5.x release, the +documentation was stripped and then completely re-written. + +## Links + +For auto-generated [Dokka](https://kotlin.github.io/dokka) markdown files based on the KDocs please see +[here](/dokka/index.md). + +For more high-level examples based on different functionality please see [here](/documentation/index.md). + +## FAQ + +You may find a list of frequently asked questions [here](/documentation/FAQ.md). + +## For Contributors + +Want to help? Have an idea for a cool feature or want to fix a bug for the community? See +[CONTRIBUTING](CONTRIBUTING.md) for how you can make impactful changes for Wisefy. ## License ## -Copyright 2021 Patches Klinefelter +Copyright 2022 Patches Barrett Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at diff --git a/app/build.gradle.kts b/app/build.gradle.kts index aa80928d..927f730c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,14 +1,16 @@ import com.isupatches.android.wisefy.build.BuildVersions import com.isupatches.android.wisefy.build.Dependencies import com.isupatches.android.wisefy.build.Versions +import com.isupatches.android.wisefy.build.compose import com.isupatches.android.wisefy.build.dagger import com.isupatches.android.wisefy.build.navigation -import java.util.Properties +import java.util.* plugins { id("com.android.application") - id("kotlin-android") + id("org.jetbrains.kotlin.android") id("kotlin-kapt") + id("dagger.hilt.android.plugin") } val keystoreProperties: Properties = Properties() @@ -18,7 +20,10 @@ if (keystoreFile.exists()) { } android { - compileSdkVersion(BuildVersions.COMPILE_SDK) + namespace = "com.isupatches.android.wisefy.sample" + testNamespace = "com.isupatches.android.wisefy.sample.test" + + compileSdk = BuildVersions.COMPILE_SDK buildToolsVersion = BuildVersions.BUILD_TOOLS defaultConfig { @@ -55,22 +60,28 @@ android { isTestCoverageEnabled = true isMinifyEnabled = false isShrinkResources = false - proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules-debug.pro") - testProguardFile(file("proguard-rules-test.pro")) + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "$rootDir/proguard/r8-app-debug.pro" + ) + testProguardFile(file("$rootDir/proguard/r8-app-test.pro")) signingConfig = signingConfigs.getByName("debug") } release { isTestCoverageEnabled = false - isMinifyEnabled = true - isShrinkResources = true - proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules-release.pro") + isMinifyEnabled = false + isShrinkResources = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "$rootDir/proguard/r8-app-release.pro" + ) signingConfig = signingConfigs.getByName("release") } } buildFeatures { - viewBinding = true + compose = true } testCoverage { @@ -78,38 +89,63 @@ android { } lint { - isCheckAllWarnings = true - isShowAll = true - isExplainIssues = true - isAbortOnError = true - isWarningsAsErrors = true - disable("UnusedIds", "ConvertToWebp") + checkAllWarnings = true + showAll = true + explainIssues = true + abortOnError = true + warningsAsErrors = true + disable += "UnusedIds" + disable += "ConvertToWebp" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + afterEvaluate { + configurations.getByName("releaseRuntimeClasspath") { + resolutionStrategy.activateDependencyLocking() + } + configurations.getByName("debugRuntimeClasspath") { + resolutionStrategy.activateDependencyLocking() + } + } + + dependencyLocking { + lockMode.set(LockMode.STRICT) + } + + composeOptions { + kotlinCompilerExtensionVersion = Versions.ANDROIDX_COMPOSE } } dependencies { /* - * Toggle these to test release binary vs. source code + * This should be uncommented to run sample app directly against source code */ - implementation(project(":wisefy")) -// implementation("com.isupatches.android:wisefy:5.0.0-RC2") { -// isChanging = true -// } +// implementation(project(":wisefy:ktx")) - implementation(Dependencies.VIEWGLU) + /* + * This should be uncommented to run sample app directly against release versions of wisefy / wisefy-ktx + */ + implementation("com.isupatches.android.wisefy:ktx:5.0.0") // AndroidX + compose() implementation(Dependencies.AndroidX.APPCOMPAT) - implementation(Dependencies.AndroidX.CONSTRAINT_LAYOUT) implementation(Dependencies.AndroidX.CORE_KTX) + implementation(Dependencies.AndroidX.DATA_STORE) navigation() // Koltin implementation(Dependencies.Kotlin.STD_LIB) - // Google - implementation(Dependencies.Google.MATERIAL) - // Dependency Injection dagger() } diff --git a/app/gradle.lockfile b/app/gradle.lockfile new file mode 100644 index 00000000..988d7522 --- /dev/null +++ b/app/gradle.lockfile @@ -0,0 +1,124 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +androidx.activity:activity-compose:1.6.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.activity:activity-ktx:1.6.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.activity:activity:1.6.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.annotation:annotation-experimental:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.annotation:annotation:1.5.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.appcompat:appcompat-resources:1.5.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.appcompat:appcompat:1.5.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.arch.core:core-common:2.1.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.arch.core:core-runtime:2.1.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.autofill:autofill:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.cardview:cardview:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.collection:collection-ktx:1.1.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.collection:collection:1.1.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation-core:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation-layout:1.2.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation:1.2.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.material:material-icons-core:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.material:material-icons-extended:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.material:material-ripple:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.material:material:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-saveable:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-geometry:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-graphics:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-text:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling-data:1.3.0=debugRuntimeClasspath +androidx.compose.ui:ui-tooling-preview:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling:1.3.0=debugRuntimeClasspath +androidx.compose.ui:ui-unit:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-util:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui:1.3.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.concurrent:concurrent-futures:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.constraintlayout:constraintlayout-solver:2.0.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.constraintlayout:constraintlayout:2.0.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.coordinatorlayout:coordinatorlayout:1.1.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.core:core-ktx:1.9.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.core:core:1.9.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.cursoradapter:cursoradapter:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.customview:customview-poolingcontainer:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.customview:customview:1.1.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.datastore:datastore-core:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.datastore:datastore-preferences-core:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.datastore:datastore-preferences:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.datastore:datastore:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.documentfile:documentfile:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.drawerlayout:drawerlayout:1.1.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.dynamicanimation:dynamicanimation:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.emoji2:emoji2-views-helper:1.2.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.emoji2:emoji2:1.2.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.fragment:fragment-ktx:1.5.4=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.fragment:fragment:1.5.4=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.interpolator:interpolator:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.legacy:legacy-support-core-utils:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-common-java8:2.5.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-common:2.5.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core-ktx:2.5.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core:2.5.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata:2.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-process:2.4.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-ktx:2.5.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime:2.5.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel:2.5.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.loader:loader:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-common-ktx:2.5.3=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-common:2.5.3=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-compose:2.5.3=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-fragment:2.5.3=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-runtime-ktx:2.5.3=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-runtime:2.5.3=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-ui:2.5.3=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.print:print:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.profileinstaller:profileinstaller:1.2.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.recyclerview:recyclerview:1.1.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.resourceinspection:resourceinspection-annotation:1.0.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-ktx:1.2.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate:1.2.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.slidingpanelayout:slidingpanelayout:1.2.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.startup:startup-runtime:1.1.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.tracing:tracing:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.transition:transition:1.4.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.vectordrawable:vectordrawable-animated:1.1.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.vectordrawable:vectordrawable:1.1.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.versionedparcelable:versionedparcelable:1.1.1=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.viewpager2:viewpager2:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.viewpager:viewpager:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +androidx.window:window:1.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +com.google.android.material:material:1.4.0-beta01=debugRuntimeClasspath,releaseRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=debugRuntimeClasspath,releaseRuntimeClasspath +com.google.dagger:dagger-lint-aar:2.44.1=debugRuntimeClasspath,releaseRuntimeClasspath +com.google.dagger:dagger:2.44.1=debugRuntimeClasspath,releaseRuntimeClasspath +com.google.dagger:hilt-android:2.44.1=debugRuntimeClasspath,releaseRuntimeClasspath +com.google.dagger:hilt-core:2.44.1=debugRuntimeClasspath,releaseRuntimeClasspath +com.google.guava:listenablefuture:1.0=debugRuntimeClasspath,releaseRuntimeClasspath +com.isupatches.android.wisefy:accesspoints:5.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +com.isupatches.android.wisefy:addnetwork:5.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +com.isupatches.android.wisefy:core:5.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +com.isupatches.android.wisefy:ktx:5.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +com.isupatches.android.wisefy:networkconnection:5.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +com.isupatches.android.wisefy:networkinfo:5.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +com.isupatches.android.wisefy:removenetwork:5.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +com.isupatches.android.wisefy:savednetworks:5.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +com.isupatches.android.wisefy:signal:5.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +com.isupatches.android.wisefy:wifi:5.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +com.isupatches.android.wisefy:wisefy:5.0.0=debugRuntimeClasspath,releaseRuntimeClasspath +javax.inject:javax.inject:1=debugRuntimeClasspath,releaseRuntimeClasspath +org.jacoco:org.jacoco.agent:0.8.8=debugRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=debugRuntimeClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21=debugRuntimeClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21=debugRuntimeClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.7.10=debugRuntimeClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4=debugRuntimeClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4=debugRuntimeClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4=debugRuntimeClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4=debugRuntimeClasspath,releaseRuntimeClasspath +org.jetbrains:annotations:13.0=debugRuntimeClasspath,releaseRuntimeClasspath +empty= diff --git a/app/proguard-rules-common.pro b/app/proguard-rules-common.pro deleted file mode 100644 index b7062e79..00000000 --- a/app/proguard-rules-common.pro +++ /dev/null @@ -1,15 +0,0 @@ --dontnote - --keepattributes *Annotation*, EnclosingMethod, InnerClasses, Signature - -# Kotlin --keep class kotlin.collections.CollectionsKt { *; } --keepclassmembers class **$WhenMappings { - ; -} --keepclassmembers class kotlin.Metadata { - public ; -} --assumenosideeffects class kotlin.jvm.internal.Intrinsics { - static void checkParameterIsNotNull(java.lang.Object, java.lang.String); -} diff --git a/app/proguard-rules-debug.pro b/app/proguard-rules-debug.pro deleted file mode 100644 index 5ca67fd6..00000000 --- a/app/proguard-rules-debug.pro +++ /dev/null @@ -1,7 +0,0 @@ --include proguard-rules-common.pro - --dontobfuscate - --keepattributes SourceFile, LineNumberTable - --keep class com.isupatches.android.wisefy.sample.DebugMainApplication { *; } diff --git a/app/proguard-rules-release.pro b/app/proguard-rules-release.pro deleted file mode 100644 index 77c5391a..00000000 --- a/app/proguard-rules-release.pro +++ /dev/null @@ -1 +0,0 @@ --include proguard-rules-common.pro diff --git a/app/proguard-test-rules.pro b/app/proguard-test-rules.pro deleted file mode 100644 index f22ec8b2..00000000 --- a/app/proguard-test-rules.pro +++ /dev/null @@ -1 +0,0 @@ --include proguard-rules.pro diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index 809412e8..4cde0735 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,13 +1,14 @@ + xmlns:tools="http://schemas.android.com/tools"> + tools:replace="android:name,android:label" + android:icon="@mipmap/ic_launcher" + /> diff --git a/app/src/debug/java/com/isupatches/android/wisefy/sample/DebugMainApplication.kt b/app/src/debug/java/com/isupatches/android/wisefy/sample/DebugMainApplication.kt index a866892d..c8061b5d 100644 --- a/app/src/debug/java/com/isupatches/android/wisefy/sample/DebugMainApplication.kt +++ b/app/src/debug/java/com/isupatches/android/wisefy/sample/DebugMainApplication.kt @@ -1,9 +1,3 @@ package com.isupatches.android.wisefy.sample -internal open class DebugMainApplication : MainApplication() { - - fun setTestComponent(component: MainApplicationComponent) { - mainApplicationComponent = component - mainApplicationComponent.inject(this) - } -} +internal open class DebugMainApplication : MainApplication() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 496b6dc0..984bcc3f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,34 +1,31 @@ + xmlns:tools="http://schemas.android.com/tools"> + + + + + - - - - - - + android:enableOnBackInvokedCallback="true" + tools:ignore="GoogleAppIndexingWarning" + tools:targetApi="tiramisu"> - + diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/MainApplication.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/MainApplication.kt index d06f4724..bfc76887 100644 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/MainApplication.kt +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/MainApplication.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Patches Klinefelter + * Copyright 2022 Patches Barrett * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,55 +16,8 @@ package com.isupatches.android.wisefy.sample import android.app.Application -import android.content.Context -import com.isupatches.android.wisefy.sample.internal.di.ScreenBindingsModule -import com.isupatches.android.wisefy.sample.internal.util.PermissionUtil -import com.isupatches.android.wisefy.sample.internal.util.PermissionsUtilImpl -import dagger.BindsInstance -import dagger.Component -import dagger.android.DispatchingAndroidInjector -import dagger.android.HasAndroidInjector -import dagger.android.support.AndroidSupportInjectionModule -import javax.inject.Inject -import javax.inject.Singleton +import dagger.hilt.android.HiltAndroidApp -@Suppress("Registered", "UndocumentedPublicClass", "UndocumentedPublicFunction") -internal open class MainApplication : Application(), HasAndroidInjector { - - override fun onCreate() { - super.onCreate() - initializeDependencyInjection() - } - - private fun initializeDependencyInjection() { - mainApplicationComponent = DaggerMainApplication_MainApplicationComponent.builder() - .application(this) - .permissionsUtil(PermissionsUtilImpl()) - .build() - mainApplicationComponent.inject(this) - } - - @Inject lateinit var androidInjector: DispatchingAndroidInjector - override fun androidInjector() = androidInjector - - protected lateinit var mainApplicationComponent: MainApplicationComponent - - @Singleton - @Component( - modules = [ - AndroidSupportInjectionModule::class, - ScreenBindingsModule::class - ] - ) - internal interface MainApplicationComponent { - - fun inject(mainApplication: MainApplication) - - @Component.Builder interface Builder { - fun build(): MainApplicationComponent - - @BindsInstance fun application(prov: Context): Builder - @BindsInstance fun permissionsUtil(prov: PermissionUtil): Builder - } - } -} +@Suppress("Registered") +@HiltAndroidApp +internal open class MainApplication : Application() diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/entities/NetworkType.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/entities/NetworkType.kt similarity index 90% rename from app/src/main/java/com/isupatches/android/wisefy/sample/internal/entities/NetworkType.kt rename to app/src/main/java/com/isupatches/android/wisefy/sample/entities/NetworkType.kt index b7351744..4af8ea2a 100644 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/entities/NetworkType.kt +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/entities/NetworkType.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Patches Klinefelter + * Copyright 2022 Patches Barrett * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.isupatches.android.wisefy.sample.internal.entities +package com.isupatches.android.wisefy.sample.entities internal enum class NetworkType(val intVal: Int) { OPEN(0), diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/entities/SSIDType.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/entities/SSIDType.kt new file mode 100644 index 00000000..b55f38c8 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/entities/SSIDType.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.entities + +internal enum class SSIDType(val intVal: Int) { + SSID(0), + BSSID(1); + + companion object { + + fun of(intVal: Int): SSIDType { + return when (intVal) { + SSID.intVal -> SSID + BSSID.intVal -> BSSID + else -> throw IllegalArgumentException("Invalid SSIDType, intVal: $intVal") + } + } + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/entities/SearchType.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/entities/SearchType.kt similarity index 91% rename from app/src/main/java/com/isupatches/android/wisefy/sample/internal/entities/SearchType.kt rename to app/src/main/java/com/isupatches/android/wisefy/sample/entities/SearchType.kt index f4e933d0..cf8cd327 100644 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/entities/SearchType.kt +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/entities/SearchType.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Patches Klinefelter + * Copyright 2022 Patches Barrett * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.isupatches.android.wisefy.sample.internal.entities +package com.isupatches.android.wisefy.sample.entities internal enum class SearchType(val intVal: Int) { ACCESS_POINT(0), diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkScreen.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkScreen.kt new file mode 100644 index 00000000..b85e4c9a --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkScreen.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.add + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleLoadingIndicator +import com.isupatches.android.wisefy.sample.util.DefaultSdkUtil +import com.isupatches.android.wisefy.sample.util.SdkUtil + +@Composable +internal fun AddNetworkScreen( + wisefy: WisefyApi, + sdkUtil: SdkUtil, + viewModel: AddNetworkViewModel = viewModel( + factory = AddNetworkViewModelFactory( + context = LocalContext.current.applicationContext, + wisefy = wisefy, + sdkUtil = sdkUtil + ) + ) +) { + WisefySampleLoadingIndicator(isLoading = { viewModel.uiState.value.loadingState.isLoading }) + AddNetworkScreenDialogContent(dialogState = { viewModel.uiState.value.dialogState }, viewModel = viewModel) + AddNetworkScreenContent(viewModel = viewModel, sdkUtil = sdkUtil) +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun AddNetworkScreenLightPreview() { + AddNetworkScreen( + wisefy = ComposablePreviewWisefy(), + sdkUtil = DefaultSdkUtil() + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun AddNetworkScreenDarkPreview() { + AddNetworkScreen( + wisefy = ComposablePreviewWisefy(), + sdkUtil = DefaultSdkUtil() + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkScreenContent.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkScreenContent.kt new file mode 100644 index 00000000..59ac3151 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkScreenContent.kt @@ -0,0 +1,249 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.add + +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.Manifest.permission.CHANGE_WIFI_STATE +import android.content.res.Configuration +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.entities.NetworkType +import com.isupatches.android.wisefy.sample.logging.WisefySampleLogger +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefyPrimaryButton +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleBodyLabel +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleEditText +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleEditTextError +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleRadioButton +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleSizes +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme +import com.isupatches.android.wisefy.sample.util.DefaultSdkUtil +import com.isupatches.android.wisefy.sample.util.SdkUtil +import kotlinx.coroutines.launch + +private const val LOG_TAG = "AddNetworkScreenContent" + +@Composable +internal fun AddNetworkScreenContent( + viewModel: AddNetworkViewModel, + sdkUtil: SdkUtil +) { + WisefySampleTheme { + val scope = rememberCoroutineScope() + + val addNetworkPermissionLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> + if (result.all { it.value }) { + scope.launch { + @Suppress("MissingPermission") + viewModel.addNetwork() + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions required to add a network are denied") + viewModel.onAddNetworkPermissionsError() + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding( + top = WisefySampleSizes.WisefySampleTopMargin, + bottom = WisefySampleSizes.WisefySampleBottomMargin, + start = WisefySampleSizes.WisefySampleHorizontalMargins, + end = WisefySampleSizes.WisefySampleHorizontalMargins + ) + ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { + WisefySampleBodyLabel(stringResId = R.string.open) + WisefySampleBodyLabel(stringResId = R.string.wpa2) + if (sdkUtil.isAtLeastQ()) { + WisefySampleBodyLabel(stringResId = R.string.wpa3) + } + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { + AddNetworkRadioButtonGroup( + networkType = { viewModel.uiState.value.networkType }, + viewModel = viewModel, + isAtLeastAndroidQ = sdkUtil.isAtLeastQ() + ) + } + AddNetworkInputRows( + networkType = { viewModel.uiState.value.networkType }, + inputState = { viewModel.uiState.value.inputState }, + viewModel = viewModel + ) + Row(modifier = Modifier.padding(top = WisefySampleSizes.Large)) { + WisefyPrimaryButton(stringResId = R.string.add_network) { + addNetworkPermissionLauncher.launch(arrayOf(ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE)) + } + } + } + } +} + +@Composable +private fun AddNetworkRadioButtonGroup( + networkType: () -> NetworkType, + viewModel: AddNetworkViewModel, + isAtLeastAndroidQ: Boolean +) { + val currentNetworkType = networkType() + WisefySampleRadioButton( + isSelected = currentNetworkType == NetworkType.OPEN, + onClick = { viewModel.onNetworkTypeSelected(NetworkType.OPEN) } + ) + WisefySampleRadioButton( + isSelected = currentNetworkType == NetworkType.WPA2, + onClick = { viewModel.onNetworkTypeSelected(NetworkType.WPA2) } + ) + if (isAtLeastAndroidQ) { + WisefySampleRadioButton( + isSelected = currentNetworkType == NetworkType.WPA3, + onClick = { viewModel.onNetworkTypeSelected(NetworkType.WPA3) } + ) + } +} + +@Composable +private fun AddNetworkInputRows( + networkType: () -> NetworkType, + inputState: () -> AddNetworkInputState, + viewModel: AddNetworkViewModel +) { + val currentNetworkType = networkType() + val currentInputState = inputState() + + Row(modifier = Modifier.padding(top = WisefySampleSizes.XLarge)) { + WisefySampleEditText( + text = currentInputState.ssidInput, + onTextChange = { newSSID -> + viewModel.onSSIDInputChanged(newSSID) + }, + labelResId = R.string.network_name, + error = when (currentInputState.ssidInputValidityState) { + is AddNetworkSSIDInputValidityState.Invalid.Empty -> { + WisefySampleEditTextError(R.string.ssid_input_empty) + } + is AddNetworkSSIDInputValidityState.Invalid.TooShort -> { + WisefySampleEditTextError(R.string.ssid_input_too_short) + } + is AddNetworkSSIDInputValidityState.Invalid.TooLong -> { + WisefySampleEditTextError(R.string.ssid_input_too_long) + } + is AddNetworkSSIDInputValidityState.Invalid.InvalidCharacters -> { + WisefySampleEditTextError(R.string.ssid_input_invalid_characters) + } + is AddNetworkSSIDInputValidityState.Invalid.InvalidStartCharacters -> { + WisefySampleEditTextError(R.string.ssid_input_invalid_start_characters) + } + is AddNetworkSSIDInputValidityState.Invalid.LeadingOrTrailingSpaces -> { + WisefySampleEditTextError(R.string.ssid_input_leading_or_trailing_spaces) + } + is AddNetworkSSIDInputValidityState.Invalid.InvalidUnicode -> { + WisefySampleEditTextError(R.string.ssid_input_not_valid_unicode) + } + is AddNetworkSSIDInputValidityState.Valid -> null + } + ) + } + Row(modifier = Modifier.padding(top = WisefySampleSizes.XLarge)) { + WisefySampleEditText( + text = currentInputState.bssidInput ?: "", + onTextChange = { newBSSID -> + viewModel.onBSSIDInputChanged(newBSSID) + }, + labelResId = R.string.bssid, + error = when (currentInputState.bssidInputValidityState) { + is AddNetworkBSSIDInputValidityState.Invalid -> { + WisefySampleEditTextError(R.string.bssid_input_invalid) + } + is AddNetworkBSSIDInputValidityState.Valid.Empty, is AddNetworkBSSIDInputValidityState.Valid.BSSID -> { + null + } + } + ) + } + if (currentNetworkType == NetworkType.WPA2 || currentNetworkType == NetworkType.WPA3) { + Row(modifier = Modifier.padding(top = WisefySampleSizes.Large)) { + WisefySampleEditText( + text = currentInputState.passphraseInput, + onTextChange = { newPassphrase -> + viewModel.onPassphraseInputChanged(newPassphrase) + }, + labelResId = R.string.network_passphrase, + isPasswordField = true, + error = when (currentInputState.passphraseInputValidityState) { + is AddNetworkPassphraseInputValidityState.Invalid.Empty -> { + WisefySampleEditTextError(R.string.passphrase_input_empty) + } + is AddNetworkPassphraseInputValidityState.Invalid.TooShort -> { + WisefySampleEditTextError(R.string.passphrase_input_too_short) + } + is AddNetworkPassphraseInputValidityState.Invalid.TooLong -> { + WisefySampleEditTextError(R.string.passphrase_input_too_long) + } + is AddNetworkPassphraseInputValidityState.Invalid.InvalidASCII -> { + WisefySampleEditTextError(R.string.passphrase_input_not_valid_ascii) + } + is AddNetworkPassphraseInputValidityState.Valid -> null + } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun AddNetworkScreenContentLightPreview() { + AddNetworkScreenContent( + viewModel = DefaultAddNetworkViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy(), + sdkUtil = DefaultSdkUtil() + ), + sdkUtil = DefaultSdkUtil() + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun AddNetworkScreenContentDarkPreview() { + AddNetworkScreenContent( + viewModel = DefaultAddNetworkViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy(), + sdkUtil = DefaultSdkUtil() + ), + sdkUtil = DefaultSdkUtil() + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkScreenDialogContent.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkScreenDialogContent.kt new file mode 100644 index 00000000..ee23811b --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkScreenDialogContent.kt @@ -0,0 +1,172 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.add + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.isupatches.android.wisefy.addnetwork.entities.AddNetworkResult +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleNoticeDialog +import com.isupatches.android.wisefy.sample.util.DefaultSdkUtil + +@Composable +internal fun AddNetworkScreenDialogContent( + dialogState: () -> AddNetworkDialogState, + viewModel: AddNetworkViewModel +) { + when (val currentDialogState = dialogState()) { + is AddNetworkDialogState.None -> { + // No-op, no dialog + } + is AddNetworkDialogState.AddNetwork.Failure -> { + WisefySampleNoticeDialog( + title = R.string.add_network, + body = R.string.failure_adding_network_args, + currentDialogState.result, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is AddNetworkDialogState.AddNetwork.Success -> { + WisefySampleNoticeDialog( + title = R.string.add_network, + body = R.string.success_adding_network_args, + currentDialogState.result, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is AddNetworkDialogState.AddNetwork.PermissionsError.AddOpenNetwork -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_add_open_network, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is AddNetworkDialogState.AddNetwork.PermissionsError.AddWPA2Network -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_add_wpa2_network, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is AddNetworkDialogState.AddNetwork.PermissionsError.AddWPA3Network -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_add_wpa3_network, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is AddNetworkDialogState.Failure.WisefyAsync -> { + WisefySampleNoticeDialog( + title = R.string.wisefy_async_error, + body = R.string.wisefy_async_error_descriptions_args, + currentDialogState.exception.message ?: "", + currentDialogState.exception.cause?.message ?: "", + onClose = { + viewModel.onDialogClosed() + } + ) + } + is AddNetworkDialogState.InputError.SSID -> { + WisefySampleNoticeDialog( + title = R.string.input_error, + body = R.string.ssid_input_invalid, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is AddNetworkDialogState.InputError.Passphrase -> { + WisefySampleNoticeDialog( + title = R.string.input_error, + body = R.string.passphrase_input_invalid, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is AddNetworkDialogState.InputError.BSSID -> { + WisefySampleNoticeDialog( + title = R.string.input_error, + body = R.string.bssid_input_invalid, + onClose = { + viewModel.onDialogClosed() + } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun AddNetworkScreenDialogContentLightPreview( + @PreviewParameter(AddNetworkDialogStatePreviewParameterProvider::class) dialogState: AddNetworkDialogState +) { + AddNetworkScreenDialogContent( + viewModel = DefaultAddNetworkViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy(), + sdkUtil = DefaultSdkUtil() + ), + dialogState = { dialogState } + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun AddNetworkScreenDialogContentDarkPreview( + @PreviewParameter(AddNetworkDialogStatePreviewParameterProvider::class) dialogState: AddNetworkDialogState +) { + AddNetworkScreenDialogContent( + viewModel = DefaultAddNetworkViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy(), + sdkUtil = DefaultSdkUtil() + ), + dialogState = { dialogState } + ) +} + +private class AddNetworkDialogStatePreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AddNetworkDialogState.Failure.WisefyAsync(WisefyException("", null)), + AddNetworkDialogState.AddNetwork.Success(AddNetworkResult.Success.ResultCode(0)), + AddNetworkDialogState.AddNetwork.Failure(AddNetworkResult.Failure.ResultCode(-1)), + AddNetworkDialogState.AddNetwork.PermissionsError.AddOpenNetwork, + AddNetworkDialogState.AddNetwork.PermissionsError.AddWPA2Network, + AddNetworkDialogState.AddNetwork.PermissionsError.AddWPA3Network, + AddNetworkDialogState.InputError.SSID, + AddNetworkDialogState.InputError.Passphrase, + AddNetworkDialogState.InputError.BSSID + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkStore.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkStore.kt new file mode 100644 index 00000000..3b2e1e8c --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkStore.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.add + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.isupatches.android.wisefy.sample.entities.NetworkType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private const val PREF_NETWORK_TYPE = "network_type" +private const val PREF_LAST_USED_NETWORK_INPUT = "last_used_network_input" +private const val PREF_LAST_USED_NETWORK_PASSPHRASE_INPUT = "last_used_network_passphrase_input" +private const val PREF_LAST_USED_NETWORK_BSSID_INPUT = "last_used_network_bssid_input" + +internal interface AddNetworkStore { + suspend fun clear() + + fun getNetworkType(): Flow + fun getLastUsedNetworkInput(): Flow + fun getLastUsedNetworkPassphraseInput(): Flow + fun getLastUsedNetworkBSSIDInput(): Flow + + suspend fun setNetworkType(networkType: NetworkType) + suspend fun setLastUsedNetworkInput(lastUsedNetworkInput: String) + suspend fun setLastUsedNetworkPassphraseInput(lastUsedNetworkPassphraseInput: String) + suspend fun setLastUsedNetworkBSSIDInput(lastUsedNetworkBSSIDInput: String) +} + +private val Context.addNetworkDataStore: DataStore by preferencesDataStore(name = "addNetworkDataStore") + +internal class DefaultAddNetworkStore( + private val context: Context +) : AddNetworkStore { + + private val networkTypeKey = intPreferencesKey(PREF_NETWORK_TYPE) + private val lastUsedNetworkInputKey = stringPreferencesKey(PREF_LAST_USED_NETWORK_INPUT) + private val lastUsedNetworkPassphraseInputKey = stringPreferencesKey(PREF_LAST_USED_NETWORK_PASSPHRASE_INPUT) + private val lastUsedNetworkBSSIDInputKey = stringPreferencesKey(PREF_LAST_USED_NETWORK_BSSID_INPUT) + + override suspend fun clear() { + context.addNetworkDataStore.edit { + clear() + } + } + + /* + * Network type + */ + + override fun getNetworkType(): Flow { + return context.addNetworkDataStore.data.map { preferences -> + NetworkType.of(preferences[networkTypeKey] ?: NetworkType.WPA2.intVal) + } + } + + override suspend fun setNetworkType(networkType: NetworkType) { + context.addNetworkDataStore.edit { preferences -> + preferences[networkTypeKey] = networkType.intVal + } + } + + /* + * Last used network input + */ + + override fun getLastUsedNetworkInput(): Flow { + return context.addNetworkDataStore.data.map { preferences -> + preferences[lastUsedNetworkInputKey] ?: "" + } + } + + override suspend fun setLastUsedNetworkInput(lastUsedNetworkInput: String) { + context.addNetworkDataStore.edit { preferences -> + preferences[lastUsedNetworkInputKey] = lastUsedNetworkInput + } + } + + /* + * Last used network passphrase input + */ + + override fun getLastUsedNetworkPassphraseInput(): Flow { + return context.addNetworkDataStore.data.map { preferences -> + preferences[lastUsedNetworkPassphraseInputKey] ?: "" + } + } + + override suspend fun setLastUsedNetworkPassphraseInput(lastUsedNetworkPassphraseInput: String) { + context.addNetworkDataStore.edit { preferences -> + preferences[lastUsedNetworkPassphraseInputKey] = lastUsedNetworkPassphraseInput + } + } + + /* + * Last used network BSSID input + */ + + override fun getLastUsedNetworkBSSIDInput(): Flow { + return context.addNetworkDataStore.data.map { preferences -> + preferences[lastUsedNetworkBSSIDInputKey] ?: "" + } + } + + override suspend fun setLastUsedNetworkBSSIDInput(lastUsedNetworkBSSIDInput: String) { + context.addNetworkDataStore.edit { preferences -> + preferences[lastUsedNetworkBSSIDInputKey] = lastUsedNetworkBSSIDInput + } + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkUIState.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkUIState.kt new file mode 100644 index 00000000..c8ab7e9d --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkUIState.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.add + +import com.isupatches.android.wisefy.addnetwork.entities.AddNetworkResult +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.sample.entities.NetworkType + +internal data class AddNetworkUIState( + val loadingState: AddNetworkLoadingState, + val dialogState: AddNetworkDialogState, + val inputState: AddNetworkInputState, + val networkType: NetworkType +) + +internal data class AddNetworkLoadingState(val isLoading: Boolean) + +internal sealed class AddNetworkDialogState { + + object None : AddNetworkDialogState() + + sealed class Failure : AddNetworkDialogState() { + data class WisefyAsync(val exception: WisefyException) : Failure() + } + + sealed class AddNetwork : AddNetworkDialogState() { + data class Failure(val result: AddNetworkResult.Failure) : AddNetwork() + + data class Success(val result: AddNetworkResult.Success) : AddNetwork() + + sealed class PermissionsError : AddNetwork() { + object AddOpenNetwork : PermissionsError() + object AddWPA2Network : PermissionsError() + object AddWPA3Network : PermissionsError() + } + } + + sealed class InputError : AddNetworkDialogState() { + object SSID : InputError() + object Passphrase : InputError() + object BSSID : InputError() + } +} + +internal data class AddNetworkInputState( + val ssidInput: String, + val ssidInputValidityState: AddNetworkSSIDInputValidityState, + val passphraseInput: String, + val passphraseInputValidityState: AddNetworkPassphraseInputValidityState, + val bssidInput: String?, + val bssidInputValidityState: AddNetworkBSSIDInputValidityState +) + +internal sealed class AddNetworkSSIDInputValidityState { + object Valid : AddNetworkSSIDInputValidityState() + + sealed class Invalid : AddNetworkSSIDInputValidityState() { + object Empty : Invalid() + object TooShort : Invalid() + object TooLong : Invalid() + object InvalidCharacters : Invalid() + object InvalidStartCharacters : Invalid() + object LeadingOrTrailingSpaces : Invalid() + object InvalidUnicode : Invalid() + } +} + +internal sealed class AddNetworkPassphraseInputValidityState { + object Valid : AddNetworkPassphraseInputValidityState() + + sealed class Invalid : AddNetworkPassphraseInputValidityState() { + object Empty : Invalid() + object TooShort : Invalid() + object TooLong : Invalid() + object InvalidASCII : Invalid() + } +} + +internal sealed class AddNetworkBSSIDInputValidityState { + sealed class Valid : AddNetworkBSSIDInputValidityState() { + object Empty : Valid() + object BSSID : Valid() + } + + object Invalid : AddNetworkBSSIDInputValidityState() +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkViewModel.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkViewModel.kt new file mode 100644 index 00000000..8292aaec --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/add/AddNetworkViewModel.kt @@ -0,0 +1,319 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.add + +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.Manifest.permission.CHANGE_WIFI_STATE +import android.content.Context +import androidx.annotation.RequiresPermission +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.viewModelScope +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.addnetwork.entities.AddNetworkRequest +import com.isupatches.android.wisefy.addnetwork.entities.AddNetworkResult +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.ktx.addNetworkAsync +import com.isupatches.android.wisefy.sample.entities.NetworkType +import com.isupatches.android.wisefy.sample.scaffolding.BaseViewModel +import com.isupatches.android.wisefy.sample.scaffolding.BaseViewModelFactory +import com.isupatches.android.wisefy.sample.util.BSSIDInputError +import com.isupatches.android.wisefy.sample.util.ErrorMessages +import com.isupatches.android.wisefy.sample.util.PassphraseInputError +import com.isupatches.android.wisefy.sample.util.SSIDInputError +import com.isupatches.android.wisefy.sample.util.SdkUtil +import com.isupatches.android.wisefy.sample.util.validateBSSID +import com.isupatches.android.wisefy.sample.util.validatePassphrase +import com.isupatches.android.wisefy.sample.util.validateSSID +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +internal abstract class AddNetworkViewModel : BaseViewModel() { + abstract val uiState: State + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE]) + abstract suspend fun addNetwork() + + abstract fun onDialogClosed() + + abstract fun onSSIDInputChanged(input: String) + abstract fun onBSSIDInputChanged(input: String) + abstract fun onPassphraseInputChanged(input: String) + + abstract fun onNetworkTypeSelected(networkType: NetworkType) + + abstract fun onAddNetworkPermissionsError() +} + +internal class DefaultAddNetworkViewModel( + context: Context, + private val wisefy: WisefyApi, + private val sdkUtil: SdkUtil, + private val addNetworkStore: AddNetworkStore = DefaultAddNetworkStore(context = context) +) : AddNetworkViewModel() { + + private val _uiState = mutableStateOf( + AddNetworkUIState( + loadingState = AddNetworkLoadingState(false), + dialogState = AddNetworkDialogState.None, + inputState = AddNetworkInputState( + ssidInput = "", + ssidInputValidityState = AddNetworkSSIDInputValidityState.Invalid.Empty, + passphraseInput = "", + passphraseInputValidityState = AddNetworkPassphraseInputValidityState.Invalid.Empty, + bssidInput = "", + bssidInputValidityState = AddNetworkBSSIDInputValidityState.Valid.Empty + ), + networkType = NetworkType.OPEN + ) + ) + override val uiState: State + get() = _uiState + + init { + viewModelScope.launch { + addNetworkStore.getNetworkType() + .collectLatest { networkType -> + _uiState.value = uiState.value.copy( + networkType = networkType + ) + } + } + + viewModelScope.launch { + addNetworkStore.getLastUsedNetworkInput() + .collectLatest { input -> + val newSSIDInputValidityState = when (input.validateSSID()) { + SSIDInputError.EMPTY -> AddNetworkSSIDInputValidityState.Invalid.Empty + SSIDInputError.TOO_SHORT -> AddNetworkSSIDInputValidityState.Invalid.TooShort + SSIDInputError.TOO_LONG -> AddNetworkSSIDInputValidityState.Invalid.TooLong + SSIDInputError.INVALID_CHARACTERS -> AddNetworkSSIDInputValidityState.Invalid.InvalidCharacters + SSIDInputError.INVALID_START_CHARACTERS -> { + AddNetworkSSIDInputValidityState.Invalid.InvalidStartCharacters + } + SSIDInputError.LEADING_OR_TRAILING_SPACES -> { + AddNetworkSSIDInputValidityState.Invalid.LeadingOrTrailingSpaces + } + SSIDInputError.NOT_VALID_UNICODE -> AddNetworkSSIDInputValidityState.Invalid.InvalidUnicode + SSIDInputError.NONE -> AddNetworkSSIDInputValidityState.Valid + } + _uiState.value = uiState.value.copy( + inputState = uiState.value.inputState.copy( + ssidInput = input, + ssidInputValidityState = newSSIDInputValidityState + ) + ) + } + } + + viewModelScope.launch { + addNetworkStore.getLastUsedNetworkPassphraseInput() + .collectLatest { input -> + val newPassphraseInputValidityState = when (input.validatePassphrase()) { + PassphraseInputError.NONE -> AddNetworkPassphraseInputValidityState.Valid + PassphraseInputError.TOO_SHORT -> AddNetworkPassphraseInputValidityState.Invalid.TooShort + PassphraseInputError.TOO_LONG -> AddNetworkPassphraseInputValidityState.Invalid.TooLong + PassphraseInputError.NOT_VALID_ASCII -> { + AddNetworkPassphraseInputValidityState.Invalid.InvalidASCII + } + } + _uiState.value = uiState.value.copy( + inputState = uiState.value.inputState.copy( + passphraseInput = input, + passphraseInputValidityState = newPassphraseInputValidityState + ) + ) + } + } + + viewModelScope.launch { + addNetworkStore.getLastUsedNetworkBSSIDInput() + .collectLatest { input -> + val newBSSIDInputValidityState = when (input.validateBSSID()) { + BSSIDInputError.NONE -> AddNetworkBSSIDInputValidityState.Valid.BSSID + BSSIDInputError.EMPTY -> AddNetworkBSSIDInputValidityState.Valid.Empty + BSSIDInputError.INVALID -> AddNetworkBSSIDInputValidityState.Invalid + } + _uiState.value = uiState.value.copy( + inputState = uiState.value.inputState.copy( + bssidInput = input, + bssidInputValidityState = newBSSIDInputValidityState + ) + ) + } + } + } + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE]) + override suspend fun addNetwork() { + if (isInputInvalid()) { + return + } + _uiState.value = uiState.value.copy( + loadingState = AddNetworkLoadingState(isLoading = true), + dialogState = AddNetworkDialogState.None + ) + val request = when (uiState.value.networkType) { + NetworkType.OPEN -> { + AddNetworkRequest.Open( + ssid = uiState.value.inputState.ssidInput, + bssid = uiState.value.inputState.bssidInput + ) + } + NetworkType.WPA2 -> { + AddNetworkRequest.WPA2( + ssid = uiState.value.inputState.ssidInput, + passphrase = uiState.value.inputState.passphraseInput, + bssid = uiState.value.inputState.bssidInput + ) + } + NetworkType.WPA3 -> { + if (sdkUtil.isAtLeastQ()) { + AddNetworkRequest.WPA3( + ssid = uiState.value.inputState.ssidInput, + passphrase = uiState.value.inputState.passphraseInput, + bssid = uiState.value.inputState.bssidInput + ) + } else { + /* + * The UI not displaying the WPA3 option on pre-Android Q / SDK 29 devices + * will prevent us from getting here. + */ + error(ErrorMessages.AddNetwork.WPA3_NETWORK_ADD_ON_PRE_ANDROID_Q_DEVICE) + } + } + } + when (val result = getAddNetworkResult(request)) { + is AddNetworkResult.Success -> { + _uiState.value = uiState.value.copy( + loadingState = AddNetworkLoadingState(false), + dialogState = AddNetworkDialogState.AddNetwork.Success(result) + ) + } + is AddNetworkResult.Failure -> { + _uiState.value = uiState.value.copy( + loadingState = AddNetworkLoadingState(false), + dialogState = AddNetworkDialogState.AddNetwork.Failure(result) + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + private fun isInputInvalid(): Boolean { + val currentInputState = uiState.value.inputState + if (currentInputState.ssidInputValidityState !is AddNetworkSSIDInputValidityState.Valid) { + _uiState.value = uiState.value.copy( + loadingState = AddNetworkLoadingState(isLoading = false), + dialogState = AddNetworkDialogState.InputError.SSID + ) + return true + } + if (currentInputState.bssidInputValidityState !is AddNetworkBSSIDInputValidityState.Valid) { + _uiState.value = uiState.value.copy( + loadingState = AddNetworkLoadingState(isLoading = false), + dialogState = AddNetworkDialogState.InputError.BSSID + ) + return true + } + return when (uiState.value.networkType) { + NetworkType.WPA2, NetworkType.WPA3 -> { + if (currentInputState.passphraseInputValidityState !is AddNetworkPassphraseInputValidityState.Valid) { + _uiState.value = uiState.value.copy( + loadingState = AddNetworkLoadingState(isLoading = false), + dialogState = AddNetworkDialogState.InputError.Passphrase + ) + true + } else { + false + } + } + NetworkType.OPEN -> false + } + } + + override fun onSSIDInputChanged(input: String) { + viewModelScope.launch { + addNetworkStore.setLastUsedNetworkInput(input) + } + } + + override fun onPassphraseInputChanged(input: String) { + viewModelScope.launch { + addNetworkStore.setLastUsedNetworkPassphraseInput(input) + } + } + + override fun onBSSIDInputChanged(input: String) { + viewModelScope.launch { + addNetworkStore.setLastUsedNetworkBSSIDInput(input) + } + } + + override fun onNetworkTypeSelected(networkType: NetworkType) { + viewModelScope.launch { + addNetworkStore.setNetworkType(networkType = networkType) + when (networkType) { + NetworkType.OPEN -> addNetworkStore.setLastUsedNetworkPassphraseInput("") + NetworkType.WPA2, NetworkType.WPA3 -> { + // No-op + } + } + } + } + + override fun onDialogClosed() { + _uiState.value = uiState.value.copy( + loadingState = AddNetworkLoadingState(isLoading = false), + dialogState = AddNetworkDialogState.None + ) + } + + override fun onAddNetworkPermissionsError() { + _uiState.value = uiState.value.copy( + loadingState = AddNetworkLoadingState(isLoading = false), + dialogState = when (uiState.value.networkType) { + NetworkType.OPEN -> AddNetworkDialogState.AddNetwork.PermissionsError.AddOpenNetwork + NetworkType.WPA2 -> AddNetworkDialogState.AddNetwork.PermissionsError.AddWPA2Network + NetworkType.WPA3 -> AddNetworkDialogState.AddNetwork.PermissionsError.AddWPA3Network + } + ) + } + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE]) + private suspend fun getAddNetworkResult(request: AddNetworkRequest): AddNetworkResult? { + return try { + wisefy.addNetworkAsync(request) + } catch (ex: WisefyException) { + _uiState.value = uiState.value.copy( + loadingState = AddNetworkLoadingState(false), + dialogState = AddNetworkDialogState.Failure.WisefyAsync(exception = ex) + ) + null + } + } +} + +internal class AddNetworkViewModelFactory( + private val context: Context, + private val wisefy: WisefyApi, + private val sdkUtil: SdkUtil +) : BaseViewModelFactory( + supportedClass = AddNetworkViewModel::class, + vmProvider = { DefaultAddNetworkViewModel(context, wisefy, sdkUtil) } +) diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscScreen.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscScreen.kt new file mode 100644 index 00000000..09c35aef --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscScreen.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleLoadingIndicator +import com.isupatches.android.wisefy.sample.util.DefaultSdkUtil +import com.isupatches.android.wisefy.sample.util.SdkUtil + +@Composable +internal fun MiscScreen( + wisefy: WisefyApi, + sdkUtil: SdkUtil, + navController: NavHostController, + viewModel: MiscViewModel = viewModel(factory = MiscViewModelFactory(wisefy)) +) { + WisefySampleLoadingIndicator(isLoading = { viewModel.uiState.value.loadingState.isLoading }) + MiscScreenDialogContent(dialogState = { viewModel.uiState.value.dialogState }, viewModel = viewModel) + MiscScreenContent( + viewModel = viewModel, + sdkUtil = sdkUtil, + router = DefaultMiscScreenRouter(navController = navController) + ) +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun MiscScreenLightPreview() { + MiscScreen( + wisefy = ComposablePreviewWisefy(), + navController = rememberNavController(), + sdkUtil = DefaultSdkUtil() + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun MiscScreenDarkPreview() { + MiscScreen( + wisefy = ComposablePreviewWisefy(), + navController = rememberNavController(), + sdkUtil = DefaultSdkUtil() + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscScreenContent.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscScreenContent.kt new file mode 100644 index 00000000..beb33b1f --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscScreenContent.kt @@ -0,0 +1,248 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc + +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.Manifest.permission.ACCESS_NETWORK_STATE +import android.Manifest.permission.ACCESS_WIFI_STATE +import android.Manifest.permission.CHANGE_WIFI_STATE +import android.content.res.Configuration +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.StringRes +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.compose.rememberNavController +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.logging.WisefySampleLogger +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefyPrimaryButton +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleSizes +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme +import com.isupatches.android.wisefy.sample.util.DefaultSdkUtil +import com.isupatches.android.wisefy.sample.util.SdkUtil +import com.isupatches.android.wisefy.wifi.entities.DisableWifiRequest +import com.isupatches.android.wisefy.wifi.entities.EnableWifiRequest +import kotlinx.coroutines.launch + +private const val LOG_TAG = "MiscScreenContent" + +@Composable +internal fun MiscScreenContent( + viewModel: MiscViewModel, + sdkUtil: SdkUtil, + router: MiscScreenRouter +) { + WisefySampleTheme { + val scope = rememberCoroutineScope() + val context = LocalContext.current + + val enableWifiPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + scope.launch { + if (sdkUtil.isAtLeastQ()) { + viewModel.enableWifi(request = EnableWifiRequest.Android29OrAbove(context)) + } else { + viewModel.enableWifi(request = EnableWifiRequest.Default) + } + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions for enabling wifi are denied") + viewModel.onEnableWifiPermissionsError() + } + } + + val disableWifiPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + scope.launch { + if (sdkUtil.isAtLeastQ()) { + viewModel.disableWifi(request = DisableWifiRequest.Android29OrAbove(context)) + } else { + viewModel.disableWifi(request = DisableWifiRequest.Default) + } + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions for disabling wifi are denied") + viewModel.onDisableWifiPermissionsError() + } + } + + val getCurrentNetworkPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + scope.launch { + viewModel.getCurrentNetwork() + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions for getting current network info are denied") + viewModel.onGetCurrentNetworkPermissionsError() + } + } + + val getNetworkConnectionStatusPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + scope.launch { + viewModel.getNetworkConnectionStatus() + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions for getting network connection status are denied") + viewModel.onGetNetworkConnectionStatusPermissionError() + } + } + + val getSavedNetworksPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> + if (result.all { it.value }) { + scope.launch { + viewModel.getSavedNetworks() + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions for getting saved networks are denied") + viewModel.onGetSavedNetworksPermissionsError() + } + } + + val isWifiEnabledPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + scope.launch { + viewModel.isWifiEnabled() + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions for checking if wifi is enabled are denied") + viewModel.onIsWifiEnabledPermissionsError() + } + } + + val onMiscOptionClicked: (MiscScreenOption) -> Unit = { option -> + when (option) { + MiscScreenOption.CHANGE_NETWORK -> { + if (sdkUtil.isAtLeastQ()) { + scope.launch { + viewModel.changeNetwork(context = context) + } + } else { + viewModel.onChangeNetworkPreAndroidQ() + } + } + MiscScreenOption.DISABLE_WIFI -> { + disableWifiPermissionsLauncher.launch(CHANGE_WIFI_STATE) + } + MiscScreenOption.ENABLE_WIFI -> { + enableWifiPermissionsLauncher.launch(CHANGE_WIFI_STATE) + } + MiscScreenOption.GET_CURRENT_NETWORK -> { + getCurrentNetworkPermissionsLauncher.launch(ACCESS_NETWORK_STATE) + } + MiscScreenOption.GET_NEARBY_ACCESS_POINTS -> router.openNearbyAccessPointsScreen() + MiscScreenOption.GET_NETWORK_CONNECTION_STATUS -> { + getNetworkConnectionStatusPermissionsLauncher.launch(ACCESS_NETWORK_STATE) + } + MiscScreenOption.GET_SAVED_NETWORKS -> { + getSavedNetworksPermissionsLauncher.launch(arrayOf(ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE)) + } + MiscScreenOption.IS_WIFI_ENABLED -> { + isWifiEnabledPermissionsLauncher.launch(ACCESS_WIFI_STATE) + } + MiscScreenOption.SIGNAL_FUNCTIONS -> router.openSignalScreen() + } + } + + val listState = rememberLazyListState() + LazyColumn( + state = listState, + verticalArrangement = Arrangement.spacedBy(WisefySampleSizes.Large), + contentPadding = PaddingValues( + top = WisefySampleSizes.WisefySampleTopMargin, + bottom = WisefySampleSizes.WisefySampleBottomMargin, + start = WisefySampleSizes.WisefySampleHorizontalMargins, + end = WisefySampleSizes.WisefySampleHorizontalMargins + ) + ) { + items(MiscScreenOption.values(), { it.id }) { option -> + @OptIn(ExperimentalFoundationApi::class) + Row(modifier = Modifier.animateItemPlacement()) { + MiscScreenOptionRow(option = option, onClick = onMiscOptionClicked) + } + } + } + } +} + +internal enum class MiscScreenOption(val id: Long, @StringRes val stringResId: Int) { + CHANGE_NETWORK(R.id.change_network.toLong(), R.string.change_network), + DISABLE_WIFI(R.id.disable_wifi.toLong(), R.string.disable_wifi), + ENABLE_WIFI(R.id.enable_wifi.toLong(), R.string.enabled_wifi), + GET_CURRENT_NETWORK(R.id.get_current_network.toLong(), R.string.get_current_network), + GET_NEARBY_ACCESS_POINTS(R.id.get_nearby_access_points.toLong(), R.string.get_nearby_access_points), + GET_NETWORK_CONNECTION_STATUS(R.id.get_network_connection_status.toLong(), R.string.get_network_connection_status), + GET_SAVED_NETWORKS(R.id.get_saved_networks.toLong(), R.string.get_saved_networks), + IS_WIFI_ENABLED(R.id.is_wifi_enabled.toLong(), R.string.is_wifi_enabled), + SIGNAL_FUNCTIONS(R.id.signal_functions.toLong(), R.string.signal_functions) +} + +@Composable +private fun MiscScreenOptionRow(option: MiscScreenOption, onClick: (MiscScreenOption) -> Unit) { + WisefyPrimaryButton( + stringResId = option.stringResId, + onClick = { + onClick(option) + } + ) +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun MiscScreenContentLightPreview() { + MiscScreenContent( + viewModel = DefaultMiscViewModel( + wisefy = ComposablePreviewWisefy() + ), + sdkUtil = DefaultSdkUtil(), + router = DefaultMiscScreenRouter( + navController = rememberNavController() + ) + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun MiscScreenContentDarkPreview() { + MiscScreenContent( + viewModel = DefaultMiscViewModel( + wisefy = ComposablePreviewWisefy() + ), + sdkUtil = DefaultSdkUtil(), + router = DefaultMiscScreenRouter( + navController = rememberNavController() + ) + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscScreenDialogContent.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscScreenDialogContent.kt new file mode 100644 index 00000000..0d818add --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscScreenDialogContent.kt @@ -0,0 +1,310 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.networkconnection.entities.ChangeNetworkResult +import com.isupatches.android.wisefy.networkinfo.entities.GetNetworkConnectionStatusResult +import com.isupatches.android.wisefy.networkinfo.entities.NetworkConnectionStatusData +import com.isupatches.android.wisefy.networkinfo.entities.NetworkData +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleNoticeDialog +import com.isupatches.android.wisefy.wifi.entities.DisableWifiResult +import com.isupatches.android.wisefy.wifi.entities.EnableWifiResult + +@Composable +internal fun MiscScreenDialogContent( + dialogState: () -> MiscDialogState, + viewModel: MiscViewModel +) { + when (val currentDialogState = dialogState()) { + is MiscDialogState.None -> { + // No-op, no dialog + } + is MiscDialogState.Failure.WisefyAsync -> { + WisefySampleNoticeDialog( + title = R.string.wisefy_async_error, + body = R.string.wisefy_async_error_descriptions_args, + currentDialogState.exception.message ?: "", + currentDialogState.exception.cause?.message ?: "", + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.ChangeNetwork.Failure -> { + WisefySampleNoticeDialog( + title = R.string.change_network, + body = R.string.failure_changing_network_args, + currentDialogState.result, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.ChangeNetwork.PreAndroidQ -> { + WisefySampleNoticeDialog( + title = R.string.change_network, + body = R.string.change_network_called_pre_android_q, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.ChangeNetwork.Success -> { + WisefySampleNoticeDialog( + title = R.string.change_network, + body = R.string.success_changing_network_args, + currentDialogState.result, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.DisableWifi.Failure -> { + WisefySampleNoticeDialog( + title = R.string.disable_wifi, + body = R.string.failure_disabling_wifi_args, + currentDialogState.result, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.DisableWifi.PermissionsError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_disable_wifi, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.DisableWifi.Success -> { + WisefySampleNoticeDialog( + title = R.string.disable_wifi, + body = R.string.success_disabling_wifi_args, + currentDialogState.result, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.EnableWifi.Failure -> { + WisefySampleNoticeDialog( + title = R.string.enabled_wifi, + body = R.string.failure_enabling_wifi_args, + currentDialogState.result, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.EnableWifi.PermissionsError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_enable_wifi, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.EnableWifi.Success -> { + WisefySampleNoticeDialog( + title = R.string.enabled_wifi, + body = R.string.success_enabling_wifi_args, + currentDialogState.result, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.GetCurrentNetwork.Failure -> { + WisefySampleNoticeDialog( + title = R.string.get_current_network, + body = R.string.no_current_network, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.GetCurrentNetwork.PermissionsError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_get_current_network, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.GetCurrentNetwork.Success -> { + WisefySampleNoticeDialog( + title = R.string.get_current_network, + body = R.string.current_network_args, + currentDialogState.network, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.GetNetworkConnectionStatus.Success -> { + WisefySampleNoticeDialog( + title = R.string.get_network_connection_status, + body = R.string.network_connection_status_args, + currentDialogState.data, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.GetNetworkConnectionStatus.PermissionsError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_get_network_connection_status, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.GetSavedNetworks.Failure -> { + WisefySampleNoticeDialog( + title = R.string.get_saved_networks, + body = R.string.no_saved_networks_found, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.GetSavedNetworks.PermissionsError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_get_saved_networks, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.GetSavedNetworks.Success -> { + WisefySampleNoticeDialog( + title = R.string.get_saved_networks, + body = R.string.saved_network_args, + currentDialogState.savedNetworks, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.IsWifiEnabled.False -> { + WisefySampleNoticeDialog( + title = R.string.is_wifi_enabled, + body = R.string.wifi_is_disabled, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.IsWifiEnabled.PermissionsError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_is_wifi_enabled, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is MiscDialogState.IsWifiEnabled.True -> { + WisefySampleNoticeDialog( + title = R.string.is_wifi_enabled, + body = R.string.wifi_is_enabled, + onClose = { + viewModel.onDialogClosed() + } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun MiscScreenDialogContentLightPreview( + @PreviewParameter(MiscScreenDialogStatePreviewParameterProvider::class) dialogState: MiscDialogState +) { + MiscScreenDialogContent( + viewModel = DefaultMiscViewModel( + wisefy = ComposablePreviewWisefy() + ), + dialogState = { dialogState } + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun MiscScreenDialogContentDarkPreview( + @PreviewParameter(MiscScreenDialogStatePreviewParameterProvider::class) dialogState: MiscDialogState +) { + MiscScreenDialogContent( + viewModel = DefaultMiscViewModel( + wisefy = ComposablePreviewWisefy() + ), + dialogState = { dialogState } + ) +} + +private class MiscScreenDialogStatePreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + MiscDialogState.Failure.WisefyAsync(WisefyException("", null)), + MiscDialogState.ChangeNetwork.Failure(ChangeNetworkResult.Failure.Assertion("")), + MiscDialogState.ChangeNetwork.PreAndroidQ, + MiscDialogState.ChangeNetwork.Success(ChangeNetworkResult.Success.InternetConnectivityPanelOpened), + MiscDialogState.DisableWifi.Success(DisableWifiResult.Success.Disabled), + MiscDialogState.DisableWifi.Failure(DisableWifiResult.Failure.UnableToDisable), + MiscDialogState.DisableWifi.PermissionsError, + MiscDialogState.EnableWifi.Success(EnableWifiResult.Success.Enabled), + MiscDialogState.EnableWifi.Failure(EnableWifiResult.Failure.UnableToEnable), + MiscDialogState.EnableWifi.PermissionsError, + MiscDialogState.GetCurrentNetwork.Failure, + MiscDialogState.GetCurrentNetwork.PermissionsError, + MiscDialogState.GetCurrentNetwork.Success(NetworkData(null, null, null, null)), + MiscDialogState.GetNetworkConnectionStatus.Success( + GetNetworkConnectionStatusResult( + value = NetworkConnectionStatusData( + isConnected = false, + isConnectedToMobileNetwork = false, + isConnectedToWifiNetwork = false, + isRoaming = false, + ssidOfNetworkConnectedTo = null, + bssidOfNetworkConnectedTo = null, + ip = null + ) + ) + ), + MiscDialogState.GetNetworkConnectionStatus.PermissionsError, + MiscDialogState.GetSavedNetworks.Failure, + MiscDialogState.GetSavedNetworks.PermissionsError, + MiscDialogState.GetSavedNetworks.Success(emptyList()), + MiscDialogState.IsWifiEnabled.False, + MiscDialogState.IsWifiEnabled.True, + MiscDialogState.IsWifiEnabled.PermissionsError + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscScreenRouter.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscScreenRouter.kt new file mode 100644 index 00000000..7286cd49 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscScreenRouter.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc + +import androidx.navigation.NavHostController +import com.isupatches.android.wisefy.sample.ui.components.navigation.WisefySampleNavGraph + +internal interface MiscScreenRouter { + fun openNearbyAccessPointsScreen() + fun openSignalScreen() +} + +internal class DefaultMiscScreenRouter( + private val navController: NavHostController +) : MiscScreenRouter { + + override fun openNearbyAccessPointsScreen() { + navController.navigate(WisefySampleNavGraph.Misc.NearbyAccessPoints.route) + } + + override fun openSignalScreen() { + navController.navigate(WisefySampleNavGraph.Misc.Signal.route) + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscUIState.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscUIState.kt new file mode 100644 index 00000000..a98cc205 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscUIState.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc + +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.networkconnection.entities.ChangeNetworkResult +import com.isupatches.android.wisefy.networkinfo.entities.GetNetworkConnectionStatusResult +import com.isupatches.android.wisefy.networkinfo.entities.NetworkData +import com.isupatches.android.wisefy.savednetworks.entities.SavedNetworkData +import com.isupatches.android.wisefy.wifi.entities.DisableWifiResult +import com.isupatches.android.wisefy.wifi.entities.EnableWifiResult + +internal data class MiscUIState( + val loadingState: MiscLoadingState, + val dialogState: MiscDialogState +) + +internal data class MiscLoadingState( + val isLoading: Boolean = false +) + +internal sealed class MiscDialogState { + + object None : MiscDialogState() + + sealed class Failure : MiscDialogState() { + data class WisefyAsync(val exception: WisefyException) : Failure() + } + + sealed class ChangeNetwork : MiscDialogState() { + data class Success(val result: ChangeNetworkResult.Success) : ChangeNetwork() + data class Failure(val result: ChangeNetworkResult.Failure) : ChangeNetwork() + object PreAndroidQ : ChangeNetwork() + } + + sealed class DisableWifi : MiscDialogState() { + data class Success(val result: DisableWifiResult.Success) : DisableWifi() + data class Failure(val result: DisableWifiResult.Failure) : DisableWifi() + object PermissionsError : DisableWifi() + } + + sealed class EnableWifi : MiscDialogState() { + data class Success(val result: EnableWifiResult.Success) : EnableWifi() + data class Failure(val result: EnableWifiResult.Failure) : EnableWifi() + object PermissionsError : EnableWifi() + } + + sealed class GetCurrentNetwork : MiscDialogState() { + data class Success(val network: NetworkData) : GetCurrentNetwork() + object Failure : GetCurrentNetwork() + object PermissionsError : GetCurrentNetwork() + } + + sealed class GetNetworkConnectionStatus : MiscDialogState() { + data class Success(val data: GetNetworkConnectionStatusResult) : GetNetworkConnectionStatus() + object PermissionsError : GetNetworkConnectionStatus() + } + + sealed class GetSavedNetworks : MiscDialogState() { + data class Success(val savedNetworks: List) : GetSavedNetworks() + object Failure : GetSavedNetworks() + object PermissionsError : GetSavedNetworks() + } + + sealed class IsWifiEnabled : MiscDialogState() { + object True : IsWifiEnabled() + object False : IsWifiEnabled() + object PermissionsError : IsWifiEnabled() + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscViewModel.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscViewModel.kt new file mode 100644 index 00000000..f09e82fe --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/MiscViewModel.kt @@ -0,0 +1,394 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc + +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.Manifest.permission.ACCESS_NETWORK_STATE +import android.Manifest.permission.ACCESS_WIFI_STATE +import android.Manifest.permission.CHANGE_WIFI_STATE +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.annotation.RequiresPermission +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.ktx.changeNetworkAsync +import com.isupatches.android.wisefy.ktx.disableWifiAsync +import com.isupatches.android.wisefy.ktx.enableWifiAsync +import com.isupatches.android.wisefy.ktx.getCurrentNetworkAsync +import com.isupatches.android.wisefy.ktx.getNetworkConnectionStatusAsync +import com.isupatches.android.wisefy.ktx.getSavedNetworksAsync +import com.isupatches.android.wisefy.ktx.isWifiEnabledAsync +import com.isupatches.android.wisefy.networkconnection.entities.ChangeNetworkRequest +import com.isupatches.android.wisefy.networkconnection.entities.ChangeNetworkResult +import com.isupatches.android.wisefy.networkinfo.entities.GetCurrentNetworkResult +import com.isupatches.android.wisefy.networkinfo.entities.GetNetworkConnectionStatusResult +import com.isupatches.android.wisefy.sample.scaffolding.BaseViewModel +import com.isupatches.android.wisefy.sample.scaffolding.BaseViewModelFactory +import com.isupatches.android.wisefy.savednetworks.entities.GetSavedNetworksResult +import com.isupatches.android.wisefy.wifi.entities.DisableWifiRequest +import com.isupatches.android.wisefy.wifi.entities.DisableWifiResult +import com.isupatches.android.wisefy.wifi.entities.EnableWifiRequest +import com.isupatches.android.wisefy.wifi.entities.EnableWifiResult +import com.isupatches.android.wisefy.wifi.entities.IsWifiEnabledResult + +internal abstract class MiscViewModel : BaseViewModel() { + abstract val uiState: State + + abstract fun onDialogClosed() + + abstract suspend fun changeNetwork(context: Context) + + @RequiresPermission(CHANGE_WIFI_STATE) + abstract suspend fun disableWifi(request: DisableWifiRequest) + + @RequiresPermission(CHANGE_WIFI_STATE) + abstract suspend fun enableWifi(request: EnableWifiRequest) + + @RequiresPermission(ACCESS_NETWORK_STATE) + abstract suspend fun getCurrentNetwork() + + @RequiresPermission(ACCESS_NETWORK_STATE) + abstract suspend fun getNetworkConnectionStatus() + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) + abstract suspend fun getSavedNetworks() + + @RequiresPermission(ACCESS_WIFI_STATE) + abstract suspend fun isWifiEnabled() + + abstract fun onChangeNetworkPreAndroidQ() + + abstract fun onDisableWifiPermissionsError() + abstract fun onEnableWifiPermissionsError() + abstract fun onGetCurrentNetworkPermissionsError() + abstract fun onGetNetworkConnectionStatusPermissionError() + abstract fun onGetSavedNetworksPermissionsError() + abstract fun onIsWifiEnabledPermissionsError() +} + +internal class DefaultMiscViewModel(private val wisefy: WisefyApi) : MiscViewModel() { + + private val _uiState = mutableStateOf( + MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.None + ) + ) + override val uiState: State + get() = _uiState + + @RequiresApi(Build.VERSION_CODES.Q) + override suspend fun changeNetwork(context: Context) { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = true), + dialogState = MiscDialogState.None + ) + + val result = try { + wisefy.changeNetworkAsync(request = ChangeNetworkRequest(context = context)) + } catch (ex: WisefyException) { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.Failure.WisefyAsync(exception = ex) + ) + null + } + + when (result) { + is ChangeNetworkResult.Success -> { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.ChangeNetwork.Success(result) + ) + } + is ChangeNetworkResult.Failure -> { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.ChangeNetwork.Failure(result) + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + @RequiresPermission(CHANGE_WIFI_STATE) + override suspend fun disableWifi(request: DisableWifiRequest) { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = true), + dialogState = MiscDialogState.None + ) + + val result = try { + wisefy.disableWifiAsync(request = request) + } catch (ex: WisefyException) { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.Failure.WisefyAsync(exception = ex) + ) + null + } + + when (result) { + is DisableWifiResult.Success -> { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.DisableWifi.Success(result) + ) + } + is DisableWifiResult.Failure -> { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.DisableWifi.Failure(result) + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + @RequiresPermission(CHANGE_WIFI_STATE) + override suspend fun enableWifi(request: EnableWifiRequest) { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = true), + dialogState = MiscDialogState.None + ) + val result = try { + wisefy.enableWifiAsync(request) + } catch (ex: WisefyException) { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.Failure.WisefyAsync(exception = ex) + ) + null + } + + when (result) { + is EnableWifiResult.Success -> { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.EnableWifi.Success(result) + ) + } + is EnableWifiResult.Failure -> { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.EnableWifi.Failure(result) + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + @RequiresPermission(ACCESS_NETWORK_STATE) + override suspend fun getCurrentNetwork() { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = true), + dialogState = MiscDialogState.None + ) + val result = try { + wisefy.getCurrentNetworkAsync() + } catch (ex: WisefyException) { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.Failure.WisefyAsync(exception = ex) + ) + null + } + + when (result) { + is GetCurrentNetworkResult -> { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.GetCurrentNetwork.Success(network = result.value) + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + @RequiresPermission(ACCESS_NETWORK_STATE) + override suspend fun getNetworkConnectionStatus() { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = true), + dialogState = MiscDialogState.None + ) + + val result = try { + wisefy.getNetworkConnectionStatusAsync() + } catch (ex: WisefyException) { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.Failure.WisefyAsync(exception = ex) + ) + null + } + + when (result) { + is GetNetworkConnectionStatusResult -> { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.GetNetworkConnectionStatus.Success(data = result) + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) + override suspend fun getSavedNetworks() { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = true), + dialogState = MiscDialogState.None + ) + val result = try { + wisefy.getSavedNetworksAsync() + } catch (ex: WisefyException) { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.Failure.WisefyAsync(exception = ex) + ) + null + } + + when (result) { + is GetSavedNetworksResult.SavedNetworks -> { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.GetSavedNetworks.Success(savedNetworks = result.value) + ) + } + is GetSavedNetworksResult.Empty -> { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.GetSavedNetworks.Failure + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + @RequiresPermission(ACCESS_WIFI_STATE) + override suspend fun isWifiEnabled() { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = true), + dialogState = MiscDialogState.None + ) + val result = try { + wisefy.isWifiEnabledAsync() + } catch (ex: WisefyException) { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.Failure.WisefyAsync(exception = ex) + ) + null + } + + when (result) { + is IsWifiEnabledResult.True -> { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.IsWifiEnabled.True + ) + } + is IsWifiEnabledResult.False -> { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.IsWifiEnabled.False + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + override fun onDialogClosed() { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.None + ) + } + + override fun onChangeNetworkPreAndroidQ() { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.ChangeNetwork.PreAndroidQ + ) + } + + override fun onDisableWifiPermissionsError() { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.DisableWifi.PermissionsError + ) + } + + override fun onEnableWifiPermissionsError() { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.EnableWifi.PermissionsError + ) + } + + override fun onGetCurrentNetworkPermissionsError() { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.GetCurrentNetwork.PermissionsError + ) + } + + override fun onGetNetworkConnectionStatusPermissionError() { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.GetNetworkConnectionStatus.PermissionsError + ) + } + + override fun onGetSavedNetworksPermissionsError() { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.GetSavedNetworks.PermissionsError + ) + } + + override fun onIsWifiEnabledPermissionsError() { + _uiState.value = MiscUIState( + loadingState = MiscLoadingState(isLoading = false), + dialogState = MiscDialogState.IsWifiEnabled.PermissionsError + ) + } +} + +internal class MiscViewModelFactory( + private val wisefy: WisefyApi +) : BaseViewModelFactory( + supportedClass = MiscViewModel::class, + vmProvider = { DefaultMiscViewModel(wisefy) } +) diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsScreen.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsScreen.kt new file mode 100644 index 00000000..b0dfa244 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsScreen.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc.nearbyaccesspoints + +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.Manifest.permission.ACCESS_WIFI_STATE +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.lifecycle.viewmodel.compose.viewModel +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.sample.logging.WisefySampleLogger +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleLoadingIndicator +import kotlinx.coroutines.launch + +private const val LOG_TAG = "NearbyAccessPointsScreen" + +@Composable +internal fun NearbyAccessPointsScreen( + wisefy: WisefyApi, + viewModel: NearbyAccessPointsViewModel = viewModel(factory = NearbyAccessPointsViewModelFactory(wisefy)) +) { + val scope = rememberCoroutineScope() + + val getNearbyAccessPointsPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> + if (result.all { it.value }) { + scope.launch { + viewModel.getNearbyAccessPoints() + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions for getting nearby access points are denied") + viewModel.onGetNearbyAccessPointsPermissionError() + } + } + + SideEffect { + getNearbyAccessPointsPermissionsLauncher.launch(arrayOf(ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE)) + } + + WisefySampleLoadingIndicator(isLoading = { viewModel.uiState.value.loadingState.isLoading }) + NearbyAccessPointsScreenDialogContent(dialogState = { viewModel.uiState.value.dialogState }, viewModel = viewModel) + NearbyAccessPointsScreenContent(accessPoints = { viewModel.uiState.value.accessPointUIData }) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsScreenContent.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsScreenContent.kt new file mode 100644 index 00000000..9637e0df --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsScreenContent.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc.nearbyaccesspoints + +import android.content.res.Configuration +import android.net.wifi.ScanResult +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.isupatches.android.wisefy.accesspoints.entities.AccessPointData +import com.isupatches.android.wisefy.accesspoints.entities.SecurityCapability +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleBodyLabel +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleSubHeaderLabel +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleSizes +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme + +@Composable +internal fun NearbyAccessPointsScreenContent( + accessPoints: () -> List +) { + WisefySampleTheme { + val listState = rememberLazyListState() + LazyColumn( + state = listState, + verticalArrangement = Arrangement.spacedBy(WisefySampleSizes.Large), + contentPadding = PaddingValues( + top = WisefySampleSizes.WisefySampleTopMargin, + bottom = WisefySampleSizes.WisefySampleBottomMargin, + start = WisefySampleSizes.WisefySampleHorizontalMargins, + end = WisefySampleSizes.WisefySampleHorizontalMargins + ) + ) { + items(accessPoints()) { accessPoint -> + @OptIn(ExperimentalFoundationApi::class) + Row(modifier = Modifier.animateItemPlacement()) { + AccessPointRow(accessPoint = accessPoint) + } + } + } + } +} + +@Composable +private fun AccessPointRow(accessPoint: AccessPointUIData) { + WisefySampleTheme { + Column { + Row { + WisefySampleSubHeaderLabel( + stringResId = R.string.access_point_header_args, + modifier = Modifier, + accessPoint.accessPoint.ssid, + accessPoint.accessPoint.bssid + ) + } + Row { + WisefySampleBodyLabel( + stringResId = R.string.access_point_is_saved_by_ssid_args, + modifier = Modifier.padding(top = WisefySampleSizes.Large), + accessPoint.isSavedBySSID + ) + } + Row { + WisefySampleBodyLabel( + stringResId = R.string.access_point_is_saved_by_bssid_args, + modifier = Modifier.padding(top = WisefySampleSizes.Medium), + accessPoint.isSavedByBSSID + ) + } + Row { + WisefySampleBodyLabel( + stringResId = R.string.access_point_security_capabilities_args, + modifier = Modifier.padding(top = WisefySampleSizes.Medium), + accessPoint.securityCapabilities + ) + } + Row { + WisefySampleBodyLabel( + stringResId = R.string.access_point_raw_value_args, + modifier = Modifier.padding(top = WisefySampleSizes.Medium), + accessPoint.accessPoint + ) + } + } + } +} + +@RequiresApi(Build.VERSION_CODES.R) +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun NearbyAccessPointsScreenContentLightPreview() { + NearbyAccessPointsScreenContent( + accessPoints = { + listOf( + AccessPointUIData( + accessPoint = AccessPointData( + rawValue = ScanResult().also { + it.capabilities = "" + }, + ssid = "", + bssid = "", + frequency = 4900 + ), + isSavedBySSID = false, + isSavedByBSSID = false, + securityCapabilities = SecurityCapability.ALL.associateWith { + false + } + ) + ) + } + ) +} + +@RequiresApi(Build.VERSION_CODES.R) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun NearbyAccessPointsScreenContentDarkPreview() { + NearbyAccessPointsScreenContent( + accessPoints = { + listOf( + AccessPointUIData( + accessPoint = AccessPointData( + rawValue = ScanResult().also { + it.capabilities = "" + }, + ssid = "", + bssid = "", + frequency = 4900 + ), + isSavedBySSID = false, + isSavedByBSSID = false, + securityCapabilities = SecurityCapability.ALL.associateWith { + false + } + ) + ) + } + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsScreenDialogContent.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsScreenDialogContent.kt new file mode 100644 index 00000000..ed937c39 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsScreenDialogContent.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc.nearbyaccesspoints + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleNoticeDialog +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme + +@Composable +internal fun NearbyAccessPointsScreenDialogContent( + dialogState: () -> NearbyAccessPointsDialogState, + viewModel: NearbyAccessPointsViewModel +) { + WisefySampleTheme { + when (val currentDialogState = dialogState()) { + is NearbyAccessPointsDialogState.None -> { + // No-op, no dialog + } + is NearbyAccessPointsDialogState.GetNearbyAccessPoints.PermissionsError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_get_nearby_access_points, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is NearbyAccessPointsDialogState.Failure.WisefyAsync -> { + WisefySampleNoticeDialog( + title = R.string.wisefy_async_error, + body = R.string.wisefy_async_error_descriptions_args, + currentDialogState.exception.message ?: "", + currentDialogState.exception.cause?.message ?: "", + onClose = { + viewModel.onDialogClosed() + } + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun AddNetworkScreenDialogContentLightPreview( + @PreviewParameter(NearbyAccessPointsDialogStatePreviewParameterProvider::class) + dialogState: NearbyAccessPointsDialogState +) { + NearbyAccessPointsScreenDialogContent( + viewModel = DefaultNearbyAccessPointsViewModel( + wisefy = ComposablePreviewWisefy() + ), + dialogState = { dialogState } + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun AddNetworkScreenDialogContentDarkPreview( + @PreviewParameter(NearbyAccessPointsDialogStatePreviewParameterProvider::class) + dialogState: NearbyAccessPointsDialogState +) { + NearbyAccessPointsScreenDialogContent( + viewModel = DefaultNearbyAccessPointsViewModel( + wisefy = ComposablePreviewWisefy() + ), + dialogState = { dialogState } + ) +} + +private class NearbyAccessPointsDialogStatePreviewParameterProvider : + PreviewParameterProvider { + override val values: Sequence = sequenceOf( + NearbyAccessPointsDialogState.Failure.WisefyAsync(WisefyException("", null)), + NearbyAccessPointsDialogState.GetNearbyAccessPoints.PermissionsError + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsUIState.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsUIState.kt new file mode 100644 index 00000000..03af0146 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsUIState.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc.nearbyaccesspoints + +import com.isupatches.android.wisefy.core.exceptions.WisefyException + +internal data class NearbyAccessPointsUIState( + val loadingState: NearbyAccessPointsLoadingState, + val dialogState: NearbyAccessPointsDialogState, + val accessPointUIData: List +) + +internal data class NearbyAccessPointsLoadingState( + val isLoading: Boolean = false +) + +internal sealed class NearbyAccessPointsDialogState { + object None : NearbyAccessPointsDialogState() + + sealed class Failure : NearbyAccessPointsDialogState() { + data class WisefyAsync(val exception: WisefyException) : Failure() + } + + sealed class GetNearbyAccessPoints : NearbyAccessPointsDialogState() { + object PermissionsError : GetNearbyAccessPoints() + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsViewModel.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsViewModel.kt new file mode 100644 index 00000000..48dc7340 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/nearbyaccesspoints/NearbyAccessPointsViewModel.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc.nearbyaccesspoints + +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.Manifest.permission.ACCESS_WIFI_STATE +import androidx.annotation.RequiresPermission +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.accesspoints.entities.AccessPointData +import com.isupatches.android.wisefy.accesspoints.entities.GetAccessPointsQuery +import com.isupatches.android.wisefy.accesspoints.entities.GetAccessPointsResult +import com.isupatches.android.wisefy.accesspoints.entities.SecurityCapability +import com.isupatches.android.wisefy.accesspoints.entities.containSecurityCapability +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.ktx.isNetworkSavedAsync +import com.isupatches.android.wisefy.sample.scaffolding.BaseViewModel +import com.isupatches.android.wisefy.sample.scaffolding.BaseViewModelFactory +import com.isupatches.android.wisefy.savednetworks.entities.IsNetworkSavedQuery +import com.isupatches.android.wisefy.savednetworks.entities.IsNetworkSavedResult + +internal abstract class NearbyAccessPointsViewModel : BaseViewModel() { + abstract val uiState: State + + abstract fun onDialogClosed() + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) + abstract suspend fun getNearbyAccessPoints() + + abstract fun onGetNearbyAccessPointsPermissionError() +} + +internal class DefaultNearbyAccessPointsViewModel( + private val wisefy: WisefyApi +) : NearbyAccessPointsViewModel() { + + private val _uiState = mutableStateOf( + NearbyAccessPointsUIState( + loadingState = NearbyAccessPointsLoadingState(isLoading = false), + dialogState = NearbyAccessPointsDialogState.None, + accessPointUIData = emptyList() + ) + ) + + override val uiState: State + get() = _uiState + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) + override suspend fun getNearbyAccessPoints() { + _uiState.value = _uiState.value.copy( + loadingState = NearbyAccessPointsLoadingState(isLoading = true), + dialogState = NearbyAccessPointsDialogState.None + ) + try { + when (val result = wisefy.getAccessPoints(GetAccessPointsQuery.All())) { + is GetAccessPointsResult.Empty -> { + _uiState.value = NearbyAccessPointsUIState( + loadingState = NearbyAccessPointsLoadingState(isLoading = false), + dialogState = NearbyAccessPointsDialogState.None, + accessPointUIData = emptyList() + ) + } + is GetAccessPointsResult.AccessPoints -> { + val accessPointUIData = result.value.map { accessPoint -> + val isSavedBySSIDResult = wisefy.isNetworkSavedAsync( + query = IsNetworkSavedQuery.SSID(accessPoint.ssid) + ) + val isSavedBySSID = when (isSavedBySSIDResult) { + IsNetworkSavedResult.True -> true + IsNetworkSavedResult.False -> false + } + val isSavedByBSSIDResult = wisefy.isNetworkSavedAsync( + query = IsNetworkSavedQuery.BSSID(accessPoint.bssid) + ) + val isSavedByBSSID = when (isSavedByBSSIDResult) { + IsNetworkSavedResult.True -> true + IsNetworkSavedResult.False -> false + } + val securityCapabilityMap = mutableMapOf() + SecurityCapability.ALL.forEach { securityCapability -> + securityCapabilityMap[securityCapability] = + accessPoint.containSecurityCapability(securityCapability) + } + AccessPointUIData( + accessPoint = accessPoint, + isSavedBySSID = isSavedBySSID, + isSavedByBSSID = isSavedByBSSID, + securityCapabilities = securityCapabilityMap + ) + } + _uiState.value = NearbyAccessPointsUIState( + loadingState = NearbyAccessPointsLoadingState(isLoading = false), + dialogState = NearbyAccessPointsDialogState.None, + accessPointUIData = accessPointUIData + ) + } + } + } catch (ex: WisefyException) { + _uiState.value = NearbyAccessPointsUIState( + loadingState = NearbyAccessPointsLoadingState(isLoading = false), + dialogState = NearbyAccessPointsDialogState.Failure.WisefyAsync(exception = ex), + accessPointUIData = emptyList() + ) + } + } + + override fun onGetNearbyAccessPointsPermissionError() { + _uiState.value = _uiState.value.copy( + loadingState = NearbyAccessPointsLoadingState(isLoading = false), + dialogState = NearbyAccessPointsDialogState.GetNearbyAccessPoints.PermissionsError + ) + } + + override fun onDialogClosed() { + _uiState.value = _uiState.value.copy( + loadingState = NearbyAccessPointsLoadingState(isLoading = false), + dialogState = NearbyAccessPointsDialogState.None + ) + } +} + +internal class NearbyAccessPointsViewModelFactory( + private val wisefy: WisefyApi +) : BaseViewModelFactory( + supportedClass = NearbyAccessPointsViewModel::class, + vmProvider = { DefaultNearbyAccessPointsViewModel(wisefy) } +) + +internal data class AccessPointUIData( + val accessPoint: AccessPointData, + val isSavedBySSID: Boolean, + val isSavedByBSSID: Boolean, + val securityCapabilities: Map +) diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalScreen.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalScreen.kt new file mode 100644 index 00000000..1b6cce53 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalScreen.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc.signal + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleLoadingIndicator +import com.isupatches.android.wisefy.sample.util.DefaultSdkUtil +import com.isupatches.android.wisefy.sample.util.SdkUtil + +@Composable +internal fun SignalScreen( + wisefy: WisefyApi, + sdkUtil: SdkUtil, + viewModel: SignalViewModel = viewModel( + factory = SignalViewModelFactory( + context = LocalContext.current.applicationContext, + wisefy = wisefy, + sdkUtil = sdkUtil + ) + ) +) { + WisefySampleLoadingIndicator(isLoading = { viewModel.uiState.value.loadingState.isLoading }) + SignalScreenDialogContent(dialogState = { viewModel.uiState.value.dialogState }, viewModel = viewModel) + SignalScreenContent(viewModel = viewModel) +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun SignalScreenLightPreview() { + SignalScreen( + wisefy = ComposablePreviewWisefy(), + sdkUtil = DefaultSdkUtil() + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun SignalScreenDarkPreview() { + SignalScreen( + wisefy = ComposablePreviewWisefy(), + sdkUtil = DefaultSdkUtil() + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalScreenContent.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalScreenContent.kt new file mode 100644 index 00000000..24466c71 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalScreenContent.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc.signal + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefyPrimaryButton +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleEditTextError +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleNumericalEditText +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleSizes +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme +import com.isupatches.android.wisefy.sample.util.DefaultSdkUtil + +@Composable +internal fun SignalScreenContent( + viewModel: SignalViewModel +) { + WisefySampleTheme { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding( + top = WisefySampleSizes.WisefySampleTopMargin, + bottom = WisefySampleSizes.WisefySampleBottomMargin, + start = WisefySampleSizes.WisefySampleHorizontalMargins, + end = WisefySampleSizes.WisefySampleHorizontalMargins + ) + ) { + SignalScreenCalculateSignalLevelInputRows( + inputState = { viewModel.uiState.value.inputState.calculateSignalLevelInputState }, + viewModel = viewModel + ) + Row(modifier = Modifier.padding(top = WisefySampleSizes.Large, bottom = WisefySampleSizes.Large)) { + WisefyPrimaryButton(stringResId = R.string.calculate_signal_level) { + viewModel.calculateSignalLevel() + } + } + SignalScreenCompareSignalLevelInputRows( + inputState = { viewModel.uiState.value.inputState.compareSignalLevelInputState }, + viewModel = viewModel + ) + Row(modifier = Modifier.padding(top = WisefySampleSizes.Large)) { + WisefyPrimaryButton(stringResId = R.string.compare_signal_level) { + viewModel.compareSignalLevel() + } + } + } + } +} + +@Composable +private fun SignalScreenCalculateSignalLevelInputRows( + inputState: () -> CalculateSignalLevelInputState, + viewModel: SignalViewModel +) { + val currentInputState = inputState() + Row { + WisefySampleNumericalEditText( + text = currentInputState.rssiLevelInput, + onTextChange = { newText -> + viewModel.onCalculateSignalLevelInputChanged(newText) + }, + labelResId = R.string.rssi_value, + error = when (currentInputState.validityState) { + is SignalInputValidityState.CalculateSignalLevel.Invalid.Empty -> { + WisefySampleEditTextError(R.string.rssi_input_empty) + } + is SignalInputValidityState.CalculateSignalLevel.Invalid.NotAnInt -> { + WisefySampleEditTextError(R.string.rssi_input_invalid_int) + } + is SignalInputValidityState.CalculateSignalLevel.Valid -> null + } + ) + } +} + +@Composable +private fun SignalScreenCompareSignalLevelInputRows( + inputState: () -> CompareSignalLevelInputState, + viewModel: SignalViewModel +) { + val currentInputState = inputState() + Row { + WisefySampleNumericalEditText( + text = currentInputState.rssi1InputState.rssiLevelInput, + onTextChange = { newText -> + viewModel.onCompareSignalLevelRSSI1InputChanged(newText) + }, + labelResId = R.string.rssi1_value, + error = when (currentInputState.rssi1InputState.validityState) { + is SignalInputValidityState.CompareSignalLevel.Invalid.Empty -> { + WisefySampleEditTextError(R.string.rssi_input_empty) + } + is SignalInputValidityState.CompareSignalLevel.Invalid.NotAnInt -> { + WisefySampleEditTextError(R.string.rssi_input_invalid_int) + } + is SignalInputValidityState.CompareSignalLevel.Valid -> null + } + ) + } + Row { + WisefySampleNumericalEditText( + text = currentInputState.rssi2InputState.rssiLevelInput, + onTextChange = { newText -> + viewModel.onCompareSignalLevelRSSI2InputChanged(newText) + }, + labelResId = R.string.rssi2_value, + error = when (currentInputState.rssi2InputState.validityState) { + is SignalInputValidityState.CompareSignalLevel.Invalid.Empty -> { + WisefySampleEditTextError(R.string.rssi_input_empty) + } + is SignalInputValidityState.CompareSignalLevel.Invalid.NotAnInt -> { + WisefySampleEditTextError(R.string.rssi_input_invalid_int) + } + is SignalInputValidityState.CompareSignalLevel.Valid -> null + } + ) + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun SignalScreenContentLightPreview() { + SignalScreenContent( + viewModel = DefaultSignalViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy(), + sdkUtil = DefaultSdkUtil() + ) + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun SignalScreenContentDarkPreview() { + SignalScreenContent( + viewModel = DefaultSignalViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy(), + sdkUtil = DefaultSdkUtil() + ) + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalScreenDialogContent.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalScreenDialogContent.kt new file mode 100644 index 00000000..b01b5f93 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalScreenDialogContent.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc.signal + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleNoticeDialog +import com.isupatches.android.wisefy.sample.util.DefaultSdkUtil +import com.isupatches.android.wisefy.signal.entities.CalculateSignalLevelResult +import com.isupatches.android.wisefy.signal.entities.CompareSignalLevelResult + +@Composable +internal fun SignalScreenDialogContent( + dialogState: () -> SignalDialogState, + viewModel: SignalViewModel +) { + when (val currentDialogState = dialogState()) { + is SignalDialogState.None -> { + // No-op, no dialog + } + is SignalDialogState.CalculateSignalLevel.Failure -> { + WisefySampleNoticeDialog( + title = R.string.calculate_signal_level, + body = R.string.failure_calculating_signal_level_args, + currentDialogState.result, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is SignalDialogState.CalculateSignalLevel.Success -> { + WisefySampleNoticeDialog( + title = R.string.calculate_signal_level, + body = R.string.success_calculating_signal_level_args, + currentDialogState.result, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is SignalDialogState.CompareSignalLevel.Success -> { + WisefySampleNoticeDialog( + title = R.string.compare_signal_level, + body = R.string.success_comparing_signal_level_args, + currentDialogState.result, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is SignalDialogState.Failure.WisefyAsync -> { + WisefySampleNoticeDialog( + title = R.string.wisefy_async_error, + body = R.string.wisefy_async_error_descriptions_args, + currentDialogState.exception.message ?: "", + currentDialogState.exception.cause?.message ?: "", + onClose = { + viewModel.onDialogClosed() + } + ) + } + is SignalDialogState.InputError.CalculateSignalLevel -> { + WisefySampleNoticeDialog( + title = R.string.input_error, + body = R.string.rssi_input_invalid, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is SignalDialogState.InputError.CompareSignalLevel.RSSI1 -> { + WisefySampleNoticeDialog( + title = R.string.input_error, + body = R.string.rssi_input_invalid, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is SignalDialogState.InputError.CompareSignalLevel.RSSI2 -> { + WisefySampleNoticeDialog( + title = R.string.input_error, + body = R.string.rssi_input_invalid, + onClose = { + viewModel.onDialogClosed() + } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun SignalScreenDialogContentLightPreview( + @PreviewParameter(SignalScreenDialogStatePreviewParameterProvider::class) dialogState: SignalDialogState +) { + SignalScreenDialogContent( + viewModel = DefaultSignalViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy(), + sdkUtil = DefaultSdkUtil() + ), + dialogState = { dialogState } + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun SignalScreenDialogContentDarkPreview( + @PreviewParameter(SignalScreenDialogStatePreviewParameterProvider::class) dialogState: SignalDialogState +) { + SignalScreenDialogContent( + viewModel = DefaultSignalViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy(), + sdkUtil = DefaultSdkUtil() + ), + dialogState = { dialogState } + ) +} + +private class SignalScreenDialogStatePreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + SignalDialogState.Failure.WisefyAsync(WisefyException("", null)), + SignalDialogState.CalculateSignalLevel.Success(CalculateSignalLevelResult.Success(0)), + SignalDialogState.CalculateSignalLevel.Failure(CalculateSignalLevelResult.Failure.Assertion("")), + SignalDialogState.CompareSignalLevel.Success(CompareSignalLevelResult.Success.RSSIValuesAreEqual(0)), + SignalDialogState.InputError.CalculateSignalLevel, + SignalDialogState.InputError.CompareSignalLevel.RSSI1, + SignalDialogState.InputError.CompareSignalLevel.RSSI2 + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalStore.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalStore.kt new file mode 100644 index 00000000..13224414 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalStore.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc.signal + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private const val PREF_CALCULATE_SIGNAL_LEVEL_RSSI = "calculate_signal_level_rssi" +private const val PREF_COMPARE_SIGNAL_LEVEL_RSSI_1 = "compare_signal_level_rssi_1" +private const val PREF_COMPARE_SIGNAL_LEVEL_RSSI_2 = "compare_signal_level_rssi_2" + +internal interface SignalStore { + suspend fun clear() + + fun getCalculateSignalLevelRSSI(): Flow + + fun getCompareSignalLevelRSSI1(): Flow + fun getCompareSignalLevelRSSI2(): Flow + + suspend fun setCalculateSignalLevelRSSI(rssiLevel: String) + + suspend fun setCompareSignalLevelRSSI1(rssiLevel: String) + suspend fun setCompareSignalLevelRSSI2(rssiLevel: String) +} + +private val Context.signalDataStore: DataStore by preferencesDataStore( + name = "signalDataStore" +) + +internal class DefaultSignalStore( + private val context: Context +) : SignalStore { + + private val calculateSignalLevelRSSIKey = stringPreferencesKey(PREF_CALCULATE_SIGNAL_LEVEL_RSSI) + private val compareSignalLevelRSSI1Key = stringPreferencesKey(PREF_COMPARE_SIGNAL_LEVEL_RSSI_1) + private val compareSignalLevelRSSI2Key = stringPreferencesKey(PREF_COMPARE_SIGNAL_LEVEL_RSSI_2) + + override suspend fun clear() { + context.signalDataStore.edit { + clear() + } + } + + override fun getCalculateSignalLevelRSSI(): Flow { + return context.signalDataStore.data.map { preferences -> + preferences[calculateSignalLevelRSSIKey] ?: "" + } + } + + override suspend fun setCalculateSignalLevelRSSI(rssiLevel: String) { + context.signalDataStore.edit { preferences -> + preferences[calculateSignalLevelRSSIKey] = rssiLevel + } + } + + override fun getCompareSignalLevelRSSI1(): Flow { + return context.signalDataStore.data.map { preferences -> + preferences[compareSignalLevelRSSI1Key] ?: "" + } + } + + override suspend fun setCompareSignalLevelRSSI1(rssiLevel: String) { + context.signalDataStore.edit { preferences -> + preferences[compareSignalLevelRSSI1Key] = rssiLevel + } + } + + override fun getCompareSignalLevelRSSI2(): Flow { + return context.signalDataStore.data.map { preferences -> + preferences[compareSignalLevelRSSI2Key] ?: "" + } + } + + override suspend fun setCompareSignalLevelRSSI2(rssiLevel: String) { + context.signalDataStore.edit { preferences -> + preferences[compareSignalLevelRSSI2Key] = rssiLevel + } + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalUIState.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalUIState.kt new file mode 100644 index 00000000..9eb46ff7 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalUIState.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc.signal + +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.signal.entities.CalculateSignalLevelResult +import com.isupatches.android.wisefy.signal.entities.CompareSignalLevelResult + +internal data class SignalUIState( + val loadingState: SignalLoadingState, + val dialogState: SignalDialogState, + val inputState: SignalInputState +) + +internal data class SignalLoadingState( + val isLoading: Boolean = false +) + +internal sealed class SignalDialogState { + object None : SignalDialogState() + + sealed class InputError : SignalDialogState() { + + object CalculateSignalLevel : InputError() + + sealed class CompareSignalLevel : InputError() { + object RSSI1 : CompareSignalLevel() + object RSSI2 : CompareSignalLevel() + } + } + + sealed class Failure : SignalDialogState() { + data class WisefyAsync(val exception: WisefyException) : Failure() + } + + sealed class CalculateSignalLevel : SignalDialogState() { + data class Success(val result: CalculateSignalLevelResult.Success) : CalculateSignalLevel() + data class Failure(val result: CalculateSignalLevelResult.Failure) : CalculateSignalLevel() + } + + sealed class CompareSignalLevel : SignalDialogState() { + data class Success(val result: CompareSignalLevelResult.Success) : CalculateSignalLevel() + } +} + +internal data class SignalInputState( + val calculateSignalLevelInputState: CalculateSignalLevelInputState, + val compareSignalLevelInputState: CompareSignalLevelInputState +) + +internal data class CalculateSignalLevelInputState( + val rssiLevelInput: String, + val validityState: SignalInputValidityState.CalculateSignalLevel +) + +internal data class CompareSignalLevelInputState( + val rssi1InputState: CompareSignalLevelRSSIInputState, + val rssi2InputState: CompareSignalLevelRSSIInputState +) + +internal data class CompareSignalLevelRSSIInputState( + val rssiLevelInput: String, + val validityState: SignalInputValidityState.CompareSignalLevel +) + +internal sealed class SignalInputValidityState { + + sealed class CalculateSignalLevel : SignalInputValidityState() { + object Valid : CalculateSignalLevel() + + sealed class Invalid : CalculateSignalLevel() { + object Empty : Invalid() + object NotAnInt : Invalid() + } + } + + sealed class CompareSignalLevel : SignalInputValidityState() { + object Valid : CompareSignalLevel() + + sealed class Invalid : CompareSignalLevel() { + object Empty : Invalid() + object NotAnInt : Invalid() + } + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalViewModel.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalViewModel.kt new file mode 100644 index 00000000..bd55a32e --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/misc/signal/SignalViewModel.kt @@ -0,0 +1,306 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.misc.signal + +import android.content.Context +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.viewModelScope +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.sample.scaffolding.BaseViewModel +import com.isupatches.android.wisefy.sample.scaffolding.BaseViewModelFactory +import com.isupatches.android.wisefy.sample.util.SdkUtil +import com.isupatches.android.wisefy.signal.entities.CalculateSignalLevelRequest +import com.isupatches.android.wisefy.signal.entities.CalculateSignalLevelResult +import com.isupatches.android.wisefy.signal.entities.CompareSignalLevelRequest +import com.isupatches.android.wisefy.signal.entities.CompareSignalLevelResult +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +internal abstract class SignalViewModel : BaseViewModel() { + abstract val uiState: State + + abstract fun calculateSignalLevel() + + abstract fun compareSignalLevel() + + abstract fun onCalculateSignalLevelInputChanged(input: String) + abstract fun onCompareSignalLevelRSSI1InputChanged(input: String) + abstract fun onCompareSignalLevelRSSI2InputChanged(input: String) + + abstract fun onDialogClosed() +} + +private const val TARGET_NUMBER_OF_SIGNAL_LEVELS = 4 + +internal class DefaultSignalViewModel( + context: Context, + private val wisefy: WisefyApi, + private val sdkUtil: SdkUtil, + private val signalStore: SignalStore = DefaultSignalStore(context = context) +) : SignalViewModel() { + + private val _uiState = mutableStateOf( + SignalUIState( + loadingState = SignalLoadingState(isLoading = false), + dialogState = SignalDialogState.None, + inputState = SignalInputState( + calculateSignalLevelInputState = CalculateSignalLevelInputState( + rssiLevelInput = "", + validityState = SignalInputValidityState.CalculateSignalLevel.Invalid.Empty + ), + compareSignalLevelInputState = CompareSignalLevelInputState( + rssi1InputState = CompareSignalLevelRSSIInputState( + rssiLevelInput = "", + validityState = SignalInputValidityState.CompareSignalLevel.Invalid.Empty + ), + rssi2InputState = CompareSignalLevelRSSIInputState( + rssiLevelInput = "", + validityState = SignalInputValidityState.CompareSignalLevel.Invalid.Empty + ) + ) + ) + ) + ) + override val uiState: State + get() = _uiState + + init { + viewModelScope.launch { + signalStore.getCalculateSignalLevelRSSI() + .collectLatest { rssiLevel -> + validateCalculateSignalLevelInputValidity(rssiLevel) + } + } + + viewModelScope.launch { + signalStore.getCompareSignalLevelRSSI1() + .collectLatest { rssi1 -> + validateCompareSignalLevelRSSI1InputValidity(rssi1) + } + } + + viewModelScope.launch { + signalStore.getCompareSignalLevelRSSI2() + .collectLatest { rssi2 -> + validateCompareSignalLevelRSSI2InputValidity(rssi2) + } + } + } + + override fun calculateSignalLevel() { + _uiState.value = uiState.value.copy( + loadingState = SignalLoadingState(isLoading = true), + dialogState = SignalDialogState.None + ) + + if (isCalculateSignalLevelInputInvalid()) { + return + } + + val rssiLevel = uiState.value.inputState.calculateSignalLevelInputState.rssiLevelInput.toInt() + val request = if (sdkUtil.isAtLeastR()) { + CalculateSignalLevelRequest.Android30AndAbove(rssiLevel = rssiLevel) + } else { + CalculateSignalLevelRequest.BelowAndroid30( + rssiLevel = rssiLevel, + numLevels = TARGET_NUMBER_OF_SIGNAL_LEVELS + ) + } + val result = try { + wisefy.calculateSignalLevel(request) + } catch (ex: WisefyException) { + _uiState.value = uiState.value.copy( + loadingState = SignalLoadingState(isLoading = false), + dialogState = SignalDialogState.Failure.WisefyAsync(exception = ex) + ) + null + } + + when (result) { + is CalculateSignalLevelResult.Failure -> { + _uiState.value = uiState.value.copy( + loadingState = SignalLoadingState(isLoading = false), + dialogState = SignalDialogState.CalculateSignalLevel.Failure(result) + ) + } + is CalculateSignalLevelResult.Success -> { + _uiState.value = uiState.value.copy( + loadingState = SignalLoadingState(isLoading = false), + dialogState = SignalDialogState.CalculateSignalLevel.Success(result) + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + override fun compareSignalLevel() { + _uiState.value = uiState.value.copy( + loadingState = SignalLoadingState(isLoading = true), + dialogState = SignalDialogState.None + ) + + if (isCompareSignalLevelInputInvalid()) { + return + } + + val rssi1 = uiState.value.inputState.compareSignalLevelInputState.rssi1InputState.rssiLevelInput.toInt() + val rssi2 = uiState.value.inputState.compareSignalLevelInputState.rssi2InputState.rssiLevelInput.toInt() + val request = CompareSignalLevelRequest(rssi1 = rssi1, rssi2 = rssi2) + val result = try { + wisefy.compareSignalLevel(request) + } catch (ex: WisefyException) { + _uiState.value = uiState.value.copy( + loadingState = SignalLoadingState(isLoading = false), + dialogState = SignalDialogState.Failure.WisefyAsync(exception = ex) + ) + null + } + + when (result) { + is CompareSignalLevelResult.Success -> { + _uiState.value = uiState.value.copy( + loadingState = SignalLoadingState(isLoading = false), + dialogState = SignalDialogState.CompareSignalLevel.Success(result) + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + override fun onCalculateSignalLevelInputChanged(input: String) { + viewModelScope.launch { + signalStore.setCalculateSignalLevelRSSI(input) + } + } + + override fun onCompareSignalLevelRSSI1InputChanged(input: String) { + viewModelScope.launch { + signalStore.setCompareSignalLevelRSSI1(input) + } + } + + override fun onCompareSignalLevelRSSI2InputChanged(input: String) { + viewModelScope.launch { + signalStore.setCompareSignalLevelRSSI2(input) + } + } + + override fun onDialogClosed() { + _uiState.value = uiState.value.copy( + loadingState = SignalLoadingState(isLoading = false), + dialogState = SignalDialogState.None + ) + } + + private fun isCalculateSignalLevelInputInvalid(): Boolean { + val currentCalculateSignalInputState = uiState.value.inputState.calculateSignalLevelInputState + if (currentCalculateSignalInputState.validityState !is SignalInputValidityState.CalculateSignalLevel.Valid) { + _uiState.value = uiState.value.copy( + loadingState = SignalLoadingState(isLoading = false), + dialogState = SignalDialogState.InputError.CalculateSignalLevel + ) + return true + } + return false + } + + private fun isCompareSignalLevelInputInvalid(): Boolean { + val currentRSSI1InputState = uiState.value.inputState.compareSignalLevelInputState.rssi1InputState + if (currentRSSI1InputState.validityState !is SignalInputValidityState.CompareSignalLevel.Valid) { + _uiState.value = uiState.value.copy( + loadingState = SignalLoadingState(isLoading = false), + dialogState = SignalDialogState.InputError.CompareSignalLevel.RSSI1 + ) + return true + } + val currentRSSI2InputState = uiState.value.inputState.compareSignalLevelInputState.rssi1InputState + if (currentRSSI2InputState.validityState !is SignalInputValidityState.CompareSignalLevel.Valid) { + _uiState.value = uiState.value.copy( + loadingState = SignalLoadingState(isLoading = false), + dialogState = SignalDialogState.InputError.CompareSignalLevel.RSSI2 + ) + return true + } + return false + } + + private fun validateCalculateSignalLevelInputValidity(rssiLevel: String) { + val validityState = when { + rssiLevel.isBlank() -> SignalInputValidityState.CalculateSignalLevel.Invalid.Empty + rssiLevel.toIntOrNull() == null -> SignalInputValidityState.CalculateSignalLevel.Invalid.NotAnInt + else -> SignalInputValidityState.CalculateSignalLevel.Valid + } + _uiState.value = uiState.value.copy( + inputState = uiState.value.inputState.copy( + calculateSignalLevelInputState = CalculateSignalLevelInputState( + rssiLevelInput = rssiLevel, + validityState = validityState + ) + ) + ) + } + + private fun validateCompareSignalLevelRSSI1InputValidity(rssi1: String) { + val validityState = when { + rssi1.isBlank() -> SignalInputValidityState.CompareSignalLevel.Invalid.Empty + rssi1.toIntOrNull() == null -> SignalInputValidityState.CompareSignalLevel.Invalid.NotAnInt + else -> SignalInputValidityState.CompareSignalLevel.Valid + } + _uiState.value = uiState.value.copy( + inputState = uiState.value.inputState.copy( + compareSignalLevelInputState = uiState.value.inputState.compareSignalLevelInputState.copy( + rssi1InputState = CompareSignalLevelRSSIInputState( + rssiLevelInput = rssi1, + validityState = validityState + ) + ) + ) + ) + } + + private fun validateCompareSignalLevelRSSI2InputValidity(rssi2: String) { + val validityState = when { + rssi2.isBlank() -> SignalInputValidityState.CompareSignalLevel.Invalid.Empty + rssi2.toIntOrNull() == null -> SignalInputValidityState.CompareSignalLevel.Invalid.NotAnInt + else -> SignalInputValidityState.CompareSignalLevel.Valid + } + _uiState.value = uiState.value.copy( + inputState = uiState.value.inputState.copy( + compareSignalLevelInputState = uiState.value.inputState.compareSignalLevelInputState.copy( + rssi2InputState = CompareSignalLevelRSSIInputState( + rssiLevelInput = rssi2, + validityState = validityState + ) + ) + ) + ) + } +} + +internal class SignalViewModelFactory( + context: Context, + sdkUtil: SdkUtil, + private val wisefy: WisefyApi +) : BaseViewModelFactory( + supportedClass = SignalViewModel::class, + vmProvider = { DefaultSignalViewModel(context, wisefy, sdkUtil) } +) diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkScreen.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkScreen.kt new file mode 100644 index 00000000..bffa8c05 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkScreen.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.remove + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleLoadingIndicator + +@Composable +internal fun RemoveNetworkScreen( + wisefy: WisefyApi, + viewModel: RemoveNetworkViewModel = viewModel( + factory = RemoveNetworkViewModelFactory( + context = LocalContext.current.applicationContext, + wisefy = wisefy + ) + ) +) { + WisefySampleLoadingIndicator(isLoading = { viewModel.uiState.value.loadingState.isLoading }) + RemoveNetworkScreenDialogContent(dialogState = { viewModel.uiState.value.dialogState }, viewModel = viewModel) + RemoveNetworkScreenContent(viewModel = viewModel) +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun RemoveNetworkScreenLightPreview() { + RemoveNetworkScreen(ComposablePreviewWisefy()) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun RemoveNetworkScreenDarkPreview() { + RemoveNetworkScreen(ComposablePreviewWisefy()) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkScreenContent.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkScreenContent.kt new file mode 100644 index 00000000..dde70212 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkScreenContent.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.remove + +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.Manifest.permission.ACCESS_WIFI_STATE +import android.Manifest.permission.CHANGE_WIFI_STATE +import android.content.res.Configuration +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.logging.WisefySampleLogger +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefyPrimaryButton +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleEditText +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleEditTextError +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleSSIDTypeSelectionRows +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleSizes +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme +import kotlinx.coroutines.launch + +private const val LOG_TAG = "RemoveNetworkScreenContent" + +@Composable +internal fun RemoveNetworkScreenContent(viewModel: RemoveNetworkViewModel) { + val scope = rememberCoroutineScope() + val removeNetworkPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> + if (result.all { it.value }) { + scope.launch { + viewModel.removeNetwork() + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions required to remove a network are denied") + viewModel.onRemoveNetworkPermissionsError() + } + } + + WisefySampleTheme { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding( + top = WisefySampleSizes.WisefySampleTopMargin, + bottom = WisefySampleSizes.WisefySampleBottomMargin, + start = WisefySampleSizes.WisefySampleHorizontalMargins, + end = WisefySampleSizes.WisefySampleHorizontalMargins + ) + ) { + RemoveNetworkInputRows( + inputState = { viewModel.uiState.value.inputState }, + viewModel = viewModel + ) + WisefySampleSSIDTypeSelectionRows( + ssidType = { viewModel.uiState.value.ssidType }, + onSSIDTypeChanged = { ssidType -> viewModel.onSSIDTypeChanged(ssidType) } + ) + Row(modifier = Modifier.padding(top = WisefySampleSizes.Large)) { + WisefyPrimaryButton(stringResId = R.string.remove_network) { + removeNetworkPermissionsLauncher.launch( + arrayOf(ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE, CHANGE_WIFI_STATE) + ) + } + } + } + } +} + +@Composable +private fun RemoveNetworkInputRows( + inputState: () -> RemoveNetworkInputState, + viewModel: RemoveNetworkViewModel +) { + val currentInputState = inputState() + Row { + WisefySampleEditText( + text = currentInputState.networkInput, + onTextChange = { newText -> + viewModel.onInputChanged(newText) + }, + labelResId = R.string.regex_for_network, + error = when (currentInputState.networkInputValidityState) { + is RemoveNetworkInputValidityState.SSID.Invalid.Empty -> { + WisefySampleEditTextError(R.string.ssid_input_empty) + } + is RemoveNetworkInputValidityState.SSID.Invalid.TooShort -> { + WisefySampleEditTextError(R.string.ssid_input_too_short) + } + is RemoveNetworkInputValidityState.SSID.Invalid.TooLong -> { + WisefySampleEditTextError(R.string.ssid_input_too_long) + } + is RemoveNetworkInputValidityState.SSID.Invalid.InvalidCharacters -> { + WisefySampleEditTextError(R.string.ssid_input_invalid_characters) + } + is RemoveNetworkInputValidityState.SSID.Invalid.InvalidStartCharacters -> { + WisefySampleEditTextError(R.string.ssid_input_invalid_start_characters) + } + is RemoveNetworkInputValidityState.SSID.Invalid.LeadingOrTrailingSpaces -> { + WisefySampleEditTextError(R.string.ssid_input_leading_or_trailing_spaces) + } + is RemoveNetworkInputValidityState.SSID.Invalid.InvalidUnicode -> { + WisefySampleEditTextError(R.string.ssid_input_not_valid_unicode) + } + is RemoveNetworkInputValidityState.BSSID.Invalid.Empty -> { + WisefySampleEditTextError(R.string.bssid_input_empty) + } + is RemoveNetworkInputValidityState.BSSID.Invalid.ImproperFormat -> { + WisefySampleEditTextError(R.string.bssid_input_improper_format) + } + is RemoveNetworkInputValidityState.SSID.Valid -> null + is RemoveNetworkInputValidityState.BSSID.Valid -> null + } + ) + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun RemoveNetworkScreenContentLightPreview() { + RemoveNetworkScreenContent( + viewModel = DefaultRemoveNetworkViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy() + ) + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun RemoveNetworkScreenContentDarkPreview() { + RemoveNetworkScreenContent( + viewModel = DefaultRemoveNetworkViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy() + ) + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkScreenDialogContent.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkScreenDialogContent.kt new file mode 100644 index 00000000..a4ce9117 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkScreenDialogContent.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.remove + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.removenetwork.entities.RemoveNetworkResult +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleNoticeDialog + +@Composable +internal fun RemoveNetworkScreenDialogContent( + dialogState: () -> RemoveNetworkDialogState, + viewModel: RemoveNetworkViewModel +) { + when (val currentDialogState = dialogState()) { + is RemoveNetworkDialogState.None -> { + // No-op, no dialog + } + is RemoveNetworkDialogState.RemoveNetwork.Success -> { + WisefySampleNoticeDialog( + title = R.string.remove_network, + body = R.string.success_removing_network_args, + currentDialogState.result, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is RemoveNetworkDialogState.RemoveNetwork.PermissionsError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_remove_network, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is RemoveNetworkDialogState.RemoveNetwork.Failure -> { + WisefySampleNoticeDialog( + title = R.string.remove_network, + body = R.string.failure_removing_network_args, + currentDialogState.result, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is RemoveNetworkDialogState.Failure.WisefyAsync -> { + WisefySampleNoticeDialog( + title = R.string.wisefy_async_error, + body = R.string.wisefy_async_error_descriptions_args, + currentDialogState.exception.message ?: "", + currentDialogState.exception.cause?.message ?: "", + onClose = { + viewModel.onDialogClosed() + } + ) + } + is RemoveNetworkDialogState.InputError.SSID -> { + WisefySampleNoticeDialog( + title = R.string.input_error, + body = R.string.ssid_input_invalid, + onClose = { + viewModel.onDialogClosed() + } + ) + } + is RemoveNetworkDialogState.InputError.BSSID -> { + WisefySampleNoticeDialog( + title = R.string.input_error, + body = R.string.bssid_input_invalid, + onClose = { + viewModel.onDialogClosed() + } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun RemoveNetworkScreenDialogContentLightPreview( + @PreviewParameter(RemoveNetworkDialogStatePreviewParameterProvider::class) dialogState: RemoveNetworkDialogState +) { + RemoveNetworkScreenDialogContent( + viewModel = DefaultRemoveNetworkViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy() + ), + dialogState = { dialogState } + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun RemoveNetworkScreenDialogContentDarkPreview( + @PreviewParameter(RemoveNetworkDialogStatePreviewParameterProvider::class) dialogState: RemoveNetworkDialogState +) { + RemoveNetworkScreenDialogContent( + viewModel = DefaultRemoveNetworkViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy() + ), + dialogState = { dialogState } + ) +} + +private class RemoveNetworkDialogStatePreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + RemoveNetworkDialogState.Failure.WisefyAsync(WisefyException("", null)), + RemoveNetworkDialogState.InputError.SSID, + RemoveNetworkDialogState.InputError.BSSID, + RemoveNetworkDialogState.RemoveNetwork.Failure(RemoveNetworkResult.Failure.False), + RemoveNetworkDialogState.RemoveNetwork.PermissionsError, + RemoveNetworkDialogState.RemoveNetwork.Success(RemoveNetworkResult.Success.True) + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkStore.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkStore.kt new file mode 100644 index 00000000..cdc0b7e9 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkStore.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.remove + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.isupatches.android.wisefy.sample.entities.SSIDType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private const val PREF_SSID_TYPE = "ssid_type" +private const val PREF_LAST_USED_NETWORK_INPUT = "last_used_network_input" + +internal interface RemoveNetworkStore { + suspend fun clear() + + fun getLastUsedNetworkInput(): Flow + fun getSSIDType(): Flow + + suspend fun setLastUsedNetworkInput(lastUsedNetworkInput: String) + suspend fun setSSIDType(ssidType: SSIDType) +} + +private val Context.removeNetworkDataStore: DataStore by preferencesDataStore( + name = "removeNetworkDataStore" +) + +internal class DefaultRemoveNetworkStore( + private val context: Context +) : RemoveNetworkStore { + + private val ssidTypeKey = intPreferencesKey(PREF_SSID_TYPE) + private val lastUsedNetworkInputKey = stringPreferencesKey(PREF_LAST_USED_NETWORK_INPUT) + + override suspend fun clear() { + context.removeNetworkDataStore.edit { + clear() + } + } + + /* + * Last used network input + */ + + override fun getLastUsedNetworkInput(): Flow { + return context.removeNetworkDataStore.data.map { preferences -> + preferences[lastUsedNetworkInputKey] ?: "" + } + } + + override suspend fun setLastUsedNetworkInput(lastUsedNetworkInput: String) { + context.removeNetworkDataStore.edit { preferences -> + preferences[lastUsedNetworkInputKey] = lastUsedNetworkInput + } + } + + /* + * SSID Type + */ + + override fun getSSIDType(): Flow { + return context.removeNetworkDataStore.data.map { preferences -> + SSIDType.of(preferences[ssidTypeKey] ?: SSIDType.SSID.intVal) + } + } + + override suspend fun setSSIDType(ssidType: SSIDType) { + context.removeNetworkDataStore.edit { preferences -> + preferences[ssidTypeKey] = ssidType.intVal + } + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkUIState.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkUIState.kt new file mode 100644 index 00000000..584ccaee --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkUIState.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.remove + +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.removenetwork.entities.RemoveNetworkResult +import com.isupatches.android.wisefy.sample.entities.SSIDType + +internal data class RemoveNetworkUIState( + val loadingState: RemoveNetworkLoadingState, + val dialogState: RemoveNetworkDialogState, + val inputState: RemoveNetworkInputState, + val ssidType: SSIDType +) + +internal data class RemoveNetworkLoadingState(val isLoading: Boolean) + +internal sealed class RemoveNetworkDialogState { + + object None : RemoveNetworkDialogState() + + sealed class Failure : RemoveNetworkDialogState() { + data class WisefyAsync(val exception: WisefyException) : Failure() + } + + sealed class RemoveNetwork : RemoveNetworkDialogState() { + data class Failure(val result: RemoveNetworkResult.Failure) : RemoveNetwork() + data class Success(val result: RemoveNetworkResult.Success) : RemoveNetwork() + + object PermissionsError : RemoveNetwork() + } + + sealed class InputError : RemoveNetworkDialogState() { + object SSID : InputError() + object BSSID : InputError() + } +} + +internal data class RemoveNetworkInputState( + val networkInput: String, + val networkInputValidityState: RemoveNetworkInputValidityState +) + +internal sealed class RemoveNetworkInputValidityState { + + sealed class SSID : RemoveNetworkInputValidityState() { + object Valid : SSID() + + sealed class Invalid : SSID() { + object Empty : Invalid() + object TooShort : Invalid() + object TooLong : Invalid() + object InvalidCharacters : Invalid() + object InvalidStartCharacters : Invalid() + object LeadingOrTrailingSpaces : Invalid() + object InvalidUnicode : Invalid() + } + } + + sealed class BSSID : RemoveNetworkInputValidityState() { + object Valid : BSSID() + + sealed class Invalid : BSSID() { + object Empty : Invalid() + object ImproperFormat : Invalid() + } + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkViewModel.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkViewModel.kt new file mode 100644 index 00000000..b41980cf --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/remove/RemoveNetworkViewModel.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.remove + +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.Manifest.permission.ACCESS_WIFI_STATE +import android.Manifest.permission.CHANGE_WIFI_STATE +import android.content.Context +import androidx.annotation.RequiresPermission +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.viewModelScope +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.ktx.removeNetworkAsync +import com.isupatches.android.wisefy.removenetwork.entities.RemoveNetworkRequest +import com.isupatches.android.wisefy.removenetwork.entities.RemoveNetworkResult +import com.isupatches.android.wisefy.sample.entities.SSIDType +import com.isupatches.android.wisefy.sample.scaffolding.BaseViewModel +import com.isupatches.android.wisefy.sample.scaffolding.BaseViewModelFactory +import com.isupatches.android.wisefy.sample.util.BSSIDInputError +import com.isupatches.android.wisefy.sample.util.SSIDInputError +import com.isupatches.android.wisefy.sample.util.validateBSSID +import com.isupatches.android.wisefy.sample.util.validateSSID +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +internal abstract class RemoveNetworkViewModel : BaseViewModel() { + abstract val uiState: State + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE, CHANGE_WIFI_STATE]) + abstract suspend fun removeNetwork() + + abstract fun onInputChanged(input: String) + abstract fun onSSIDTypeChanged(ssidType: SSIDType) + + abstract fun onRemoveNetworkPermissionsError() + + abstract fun onDialogClosed() +} + +internal class DefaultRemoveNetworkViewModel( + context: Context, + private val wisefy: WisefyApi, + private val removeNetworkStore: RemoveNetworkStore = DefaultRemoveNetworkStore(context = context) +) : RemoveNetworkViewModel() { + + private val _uiState = mutableStateOf( + RemoveNetworkUIState( + loadingState = RemoveNetworkLoadingState(isLoading = false), + dialogState = RemoveNetworkDialogState.None, + inputState = RemoveNetworkInputState( + networkInput = "", + networkInputValidityState = RemoveNetworkInputValidityState.SSID.Invalid.Empty + ), + ssidType = SSIDType.SSID + ) + ) + override val uiState: State + get() = _uiState + + init { + viewModelScope.launch { + removeNetworkStore.getLastUsedNetworkInput() + .collectLatest { input -> + validateInput(uiState.value.ssidType, input) + } + } + + viewModelScope.launch { + removeNetworkStore.getSSIDType() + .collectLatest { ssidType -> + _uiState.value = uiState.value.copy(ssidType = ssidType) + validateInput(ssidType, uiState.value.inputState.networkInput) + } + } + } + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE, CHANGE_WIFI_STATE]) + override suspend fun removeNetwork() { + val currentInputState = uiState.value.inputState + when (uiState.value.ssidType) { + SSIDType.SSID -> { + if (currentInputState.networkInputValidityState != RemoveNetworkInputValidityState.SSID.Valid) { + _uiState.value = uiState.value.copy( + loadingState = RemoveNetworkLoadingState(isLoading = false), + dialogState = RemoveNetworkDialogState.InputError.SSID + ) + return + } + } + SSIDType.BSSID -> { + if (currentInputState.networkInputValidityState != RemoveNetworkInputValidityState.BSSID.Valid) { + _uiState.value = uiState.value.copy( + loadingState = RemoveNetworkLoadingState(isLoading = false), + dialogState = RemoveNetworkDialogState.InputError.BSSID + ) + return + } + } + } + _uiState.value = uiState.value.copy( + loadingState = RemoveNetworkLoadingState(isLoading = true), + dialogState = RemoveNetworkDialogState.None + ) + val request = when (uiState.value.ssidType) { + SSIDType.SSID -> RemoveNetworkRequest.SSID(ssid = currentInputState.networkInput) + SSIDType.BSSID -> RemoveNetworkRequest.BSSID(bssid = currentInputState.networkInput) + } + val result = try { + wisefy.removeNetworkAsync(request = request) + } catch (ex: WisefyException) { + _uiState.value = uiState.value.copy( + loadingState = RemoveNetworkLoadingState(isLoading = false), + dialogState = RemoveNetworkDialogState.Failure.WisefyAsync(exception = ex) + ) + null + } + + when (result) { + is RemoveNetworkResult.Success -> { + _uiState.value = uiState.value.copy( + loadingState = RemoveNetworkLoadingState(isLoading = false), + dialogState = RemoveNetworkDialogState.RemoveNetwork.Success(result) + ) + } + is RemoveNetworkResult.Failure -> { + _uiState.value = uiState.value.copy( + loadingState = RemoveNetworkLoadingState(isLoading = false), + dialogState = RemoveNetworkDialogState.RemoveNetwork.Failure(result) + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + override fun onInputChanged(input: String) { + viewModelScope.launch { + removeNetworkStore.setLastUsedNetworkInput(input) + } + } + + override fun onSSIDTypeChanged(ssidType: SSIDType) { + viewModelScope.launch { + removeNetworkStore.setSSIDType(ssidType) + } + } + + override fun onDialogClosed() { + _uiState.value = uiState.value.copy( + loadingState = RemoveNetworkLoadingState(isLoading = false), + dialogState = RemoveNetworkDialogState.None + ) + } + + override fun onRemoveNetworkPermissionsError() { + _uiState.value = uiState.value.copy( + loadingState = RemoveNetworkLoadingState(isLoading = false), + dialogState = RemoveNetworkDialogState.RemoveNetwork.PermissionsError + ) + } + + private fun validateInput(ssidType: SSIDType, input: String) { + val validityState = when (ssidType) { + SSIDType.SSID -> { + when (input.validateSSID()) { + SSIDInputError.EMPTY -> RemoveNetworkInputValidityState.SSID.Invalid.Empty + SSIDInputError.TOO_SHORT -> RemoveNetworkInputValidityState.SSID.Invalid.TooShort + SSIDInputError.TOO_LONG -> RemoveNetworkInputValidityState.SSID.Invalid.TooLong + SSIDInputError.INVALID_CHARACTERS -> { + RemoveNetworkInputValidityState.SSID.Invalid.InvalidCharacters + } + SSIDInputError.INVALID_START_CHARACTERS -> { + RemoveNetworkInputValidityState.SSID.Invalid.InvalidStartCharacters + } + SSIDInputError.LEADING_OR_TRAILING_SPACES -> { + RemoveNetworkInputValidityState.SSID.Invalid.LeadingOrTrailingSpaces + } + SSIDInputError.NOT_VALID_UNICODE -> { + RemoveNetworkInputValidityState.SSID.Invalid.InvalidUnicode + } + SSIDInputError.NONE -> RemoveNetworkInputValidityState.SSID.Valid + } + } + SSIDType.BSSID -> { + when (input.validateBSSID()) { + BSSIDInputError.EMPTY -> RemoveNetworkInputValidityState.BSSID.Invalid.Empty + BSSIDInputError.INVALID -> RemoveNetworkInputValidityState.BSSID.Invalid.ImproperFormat + BSSIDInputError.NONE -> RemoveNetworkInputValidityState.BSSID.Valid + } + } + } + _uiState.value = uiState.value.copy( + inputState = RemoveNetworkInputState( + networkInput = input, + networkInputValidityState = validityState + ) + ) + } +} + +internal class RemoveNetworkViewModelFactory( + private val context: Context, + private val wisefy: WisefyApi +) : BaseViewModelFactory( + supportedClass = RemoveNetworkViewModel::class, + vmProvider = { DefaultRemoveNetworkViewModel(context, wisefy) } +) diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchScreen.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchScreen.kt new file mode 100644 index 00000000..c615dfaf --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchScreen.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.search + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleLoadingIndicator + +@Composable +internal fun SearchScreen( + wisefy: WisefyApi, + viewModel: SearchViewModel = viewModel( + factory = SearchViewModelFactory( + context = LocalContext.current.applicationContext, + wisefy = wisefy + ) + ) +) { + WisefySampleLoadingIndicator(isLoading = { viewModel.uiState.value.loadingState.isLoading }) + SearchScreenDialogContent(dialogState = { viewModel.uiState.value.dialogState }, viewModel = viewModel) + SearchScreenContent(viewModel = viewModel) +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun SearchScreenLightPreview() { + SearchScreen(ComposablePreviewWisefy()) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun SearchScreenDarkPreview() { + SearchScreen(ComposablePreviewWisefy()) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchScreenContent.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchScreenContent.kt new file mode 100644 index 00000000..8be5898a --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchScreenContent.kt @@ -0,0 +1,434 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.search + +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.Manifest.permission.ACCESS_WIFI_STATE +import android.content.res.Configuration +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.tooling.preview.Preview +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.entities.SearchType +import com.isupatches.android.wisefy.sample.logging.WisefySampleLogger +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefyPrimaryButton +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleBodyLabel +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleCaptionLabel +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleEditText +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleEditTextError +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleRadioButton +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleSSIDTypeSelectionRows +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleSlider +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleSubHeaderLabel +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleSizes +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +private const val LOG_TAG = "SearchScreenContent" + +private const val MIN_SEARCH_TIMEOUT = 1f +private const val MAX_SEARCH_TIMEOUT = 60f + +@Composable +internal fun SearchScreenContent(viewModel: SearchViewModel) { + val scope = rememberCoroutineScope() + + val searchForAccessPointPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + scope.launch { + viewModel.searchForAccessPoint() + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions required to search for an access point are denied") + viewModel.onSearchForAccessPointPermissionError() + } + } + + val searchForAccessPointsPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + scope.launch { + viewModel.searchForAccessPoints() + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions required to search for access points are denied") + viewModel.onSearchForAccessPointsPermissionError() + } + } + + val searchForSavedNetworkPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> + if (result.all { it.value }) { + scope.launch { + viewModel.searchForSavedNetwork() + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions required to search for a saved network are denied") + viewModel.onSearchForSavedNetworkPermissionError() + } + } + + val searchForSavedNetworksPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> + if (result.all { it.value }) { + scope.launch { + viewModel.searchForSavedNetworks() + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions required to search for saved networks are denied") + viewModel.onSearchForSavedNetworksPermissionError() + } + } + + val searchForSSIDPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + scope.launch { + viewModel.searchForSSID() + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions required to search for an SSID are denied") + viewModel.onSearchForSSIDPermissionError() + } + } + + val searchForSSIDsPermissionsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + scope.launch { + viewModel.searchForSSIDs() + } + } else { + WisefySampleLogger.w(LOG_TAG, "Permissions required to search for SSIDs are denied") + viewModel.onSearchForSSIDsPermissionError() + } + } + + WisefySampleTheme { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding( + top = WisefySampleSizes.WisefySampleTopMargin, + bottom = WisefySampleSizes.WisefySampleBottomMargin, + start = WisefySampleSizes.WisefySampleHorizontalMargins, + end = WisefySampleSizes.WisefySampleHorizontalMargins + ) + ) { + SearchScreenNetworkInputRows( + inputState = { viewModel.uiState.value.inputState }, + viewModel = viewModel + ) + Row { + Box(Modifier.padding(top = WisefySampleSizes.XLarge)) { + WisefyPrimaryButton(stringResId = R.string.search) { + when (viewModel.uiState.value.searchType) { + SearchType.ACCESS_POINT -> { + if (viewModel.uiState.value.returnFullList) { + searchForAccessPointsPermissionsLauncher.launch(ACCESS_FINE_LOCATION) + } else { + searchForAccessPointPermissionsLauncher.launch(ACCESS_FINE_LOCATION) + } + } + SearchType.SAVED_NETWORK -> { + if (viewModel.uiState.value.returnFullList) { + searchForSavedNetworksPermissionsLauncher.launch( + arrayOf(ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE) + ) + } else { + searchForSavedNetworkPermissionsLauncher.launch( + arrayOf(ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE) + ) + } + } + SearchType.SSID -> { + if (viewModel.uiState.value.returnFullList) { + searchForSSIDsPermissionsLauncher.launch(ACCESS_FINE_LOCATION) + } else { + searchForSSIDPermissionsLauncher.launch(ACCESS_FINE_LOCATION) + } + } + } + } + } + } + SearchScreenSearchTypeInputRows( + searchType = { viewModel.uiState.value.searchType }, + viewModel = viewModel + ) + WisefySampleSSIDTypeSelectionRows( + ssidType = { viewModel.uiState.value.ssidType }, + onSSIDTypeChanged = { ssidType -> viewModel.onSSIDTypeChanged(ssidType) } + ) + SearchScreenReturnFullListInputRows( + returnFullList = { viewModel.uiState.value.returnFullList }, + viewModel = viewModel + ) + SearchScreenFilterDuplicatesInputRows( + filterDuplicates = { viewModel.uiState.value.filterDuplicates }, + viewModel = viewModel + ) + if (viewModel.uiState.value.searchType != SearchType.SAVED_NETWORK) { + val timeoutInSeconds = viewModel.uiState.value.timeoutInSeconds + if (timeoutInSeconds != null) { + SearchScreenTimeoutInputRows( + timeout = { timeoutInSeconds }, + viewModel = viewModel + ) + } + } + } + } +} + +@Composable +private fun SearchScreenNetworkInputRows( + inputState: () -> SearchInputState, + viewModel: SearchViewModel +) { + val currentInputState = inputState() + Row { + WisefySampleEditText( + text = currentInputState.input, + onTextChange = { newText -> + viewModel.onSearchNetworkInputChanged(newText) + }, + labelResId = R.string.regex_for_network, + error = when (currentInputState.inputValidityState) { + is SearchInputValidityState.SSID.Invalid.Empty -> { + WisefySampleEditTextError(R.string.ssid_input_empty) + } + is SearchInputValidityState.SSID.Invalid.TooShort -> { + WisefySampleEditTextError(R.string.ssid_input_too_short) + } + is SearchInputValidityState.SSID.Invalid.TooLong -> { + WisefySampleEditTextError(R.string.ssid_input_too_long) + } + is SearchInputValidityState.SSID.Invalid.InvalidCharacters -> { + WisefySampleEditTextError(R.string.ssid_input_invalid_characters) + } + is SearchInputValidityState.SSID.Invalid.InvalidStartCharacters -> { + WisefySampleEditTextError(R.string.ssid_input_invalid_start_characters) + } + is SearchInputValidityState.SSID.Invalid.LeadingOrTrailingSpaces -> { + WisefySampleEditTextError(R.string.ssid_input_leading_or_trailing_spaces) + } + is SearchInputValidityState.SSID.Invalid.InvalidUnicode -> { + WisefySampleEditTextError(R.string.ssid_input_not_valid_unicode) + } + is SearchInputValidityState.SSID.Valid -> null + is SearchInputValidityState.BSSID.Invalid.Empty -> { + WisefySampleEditTextError(R.string.bssid_input_empty) + } + is SearchInputValidityState.BSSID.Invalid.ImproperFormat -> { + WisefySampleEditTextError(R.string.bssid_input_improper_format) + } + is SearchInputValidityState.BSSID.Valid -> null + } + ) + } +} + +@Composable +private fun SearchScreenSearchTypeInputRows( + searchType: () -> SearchType, + viewModel: SearchViewModel +) { + val currentSearchType = searchType() + Row { + WisefySampleSubHeaderLabel( + stringResId = R.string.search_for, + modifier = Modifier.padding(top = WisefySampleSizes.XLarge) + ) + } + Row(verticalAlignment = Alignment.CenterVertically) { + WisefySampleRadioButton( + isSelected = currentSearchType == SearchType.ACCESS_POINT, + onClick = { viewModel.onSearchTypeSelected(SearchType.ACCESS_POINT) } + ) + WisefySampleBodyLabel(stringResId = R.string.access_point) + } + Row(verticalAlignment = Alignment.CenterVertically) { + WisefySampleRadioButton( + isSelected = currentSearchType == SearchType.SSID, + onClick = { viewModel.onSearchTypeSelected(SearchType.SSID) } + ) + WisefySampleBodyLabel(stringResId = R.string.ssid) + } + Row(verticalAlignment = Alignment.CenterVertically) { + WisefySampleRadioButton( + isSelected = currentSearchType == SearchType.SAVED_NETWORK, + onClick = { viewModel.onSearchTypeSelected(SearchType.SAVED_NETWORK) } + ) + WisefySampleBodyLabel(stringResId = R.string.saved_network) + } +} + +@Composable +private fun SearchScreenReturnFullListInputRows( + returnFullList: () -> Boolean, + viewModel: SearchViewModel +) { + val currentReturnFullListValue = returnFullList() + Row { + WisefySampleSubHeaderLabel( + modifier = Modifier.padding(top = WisefySampleSizes.Medium), + stringResId = R.string.return_full_list_label + ) + } + Row { + WisefySampleBodyLabel( + modifier = Modifier.padding(top = WisefySampleSizes.Large), + stringResId = R.string.return_full_list_description + ) + } + Row( + modifier = Modifier.padding(top = WisefySampleSizes.Medium), + verticalAlignment = Alignment.CenterVertically + ) { + WisefySampleRadioButton( + isSelected = currentReturnFullListValue, + onClick = { + viewModel.onReturnFullListChanged(true) + } + ) + WisefySampleBodyLabel(stringResId = R.string.yes) + WisefySampleRadioButton( + isSelected = !currentReturnFullListValue, + onClick = { + viewModel.onReturnFullListChanged(false) + } + ) + WisefySampleBodyLabel(stringResId = R.string.no) + } +} + +@Composable +private fun SearchScreenFilterDuplicatesInputRows( + filterDuplicates: () -> Boolean, + viewModel: SearchViewModel +) { + val currentFilterDuplicatesValue = filterDuplicates() + Row { + WisefySampleSubHeaderLabel( + modifier = Modifier.padding(top = WisefySampleSizes.Large), + stringResId = R.string.filter_duplicates + ) + } + Row( + modifier = Modifier.padding(top = WisefySampleSizes.Medium), + verticalAlignment = Alignment.CenterVertically + ) { + WisefySampleRadioButton( + isSelected = currentFilterDuplicatesValue, + onClick = { + viewModel.onFilterDuplicatesChanged(true) + } + ) + WisefySampleBodyLabel(stringResId = R.string.yes) + WisefySampleRadioButton( + isSelected = !currentFilterDuplicatesValue, + onClick = { + viewModel.onFilterDuplicatesChanged(false) + } + ) + WisefySampleBodyLabel(stringResId = R.string.no) + } +} + +@Composable +private fun SearchScreenTimeoutInputRows( + timeout: () -> Int, + viewModel: SearchViewModel +) { + val searchTimeout = remember { mutableStateOf(timeout()) } + Row(modifier = Modifier.padding(top = WisefySampleSizes.Medium)) { + WisefySampleSlider( + startPosition = { searchTimeout.value.toFloat() }, + valueRange = object : ClosedFloatingPointRange { + override val start: Float = MIN_SEARCH_TIMEOUT + override val endInclusive: Float = MAX_SEARCH_TIMEOUT + + override fun lessThanOrEquals(a: Float, b: Float): Boolean { + return a <= b + } + }, + onValueChange = { timeout -> + searchTimeout.value = timeout.roundToInt() + }, + onValueChangeFinished = { timeout -> + viewModel.onSearchTimeoutValueChangeFinished(timeout.roundToInt()) + } + ) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + SearchScreenTimeoutLabelRow(timeout = { searchTimeout.value }) + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun SearchScreenTimeoutLabelRow(timeout: () -> Int) { + WisefySampleCaptionLabel( + text = pluralStringResource(R.plurals.timeout_after_x_seconds_args_html, timeout(), timeout()), + modifier = Modifier + ) +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun SearchScreenContentLightPreview() { + SearchScreenContent( + viewModel = DefaultSearchViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy() + ) + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun SearchScreenContentDarkPreview() { + SearchScreenContent( + viewModel = DefaultSearchViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy() + ) + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchScreenDialogContent.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchScreenDialogContent.kt new file mode 100644 index 00000000..50aa7169 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchScreenDialogContent.kt @@ -0,0 +1,269 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.search + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.ComposablePreviewWisefy +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleNoticeDialog +import com.isupatches.android.wisefy.savednetworks.entities.SavedNetworkData + +@Composable +internal fun SearchScreenDialogContent( + dialogState: () -> SearchDialogState, + viewModel: SearchViewModel +) { + when (val currentDialogState = dialogState()) { + is SearchDialogState.None -> { + // No-op, no dialog + } + is SearchDialogState.SearchForAccessPoint.NoAccessPointFound -> { + WisefySampleNoticeDialog( + title = R.string.search_result, + body = R.string.access_point_not_found + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForAccessPoints.NoAccessPointsFound -> { + WisefySampleNoticeDialog( + title = R.string.search_result, + body = R.string.no_access_points_found + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForSSID.NoSSIDFound -> { + WisefySampleNoticeDialog( + title = R.string.search_result, + body = R.string.ssid_not_found + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForSSIDs.NoSSIDsFound -> { + WisefySampleNoticeDialog( + title = R.string.search_result, + body = R.string.no_ssids_found + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForSavedNetwork.NoSavedNetworkFound -> { + WisefySampleNoticeDialog( + title = R.string.search_result, + body = R.string.saved_network_not_found + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForSavedNetworks.NoSavedNetworksFound -> { + WisefySampleNoticeDialog( + title = R.string.search_result, + body = R.string.no_saved_networks_found + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.Failure.WisefyAsync -> { + WisefySampleNoticeDialog( + title = R.string.wisefy_async_error, + body = R.string.wisefy_async_error_descriptions_args, + currentDialogState.exception.message ?: "", + currentDialogState.exception.cause?.message ?: "", + onClose = { + viewModel.onDialogClosed() + } + ) + } + is SearchDialogState.InputError.SSID -> { + WisefySampleNoticeDialog(title = R.string.input_error, body = R.string.ssid_input_invalid) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.InputError.BSSID -> { + WisefySampleNoticeDialog(title = R.string.input_error, body = R.string.bssid_input_invalid) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForAccessPoint.PermissionError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_search_for_access_point + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForAccessPoints.PermissionError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_search_for_access_points + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForSSID.PermissionError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_search_for_ssid + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForSSIDs.PermissionError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_search_for_ssids + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForSavedNetwork.PermissionError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_search_for_saved_network + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForSavedNetworks.PermissionError -> { + WisefySampleNoticeDialog( + title = R.string.permission_error, + body = R.string.permission_error_search_for_saved_networks + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForAccessPoint.Success -> { + WisefySampleNoticeDialog( + title = R.string.search_result, + body = R.string.access_point_args, + currentDialogState.data + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForAccessPoints.Success -> { + WisefySampleNoticeDialog( + title = R.string.search_result, + body = R.string.access_points_args, + currentDialogState.data + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForSSID.Success -> { + WisefySampleNoticeDialog( + title = R.string.search_result, + body = R.string.ssid_args, + currentDialogState.data + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForSSIDs.Success -> { + WisefySampleNoticeDialog( + title = R.string.search_result, + body = R.string.ssids_args, + currentDialogState.data + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForSavedNetwork.Success -> { + WisefySampleNoticeDialog( + title = R.string.search_result, + body = R.string.saved_network_args, + currentDialogState.data + ) { + viewModel.onDialogClosed() + } + } + is SearchDialogState.SearchForSavedNetworks.Success -> { + WisefySampleNoticeDialog( + title = R.string.search_result, + body = R.string.saved_networks_args, + currentDialogState.data + ) { + viewModel.onDialogClosed() + } + } + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun SearchScreenDialogContentLightPreview( + @PreviewParameter(SearchScreenDialogStatePreviewParameterProvider::class) dialogState: SearchDialogState +) { + SearchScreenDialogContent( + viewModel = DefaultSearchViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy() + ), + dialogState = { dialogState } + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun RemoveNetworkScreenDialogContentDarkPreview( + @PreviewParameter(SearchScreenDialogStatePreviewParameterProvider::class) dialogState: SearchDialogState +) { + SearchScreenDialogContent( + viewModel = DefaultSearchViewModel( + context = LocalContext.current, + wisefy = ComposablePreviewWisefy() + ), + dialogState = { dialogState } + ) +} + +@Suppress("Deprecation") +private class SearchScreenDialogStatePreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + SearchDialogState.Failure.WisefyAsync(WisefyException("", null)), + SearchDialogState.InputError.SSID, + SearchDialogState.InputError.BSSID, + SearchDialogState.SearchForSSID.Success(""), + SearchDialogState.SearchForSSID.NoSSIDFound, + SearchDialogState.SearchForSSID.PermissionError, + SearchDialogState.SearchForAccessPoint.NoAccessPointFound, + SearchDialogState.SearchForAccessPoint.PermissionError, + SearchDialogState.SearchForSavedNetwork.Success( + SavedNetworkData.Configuration(android.net.wifi.WifiConfiguration()) + ), + SearchDialogState.SearchForSavedNetwork.NoSavedNetworkFound, + SearchDialogState.SearchForSavedNetwork.PermissionError, + SearchDialogState.SearchForSSIDs.Success(emptyList()), + SearchDialogState.SearchForSSIDs.NoSSIDsFound, + SearchDialogState.SearchForSSIDs.PermissionError, + SearchDialogState.SearchForAccessPoints.Success(emptyList()), + SearchDialogState.SearchForAccessPoints.NoAccessPointsFound, + SearchDialogState.SearchForAccessPoints.PermissionError, + SearchDialogState.SearchForSavedNetworks.Success(emptyList()), + SearchDialogState.SearchForSavedNetworks.NoSavedNetworksFound, + SearchDialogState.SearchForSavedNetworks.PermissionError + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchScreenUIState.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchScreenUIState.kt new file mode 100644 index 00000000..b3509bdc --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchScreenUIState.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.search + +import com.isupatches.android.wisefy.accesspoints.entities.AccessPointData +import com.isupatches.android.wisefy.sample.entities.SSIDType +import com.isupatches.android.wisefy.sample.entities.SearchType +import com.isupatches.android.wisefy.savednetworks.entities.SavedNetworkData + +internal data class SearchUIState( + val loadingState: SearchLoadingState, + val dialogState: SearchDialogState, + val inputState: SearchInputState, + val searchType: SearchType, + val ssidType: SSIDType, + val returnFullList: Boolean, + val filterDuplicates: Boolean, + val timeoutInSeconds: Int? +) + +internal data class SearchLoadingState(val isLoading: Boolean) + +internal sealed class SearchDialogState { + object None : SearchDialogState() + + sealed class InputError : SearchDialogState() { + object SSID : InputError() + object BSSID : InputError() + } + + sealed class Failure : SearchDialogState() { + data class WisefyAsync(val exception: Throwable) : Failure() + } + + sealed class SearchForAccessPoint : SearchDialogState() { + data class Success(val data: AccessPointData) : SearchForAccessPoint() + object NoAccessPointFound : SearchForAccessPoint() + object PermissionError : SearchForAccessPoint() + } + + sealed class SearchForAccessPoints : SearchDialogState() { + data class Success(val data: List) : SearchForAccessPoints() + object NoAccessPointsFound : SearchForAccessPoints() + object PermissionError : SearchForAccessPoints() + } + + sealed class SearchForSSID : SearchDialogState() { + data class Success(val data: String) : SearchForSSID() + object NoSSIDFound : SearchForSSID() + object PermissionError : SearchForSSID() + } + + sealed class SearchForSSIDs : SearchDialogState() { + data class Success(val data: List) : SearchForSSIDs() + object NoSSIDsFound : SearchForSSIDs() + object PermissionError : SearchForSSIDs() + } + + sealed class SearchForSavedNetwork : SearchDialogState() { + data class Success(val data: SavedNetworkData) : SearchForSavedNetwork() + object NoSavedNetworkFound : SearchForSavedNetwork() + object PermissionError : SearchForSavedNetwork() + } + + sealed class SearchForSavedNetworks : SearchDialogState() { + data class Success(val data: List) : SearchForSavedNetworks() + object NoSavedNetworksFound : SearchForSavedNetworks() + object PermissionError : SearchForSavedNetworks() + } +} + +internal data class SearchInputState( + val input: String, + val inputValidityState: SearchInputValidityState +) + +internal sealed class SearchInputValidityState { + sealed class SSID : SearchInputValidityState() { + object Valid : SearchInputValidityState() + + sealed class Invalid : SearchInputValidityState() { + object Empty : Invalid() + object TooShort : Invalid() + object TooLong : Invalid() + object InvalidCharacters : Invalid() + object InvalidStartCharacters : Invalid() + object LeadingOrTrailingSpaces : Invalid() + object InvalidUnicode : Invalid() + } + } + + sealed class BSSID : SearchInputValidityState() { + object Valid : BSSID() + + sealed class Invalid : BSSID() { + object Empty : Invalid() + object ImproperFormat : Invalid() + } + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchStore.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchStore.kt new file mode 100644 index 00000000..651f52e0 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchStore.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.search + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.isupatches.android.wisefy.sample.entities.SSIDType +import com.isupatches.android.wisefy.sample.entities.SearchType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private const val PREF_LAST_USED_NETWORK_INPUT = "last_used_network_input" +private const val PREF_SEARCH_TYPE = "search_type" +private const val PREF_SSID_TYPE = "ssid_type" +private const val PREF_RETURN_FULL_LIST = "return_full_list" +private const val PREF_FILTER_DUPLICATES = "filter_duplicates" +private const val PREF_TIMEOUT = "timeout" + +internal interface SearchStore { + suspend fun clear() + + fun getLastUsedNetworkInput(): Flow + fun getSearchType(): Flow + fun getSSIDType(): Flow + fun shouldReturnFullList(): Flow + fun shouldFilterDuplicates(): Flow + fun getTimeout(): Flow + + suspend fun setLastUsedNetworkInput(lastUsedNetworkInput: String) + suspend fun setSearchType(searchType: SearchType) + suspend fun setSSIDType(ssidType: SSIDType) + suspend fun setReturnFullList(returnFullList: Boolean) + suspend fun setFilterDuplicates(filterDuplicates: Boolean) + suspend fun setTimeout(timeout: Int) +} + +private val Context.searchDataStore: DataStore by preferencesDataStore(name = "searchDataStore") + +internal class DefaultSearchStore( + private val context: Context +) : SearchStore { + + private val lastUsedNetworkInputKey = stringPreferencesKey(PREF_LAST_USED_NETWORK_INPUT) + private val searchTypeKey = intPreferencesKey(PREF_SEARCH_TYPE) + private val ssidTypeKey = intPreferencesKey(PREF_SSID_TYPE) + private val returnFullListKey = booleanPreferencesKey(PREF_RETURN_FULL_LIST) + private val filterDuplicatesKey = booleanPreferencesKey(PREF_FILTER_DUPLICATES) + private val timeoutKey = intPreferencesKey(PREF_TIMEOUT) + + override suspend fun clear() { + context.searchDataStore.edit { + clear() + } + } + + /* + * Last used network input + */ + + override fun getLastUsedNetworkInput(): Flow { + return context.searchDataStore.data.map { preferences -> + preferences[lastUsedNetworkInputKey] ?: "" + } + } + + override suspend fun setLastUsedNetworkInput(lastUsedNetworkInput: String) { + context.searchDataStore.edit { preferences -> + preferences[lastUsedNetworkInputKey] = lastUsedNetworkInput + } + } + + /* + * Search type + */ + override fun getSearchType(): Flow { + return context.searchDataStore.data.map { preferences -> + SearchType.of(preferences[searchTypeKey] ?: SearchType.ACCESS_POINT.intVal) + } + } + + override suspend fun setSearchType(searchType: SearchType) { + context.searchDataStore.edit { preferences -> + preferences[searchTypeKey] = searchType.intVal + } + } + + /* + * SSID Type + */ + + override fun getSSIDType(): Flow { + return context.searchDataStore.data.map { preferences -> + SSIDType.of(preferences[ssidTypeKey] ?: SSIDType.SSID.intVal) + } + } + + override suspend fun setSSIDType(ssidType: SSIDType) { + context.searchDataStore.edit { preferences -> + preferences[ssidTypeKey] = ssidType.intVal + } + } + + /* + * Return full list + */ + + override fun shouldReturnFullList(): Flow { + return context.searchDataStore.data.map { preferences -> + preferences[returnFullListKey] ?: true + } + } + + override suspend fun setReturnFullList(returnFullList: Boolean) { + context.searchDataStore.edit { preferences -> + preferences[returnFullListKey] = returnFullList + } + } + + /* + * Filter duplicates + */ + + override fun shouldFilterDuplicates(): Flow { + return context.searchDataStore.data.map { preferences -> + preferences[filterDuplicatesKey] ?: true + } + } + + override suspend fun setFilterDuplicates(filterDuplicates: Boolean) { + context.searchDataStore.edit { preferences -> + preferences[filterDuplicatesKey] = filterDuplicates + } + } + + /* + * Timeout + */ + + override fun getTimeout(): Flow { + return context.searchDataStore.data.map { preferences -> + preferences[timeoutKey] ?: 1 + } + } + + override suspend fun setTimeout(timeout: Int) { + context.searchDataStore.edit { preferences -> + preferences[timeoutKey] = timeout + } + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchViewModel.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchViewModel.kt new file mode 100644 index 00000000..bc61ae0d --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/features/search/SearchViewModel.kt @@ -0,0 +1,531 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.features.search + +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.Manifest.permission.ACCESS_WIFI_STATE +import android.content.Context +import androidx.annotation.RequiresPermission +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.viewModelScope +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.accesspoints.entities.GetAccessPointsQuery +import com.isupatches.android.wisefy.accesspoints.entities.GetAccessPointsResult +import com.isupatches.android.wisefy.core.exceptions.WisefyException +import com.isupatches.android.wisefy.ktx.getAccessPointsAsync +import com.isupatches.android.wisefy.ktx.getSavedNetworksAsync +import com.isupatches.android.wisefy.sample.entities.SSIDType +import com.isupatches.android.wisefy.sample.entities.SearchType +import com.isupatches.android.wisefy.sample.scaffolding.BaseViewModel +import com.isupatches.android.wisefy.sample.scaffolding.BaseViewModelFactory +import com.isupatches.android.wisefy.sample.util.BSSIDInputError +import com.isupatches.android.wisefy.sample.util.SSIDInputError +import com.isupatches.android.wisefy.sample.util.validateBSSID +import com.isupatches.android.wisefy.sample.util.validateSSID +import com.isupatches.android.wisefy.savednetworks.entities.GetSavedNetworksQuery +import com.isupatches.android.wisefy.savednetworks.entities.GetSavedNetworksResult +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +private const val SECONDS_TO_MILLIS = 1000 + +internal abstract class SearchViewModel : BaseViewModel() { + abstract val uiState: State + + abstract fun onSearchForAccessPointPermissionError() + abstract fun onSearchForAccessPointsPermissionError() + abstract fun onSearchForSavedNetworkPermissionError() + abstract fun onSearchForSavedNetworksPermissionError() + abstract fun onSearchForSSIDPermissionError() + abstract fun onSearchForSSIDsPermissionError() + + @RequiresPermission(ACCESS_FINE_LOCATION) + abstract suspend fun searchForAccessPoint() + + @RequiresPermission(ACCESS_FINE_LOCATION) + abstract suspend fun searchForAccessPoints() + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) + abstract suspend fun searchForSavedNetwork() + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) + abstract suspend fun searchForSavedNetworks() + + @RequiresPermission(ACCESS_FINE_LOCATION) + abstract suspend fun searchForSSID() + + @RequiresPermission(ACCESS_FINE_LOCATION) + abstract suspend fun searchForSSIDs() + + abstract fun onSearchNetworkInputChanged(input: String) + + abstract fun onSearchTypeSelected(searchType: SearchType) + abstract fun onReturnFullListChanged(enabled: Boolean) + abstract fun onFilterDuplicatesChanged(enabled: Boolean) + abstract fun onSSIDTypeChanged(ssidType: SSIDType) + + abstract fun onSearchTimeoutValueChangeFinished(timeoutInSeconds: Int) + + abstract fun onDialogClosed() +} + +internal class DefaultSearchViewModel( + context: Context, + private val wisefy: WisefyApi, + private val searchStore: SearchStore = DefaultSearchStore(context = context) +) : SearchViewModel() { + + private val _uiState = mutableStateOf( + SearchUIState( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.None, + inputState = SearchInputState( + input = "", + inputValidityState = SearchInputValidityState.SSID.Invalid.Empty + ), + searchType = SearchType.ACCESS_POINT, + ssidType = SSIDType.SSID, + returnFullList = true, + filterDuplicates = true, + timeoutInSeconds = null + ) + ) + override val uiState: State + get() = _uiState + + init { + viewModelScope.launch { + searchStore.getLastUsedNetworkInput() + .collectLatest { input -> + validateInput(input, uiState.value.ssidType) + } + } + + viewModelScope.launch { + searchStore.getSearchType() + .collectLatest { searchType -> + _uiState.value = uiState.value.copy(searchType = searchType) + } + } + + viewModelScope.launch { + searchStore.getSSIDType() + .collectLatest { ssidType -> + _uiState.value = uiState.value.copy(ssidType = ssidType) + validateInput(uiState.value.inputState.input, ssidType) + } + } + + viewModelScope.launch { + searchStore.shouldReturnFullList() + .collectLatest { returnFullList -> + _uiState.value = uiState.value.copy(returnFullList = returnFullList) + } + } + + viewModelScope.launch { + searchStore.shouldFilterDuplicates() + .collectLatest { filterDuplicates -> + _uiState.value = uiState.value.copy(filterDuplicates = filterDuplicates) + } + } + + viewModelScope.launch { + searchStore.getTimeout() + .collectLatest { timeout -> + _uiState.value = uiState.value.copy(timeoutInSeconds = timeout) + } + } + } + + @RequiresPermission(ACCESS_FINE_LOCATION) + override suspend fun searchForAccessPoint() { + if (isInputInvalid()) { + return + } + showProgress() + when (val result = getAccessPoints()) { + is GetAccessPointsResult.AccessPoints -> { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForAccessPoint.Success(result.value.first()) + ) + } + is GetAccessPointsResult.Empty -> { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForAccessPoint.NoAccessPointFound + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + @RequiresPermission(ACCESS_FINE_LOCATION) + override suspend fun searchForAccessPoints() { + if (isInputInvalid()) { + return + } + showProgress() + when (val result = getAccessPoints()) { + is GetAccessPointsResult.AccessPoints -> { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForAccessPoints.Success(result.value) + ) + } + is GetAccessPointsResult.Empty -> { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForAccessPoints.NoAccessPointsFound + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) + override suspend fun searchForSavedNetwork() { + if (isInputInvalid()) { + return + } + showProgress() + when (val result = getSavedNetworks()) { + is GetSavedNetworksResult.Empty -> { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForSavedNetwork.NoSavedNetworkFound + ) + } + is GetSavedNetworksResult.SavedNetworks -> { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForSavedNetwork.Success(result.value.first()) + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) + override suspend fun searchForSavedNetworks() { + if (isInputInvalid()) { + return + } + showProgress() + when (val result = getSavedNetworks()) { + is GetSavedNetworksResult.Empty -> { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForSavedNetworks.NoSavedNetworksFound + ) + } + is GetSavedNetworksResult.SavedNetworks -> { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForSavedNetworks.Success(result.value) + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + @RequiresPermission(ACCESS_FINE_LOCATION) + override suspend fun searchForSSID() { + if (isInputInvalid()) { + return + } + showProgress() + when (val result = getAccessPoints()) { + is GetAccessPointsResult.AccessPoints -> { + val ssid = when (uiState.value.ssidType) { + SSIDType.SSID -> result.value.first().ssid + SSIDType.BSSID -> result.value.first().bssid + } + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForSSID.Success(ssid) + ) + } + is GetAccessPointsResult.Empty -> { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForSSID.NoSSIDFound + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + @RequiresPermission(ACCESS_FINE_LOCATION) + override suspend fun searchForSSIDs() { + if (isInputInvalid()) { + return + } + showProgress() + when (val result = getAccessPoints()) { + is GetAccessPointsResult.AccessPoints -> { + val ssids = when (uiState.value.ssidType) { + SSIDType.SSID -> result.value.map { it.ssid } + SSIDType.BSSID -> result.value.map { it.bssid } + } + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForSSIDs.Success(ssids) + ) + } + is GetAccessPointsResult.Empty -> { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForSSIDs.NoSSIDsFound + ) + } + null -> { + // Case handled above in the catch clause + } + } + } + + override fun onSearchNetworkInputChanged(input: String) { + viewModelScope.launch { + searchStore.setLastUsedNetworkInput(input) + } + } + + override fun onSearchTypeSelected(searchType: SearchType) { + viewModelScope.launch { + searchStore.setSearchType(searchType) + } + } + + override fun onSSIDTypeChanged(ssidType: SSIDType) { + viewModelScope.launch { + searchStore.setSSIDType(ssidType) + } + } + + override fun onReturnFullListChanged(enabled: Boolean) { + viewModelScope.launch { + searchStore.setReturnFullList(enabled) + } + } + + override fun onFilterDuplicatesChanged(enabled: Boolean) { + viewModelScope.launch { + searchStore.setFilterDuplicates(enabled) + } + } + + override fun onSearchTimeoutValueChangeFinished(timeoutInSeconds: Int) { + viewModelScope.launch { + _uiState.value = uiState.value.copy(timeoutInSeconds = timeoutInSeconds) + searchStore.setTimeout(timeoutInSeconds) + } + } + + override fun onSearchForAccessPointPermissionError() { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForAccessPoint.PermissionError + ) + } + + override fun onSearchForAccessPointsPermissionError() { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForAccessPoints.PermissionError + ) + } + + override fun onSearchForSavedNetworkPermissionError() { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForSavedNetwork.PermissionError + ) + } + + override fun onSearchForSavedNetworksPermissionError() { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForSavedNetworks.PermissionError + ) + } + + override fun onSearchForSSIDPermissionError() { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForSSID.PermissionError + ) + } + + override fun onSearchForSSIDsPermissionError() { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.SearchForSSIDs.PermissionError + ) + } + + override fun onDialogClosed() { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.None + ) + } + + @RequiresPermission(ACCESS_FINE_LOCATION) + private suspend fun getAccessPoints(): GetAccessPointsResult? { + return try { + wisefy.getAccessPointsAsync(query = getAccessPointsQuery()) + } catch (ex: WisefyException) { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.Failure.WisefyAsync(exception = ex) + ) + null + } + } + + private fun getAccessPointsQuery(): GetAccessPointsQuery { + val networkInput = uiState.value.inputState + val timeoutInSeconds = if (uiState.value.searchType == SearchType.SAVED_NETWORK) { + null + } else { + uiState.value.timeoutInSeconds + } + return when (uiState.value.ssidType) { + SSIDType.SSID -> { + GetAccessPointsQuery.BySSID( + regex = networkInput.input, + timeoutInMillis = timeoutInSeconds?.let { it * SECONDS_TO_MILLIS } + ) + } + SSIDType.BSSID -> { + GetAccessPointsQuery.ByBSSID( + regex = networkInput.input, + timeoutInMillis = timeoutInSeconds?.let { it * SECONDS_TO_MILLIS } + ) + } + } + } + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) + private suspend fun getSavedNetworks(): GetSavedNetworksResult? { + return try { + wisefy.getSavedNetworksAsync(query = getSavedNetworksQuery()) + } catch (ex: WisefyException) { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.Failure.WisefyAsync(exception = ex) + ) + null + } + } + + private fun getSavedNetworksQuery(): GetSavedNetworksQuery { + val networkInput = uiState.value.inputState + return when (uiState.value.ssidType) { + SSIDType.SSID -> GetSavedNetworksQuery.BySSID(regex = networkInput.input) + SSIDType.BSSID -> GetSavedNetworksQuery.ByBSSID(regex = networkInput.input) + } + } + + private fun isInputInvalid(): Boolean { + val currentInputState = uiState.value.inputState + when (uiState.value.ssidType) { + SSIDType.SSID -> { + if (currentInputState.inputValidityState != SearchInputValidityState.SSID.Valid) { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.InputError.SSID + ) + return true + } + } + SSIDType.BSSID -> { + if (currentInputState.inputValidityState != SearchInputValidityState.BSSID.Valid) { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = false), + dialogState = SearchDialogState.InputError.BSSID + ) + return true + } + } + } + return false + } + + private fun showProgress() { + _uiState.value = uiState.value.copy( + loadingState = SearchLoadingState(isLoading = true), + dialogState = SearchDialogState.None + ) + } + + private fun validateInput(input: String, ssidType: SSIDType) { + val validityState = when (ssidType) { + SSIDType.SSID -> { + when (input.validateSSID()) { + SSIDInputError.EMPTY -> SearchInputValidityState.SSID.Invalid.Empty + SSIDInputError.TOO_SHORT -> SearchInputValidityState.SSID.Invalid.TooShort + SSIDInputError.TOO_LONG -> SearchInputValidityState.SSID.Invalid.TooLong + SSIDInputError.INVALID_CHARACTERS -> { + SearchInputValidityState.SSID.Invalid.InvalidCharacters + } + SSIDInputError.INVALID_START_CHARACTERS -> { + SearchInputValidityState.SSID.Invalid.InvalidStartCharacters + } + SSIDInputError.LEADING_OR_TRAILING_SPACES -> { + SearchInputValidityState.SSID.Invalid.LeadingOrTrailingSpaces + } + SSIDInputError.NOT_VALID_UNICODE -> SearchInputValidityState.SSID.Invalid.InvalidUnicode + SSIDInputError.NONE -> SearchInputValidityState.SSID.Valid + } + } + SSIDType.BSSID -> { + when (input.validateBSSID()) { + BSSIDInputError.EMPTY -> SearchInputValidityState.BSSID.Invalid.Empty + BSSIDInputError.INVALID -> SearchInputValidityState.BSSID.Invalid.ImproperFormat + BSSIDInputError.NONE -> SearchInputValidityState.BSSID.Valid + } + } + } + _uiState.value = uiState.value.copy( + inputState = SearchInputState( + input = input, + inputValidityState = validityState + ) + ) + } +} + +internal class SearchViewModelFactory( + private val context: Context, + private val wisefy: WisefyApi +) : BaseViewModelFactory( + supportedClass = SearchViewModel::class, + vmProvider = { DefaultSearchViewModel(context, wisefy) } +) diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/base/BaseActivity.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/base/BaseActivity.kt deleted file mode 100644 index a9470c52..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/base/BaseActivity.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.base - -import android.os.Bundle -import androidx.viewbinding.ViewBinding -import dagger.android.support.DaggerAppCompatActivity - -internal abstract class BaseActivity : DaggerAppCompatActivity() { - - protected abstract val binding: ViewBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/base/BaseDialogFragment.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/base/BaseDialogFragment.kt deleted file mode 100644 index a5f6f644..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/base/BaseDialogFragment.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.base - -import android.os.Bundle -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.FragmentManager - -internal abstract class BaseDialogFragment : DialogFragment() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - } - - fun showNoDuplicates(manager: FragmentManager, tag: String) { - if (manager.findFragmentByTag(tag) != null) return - show(manager, tag) - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/base/BaseFragment.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/base/BaseFragment.kt deleted file mode 100644 index 689da0cf..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/base/BaseFragment.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.base - -import android.content.Context -import androidx.annotation.StringRes -import com.isupatches.android.wisefy.sample.R -import com.isupatches.android.wisefy.sample.internal.scaffolding.BaseView -import com.isupatches.android.wisefy.sample.internal.util.PermissionUtil -import com.isupatches.android.wisefy.sample.ui.dialogs.FullScreenNoticeDialogFragment -import com.isupatches.android.wisefy.sample.ui.dialogs.NoticeDialogFragment -import dagger.android.support.AndroidSupportInjection -import javax.inject.Inject - -internal abstract class BaseFragment : BaseView() { - - @Inject lateinit var permissionUtil: PermissionUtil - - private fun isActivityInvalid(): Boolean = activity == null || (activity?.isFinishing == true) - - override fun onAttach(context: Context) { - AndroidSupportInjection.inject(this) - super.onAttach(context) - } - - protected fun displayInfo(@StringRes infoMessageResId: Int, @StringRes infoTitleResId: Int = R.string.info) { - showDialogNoDuplicates( - tag = NoticeDialogFragment.TAG, - dialog = NoticeDialogFragment.newInstance( - title = getString(infoTitleResId), - message = getString(infoMessageResId) - ) - ) - } - - protected fun displayInfo(infoMessage: String, @StringRes infoTitleResId: Int) { - showDialogNoDuplicates( - tag = NoticeDialogFragment.TAG, - dialog = NoticeDialogFragment.newInstance( - title = getString(infoTitleResId), - message = infoMessage - ) - ) - } - - protected fun displayInfoFullScreen(infoMessage: String, @StringRes infoTitleResId: Int) { - showDialogNoDuplicates( - tag = FullScreenNoticeDialogFragment.TAG, - dialog = FullScreenNoticeDialogFragment.newInstance( - title = getString(infoTitleResId), - message = infoMessage - ) - ) - } - - protected fun displayPermissionErrorDialog(@StringRes permissionErrorMessageResId: Int) { - showDialogNoDuplicates( - tag = NoticeDialogFragment.TAG, - dialog = NoticeDialogFragment.newInstance( - title = getString(R.string.permission_error), - message = getString(permissionErrorMessageResId) - ) - ) - } - - protected fun displayPermissionErrorDialog(permissionErrorMessage: String) { - showDialogNoDuplicates( - tag = NoticeDialogFragment.TAG, - dialog = NoticeDialogFragment.newInstance( - title = getString(R.string.permission_error), - message = permissionErrorMessage - ) - ) - } - - protected fun displayWisefyAsyncErrorDialog(throwable: Throwable) { - showDialogNoDuplicates( - tag = NoticeDialogFragment.TAG, - dialog = NoticeDialogFragment.newInstance( - title = getString(R.string.wisefy_async_error), - message = getString(R.string.wisefy_async_error_descriptions_args, throwable.message) - ) - ) - } - - private fun showDialogNoDuplicates(tag: String, dialog: T) { - if (isActivityInvalid()) return - dialog.showNoDuplicates(childFragmentManager, tag) - } - - protected fun isPermissionGranted(permission: String, requestCode: Int): Boolean { - return if (!permissionUtil.isPermissionGranted(requireActivity(), permission)) { - // Logic may be added here to display rationale if needed - requestPermissions(arrayOf(permission), requestCode) - false - } else { - true - } - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/di/ScreenBindingsModule.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/di/ScreenBindingsModule.kt deleted file mode 100644 index f08bd4bb..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/di/ScreenBindingsModule.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.di - -import com.isupatches.android.wisefy.sample.ui.main.MainActivity -import com.isupatches.android.wisefy.sample.ui.main.MainActivityFragmentBindings -import com.isupatches.android.wisefy.sample.ui.main.MainActivityModule -import dagger.Module -import dagger.android.ContributesAndroidInjector - -@Suppress("unused") -@Module internal interface ScreenBindingsModule { - - @ContributesAndroidInjector( - modules = [ - MainActivityModule::class, - MainActivityFragmentBindings::class - ] - ) - fun mainActivity(): MainActivity -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BaseModel.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BaseModel.kt deleted file mode 100644 index b7b62b59..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BaseModel.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.scaffolding - -private interface Model - -internal abstract class BaseModel : Model diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BasePresenter.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BasePresenter.kt deleted file mode 100644 index 7b4cedea..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BasePresenter.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.scaffolding - -import com.isupatches.android.wisefy.sample.internal.logging.WisefySampleLogger -import java.lang.RuntimeException - -internal interface Presenter { - fun attachView(view: VIEW) - fun detachView() -} - -private const val LOG_TAG = "BasePresenter" - -internal abstract class BasePresenter : Presenter { - - private var view: VIEW? = null - - private val isViewAttached: Boolean - get() = view != null - - override fun attachView(view: VIEW) { - this.view = view - } - - override fun detachView() { - view = null - } - - protected fun getView(): VIEW = view ?: throw ViewNotAttachedException() - - protected fun doSafelyWithView(viewCommand: (view: VIEW) -> Unit) { - if (isViewAttached) { -// view?.scheduleDirect { - if (isViewAttached) { - val view = view - requireNotNull(view) - viewCommand(view) - } else { - WisefySampleLogger.w(LOG_TAG, "ViewCommand was scheduled., but view is now detached!") - } -// } - } - } -} - -private class ViewNotAttachedException : RuntimeException( - "New view attached. Did you forget to call attachView()?" -) diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BaseStore.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BaseStore.kt deleted file mode 100644 index 92183a6e..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BaseStore.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.scaffolding - -import android.content.Context -import android.content.SharedPreferences -import androidx.annotation.StringRes -import androidx.core.content.edit - -internal interface Store - -private const val PREF_LAST_USED_REGEX = "last used regex" - -internal abstract class BaseSharedPreferenceStore : Store { - - protected fun getSharedPreferences(context: Context, @StringRes prefKey: Int): SharedPreferences { - return context.getSharedPreferences(context.getString(prefKey), Context.MODE_PRIVATE) - } - - protected fun SharedPreferences.getLastUsedRegex() = getNonNullString(PREF_LAST_USED_REGEX) - - protected fun SharedPreferences.setLastUsedRegex(lastUsedRegex: String) { - edit { - putString(PREF_LAST_USED_REGEX, lastUsedRegex) - } - } - - protected fun SharedPreferences.getNonNullString(key: String) = getString(key, "") ?: "" -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BaseView.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BaseView.kt deleted file mode 100644 index 1445c678..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BaseView.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.scaffolding - -import androidx.fragment.app.Fragment - -internal interface View { - fun displayWisefyAsyncError(throwable: Throwable) -} - -internal abstract class BaseView : Fragment(), View diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/BundleUtil.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/BundleUtil.kt deleted file mode 100644 index 9e7eafdd..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/BundleUtil.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.util - -import android.os.Bundle -import androidx.fragment.app.Fragment - -internal fun T.applyArguments(block: Bundle.() -> Unit): T { - arguments = Bundle().apply(block) - return this -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/EditTextUtil.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/EditTextUtil.kt deleted file mode 100644 index 368138d8..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/EditTextUtil.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.util - -import android.widget.EditText - -internal fun EditText.getTrimmedInput(): String = text.toString().trim() diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/HtmlUtil.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/HtmlUtil.kt deleted file mode 100644 index 09930bd5..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/HtmlUtil.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.util - -import android.os.Build -import android.text.Html -import android.text.Spanned - -@Suppress("deprecation") -internal fun String.asHtmlSpanned(): Spanned { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - Html.fromHtml(this, Html.FROM_HTML_MODE_COMPACT) - } else { - Html.fromHtml(this) - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/KeyboardUtil.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/KeyboardUtil.kt deleted file mode 100644 index 3ca8f3f3..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/KeyboardUtil.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.util - -import android.app.Activity -import android.view.View -import android.view.inputmethod.InputMethodManager -import com.isupatches.android.wisefy.sample.internal.base.BaseFragment - -internal fun BaseFragment.hideKeyboardFrom(view: View) { - activity?.let { - (it.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager).also { inputManger -> - inputManger.hideSoftInputFromWindow(view.windowToken, 0) - } - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/PermissionsUtil.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/PermissionsUtil.kt deleted file mode 100644 index aa034689..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/PermissionsUtil.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.util - -import android.content.Context -import android.content.pm.PackageManager -import androidx.core.content.ContextCompat -import javax.inject.Inject - -internal interface PermissionUtil { - fun isPermissionGranted(context: Context, permission: String): Boolean -} - -internal class PermissionsUtilImpl @Inject constructor() : PermissionUtil { - - override fun isPermissionGranted(context: Context, permission: String): Boolean { - return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/SdkUtil.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/SdkUtil.kt deleted file mode 100644 index 2e896cff..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/SdkUtil.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.util - -import android.os.Build -import javax.inject.Inject - -internal interface SdkUtil { - - fun isAtLeastQ(): Boolean -} - -internal class SdkUtilImpl @Inject constructor() : SdkUtil { - - override fun isAtLeastQ() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/WiseFyFactory.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/WiseFyFactory.kt deleted file mode 100644 index 7698aaaa..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/WiseFyFactory.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.internal.util - -import android.content.Context -import com.isupatches.android.wisefy.Wisefy -import com.isupatches.android.wisefy.WisefyApi -import com.isupatches.android.wisefy.sample.internal.logging.WisefySampleLogger - -internal fun createWiseFy(context: Context): WisefyApi { - return Wisefy.Brains(context, WisefySampleLogger).getSmarts() -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/logging/WisefySampleLogger.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/logging/WisefySampleLogger.kt similarity index 94% rename from app/src/main/java/com/isupatches/android/wisefy/sample/internal/logging/WisefySampleLogger.kt rename to app/src/main/java/com/isupatches/android/wisefy/sample/logging/WisefySampleLogger.kt index d6b66e0c..b74fdf5c 100644 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/internal/logging/WisefySampleLogger.kt +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/logging/WisefySampleLogger.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Patches Klinefelter + * Copyright 2022 Patches Barrett * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.isupatches.android.wisefy.sample.internal.logging +package com.isupatches.android.wisefy.sample.logging import android.util.Log -import com.isupatches.android.wisefy.logging.WisefyLogger +import com.isupatches.android.wisefy.core.logging.WisefyLogger import com.isupatches.android.wisefy.sample.BuildConfig import java.util.Locale diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/main/HomeScreen.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/main/HomeScreen.kt new file mode 100644 index 00000000..35fdf419 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/main/HomeScreen.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.main + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleSizes +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme + +@Composable +internal fun HomeScreen() { + WisefySampleTheme { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding( + top = WisefySampleSizes.WisefySampleTopMargin, + bottom = WisefySampleSizes.WisefySampleBottomMargin, + start = WisefySampleSizes.WisefySampleHorizontalMargins, + end = WisefySampleSizes.WisefySampleHorizontalMargins + ) + ) { + Row { + Box { + Image( + painter = painterResource(id = R.drawable.ic_logo), + contentDescription = stringResource(R.string.content_description_logo), + colorFilter = ColorFilter.tint(MaterialTheme.colors.primary) + ) + } + } + Row { + Box(modifier = Modifier.padding(top = WisefySampleSizes.Medium)) { + Text( + text = stringResource(R.string.wisefy), + style = MaterialTheme.typography.h2, + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) + } + } + Row { + Box(modifier = Modifier.padding(top = WisefySampleSizes.XLarge)) { + Text( + text = stringResource(R.string.wisefy_sample_description), + style = MaterialTheme.typography.subtitle1, + textAlign = TextAlign.Center, + color = MaterialTheme.colors.onBackground + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun HomeScreenLayoutLightPreview() { + HomeScreen() +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun HomeScreenLayoutDarkPreview() { + HomeScreen() +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/main/MainActivity.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/main/MainActivity.kt new file mode 100644 index 00000000..01ea1414 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/main/MainActivity.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.main + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.toArgb +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.sample.ui.components.WisefySampleToolbar +import com.isupatches.android.wisefy.sample.ui.components.navigation.WisefySampleBottomNavigation +import com.isupatches.android.wisefy.sample.ui.components.navigation.WisefySampleNavGraph +import com.isupatches.android.wisefy.sample.ui.components.navigation.WisefySampleNavHost +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme +import com.isupatches.android.wisefy.sample.util.SdkUtil +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class MainActivity : ComponentActivity() { + + @Inject + lateinit var wisefy: WisefyApi + + @Inject + lateinit var sdkUtil: SdkUtil + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + wisefy.init() + setContent { + WisefySampleTheme { + window?.statusBarColor = MaterialTheme.colors.primaryVariant.toArgb() + MainScreenLayout(wisefy, sdkUtil) + } + } + } + + override fun onDestroy() { + wisefy.dump() + super.onDestroy() + } +} + +@Composable +internal fun MainScreenLayout(wisefy: WisefyApi, sdkUtil: SdkUtil) { + val navController = rememberNavController() + Scaffold( + topBar = { WisefySampleToolbar() }, + content = { padding -> + WisefySampleNavHost(navController, wisefy, sdkUtil, padding) + }, + bottomBar = { + val showBottomNav = when (currentRoute(navController = navController)) { + WisefySampleNavGraph.Main.Add.route -> true + WisefySampleNavGraph.Main.Remove.route -> true + WisefySampleNavGraph.Main.Home.route -> true + WisefySampleNavGraph.Main.Misc.route -> true + WisefySampleNavGraph.Main.Search.route -> true + else -> false + } + if (showBottomNav) { + WisefySampleBottomNavigation(navController = navController) + } + } + ) +} + +@Composable +private fun currentRoute(navController: NavHostController): String? { + val navBackStackEntry by navController.currentBackStackEntryAsState() + return navBackStackEntry?.destination?.route +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/main/MainActivityModule.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/main/MainActivityModule.kt new file mode 100644 index 00000000..38961b7f --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/main/MainActivityModule.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.main + +import android.content.Context +import com.isupatches.android.wisefy.sample.util.DefaultSdkUtil +import com.isupatches.android.wisefy.sample.util.SdkUtil +import com.isupatches.android.wisefy.sample.util.createWisefy +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.qualifiers.ApplicationContext + +@Suppress("unused", "UnnecessaryAbstractClass") +@Module +@InstallIn(ActivityComponent::class) +internal abstract class MainActivityModule { + + @Binds + abstract fun bindSdkUtil(impl: DefaultSdkUtil): SdkUtil + + companion object { + @Provides + fun provideWiseFy(@ApplicationContext app: Context) = createWisefy(app) + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/scaffolding/BaseActivity.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/scaffolding/BaseActivity.kt new file mode 100644 index 00000000..73502762 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/scaffolding/BaseActivity.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.scaffolding + +import androidx.activity.ComponentActivity + +@Suppress("Registered") +internal open class BaseActivity : ComponentActivity() diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/scaffolding/BaseViewModel.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/scaffolding/BaseViewModel.kt new file mode 100644 index 00000000..640416c6 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/scaffolding/BaseViewModel.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.scaffolding + +import androidx.lifecycle.ViewModel + +internal open class BaseViewModel : ViewModel() diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/scaffolding/BaseViewModelFactory.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/scaffolding/BaseViewModelFactory.kt new file mode 100644 index 00000000..af48e6e9 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/scaffolding/BaseViewModelFactory.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.scaffolding + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import kotlin.reflect.KClass + +internal abstract class BaseViewModelFactory( + private val supportedClass: KClass, + private val vmProvider: () -> VIEW_MODEL +) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(supportedClass.java)) { + @Suppress("UNCHECKED_CAST") + return vmProvider() as T + } else { + throw IllegalArgumentException( + "${modelClass.simpleName} is an unknown type of view model. " + + "Expected: ${supportedClass.java.simpleName}" + ) + } + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/ComposablePreviewWisefy.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/ComposablePreviewWisefy.kt new file mode 100644 index 00000000..7a198ed8 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/ComposablePreviewWisefy.kt @@ -0,0 +1,237 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui + +import android.os.Build +import androidx.annotation.RequiresApi +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.accesspoints.callbacks.GetAccessPointsCallbacks +import com.isupatches.android.wisefy.accesspoints.entities.GetAccessPointsQuery +import com.isupatches.android.wisefy.accesspoints.entities.GetAccessPointsResult +import com.isupatches.android.wisefy.addnetwork.callbacks.AddNetworkCallbacks +import com.isupatches.android.wisefy.addnetwork.entities.AddNetworkRequest +import com.isupatches.android.wisefy.addnetwork.entities.AddNetworkResult +import com.isupatches.android.wisefy.core.constants.DeprecationMessages +import com.isupatches.android.wisefy.networkconnection.callbacks.ChangeNetworkCallbacks +import com.isupatches.android.wisefy.networkconnection.callbacks.ConnectToNetworkCallbacks +import com.isupatches.android.wisefy.networkconnection.callbacks.DisconnectFromCurrentNetworkCallbacks +import com.isupatches.android.wisefy.networkconnection.entities.ChangeNetworkRequest +import com.isupatches.android.wisefy.networkconnection.entities.ChangeNetworkResult +import com.isupatches.android.wisefy.networkconnection.entities.ConnectToNetworkRequest +import com.isupatches.android.wisefy.networkconnection.entities.ConnectToNetworkResult +import com.isupatches.android.wisefy.networkconnection.entities.DisconnectFromCurrentNetworkRequest +import com.isupatches.android.wisefy.networkconnection.entities.DisconnectFromCurrentNetworkResult +import com.isupatches.android.wisefy.networkinfo.callbacks.GetCurrentNetworkCallbacks +import com.isupatches.android.wisefy.networkinfo.callbacks.GetNetworkConnectionStatusCallbacks +import com.isupatches.android.wisefy.networkinfo.entities.GetCurrentNetworkQuery +import com.isupatches.android.wisefy.networkinfo.entities.GetCurrentNetworkResult +import com.isupatches.android.wisefy.networkinfo.entities.GetNetworkConnectionStatusQuery +import com.isupatches.android.wisefy.networkinfo.entities.GetNetworkConnectionStatusResult +import com.isupatches.android.wisefy.networkinfo.entities.NetworkConnectionStatusData +import com.isupatches.android.wisefy.networkinfo.entities.NetworkData +import com.isupatches.android.wisefy.removenetwork.callbacks.RemoveNetworkCallbacks +import com.isupatches.android.wisefy.removenetwork.entities.RemoveNetworkRequest +import com.isupatches.android.wisefy.removenetwork.entities.RemoveNetworkResult +import com.isupatches.android.wisefy.savednetworks.callbacks.GetSavedNetworksCallbacks +import com.isupatches.android.wisefy.savednetworks.callbacks.IsNetworkSavedCallbacks +import com.isupatches.android.wisefy.savednetworks.entities.GetSavedNetworksQuery +import com.isupatches.android.wisefy.savednetworks.entities.GetSavedNetworksResult +import com.isupatches.android.wisefy.savednetworks.entities.IsNetworkSavedQuery +import com.isupatches.android.wisefy.savednetworks.entities.IsNetworkSavedResult +import com.isupatches.android.wisefy.signal.entities.CalculateSignalLevelRequest +import com.isupatches.android.wisefy.signal.entities.CalculateSignalLevelResult +import com.isupatches.android.wisefy.signal.entities.CompareSignalLevelRequest +import com.isupatches.android.wisefy.signal.entities.CompareSignalLevelResult +import com.isupatches.android.wisefy.wifi.callbacks.DisableWifiCallbacks +import com.isupatches.android.wisefy.wifi.callbacks.EnableWifiCallbacks +import com.isupatches.android.wisefy.wifi.callbacks.IsWifiEnabledCallbacks +import com.isupatches.android.wisefy.wifi.entities.DisableWifiRequest +import com.isupatches.android.wisefy.wifi.entities.DisableWifiResult +import com.isupatches.android.wisefy.wifi.entities.EnableWifiRequest +import com.isupatches.android.wisefy.wifi.entities.EnableWifiResult +import com.isupatches.android.wisefy.wifi.entities.IsWifiEnabledQuery +import com.isupatches.android.wisefy.wifi.entities.IsWifiEnabledResult + +internal class ComposablePreviewWisefy : WisefyApi { + + override fun init() { + // No-op + } + + override fun dump() { + // No-op + } + + override fun addNetwork(request: AddNetworkRequest): AddNetworkResult { + return AddNetworkResult.Success.ResultCode(1) + } + + override fun addNetwork(request: AddNetworkRequest, callbacks: AddNetworkCallbacks?) { + callbacks?.onSuccessAddingNetwork(AddNetworkResult.Success.ResultCode(1)) + } + + override fun calculateSignalLevel(request: CalculateSignalLevelRequest): CalculateSignalLevelResult { + return CalculateSignalLevelResult.Success(value = 0) + } + + override fun compareSignalLevel(request: CompareSignalLevelRequest): CompareSignalLevelResult { + return CompareSignalLevelResult.Success.RSSIValuesAreEqual(value = 0) + } + + @RequiresApi(Build.VERSION_CODES.Q) + override fun changeNetwork(request: ChangeNetworkRequest): ChangeNetworkResult { + return ChangeNetworkResult.Success.InternetConnectivityPanelOpened + } + + @RequiresApi(Build.VERSION_CODES.Q) + override fun changeNetwork(request: ChangeNetworkRequest, callbacks: ChangeNetworkCallbacks?) { + callbacks?.onSuccessChangingNetworks(result = ChangeNetworkResult.Success.InternetConnectivityPanelOpened) + } + + @Deprecated(DeprecationMessages.NetworkConnection.CONNECT_TO_NETWORK) + override fun connectToNetwork(request: ConnectToNetworkRequest): ConnectToNetworkResult { + return ConnectToNetworkResult.Success.True + } + + @Deprecated(DeprecationMessages.NetworkConnection.CONNECT_TO_NETWORK) + override fun connectToNetwork(request: ConnectToNetworkRequest, callbacks: ConnectToNetworkCallbacks?) { + callbacks?.onSuccessConnectingToNetwork(result = ConnectToNetworkResult.Success.True) + } + + override fun disableWifi(request: DisableWifiRequest): DisableWifiResult { + return DisableWifiResult.Success.Disabled + } + + override fun disableWifi(request: DisableWifiRequest, callbacks: DisableWifiCallbacks?) { + callbacks?.onSuccessDisablingWifi(result = DisableWifiResult.Success.Disabled) + } + + @Deprecated(DeprecationMessages.NetworkConnection.DISCONNECT_FROM_CURRENT_NETWORK) + override fun disconnectFromCurrentNetwork( + request: DisconnectFromCurrentNetworkRequest + ): DisconnectFromCurrentNetworkResult { + return DisconnectFromCurrentNetworkResult.Success.True + } + + @Deprecated(DeprecationMessages.NetworkConnection.DISCONNECT_FROM_CURRENT_NETWORK) + override fun disconnectFromCurrentNetwork( + request: DisconnectFromCurrentNetworkRequest, + callbacks: DisconnectFromCurrentNetworkCallbacks? + ) { + callbacks?.onSuccessDisconnectingFromCurrentNetwork(result = DisconnectFromCurrentNetworkResult.Success.True) + } + + override fun enableWifi(request: EnableWifiRequest): EnableWifiResult { + return EnableWifiResult.Success.Enabled + } + + override fun enableWifi(request: EnableWifiRequest, callbacks: EnableWifiCallbacks?) { + callbacks?.onSuccessEnablingWifi(result = EnableWifiResult.Success.Enabled) + } + + override fun getAccessPoints(query: GetAccessPointsQuery): GetAccessPointsResult { + return GetAccessPointsResult.Empty + } + + override fun getAccessPoints(query: GetAccessPointsQuery, callbacks: GetAccessPointsCallbacks?) { + callbacks?.onNoNearbyAccessPoints() + } + + override fun getCurrentNetwork(query: GetCurrentNetworkQuery): GetCurrentNetworkResult { + return GetCurrentNetworkResult( + value = NetworkData( + network = null, + connectionInfo = null, + capabilities = null, + linkProperties = null + ) + ) + } + + override fun getCurrentNetwork(query: GetCurrentNetworkQuery, callbacks: GetCurrentNetworkCallbacks?) { + callbacks?.onCurrentNetworkRetrieved( + NetworkData( + network = null, + connectionInfo = null, + capabilities = null, + linkProperties = null + ) + ) + } + + override fun getNetworkConnectionStatus(query: GetNetworkConnectionStatusQuery): GetNetworkConnectionStatusResult { + return GetNetworkConnectionStatusResult( + value = NetworkConnectionStatusData( + isConnected = false, + isConnectedToMobileNetwork = false, + isConnectedToWifiNetwork = false, + isRoaming = false, + ssidOfNetworkConnectedTo = null, + bssidOfNetworkConnectedTo = null, + ip = null + ) + ) + } + + override fun getNetworkConnectionStatus( + query: GetNetworkConnectionStatusQuery, + callbacks: GetNetworkConnectionStatusCallbacks? + ) { + callbacks?.onDeviceNetworkConnectionStatusRetrieved( + networkConnectionStatus = NetworkConnectionStatusData( + isConnected = false, + isConnectedToMobileNetwork = false, + isConnectedToWifiNetwork = false, + isRoaming = false, + ssidOfNetworkConnectedTo = null, + bssidOfNetworkConnectedTo = null, + ip = null + ) + ) + } + + override fun getSavedNetworks(query: GetSavedNetworksQuery): GetSavedNetworksResult { + return GetSavedNetworksResult.Empty + } + + override fun getSavedNetworks(query: GetSavedNetworksQuery, callbacks: GetSavedNetworksCallbacks?) { + callbacks?.onNoSavedNetworksFound() + } + + override fun isNetworkSaved(query: IsNetworkSavedQuery): IsNetworkSavedResult { + return IsNetworkSavedResult.False + } + + override fun isNetworkSaved(query: IsNetworkSavedQuery, callbacks: IsNetworkSavedCallbacks?) { + callbacks?.onNetworkIsNotSaved() + } + + override fun isWifiEnabled(query: IsWifiEnabledQuery): IsWifiEnabledResult { + return IsWifiEnabledResult.True + } + + override fun isWifiEnabled(query: IsWifiEnabledQuery, callbacks: IsWifiEnabledCallbacks?) { + callbacks?.onWifiIsEnabled() + } + + override fun removeNetwork(request: RemoveNetworkRequest): RemoveNetworkResult { + return RemoveNetworkResult.Success.True + } + + override fun removeNetwork(request: RemoveNetworkRequest, callbacks: RemoveNetworkCallbacks?) { + callbacks?.onSuccessRemovingNetwork(RemoveNetworkResult.Success.True) + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkFragment.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkFragment.kt deleted file mode 100644 index 68c963fe..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkFragment.kt +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.add - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.Manifest.permission.CHANGE_WIFI_STATE -import android.app.Activity -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Build -import android.os.Bundle -import android.provider.Settings.ADD_WIFI_RESULT_SUCCESS -import android.provider.Settings.EXTRA_WIFI_NETWORK_RESULT_LIST -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.ActivityResult -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.VisibleForTesting -import com.isupatches.android.viewglu.paste -import com.isupatches.android.wisefy.addnetwork.entities.AddNetworkResult -import com.isupatches.android.wisefy.sample.R -import com.isupatches.android.wisefy.sample.databinding.FragmentAddNetworkBinding -import com.isupatches.android.wisefy.sample.internal.base.BaseFragment -import com.isupatches.android.wisefy.sample.internal.entities.NetworkType -import com.isupatches.android.wisefy.sample.internal.logging.WisefySampleLogger -import com.isupatches.android.wisefy.sample.internal.util.SdkUtil -import com.isupatches.android.wisefy.sample.internal.util.getTrimmedInput -import com.isupatches.android.wisefy.sample.internal.util.hideKeyboardFrom -import javax.inject.Inject - -@VisibleForTesting internal const val WISEFY_ADD_OPEN_NETWORK_REQUEST_CODE = 1 -@VisibleForTesting internal const val WISEFY_ADD_WPA2_NETWORK_REQUEST_CODE = 2 -@VisibleForTesting internal const val WISEFY_ADD_WPA3_NETWORK_REQUEST_CODE = 3 - -private const val LOG_TAG = "AddNetworkFragment" - -internal interface AddNetworkView { - fun displayFailureAddingNetwork(result: AddNetworkResult) - fun displayNetworkAdded(result: AddNetworkResult) -} - -internal class AddNetworkFragment : BaseFragment(), AddNetworkView { - - @AddNetworkScope @Inject lateinit var presenter: AddNetworkPresenter - @AddNetworkScope @Inject lateinit var addNetworkStore: AddNetworkStore - @AddNetworkScope @Inject lateinit var sdkUtil: SdkUtil - - private var binding: FragmentAddNetworkBinding by paste() - private lateinit var addNetworkResult: ActivityResultLauncher - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = FragmentAddNetworkBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - addNetworkResult = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result: ActivityResult -> - when (result.resultCode) { - Activity.RESULT_OK -> { - val data = result.data ?: return@registerForActivityResult - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val networkResultList = data.getIntegerArrayListExtra(EXTRA_WIFI_NETWORK_RESULT_LIST) - ?: emptyList() - for (resultCode in networkResultList) { - if (resultCode == ADD_WIFI_RESULT_SUCCESS) { - displayNetworkAdded(AddNetworkResult.ResultCode(resultCode)) - } else { - displayFailureAddingNetwork(AddNetworkResult.ResultCode(resultCode)) - } - } - } - } - } - } - } - - override fun onStart() { - super.onStart() - presenter.attachView(this) - } - - override fun onStop() { - presenter.detachView() - super.onStop() - addNetworkStore.setLastUsedNetworkName(binding.networkNameEdt.getTrimmedInput()) - addNetworkStore.setLastUsedNetworkPassword(binding.networkPasswordEdt.getTrimmedInput()) - hideKeyboardFrom(binding.addNetworkBtn) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - if (savedInstanceState == null) { - restoreUI() - } - - binding.addNetworkBtn.setOnClickListener { - when (binding.addNetworkTypeRdg.checkedRadioButtonId) { - R.id.openNetworkRdb -> addOpenNetwork() - R.id.wpa2NetworkRdb -> addWPA2Network() - R.id.wpa3NetworkRdb -> addWPA3Network() - } - } - binding.addNetworkTypeRdg.setOnCheckedChangeListener { _, checkedId -> - when (checkedId) { - R.id.openNetworkRdb -> addNetworkStore.setNetworkType(NetworkType.OPEN) - R.id.wpa2NetworkRdb -> addNetworkStore.setNetworkType(NetworkType.WPA2) - R.id.wpa3NetworkRdb -> addNetworkStore.setNetworkType(NetworkType.WPA2) - } - updateUI() - } - } - - /* - * View helpers - */ - - private fun adjustNetworkPasswordVisibility(visibility: Int) { - binding.networkPasswordTil.visibility = visibility - binding.networkPasswordEdt.visibility = visibility - } - - private fun restoreUI() { - // Restore edit text values - binding.networkNameEdt.setText(addNetworkStore.getLastUsedNetworkName()) - binding.networkPasswordEdt.setText(addNetworkStore.getLastUsedNetworkPassword()) - - // Restore checked state - val checkedId = when (addNetworkStore.getNetworkType()) { - NetworkType.OPEN -> R.id.openNetworkRdb - NetworkType.WPA2 -> R.id.wpa2NetworkRdb - NetworkType.WPA3 -> R.id.wpa3NetworkRdb - } - binding.addNetworkTypeRdg.check(checkedId) - - // Show/hide password edit - when (addNetworkStore.getNetworkType()) { - NetworkType.OPEN -> adjustNetworkPasswordVisibility(View.INVISIBLE) - else -> adjustNetworkPasswordVisibility(View.VISIBLE) - } - } - - private fun updateUI() { - when (binding.addNetworkTypeRdg.checkedRadioButtonId) { - R.id.openNetworkRdb -> adjustNetworkPasswordVisibility(View.INVISIBLE) - else -> adjustNetworkPasswordVisibility(View.VISIBLE) - } - } - - /* - * Presenter callback overrides - */ - - override fun displayFailureAddingNetwork(result: AddNetworkResult) { - displayInfo(getString(R.string.failed_adding_network_args, result), R.string.add_network_result) - } - - override fun displayNetworkAdded(result: AddNetworkResult) { - displayInfoFullScreen( - getString(R.string.succeeded_adding_network_args, result), - R.string.add_network_result - ) - } - - override fun displayWisefyAsyncError(throwable: Throwable) { - displayWisefyAsyncErrorDialog(throwable) - } - - /* - * WiseFy helpers - */ - - @Throws(SecurityException::class) - private fun addOpenNetwork() { - if (checkAddOpenNetworkPermissions()) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - presenter.addOpenNetwork( - ssid = binding.networkNameEdt.getTrimmedInput(), - activityResultLauncher = addNetworkResult - ) - } else { - presenter.addOpenNetwork(ssid = binding.networkNameEdt.getTrimmedInput()) - } - } - } - - @Throws(SecurityException::class) - private fun addWPA2Network() { - if (checkAddWPA2NetworkPermissions()) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - presenter.addWPA2Network( - ssid = binding.networkNameEdt.getTrimmedInput(), - passphrase = binding.networkPasswordEdt.getTrimmedInput(), - activityResultLauncher = addNetworkResult - ) - } else { - presenter.addWPA2Network( - ssid = binding.networkNameEdt.getTrimmedInput(), - passphrase = binding.networkPasswordEdt.getTrimmedInput() - ) - } - } - } - - @Throws(SecurityException::class) - private fun addWPA3Network() { - if (sdkUtil.isAtLeastQ()) { - if (checkAddWPA3NetworkPermissions()) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - presenter.addWPA3Network( - ssid = binding.networkNameEdt.getTrimmedInput(), - passphrase = binding.networkPasswordEdt.getTrimmedInput(), - activityResultLauncher = addNetworkResult - ) - } else { - presenter.addWPA3Network( - ssid = binding.networkNameEdt.getTrimmedInput(), - passphrase = binding.networkPasswordEdt.getTrimmedInput() - ) - } - } - } else { - displayInfo(R.string.add_wpa3_android_q_notice) - } - } - - /* - * Permission helpers - */ - - private fun checkAddOpenNetworkPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_ADD_OPEN_NETWORK_REQUEST_CODE) && - isPermissionGranted(CHANGE_WIFI_STATE, WISEFY_ADD_OPEN_NETWORK_REQUEST_CODE) - } - - private fun checkAddWPA2NetworkPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_ADD_WPA2_NETWORK_REQUEST_CODE) && - isPermissionGranted(CHANGE_WIFI_STATE, WISEFY_ADD_WPA2_NETWORK_REQUEST_CODE) - } - - private fun checkAddWPA3NetworkPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_ADD_WPA3_NETWORK_REQUEST_CODE) && - isPermissionGranted(CHANGE_WIFI_STATE, WISEFY_ADD_WPA3_NETWORK_REQUEST_CODE) - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - when (requestCode) { - WISEFY_ADD_OPEN_NETWORK_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - addOpenNetwork() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for adding an open network are denied") - displayPermissionErrorDialog(R.string.permission_error_add_open_network) - } - } - WISEFY_ADD_WPA2_NETWORK_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - addWPA2Network() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for adding a WPA2 network are denied") - displayPermissionErrorDialog(R.string.permission_error_add_wpa2_network) - } - } - WISEFY_ADD_WPA3_NETWORK_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - addWPA3Network() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for adding a WPA3 network are denied") - displayPermissionErrorDialog(R.string.permission_error_add_wpa3_network) - } - } - else -> { - WisefySampleLogger.wtf(LOG_TAG, "Weird permission requested, not handled") - displayPermissionErrorDialog( - getString(R.string.permission_error_unhandled_request_code_args, requestCode) - ) - } - } - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkModel.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkModel.kt deleted file mode 100644 index bf1a9c39..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkModel.kt +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.add - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.Manifest.permission.CHANGE_WIFI_STATE -import android.content.Intent -import android.os.Build -import androidx.activity.result.ActivityResultLauncher -import androidx.annotation.RequiresApi -import androidx.annotation.RequiresPermission -import com.isupatches.android.wisefy.WisefyApi -import com.isupatches.android.wisefy.addnetwork.entities.AddOpenNetworkRequest -import com.isupatches.android.wisefy.addnetwork.entities.AddWPA2NetworkRequest -import com.isupatches.android.wisefy.addnetwork.entities.AddWPA3NetworkRequest -import com.isupatches.android.wisefy.callbacks.AddNetworkCallbacks -import com.isupatches.android.wisefy.sample.internal.scaffolding.BaseModel -import javax.inject.Inject - -internal interface AddNetworkModel { - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun addOpenNetwork(ssid: String, callbacks: AddNetworkCallbacks?) - - @RequiresApi(Build.VERSION_CODES.R) - @RequiresPermission(ACCESS_FINE_LOCATION) - fun addOpenNetwork( - ssid: String, - activityResultLauncher: ActivityResultLauncher, - callbacks: AddNetworkCallbacks? - ) - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun addWPA2Network( - ssid: String, - passphrase: String, - callbacks: AddNetworkCallbacks? - ) - - @RequiresApi(Build.VERSION_CODES.R) - @RequiresPermission(ACCESS_FINE_LOCATION) - fun addWPA2Network( - ssid: String, - passphrase: String, - activityResultLauncher: ActivityResultLauncher, - callbacks: AddNetworkCallbacks? - ) - - @RequiresApi(Build.VERSION_CODES.Q) - @RequiresPermission(ACCESS_FINE_LOCATION) - fun addWPA3Network( - ssid: String, - passphrase: String, - callbacks: AddNetworkCallbacks? - ) - - @RequiresApi(Build.VERSION_CODES.R) - @RequiresPermission(ACCESS_FINE_LOCATION) - fun addWPA3Network( - ssid: String, - passphrase: String, - activityResultLauncher: ActivityResultLauncher, - callbacks: AddNetworkCallbacks? - ) -} - -@AddNetworkScope -internal class DefaultAddNetworkModel @Inject constructor( - private val wisefy: WisefyApi -) : BaseModel(), AddNetworkModel { - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE]) - override fun addOpenNetwork( - ssid: String, - callbacks: AddNetworkCallbacks? - ) { - wisefy.addOpenNetwork( - request = AddOpenNetworkRequest.Ssid(ssid = ssid), - callbacks = callbacks - ) - } - - @RequiresApi(Build.VERSION_CODES.R) - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE]) - override fun addOpenNetwork( - ssid: String, - activityResultLauncher: ActivityResultLauncher, - callbacks: AddNetworkCallbacks? - ) { - wisefy.addOpenNetwork( - request = AddOpenNetworkRequest.SsidAndActivityResultLauncher( - ssid = ssid, - activityResultLauncher = activityResultLauncher - ), - callbacks = callbacks - ) - } - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE]) - override fun addWPA2Network( - ssid: String, - passphrase: String, - callbacks: AddNetworkCallbacks? - ) { - wisefy.addWPA2Network( - request = AddWPA2NetworkRequest.SsidAndPassphrase( - ssid = ssid, - passphrase = passphrase - ), - callbacks = callbacks - ) - } - - @RequiresApi(Build.VERSION_CODES.R) - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE]) - override fun addWPA2Network( - ssid: String, - passphrase: String, - activityResultLauncher: ActivityResultLauncher, - callbacks: AddNetworkCallbacks? - ) { - wisefy.addWPA2Network( - request = AddWPA2NetworkRequest.SsidPassphraseAndActivityResultLauncher( - ssid = ssid, - passphrase = passphrase, - activityResultLauncher = activityResultLauncher - ), - callbacks = callbacks - ) - } - - @RequiresApi(Build.VERSION_CODES.Q) - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE]) - override fun addWPA3Network( - ssid: String, - passphrase: String, - callbacks: AddNetworkCallbacks? - ) { - wisefy.addWPA3Network( - request = AddWPA3NetworkRequest.SsidAndPassphrase( - ssid = ssid, - passphrase = passphrase - ), - callbacks = callbacks - ) - } - - @RequiresApi(Build.VERSION_CODES.R) - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE]) - override fun addWPA3Network( - ssid: String, - passphrase: String, - activityResultLauncher: ActivityResultLauncher, - callbacks: AddNetworkCallbacks? - ) { - wisefy.addWPA3Network( - request = AddWPA3NetworkRequest.SsidPassphraseAndActivityResultLauncher( - ssid = ssid, - passphrase = passphrase, - activityResultLauncher = activityResultLauncher - ), - callbacks = callbacks - ) - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkModule.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkModule.kt deleted file mode 100644 index b2b7f8a0..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkModule.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.add - -import dagger.Binds -import dagger.Module -import javax.inject.Scope - -@Scope -@Retention(AnnotationRetention.RUNTIME) -internal annotation class AddNetworkScope - -@Suppress("unused") -@Module -internal interface AddNetworkFragmentModule { - @Binds - @AddNetworkScope - fun bindAddNetworkModel(impl: DefaultAddNetworkModel): AddNetworkModel - - @Binds - @AddNetworkScope - fun bindAddNetworkPresenter(impl: DefaultAddNetworkPresenter): AddNetworkPresenter - - @Binds - @AddNetworkScope - fun bindAddNetworkStore(impl: SharedPreferencesAddNetworkStore): AddNetworkStore -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkPresenter.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkPresenter.kt deleted file mode 100644 index 958b7eec..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkPresenter.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.add - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.content.Intent -import android.os.Build -import androidx.activity.result.ActivityResultLauncher -import androidx.annotation.RequiresApi -import androidx.annotation.RequiresPermission -import com.isupatches.android.wisefy.addnetwork.entities.AddNetworkResult -import com.isupatches.android.wisefy.callbacks.AddNetworkCallbacks -import com.isupatches.android.wisefy.sample.internal.scaffolding.BasePresenter -import com.isupatches.android.wisefy.sample.internal.scaffolding.Presenter -import javax.inject.Inject - -internal interface AddNetworkPresenter : Presenter { - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun addOpenNetwork(ssid: String) - - @RequiresApi(Build.VERSION_CODES.R) - @RequiresPermission(ACCESS_FINE_LOCATION) - fun addOpenNetwork( - ssid: String, - activityResultLauncher: ActivityResultLauncher - ) - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun addWPA2Network(ssid: String, passphrase: String) - - @RequiresApi(Build.VERSION_CODES.R) - @RequiresPermission(ACCESS_FINE_LOCATION) - fun addWPA2Network( - ssid: String, - passphrase: String, - activityResultLauncher: ActivityResultLauncher - ) - - @RequiresApi(Build.VERSION_CODES.Q) - @RequiresPermission(ACCESS_FINE_LOCATION) - fun addWPA3Network(ssid: String, passphrase: String) - - @RequiresApi(Build.VERSION_CODES.R) - @RequiresPermission(ACCESS_FINE_LOCATION) - fun addWPA3Network( - ssid: String, - passphrase: String, - activityResultLauncher: ActivityResultLauncher - ) -} - -@AddNetworkScope -internal class DefaultAddNetworkPresenter @Inject constructor( - private val model: AddNetworkModel, -) : BasePresenter(), AddNetworkPresenter { - - private val addNetworkCallbacks = object : AddNetworkCallbacks { - override fun onFailureAddingNetwork(result: AddNetworkResult) { - doSafelyWithView { view -> - view.displayFailureAddingNetwork(result) - } - } - - override fun onNetworkAdded(result: AddNetworkResult) { - doSafelyWithView { view -> - view.displayNetworkAdded(result) - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - - /* - * Model call-throughs - */ - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun addOpenNetwork(ssid: String) { - model.addOpenNetwork( - ssid = ssid, - callbacks = addNetworkCallbacks - ) - } - - @RequiresApi(Build.VERSION_CODES.R) - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun addOpenNetwork( - ssid: String, - activityResultLauncher: ActivityResultLauncher - ) { - model.addOpenNetwork( - ssid = ssid, - activityResultLauncher = activityResultLauncher, - callbacks = addNetworkCallbacks - ) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun addWPA2Network(ssid: String, passphrase: String) { - model.addWPA2Network( - ssid = ssid, - passphrase = passphrase, - callbacks = addNetworkCallbacks - ) - } - - @RequiresApi(Build.VERSION_CODES.R) - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun addWPA2Network( - ssid: String, - passphrase: String, - activityResultLauncher: ActivityResultLauncher - ) { - model.addWPA2Network( - ssid = ssid, - passphrase = passphrase, - activityResultLauncher = activityResultLauncher, - callbacks = addNetworkCallbacks - ) - } - - @RequiresApi(Build.VERSION_CODES.Q) - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun addWPA3Network(ssid: String, passphrase: String) { - model.addWPA3Network( - ssid = ssid, - passphrase = passphrase, - callbacks = addNetworkCallbacks - ) - } - - @RequiresApi(Build.VERSION_CODES.R) - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun addWPA3Network( - ssid: String, - passphrase: String, - activityResultLauncher: ActivityResultLauncher - ) { - model.addWPA3Network( - ssid = ssid, - passphrase = passphrase, - activityResultLauncher = activityResultLauncher, - callbacks = addNetworkCallbacks - ) - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkStore.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkStore.kt deleted file mode 100644 index 3366ddd3..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkStore.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.add - -import android.content.Context -import android.content.SharedPreferences -import androidx.annotation.VisibleForTesting -import androidx.core.content.edit -import com.isupatches.android.wisefy.sample.R -import com.isupatches.android.wisefy.sample.internal.entities.NetworkType -import com.isupatches.android.wisefy.sample.internal.scaffolding.BaseSharedPreferenceStore -import javax.inject.Inject - -@VisibleForTesting internal const val PREF_NETWORK_TYPE = "network type" -@VisibleForTesting internal const val PREF_LAST_USED_NETWORK_NAME = "last used network name" -@VisibleForTesting internal const val PREF_LAST_USED_NETWORK_PASSWORD = "last used network password" - -internal interface AddNetworkStore { - fun clear() - - fun getNetworkType(): NetworkType - fun getLastUsedNetworkName(): String - fun getLastUsedNetworkPassword(): String - - fun setNetworkType(networkType: NetworkType) - fun setLastUsedNetworkName(lastUsedNetworkName: String) - fun setLastUsedNetworkPassword(lastUsedNetworkPassword: String) -} - -@AddNetworkScope -internal class SharedPreferencesAddNetworkStore @Inject constructor( - context: Context -) : BaseSharedPreferenceStore(), AddNetworkStore { - - private val sharedPreferences: SharedPreferences = getSharedPreferences( - context, - R.string.preferences_add_network_data - ) - - override fun clear() { - sharedPreferences.edit { clear() } - } - - /* - * Network type - */ - - override fun getNetworkType() = NetworkType.of( - sharedPreferences.getInt(PREF_NETWORK_TYPE, NetworkType.WPA2.intVal) - ) - - override fun setNetworkType(networkType: NetworkType) { - sharedPreferences.edit { - putInt(PREF_NETWORK_TYPE, networkType.intVal) - } - } - - /* - * Last used network name - */ - - override fun getLastUsedNetworkName() = sharedPreferences.getNonNullString( - PREF_LAST_USED_NETWORK_NAME - ) - - override fun setLastUsedNetworkName(lastUsedNetworkName: String) { - sharedPreferences.edit { - putString(PREF_LAST_USED_NETWORK_NAME, lastUsedNetworkName) - } - } - - /* - * Last used network password - */ - - override fun getLastUsedNetworkPassword() = sharedPreferences.getNonNullString( - PREF_LAST_USED_NETWORK_PASSWORD - ) - - override fun setLastUsedNetworkPassword(lastUsedNetworkPassword: String) { - sharedPreferences.edit { - putString(PREF_LAST_USED_NETWORK_PASSWORD, lastUsedNetworkPassword) - } - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleButtons.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleButtons.kt new file mode 100644 index 00000000..681d3ba1 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleButtons.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.components + +import android.content.res.Configuration +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySamplePrimaryButtonColors +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleSizes +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme + +@Composable +internal fun WisefyPrimaryButton( + @StringRes stringResId: Int, + onClick: () -> Unit +) { + WisefySampleTheme { + Button( + modifier = Modifier.fillMaxWidth(), + content = { + Text( + text = stringResource(stringResId), + style = MaterialTheme.typography.subtitle1, + modifier = Modifier.padding( + top = WisefySampleSizes.Medium, + bottom = WisefySampleSizes.Medium, + start = WisefySampleSizes.Large, + end = WisefySampleSizes.Large + ), + color = MaterialTheme.colors.onPrimary + ) + }, + colors = WisefySamplePrimaryButtonColors(), + onClick = { + onClick() + } + ) + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefyPrimaryButtonLightPreview() { + WisefyPrimaryButton(R.string.wisefy) { } +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefyPrimaryButtonDarkPreview() { + WisefyPrimaryButton(R.string.wisefy) { } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleEditText.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleEditText.kt new file mode 100644 index 00000000..d2558491 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleEditText.kt @@ -0,0 +1,249 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.components + +import android.content.res.Configuration +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleSizes +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleTextFieldColors +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme + +@Composable +internal fun WisefySampleEditText( + text: String, + onTextChange: (String) -> Unit, + @StringRes labelResId: Int, + singleLine: Boolean = true, + error: WisefySampleEditTextError? = null, + isPasswordField: Boolean = false +) { + WisefySampleTheme { + val colors = WisefySampleTextFieldColors() + val passwordVisible = rememberSaveable { mutableStateOf(false) } + Column { + Row { + TextField( + value = text, + onValueChange = onTextChange, + label = { + Text( + text = stringResource(labelResId), + style = MaterialTheme.typography.body1, + color = colors.placeholderColor(enabled = true).value + ) + }, + singleLine = singleLine, + textStyle = MaterialTheme.typography.body1, + colors = colors, + modifier = Modifier.fillMaxWidth(), + isError = error != null, + visualTransformation = if (isPasswordField) { + if (passwordVisible.value) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + } + } else { + VisualTransformation.None + }, + trailingIcon = { + if (isPasswordField) { + val image = if (passwordVisible.value) { + Icons.Filled.Visibility + } else { + Icons.Filled.VisibilityOff + } + + val description = if (passwordVisible.value) { + stringResource(R.string.hide_password) + } else { + stringResource(R.string.show_password) + } + + IconButton(onClick = { passwordVisible.value = !passwordVisible.value }) { + Icon(imageVector = image, description) + } + } + } + ) + } + WisefySampleEditTextErrorMessage(error) + } + } +} + +@Composable +internal fun WisefySampleNumericalEditText( + text: String, + onTextChange: (String) -> Unit, + @StringRes labelResId: Int, + error: WisefySampleEditTextError? = null +) { + WisefySampleTheme { + val colors = WisefySampleTextFieldColors() + Column { + Row { + TextField( + value = text, + onValueChange = onTextChange, + label = { + Text( + text = stringResource(labelResId), + style = MaterialTheme.typography.body1, + color = colors.placeholderColor(enabled = true).value + ) + }, + singleLine = true, + textStyle = MaterialTheme.typography.body1, + colors = colors, + modifier = Modifier.fillMaxWidth(), + visualTransformation = VisualTransformation.None, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + ) + } + WisefySampleEditTextErrorMessage(error) + } + } +} + +@Composable +internal fun WisefySampleEditTextErrorMessage( + error: WisefySampleEditTextError? = null +) { + error?.let { + Row(modifier = Modifier.padding(top = WisefySampleSizes.Medium, bottom = WisefySampleSizes.Medium)) { + Text( + text = stringResource(it.errorMessageResId), + color = MaterialTheme.colors.error, + style = MaterialTheme.typography.caption, + modifier = Modifier.padding(start = WisefySampleSizes.Large) + ) + } + } +} + +internal data class WisefySampleEditTextError( + @StringRes val errorMessageResId: Int +) + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleEditTextLightPreview( + @PreviewParameter(WisefySampleEditTextPreviewParameterProvider::class) + previewTriple: Triple +) { + WisefySampleEditText( + text = "test text", + onTextChange = { }, + labelResId = R.string.wisefy, + isPasswordField = previewTriple.first, + singleLine = previewTriple.second, + error = previewTriple.third + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleEditTextDarkPreview( + @PreviewParameter(WisefySampleEditTextPreviewParameterProvider::class) + previewTriple: Triple +) { + WisefySampleEditText( + text = "test text", + onTextChange = { }, + labelResId = R.string.wisefy, + isPasswordField = previewTriple.first, + singleLine = previewTriple.second, + error = previewTriple.third + ) +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleNumbericalEditTextLightPreview( + @PreviewParameter(WisefySampleNumericalEditTextPreviewParameterProvider::class) error: WisefySampleEditTextError? +) { + WisefySampleNumericalEditText( + text = "10", + onTextChange = { }, + labelResId = R.string.wisefy, + error = error + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleEditTextDarkPreview( + @PreviewParameter(WisefySampleNumericalEditTextPreviewParameterProvider::class) error: WisefySampleEditTextError? +) { + WisefySampleNumericalEditText( + text = "10", + onTextChange = { }, + labelResId = R.string.wisefy, + error = error + ) +} + +private class WisefySampleEditTextPreviewParameterProvider : + PreviewParameterProvider> { + override val values: Sequence> = sequenceOf( + Triple(true, false, null), + Triple(true, false, WisefySampleEditTextError(R.string.input_error)), + Triple(true, true, null), + Triple(true, true, WisefySampleEditTextError(R.string.input_error)), + Triple(false, false, null), + Triple(false, false, WisefySampleEditTextError(R.string.input_error)), + Triple(false, true, null), + Triple(false, true, WisefySampleEditTextError(R.string.input_error)) + ) +} + +private class WisefySampleNumericalEditTextPreviewParameterProvider : + PreviewParameterProvider { + override val values: Sequence = sequenceOf( + null, + WisefySampleEditTextError(R.string.input_error) + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleLabels.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleLabels.kt new file mode 100644 index 00000000..7ac4f728 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleLabels.kt @@ -0,0 +1,191 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.components + +import android.content.res.Configuration +import androidx.annotation.StringRes +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleTypography +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme + +@Composable +internal fun WisefySampleDialogTitleLabel( + @StringRes stringResId: Int, + modifier: Modifier = Modifier +) { + WisefySampleTheme { + Text( + text = stringResource(stringResId), + style = WisefySampleTypography.h5, + color = MaterialTheme.colors.primary, + modifier = modifier + ) + } +} + +@Composable +internal fun WisefySampleDialogBodyLabel( + @StringRes stringResId: Int, + modifier: Modifier = Modifier, + vararg formatArgs: Any +) { + WisefySampleTheme { + val text = if (formatArgs.any()) { + stringResource(stringResId, *formatArgs) + } else { + stringResource(stringResId) + } + Text( + text = text, + style = WisefySampleTypography.body1, + color = MaterialTheme.colors.onSurface, + modifier = modifier + ) + } +} + +@Composable +internal fun WisefySampleBodyLabel( + @StringRes stringResId: Int, + modifier: Modifier = Modifier, + vararg formatArgs: Any +) { + WisefySampleTheme { + val text = if (formatArgs.any()) { + stringResource(stringResId, *formatArgs) + } else { + stringResource(stringResId) + } + Text( + text = text, + style = WisefySampleTypography.body1, + color = MaterialTheme.colors.onBackground, + modifier = modifier + ) + } +} + +@Composable +internal fun WisefySampleSubHeaderLabel( + @StringRes stringResId: Int, + modifier: Modifier = Modifier, + vararg formatArgs: Any +) { + WisefySampleTheme { + val text = if (formatArgs.any()) { + stringResource(stringResId, *formatArgs) + } else { + stringResource(stringResId) + } + Text( + text = text, + style = WisefySampleTypography.h6, + color = MaterialTheme.colors.onBackground, + modifier = modifier + ) + } +} + +@Composable +internal fun WisefySampleCaptionLabel( + text: String, + modifier: Modifier = Modifier +) { + WisefySampleTheme { + Text( + text = text, + style = WisefySampleTypography.caption, + color = MaterialTheme.colors.onBackground, + modifier = modifier + ) + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleDialogTitleLabelLightPreview() { + WisefySampleDialogTitleLabel(R.string.wisefy) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleDialogTitleLabelDarkPreview() { + WisefySampleDialogTitleLabel(R.string.wisefy) +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleDialogBodyLabelLightPreview() { + WisefySampleDialogBodyLabel(R.string.wisefy) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleDialogBodyLabelDarkPreview() { + WisefySampleDialogBodyLabel(R.string.wisefy) +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleSubHeaderLabelLightPreview() { + WisefySampleSubHeaderLabel(R.string.wisefy) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleSubHeaderLabelDarkPreview() { + WisefySampleSubHeaderLabel(R.string.wisefy) +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleBodyLabelLightPreview() { + WisefySampleBodyLabel(R.string.wisefy) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleBodyLabelDarkPreview() { + WisefySampleBodyLabel(R.string.wisefy) +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleCaptionLabelLightPreview() { + WisefySampleCaptionLabel(stringResource(id = R.string.wisefy), Modifier) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleCaptionLabelDarkPreview() { + WisefySampleCaptionLabel(stringResource(id = R.string.wisefy), Modifier) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleLoadingIndicator.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleLoadingIndicator.kt new file mode 100644 index 00000000..69ab43d9 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleLoadingIndicator.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme + +@Composable +internal fun WisefySampleLoadingIndicator( + isLoading: () -> Boolean +) { + WisefySampleTheme { + if (isLoading()) { + LinearProgressIndicator(color = MaterialTheme.colors.secondary, modifier = Modifier.fillMaxWidth()) + } + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleLoadingIndicatorLightPreview() { + WisefySampleLoadingIndicator { true } +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleLoadingIndicatorDarkPreview() { + WisefySampleLoadingIndicator { true } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleNoticeDialog.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleNoticeDialog.kt new file mode 100644 index 00000000..d219fee4 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleNoticeDialog.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.components + +import android.content.res.Configuration +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.Dialog +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleCornerRadii +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleSizes +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme + +@Composable +internal fun WisefySampleNoticeDialog( + @StringRes title: Int, + @StringRes body: Int, + vararg bodyFormatArgs: Any, + onClose: () -> Unit +) { + WisefySampleTheme { + Dialog(onDismissRequest = onClose) { + Box(modifier = Modifier.padding(top = WisefySampleSizes.XXLarge, bottom = WisefySampleSizes.XXLarge)) { + Surface( + shape = RoundedCornerShape(WisefySampleCornerRadii.Default), + color = MaterialTheme.colors.surface + ) { + Column( + modifier = Modifier.padding( + top = WisefySampleSizes.Large, + bottom = WisefySampleSizes.XLarge, + start = WisefySampleSizes.WisefySampleHorizontalMargins, + end = WisefySampleSizes.WisefySampleHorizontalMargins + ) + ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + WisefySampleDialogTitleLabel(stringResId = title) + } + Row( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(state = rememberScrollState()) + .weight(weight = 1f, fill = false) + ) { + WisefySampleDialogBodyLabel( + stringResId = body, + modifier = Modifier.padding(top = WisefySampleSizes.Large), + formatArgs = bodyFormatArgs + ) + } + Row(modifier = Modifier.fillMaxWidth().padding(top = WisefySampleSizes.XLarge)) { + WisefyPrimaryButton(stringResId = R.string.ok, onClick = onClose) + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleNoticeDialogLightPreview() { + WisefySampleNoticeDialog( + R.string.permission_error, + R.string.permission_error_add_open_network, + onClose = { } + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleNoticeDialogDarkPreview() { + WisefySampleNoticeDialog( + R.string.permission_error, + R.string.permission_error_add_open_network, + onClose = { } + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleRadioButton.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleRadioButton.kt new file mode 100644 index 00000000..d8adb00a --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleRadioButton.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.components + +import android.content.res.Configuration +import androidx.compose.material.RadioButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme + +@Composable +internal fun WisefySampleRadioButton( + isSelected: Boolean, + onClick: () -> Unit +) { + WisefySampleTheme { + RadioButton(selected = isSelected, onClick = onClick) + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleRadioButtonLightPreview() { + WisefySampleRadioButton(isSelected = true, onClick = { }) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleRadioButtonDarkPreview() { + WisefySampleRadioButton(isSelected = true, onClick = { }) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleSSIDTypeSelectionRows.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleSSIDTypeSelectionRows.kt new file mode 100644 index 00000000..f4bf58cf --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleSSIDTypeSelectionRows.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.entities.SSIDType +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleSizes +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme + +@Composable +internal fun WisefySampleSSIDTypeSelectionRows( + ssidType: () -> SSIDType, + onSSIDTypeChanged: (SSIDType) -> Unit +) { + WisefySampleTheme { + Column { + val currentSSIDType = ssidType() + Row { + WisefySampleSubHeaderLabel( + modifier = Modifier.padding(top = WisefySampleSizes.Large), + stringResId = R.string.ssid_type + ) + } + Row( + modifier = Modifier.padding(top = WisefySampleSizes.Medium), + verticalAlignment = Alignment.CenterVertically + ) { + WisefySampleRadioButton( + isSelected = currentSSIDType == SSIDType.SSID, + onClick = { + onSSIDTypeChanged(SSIDType.SSID) + } + ) + WisefySampleBodyLabel(stringResId = R.string.ssid) + WisefySampleRadioButton( + isSelected = currentSSIDType == SSIDType.BSSID, + onClick = { + onSSIDTypeChanged(SSIDType.BSSID) + } + ) + WisefySampleBodyLabel(stringResId = R.string.bssid) + } + } + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleSSIDTypeSelectionRowsLightPreview() { + WisefySampleSSIDTypeSelectionRows( + onSSIDTypeChanged = { }, + ssidType = { SSIDType.SSID } + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleSSIDTypeSelectionRowsDarkPreview() { + WisefySampleSSIDTypeSelectionRows( + onSSIDTypeChanged = { }, + ssidType = { SSIDType.SSID } + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleSlider.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleSlider.kt new file mode 100644 index 00000000..31cf214a --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleSlider.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.components + +import android.content.res.Configuration +import androidx.compose.material.Slider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.tooling.preview.Preview +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySliderColors +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme + +@Composable +internal fun WisefySampleSlider( + startPosition: () -> Float?, + valueRange: ClosedFloatingPointRange, + onValueChange: (Float) -> Unit, + onValueChangeFinished: (Float) -> Unit +) { + WisefySampleTheme { + var sliderPosition by remember { mutableStateOf(startPosition() ?: 0f) } + Slider( + value = sliderPosition, + valueRange = valueRange, + onValueChange = { + sliderPosition = it + onValueChange(it) + }, + onValueChangeFinished = { + onValueChangeFinished(sliderPosition) + }, + colors = WisefySliderColors() + ) + } +} + +private const val PREVIEW_START_VALUE = 0f +private const val PREVIEW_END_VALUE = 30f + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleSliderLightPreview() { + WisefySampleSlider( + startPosition = { 0f }, + valueRange = object : ClosedFloatingPointRange { + override val start: Float = PREVIEW_START_VALUE + override val endInclusive: Float = PREVIEW_END_VALUE + + override fun lessThanOrEquals(a: Float, b: Float): Boolean { + return a <= b + } + }, + onValueChange = { }, + onValueChangeFinished = { } + ) +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleSliderDarkPreview() { + WisefySampleSlider( + startPosition = { 0f }, + valueRange = object : ClosedFloatingPointRange { + override val start: Float = PREVIEW_START_VALUE + override val endInclusive: Float = PREVIEW_END_VALUE + + override fun lessThanOrEquals(a: Float, b: Float): Boolean { + return a <= b + } + }, + onValueChange = { }, + onValueChangeFinished = { } + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleToolbar.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleToolbar.kt new file mode 100644 index 00000000..5ffe38e2 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/WisefySampleToolbar.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.components + +import android.content.res.Configuration +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.isupatches.android.wisefy.sample.R +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme + +@Composable +internal fun WisefySampleToolbar() { + WisefySampleTheme { + TopAppBar( + title = { + Text( + text = stringResource(R.string.app_name), + color = MaterialTheme.colors.onPrimary, + style = MaterialTheme.typography.h5 + ) + }, + backgroundColor = MaterialTheme.colors.primary, + contentColor = MaterialTheme.colors.onPrimary + ) + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleToolbarLightPreview() { + WisefySampleToolbar() +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleToolbarDarkPreview() { + WisefySampleToolbar() +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/navigation/WisefySampleBottomNavigation.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/navigation/WisefySampleBottomNavigation.kt new file mode 100644 index 00000000..4a791e21 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/navigation/WisefySampleBottomNavigation.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.components.navigation + +import android.content.res.Configuration +import androidx.compose.material.BottomNavigation +import androidx.compose.material.BottomNavigationItem +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleTypography +import com.isupatches.android.wisefy.sample.ui.theme.WisefySampleTheme + +private const val UNSELECTED_NAVIGATION_ITEM_ALPHA = 0.45f + +@Composable +internal fun WisefySampleBottomNavigation(navController: NavController) { + val items = listOf( + WisefySampleBottomNavigationItem.Add, + WisefySampleBottomNavigationItem.Remove, + WisefySampleBottomNavigationItem.Home, + WisefySampleBottomNavigationItem.Misc, + WisefySampleBottomNavigationItem.Search + ) + BottomNavigation( + backgroundColor = MaterialTheme.colors.primary, + contentColor = MaterialTheme.colors.onPrimary + ) { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route + items.forEach { item -> + BottomNavigationItem( + icon = { + Icon( + painter = painterResource(id = item.icon), + contentDescription = stringResource(item.stringResId) + ) + }, + label = { + Text( + text = stringResource(item.stringResId), + style = WisefySampleTypography.caption + ) + }, + selectedContentColor = MaterialTheme.colors.onPrimary, + unselectedContentColor = MaterialTheme.colors.onPrimary.copy(UNSELECTED_NAVIGATION_ITEM_ALPHA), + alwaysShowLabel = true, + selected = currentRoute == item.route, + onClick = { + navController.navigate(item.route) { + // Pop up to the start destination of the graph to avoid building up a large stack of + // destinations on the back stack as users select items + navController.graph.startDestinationRoute?.let { route -> + popUpTo(route) { + saveState = true + } + } + // Avoid multiple copies of the same destination when re-selecting the same item + launchSingleTop = true + // Restore state when re-selecting a previously selected item + restoreState = true + } + } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleBottomNavigationLightPreview() { + WisefySampleTheme { + WisefySampleBottomNavigation(navController = NavController(LocalContext.current)) + } +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +@Suppress("UnusedPrivateMember") +private fun WisefySampleBottomNavigationDarkPreview() { + WisefySampleTheme { + WisefySampleBottomNavigation(navController = NavController(LocalContext.current)) + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/navigation/WisefySampleBottomNavigationItem.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/navigation/WisefySampleBottomNavigationItem.kt new file mode 100644 index 00000000..f9924f01 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/navigation/WisefySampleBottomNavigationItem.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.components.navigation + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.isupatches.android.wisefy.sample.R + +internal sealed class WisefySampleBottomNavigationItem( + val route: String, + @DrawableRes val icon: Int, + @StringRes val stringResId: Int +) { + + object Add : WisefySampleBottomNavigationItem( + route = WisefySampleNavGraph.Main.Add.route, + icon = R.drawable.ic_add_circle, + stringResId = R.string.add + ) + + object Remove : WisefySampleBottomNavigationItem( + route = WisefySampleNavGraph.Main.Remove.route, + icon = R.drawable.ic_remove_circle, + stringResId = R.string.remove + ) + + object Home : WisefySampleBottomNavigationItem( + route = WisefySampleNavGraph.Main.Home.route, + icon = R.drawable.ic_home, + stringResId = R.string.home + ) + + object Misc : WisefySampleBottomNavigationItem( + route = WisefySampleNavGraph.Main.Misc.route, + icon = R.drawable.ic_apps, + stringResId = R.string.misc + ) + + object Search : WisefySampleBottomNavigationItem( + route = WisefySampleNavGraph.Main.Search.route, + icon = R.drawable.ic_search, + stringResId = R.string.search + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/navigation/WisefySampleNavGraph.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/navigation/WisefySampleNavGraph.kt new file mode 100644 index 00000000..61219d9a --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/navigation/WisefySampleNavGraph.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.components.navigation + +internal sealed class WisefySampleNavGraph(open val route: String) { + + sealed class Main(override val route: String) : WisefySampleNavGraph(route) { + object Add : Main(route = "add") + object Remove : Main(route = "remove") + object Home : Main(route = "home") + object Misc : Main(route = "misc") + object Search : Main(route = "search") + } + + sealed class Misc(override val route: String) : WisefySampleNavGraph(route) { + object Signal : Misc(route = "signal") + object NearbyAccessPoints : Misc(route = "nearbyAccessPoints") + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/navigation/WisefySampleNavHost.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/navigation/WisefySampleNavHost.kt new file mode 100644 index 00000000..1390c433 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/components/navigation/WisefySampleNavHost.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.components.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.sample.features.add.AddNetworkScreen +import com.isupatches.android.wisefy.sample.features.misc.MiscScreen +import com.isupatches.android.wisefy.sample.features.misc.nearbyaccesspoints.NearbyAccessPointsScreen +import com.isupatches.android.wisefy.sample.features.misc.signal.SignalScreen +import com.isupatches.android.wisefy.sample.features.remove.RemoveNetworkScreen +import com.isupatches.android.wisefy.sample.features.search.SearchScreen +import com.isupatches.android.wisefy.sample.main.HomeScreen +import com.isupatches.android.wisefy.sample.util.SdkUtil + +@Composable +internal fun WisefySampleNavHost( + navController: NavHostController, + wisefy: WisefyApi, + sdkUtil: SdkUtil, + padding: PaddingValues +) { + NavHost( + navController = navController, + startDestination = WisefySampleNavGraph.Main.Home.route, + modifier = Modifier.padding(paddingValues = padding) + ) { + composable(WisefySampleNavGraph.Main.Add.route) { + AddNetworkScreen(wisefy = wisefy, sdkUtil = sdkUtil) + } + composable(WisefySampleNavGraph.Main.Remove.route) { + RemoveNetworkScreen(wisefy = wisefy) + } + composable(WisefySampleNavGraph.Main.Home.route) { + HomeScreen() + } + composable(WisefySampleNavGraph.Main.Misc.route) { + MiscScreen(wisefy = wisefy, sdkUtil = sdkUtil, navController = navController) + } + composable(WisefySampleNavGraph.Main.Search.route) { + SearchScreen(wisefy = wisefy) + } + composable(WisefySampleNavGraph.Misc.Signal.route) { + SignalScreen(wisefy = wisefy, sdkUtil = sdkUtil) + } + composable(WisefySampleNavGraph.Misc.NearbyAccessPoints.route) { + NearbyAccessPointsScreen(wisefy = wisefy) + } + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/dialogs/BaseNoticeDialogFragment.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/dialogs/BaseNoticeDialogFragment.kt deleted file mode 100644 index cb96adc4..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/dialogs/BaseNoticeDialogFragment.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.dialogs - -import android.os.Bundle -import com.isupatches.android.wisefy.sample.internal.base.BaseDialogFragment - -internal const val EXTRA_DIALOG_TITLE = "dialog title" -internal const val EXTRA_DIALOG_MESSAGE = "dialog message" - -internal abstract class BaseNoticeDialogFragment : BaseDialogFragment() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - isCancelable = false - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/dialogs/FullScreenNoticeDialogFragment.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/dialogs/FullScreenNoticeDialogFragment.kt deleted file mode 100644 index ef2160ec..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/dialogs/FullScreenNoticeDialogFragment.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.dialogs - -import android.app.Dialog -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.isupatches.android.viewglu.paste -import com.isupatches.android.wisefy.sample.databinding.DialogBaseFullscreenBinding -import com.isupatches.android.wisefy.sample.internal.util.applyArguments - -internal class FullScreenNoticeDialogFragment : BaseNoticeDialogFragment() { - - private var binding: DialogBaseFullscreenBinding by paste() - - private val dialogTitle: String by lazy { - arguments?.getString(EXTRA_DIALOG_TITLE) ?: "" - } - - private val dialogMessage: String by lazy { - arguments?.getString(EXTRA_DIALOG_MESSAGE) ?: "" - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = DialogBaseFullscreenBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.dialogTitleTxt.text = dialogTitle - binding.dialogMessageTxt.text = dialogMessage - binding.okBtn.setOnClickListener { - dismiss() - } - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - dialog?.window?.setLayout( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - return super.onCreateDialog(savedInstanceState) - } - - companion object { - val TAG: String = FullScreenNoticeDialogFragment::class.java.simpleName - - fun newInstance(title: String, message: String): FullScreenNoticeDialogFragment { - return FullScreenNoticeDialogFragment().applyArguments { - putString(EXTRA_DIALOG_TITLE, title) - putString(EXTRA_DIALOG_MESSAGE, message) - } - } - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/dialogs/NoticeDialogFragment.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/dialogs/NoticeDialogFragment.kt deleted file mode 100644 index 79a30e0c..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/dialogs/NoticeDialogFragment.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.dialogs - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.isupatches.android.viewglu.paste -import com.isupatches.android.wisefy.sample.databinding.DialogBaseBinding -import com.isupatches.android.wisefy.sample.internal.util.applyArguments - -internal class NoticeDialogFragment : BaseNoticeDialogFragment() { - - private var binding: DialogBaseBinding by paste() - - private val dialogTitle: String by lazy { - arguments?.getString(EXTRA_DIALOG_TITLE) ?: "" - } - - private val dialogMessage: String by lazy { - arguments?.getString(EXTRA_DIALOG_MESSAGE) ?: "" - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = DialogBaseBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.dialogTitleTxt.text = dialogTitle - binding.dialogMessageTxt.text = dialogMessage - binding.okBtn.setOnClickListener { - dismiss() - } - } - - companion object { - val TAG: String = NoticeDialogFragment::class.java.simpleName - - fun newInstance(title: String, message: String): NoticeDialogFragment { - return NoticeDialogFragment().applyArguments { - putString(EXTRA_DIALOG_TITLE, title) - putString(EXTRA_DIALOG_MESSAGE, message) - } - } - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/main/MainActivity.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/main/MainActivity.kt deleted file mode 100644 index 54c03544..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/main/MainActivity.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.main - -import android.content.Context -import android.os.Bundle -import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.NavigationUI -import com.isupatches.android.viewglu.paste -import com.isupatches.android.wisefy.WisefyApi -import com.isupatches.android.wisefy.sample.R -import com.isupatches.android.wisefy.sample.databinding.ActivityMainBinding -import com.isupatches.android.wisefy.sample.internal.base.BaseActivity -import com.isupatches.android.wisefy.sample.internal.util.SdkUtil -import com.isupatches.android.wisefy.sample.internal.util.SdkUtilImpl -import com.isupatches.android.wisefy.sample.internal.util.createWiseFy -import com.isupatches.android.wisefy.sample.ui.add.AddNetworkFragment -import com.isupatches.android.wisefy.sample.ui.add.AddNetworkFragmentModule -import com.isupatches.android.wisefy.sample.ui.add.AddNetworkScope -import com.isupatches.android.wisefy.sample.ui.misc.MiscFragment -import com.isupatches.android.wisefy.sample.ui.misc.MiscFragmentModule -import com.isupatches.android.wisefy.sample.ui.misc.MiscScope -import com.isupatches.android.wisefy.sample.ui.remove.RemoveNetworkFragment -import com.isupatches.android.wisefy.sample.ui.remove.RemoveNetworkFragmentModule -import com.isupatches.android.wisefy.sample.ui.remove.RemoveNetworkScope -import com.isupatches.android.wisefy.sample.ui.search.SearchFragment -import com.isupatches.android.wisefy.sample.ui.search.SearchFragmentModule -import com.isupatches.android.wisefy.sample.ui.search.SearchScope -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.android.ContributesAndroidInjector -import javax.inject.Inject - -internal class MainActivity : BaseActivity() { - - override val binding: ActivityMainBinding by paste(ActivityMainBinding::inflate) - - @Inject lateinit var wisefy: WisefyApi - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val navHostFragment = supportFragmentManager.findFragmentById(R.id.main_nav_host) as NavHostFragment - val navController = navHostFragment.navController - NavigationUI.setupWithNavController(binding.bottomNavigationView, navController) - - wisefy.attachNetworkWatcher() - } - - override fun onDestroy() { - wisefy.detachNetworkWatcher() - super.onDestroy() - } -} - -@Suppress("unused") -@Module internal interface MainActivityFragmentBindings { - - @AddNetworkScope - @ContributesAndroidInjector( - modules = [ - AddNetworkFragmentModule::class - ] - ) fun addNetworkFragment(): AddNetworkFragment - - @RemoveNetworkScope - @ContributesAndroidInjector( - modules = [ - RemoveNetworkFragmentModule::class - ] - ) fun removeNetworkFragment(): RemoveNetworkFragment - - @ContributesAndroidInjector - fun mainFragment(): MainFragment - - @MiscScope - @ContributesAndroidInjector( - modules = [ - MiscFragmentModule::class - ] - ) fun miscFragment(): MiscFragment - - @SearchScope - @ContributesAndroidInjector( - modules = [ - SearchFragmentModule::class - ] - ) fun searchFragment(): SearchFragment -} - -@Suppress("unused") -@Module internal abstract class MainActivityModule { - - @Binds abstract fun bindSdkUtil(impl: SdkUtilImpl): SdkUtil - - companion object { - @Provides - fun provideWiseFy(app: Context) = createWiseFy(app) - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/main/MainFragment.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/main/MainFragment.kt deleted file mode 100644 index 55a4f39f..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/main/MainFragment.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.main - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import com.isupatches.android.viewglu.paste -import com.isupatches.android.wisefy.sample.databinding.FragmentMainBinding - -internal class MainFragment : Fragment() { - - private var binding: FragmentMainBinding by paste() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentMainBinding.inflate(inflater, container, false) - return binding.root - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscFragment.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscFragment.kt deleted file mode 100644 index a3414bc5..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscFragment.kt +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.misc - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.content.pm.PackageManager -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.VisibleForTesting -import com.isupatches.android.viewglu.paste -import com.isupatches.android.wisefy.accesspoints.entities.AccessPointData -import com.isupatches.android.wisefy.frequency.entities.FrequencyData -import com.isupatches.android.wisefy.networkinfo.entities.CurrentNetworkData -import com.isupatches.android.wisefy.networkinfo.entities.CurrentNetworkInfoData -import com.isupatches.android.wisefy.networkinfo.entities.IPData -import com.isupatches.android.wisefy.sample.R -import com.isupatches.android.wisefy.sample.databinding.FragmentMiscBinding -import com.isupatches.android.wisefy.sample.internal.base.BaseFragment -import com.isupatches.android.wisefy.sample.internal.logging.WisefySampleLogger -import com.isupatches.android.wisefy.savednetworks.entities.SavedNetworkData -import javax.inject.Inject - -@VisibleForTesting internal const val WISEFY_GET_FREQUENCY_REQUEST_CODE = 1 -@VisibleForTesting internal const val WISEFY_GET_IP_REQUEST_CODE = 2 -@VisibleForTesting internal const val WISEFY_GET_NEARBY_ACCESS_POINTS_REQUEST_CODE = 3 -@VisibleForTesting internal const val WISEFY_GET_SAVED_NETWORKS_REQUEST_CODE = 4 - -private const val LOG_TAG = "MiscFragment" - -internal interface MiscView { - fun displayAndroidQWifiMessage() - fun displayWifiDisabled() - fun displayFailureDisablingWifi() - fun displayWifiEnabled() - fun displayFailureEnablingWifi() - fun displayCurrentNetwork(currentNetwork: CurrentNetworkData) - fun displayNoCurrentNetwork() - fun displayCurrentNetworkInfo(currentNetworkInfo: CurrentNetworkInfoData) - fun displayNoCurrentNetworkInfo() - fun displayFrequency(frequency: FrequencyData) - fun displayFailureRetrievingFrequency() - fun displayIP(ip: IPData) - fun displayFailureRetrievingIP() - fun displayNearbyAccessPoints(accessPoints: List) - fun displayNoAccessPointsFound() - fun displayNoSavedNetworksFound() - fun displaySavedNetworks(savedNetworks: List) -} - -@Suppress("LargeClass") -internal class MiscFragment : BaseFragment(), MiscView { - - @MiscScope @Inject lateinit var presenter: MiscPresenter - - private var binding: FragmentMiscBinding by paste() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = FragmentMiscBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.disableWifiBtn.setOnClickListener { - presenter.disableWifi() - } - binding.enableWifiBtn.setOnClickListener { - presenter.enableWifi() - } - binding.getCurrentNetworkBtn.setOnClickListener { - presenter.getCurrentNetwork() - } - binding.getCurrentNetworkInfoBtn.setOnClickListener { - presenter.getCurrentNetworkInfo() - } - binding.getFrequencyBtn.setOnClickListener { - getFrequency() - } - binding.getIPBtn.setOnClickListener { - getIP() - } - binding.getNearbyAccessPointsBtn.setOnClickListener { - getNearbyAccessPoints() - } - binding.getSavedNetworksBtn.setOnClickListener { - getSavedNetworks() - } - } - - override fun onStart() { - super.onStart() - presenter.attachView(this) - } - - override fun onStop() { - presenter.detachView() - super.onStop() - } - - /* - * Presenter overrides - */ - - override fun displayAndroidQWifiMessage() { - displayInfo(R.string.android_q_wifi_message, R.string.android_q_notice) - } - - override fun displayWifiDisabled() { - displayInfo(R.string.wifi_disabled, R.string.wisefy_action_result) - } - - override fun displayFailureDisablingWifi() { - displayInfo(R.string.failure_disabling_wifi, R.string.wisefy_action_result) - } - - override fun displayWifiEnabled() { - displayInfo(R.string.wifi_enabled, R.string.wisefy_action_result) - } - - override fun displayFailureEnablingWifi() { - displayInfo(R.string.failure_enabling_wifi, R.string.wisefy_action_result) - } - - override fun displayCurrentNetwork(currentNetwork: CurrentNetworkData) { - displayInfoFullScreen( - getString(R.string.current_network_args, currentNetwork), - R.string.wisefy_action_result - ) - } - - override fun displayNoCurrentNetwork() { - displayInfo(R.string.no_current_network, R.string.wisefy_action_result) - } - - override fun displayCurrentNetworkInfo(currentNetworkInfo: CurrentNetworkInfoData) { - displayInfoFullScreen( - getString(R.string.current_network_info_args, currentNetworkInfo), - R.string.wisefy_action_result - ) - } - - override fun displayNoCurrentNetworkInfo() { - displayInfo(R.string.no_current_network_info, R.string.wisefy_action_result) - } - - override fun displayFrequency(frequency: FrequencyData) { - displayInfo(getString(R.string.frequency_args, frequency.value), R.string.wisefy_action_result) - } - - override fun displayFailureRetrievingFrequency() { - displayInfo(R.string.failure_retrieving_frequency, R.string.wisefy_action_result) - } - - override fun displayIP(ip: IPData) { - displayInfo(getString(R.string.ip_args, ip.ip), R.string.wisefy_action_result) - } - - override fun displayFailureRetrievingIP() { - displayInfo(R.string.failure_retrieving_ip, R.string.wisefy_action_result) - } - - override fun displayNearbyAccessPoints(accessPoints: List) { - displayInfoFullScreen( - getString(R.string.access_points_args, accessPoints), - R.string.wisefy_action_result - ) - } - - override fun displayNoAccessPointsFound() { - displayInfo(R.string.no_access_points_found, R.string.wisefy_action_result) - } - - override fun displaySavedNetworks(savedNetworks: List) { - displayInfoFullScreen( - getString(R.string.saved_networks_args, savedNetworks), - R.string.wisefy_action_result - ) - } - - override fun displayNoSavedNetworksFound() { - displayInfo(R.string.no_saved_networks_found, R.string.wisefy_action_result) - } - - override fun displayWisefyAsyncError(throwable: Throwable) { - displayWisefyAsyncErrorDialog(throwable) - } - - /* - * WiseFy helpers - */ - - @Throws(SecurityException::class) - private fun getFrequency() { - if (checkGetFrequencyPermissions()) { - presenter.getFrequency() - } - } - - @Throws(SecurityException::class) - private fun getIP() { - if (checkGetIPPermissions()) { - presenter.getIP() - } - } - - @Throws(SecurityException::class) - private fun getNearbyAccessPoints() { - if (checkGetNearbyAccessPointsPermissions()) { - presenter.getNearbyAccessPoints() - } - } - - @Throws(SecurityException::class) - private fun getSavedNetworks() { - if (checkGetSavedNetworksPermissions()) { - presenter.getSavedNetworks() - } - } - - /* - * Permission helpers - */ - - private fun checkGetFrequencyPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_GET_FREQUENCY_REQUEST_CODE) - } - - private fun checkGetIPPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_GET_IP_REQUEST_CODE) - } - - private fun checkGetNearbyAccessPointsPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_GET_NEARBY_ACCESS_POINTS_REQUEST_CODE) - } - - private fun checkGetSavedNetworksPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_GET_SAVED_NETWORKS_REQUEST_CODE) - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - when (requestCode) { - WISEFY_GET_FREQUENCY_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - getFrequency() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for getting frequency are denied") - displayPermissionErrorDialog(R.string.permission_error_get_frequency) - } - } - WISEFY_GET_IP_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - getIP() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for getting ip are denied") - displayPermissionErrorDialog(R.string.permission_error_get_ip) - } - } - WISEFY_GET_NEARBY_ACCESS_POINTS_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - getNearbyAccessPoints() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for getting nearby access points are denied") - displayPermissionErrorDialog(R.string.permission_error_get_nearby_access_points) - } - } - WISEFY_GET_SAVED_NETWORKS_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - getSavedNetworks() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for getting saved networks are denied") - displayPermissionErrorDialog(R.string.permission_error_get_saved_networks) - } - } - else -> { - WisefySampleLogger.wtf(LOG_TAG, "Weird permission requested, not handled") - displayPermissionErrorDialog( - getString(R.string.permission_error_unhandled_request_code_args, requestCode) - ) - } - } - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscModel.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscModel.kt deleted file mode 100644 index 48b584b5..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscModel.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.misc - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.Manifest.permission.ACCESS_WIFI_STATE -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.annotation.RequiresPermission -import com.isupatches.android.wisefy.WisefyApi -import com.isupatches.android.wisefy.accesspoints.entities.GetNearbyAccessPointsRequest -import com.isupatches.android.wisefy.callbacks.DisableWifiCallbacks -import com.isupatches.android.wisefy.callbacks.EnableWifiCallbacks -import com.isupatches.android.wisefy.callbacks.GetCurrentNetworkCallbacks -import com.isupatches.android.wisefy.callbacks.GetCurrentNetworkInfoCallbacks -import com.isupatches.android.wisefy.callbacks.GetFrequencyCallbacks -import com.isupatches.android.wisefy.callbacks.GetIPCallbacks -import com.isupatches.android.wisefy.callbacks.GetNearbyAccessPointCallbacks -import com.isupatches.android.wisefy.callbacks.GetSavedNetworksCallbacks -import com.isupatches.android.wisefy.sample.internal.scaffolding.BaseModel -import javax.inject.Inject - -internal interface MiscModel { - - fun disableWifi(callbacks: DisableWifiCallbacks?) - - fun enableWifi(callbacks: EnableWifiCallbacks?) - - fun getCurrentNetwork(callbacks: GetCurrentNetworkCallbacks?) - - fun getCurrentNetworkInfo(callbacks: GetCurrentNetworkInfoCallbacks?) - - @RequiresPermission(ACCESS_FINE_LOCATION) - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - fun getFrequency(callbacks: GetFrequencyCallbacks?) - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun getIP(callbacks: GetIPCallbacks?) - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun getNearbyAccessPoints(callbacks: GetNearbyAccessPointCallbacks?) - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun getSavedNetworks(callbacks: GetSavedNetworksCallbacks?) -} - -@MiscScope -internal class DefaultMiscModel @Inject constructor( - private val wiseFy: WisefyApi -) : BaseModel(), MiscModel { - - override fun disableWifi(callbacks: DisableWifiCallbacks?) { - wiseFy.disableWifi(callbacks) - } - - override fun enableWifi(callbacks: EnableWifiCallbacks?) { - wiseFy.enableWifi(callbacks) - } - - override fun getCurrentNetwork(callbacks: GetCurrentNetworkCallbacks?) { - wiseFy.getCurrentNetwork(callbacks) - } - - override fun getCurrentNetworkInfo(callbacks: GetCurrentNetworkInfoCallbacks?) { - wiseFy.getCurrentNetworkInfo(callbacks) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - override fun getFrequency(callbacks: GetFrequencyCallbacks?) { - wiseFy.getFrequency(callbacks) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun getIP(callbacks: GetIPCallbacks?) { - wiseFy.getIP(callbacks) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun getNearbyAccessPoints(callbacks: GetNearbyAccessPointCallbacks?) { - return wiseFy.getNearbyAccessPoints( - request = GetNearbyAccessPointsRequest.All(), - callbacks = callbacks - ) - } - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) - override fun getSavedNetworks(callbacks: GetSavedNetworksCallbacks?) { - wiseFy.getSavedNetworks() - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscModule.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscModule.kt deleted file mode 100644 index 1d19bed2..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscModule.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.misc - -import dagger.Binds -import dagger.Module -import javax.inject.Scope - -@Scope -@Retention(AnnotationRetention.RUNTIME) -internal annotation class MiscScope - -@Suppress("unused") -@Module -internal interface MiscFragmentModule { - @Binds @MiscScope fun bindMiscModel(impl: DefaultMiscModel): MiscModel - @Binds @MiscScope fun bindMiscPresenter(impl: DefaultMiscPresenter): MiscPresenter -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscPresenter.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscPresenter.kt deleted file mode 100644 index 2a15f277..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscPresenter.kt +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.misc - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.annotation.RequiresPermission -import com.isupatches.android.wisefy.accesspoints.entities.AccessPointData -import com.isupatches.android.wisefy.callbacks.DisableWifiCallbacks -import com.isupatches.android.wisefy.callbacks.EnableWifiCallbacks -import com.isupatches.android.wisefy.callbacks.GetCurrentNetworkCallbacks -import com.isupatches.android.wisefy.callbacks.GetCurrentNetworkInfoCallbacks -import com.isupatches.android.wisefy.callbacks.GetFrequencyCallbacks -import com.isupatches.android.wisefy.callbacks.GetIPCallbacks -import com.isupatches.android.wisefy.callbacks.GetNearbyAccessPointCallbacks -import com.isupatches.android.wisefy.callbacks.GetSavedNetworksCallbacks -import com.isupatches.android.wisefy.frequency.entities.FrequencyData -import com.isupatches.android.wisefy.networkinfo.entities.CurrentNetworkData -import com.isupatches.android.wisefy.networkinfo.entities.CurrentNetworkInfoData -import com.isupatches.android.wisefy.networkinfo.entities.IPData -import com.isupatches.android.wisefy.sample.internal.scaffolding.BasePresenter -import com.isupatches.android.wisefy.sample.internal.scaffolding.Presenter -import com.isupatches.android.wisefy.savednetworks.entities.SavedNetworkData -import javax.inject.Inject - -internal interface MiscPresenter : Presenter { - - fun disableWifi() - - fun enableWifi() - - fun getCurrentNetwork() - - fun getCurrentNetworkInfo() - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun getFrequency() - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun getIP() - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun getNearbyAccessPoints() - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun getSavedNetworks() -} - -@MiscScope -internal class DefaultMiscPresenter @Inject constructor( - private val model: MiscModel, -) : BasePresenter(), MiscPresenter { - - /* - * Model call-throughs - */ - - override fun disableWifi() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - model.disableWifi( - callbacks = object : DisableWifiCallbacks { - override fun onFailureDisablingWifi() { - doSafelyWithView { view -> - view.displayFailureDisablingWifi() - } - } - - override fun onWifiDisabled() { - doSafelyWithView { view -> - view.displayWifiDisabled() - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } else { - doSafelyWithView { view -> view.displayAndroidQWifiMessage() } - } - } - - override fun enableWifi() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - model.enableWifi( - callbacks = object : EnableWifiCallbacks { - override fun onFailureEnablingWifi() { - doSafelyWithView { view -> - view.displayFailureEnablingWifi() - } - } - - override fun onWifiEnabled() { - doSafelyWithView { view -> - view.displayWifiEnabled() - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } else { - doSafelyWithView { view -> view.displayAndroidQWifiMessage() } - } - } - - override fun getCurrentNetwork() { - model.getCurrentNetwork( - callbacks = object : GetCurrentNetworkCallbacks { - override fun onNoCurrentNetwork() { - doSafelyWithView { view -> - view.displayNoCurrentNetwork() - } - } - - override fun onCurrentNetworkRetrieved(currentNetwork: CurrentNetworkData) { - doSafelyWithView { view -> - view.displayCurrentNetwork(currentNetwork) - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } - - override fun getCurrentNetworkInfo() { - model.getCurrentNetworkInfo( - callbacks = object : GetCurrentNetworkInfoCallbacks { - override fun onNoCurrentNetworkInfo() { - doSafelyWithView { view -> - view.displayNoCurrentNetworkInfo() - } - } - - override fun onCurrentNetworkInfoRetrieved(currentNetworkInfo: CurrentNetworkInfoData) { - doSafelyWithView { view -> - view.displayCurrentNetworkInfo(currentNetworkInfo) - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - override fun getFrequency() { - model.getFrequency( - callbacks = object : GetFrequencyCallbacks { - override fun onFailureRetrievingFrequency() { - doSafelyWithView { view -> - view.displayFailureRetrievingFrequency() - } - } - - override fun onFrequencyRetrieved(frequency: FrequencyData) { - doSafelyWithView { view -> - view.displayFrequency(frequency) - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun getIP() { - model.getIP( - callbacks = object : GetIPCallbacks { - override fun onFailureRetrievingIP() { - doSafelyWithView { view -> - view.displayFailureRetrievingIP() - } - } - - override fun onIPRetrieved(ip: IPData) { - doSafelyWithView { view -> - view.displayIP(ip) - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun getNearbyAccessPoints() { - model.getNearbyAccessPoints( - callbacks = object : GetNearbyAccessPointCallbacks { - override fun onNearbyAccessPointsRetrieved(accessPoints: List) { - doSafelyWithView { view -> - view.displayNearbyAccessPoints(accessPoints) - } - } - - override fun onNoNearbyAccessPoints() { - doSafelyWithView { view -> - view.displayNoAccessPointsFound() - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun getSavedNetworks() { - model.getSavedNetworks( - callbacks = object : GetSavedNetworksCallbacks { - override fun onNoSavedNetworksFound() { - doSafelyWithView { view -> - view.displayNoSavedNetworksFound() - } - } - - override fun onSavedNetworksRetrieved(savedNetworks: List) { - doSafelyWithView { view -> - view.displaySavedNetworks(savedNetworks) - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/primitives/WisefySampleColorPalette.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/primitives/WisefySampleColorPalette.kt new file mode 100644 index 00000000..582c2455 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/primitives/WisefySampleColorPalette.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.primitives + +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.ButtonColors +import androidx.compose.material.MaterialTheme +import androidx.compose.material.SliderColors +import androidx.compose.material.TextFieldColors +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.graphics.Color + +internal object WisefySampleColorPalette { + val Primary = Color(red = 0, green = 188, blue = 212, alpha = 255) + val PrimaryDark = Color(red = 0, green = 139, blue = 163, alpha = 255) + val Secondary = Color(red = 255, green = 64, blue = 129, alpha = 255) + val SecondaryLight = Color(red = 255, green = 100, blue = 152, alpha = 255) + val Gray1 = Color(red = 250, green = 250, blue = 250, alpha = 255) + val Gray2 = Color(red = 245, green = 245, blue = 245, alpha = 255) + val Gray3 = Color(red = 238, green = 238, blue = 238, alpha = 255) + val Gray4 = Color(red = 224, green = 224, blue = 224, alpha = 255) + val Gray5 = Color(red = 189, green = 189, blue = 189, alpha = 255) + val Gray6 = Color(red = 158, green = 158, blue = 158, alpha = 255) + val Gray7 = Color(red = 117, green = 117, blue = 117, alpha = 255) + val Gray8 = Color(red = 97, green = 97, blue = 97, alpha = 255) + val Gray9 = Color(red = 66, green = 66, blue = 66, alpha = 255) + val Gray10 = Color(red = 33, green = 33, blue = 33, alpha = 255) + val Error = Color(red = 176, green = 0, blue = 32, alpha = 255) + val ErrorDarkMode = Color(red = 207, green = 102, blue = 121, alpha = 255) + val Success = Color(red = 102, green = 187, blue = 106, alpha = 255) +} + +internal class WisefySamplePrimaryButtonColors : ButtonColors { + + @Composable + override fun backgroundColor(enabled: Boolean): State { + return object : State { + override val value: Color = MaterialTheme.colors.primary + } + } + + @Composable + override fun contentColor(enabled: Boolean): State { + return object : State { + override val value: Color = MaterialTheme.colors.onPrimary + } + } +} + +internal class WisefySampleTextFieldColors : TextFieldColors { + + @Composable + override fun backgroundColor(enabled: Boolean): State { + return object : State { + override val value: Color = MaterialTheme.colors.surface + } + } + + @Composable + override fun cursorColor(isError: Boolean): State { + return object : State { + override val value: Color = if (isError) { + MaterialTheme.colors.error + } else { + MaterialTheme.colors.primary + } + } + } + + @Composable + override fun indicatorColor( + enabled: Boolean, + isError: Boolean, + interactionSource: InteractionSource + ): State { + return object : State { + override val value: Color = if (isError) { + MaterialTheme.colors.error + } else { + MaterialTheme.colors.primary + } + } + } + + @Composable + override fun labelColor(enabled: Boolean, error: Boolean, interactionSource: InteractionSource): State { + return object : State { + override val value: Color = MaterialTheme.colors.background + } + } + + @Composable + override fun leadingIconColor(enabled: Boolean, isError: Boolean): State { + return object : State { + override val value: Color = MaterialTheme.colors.primary + } + } + + @Composable + override fun placeholderColor(enabled: Boolean): State { + return object : State { + override val value: Color = if (isSystemInDarkTheme()) { + WisefySampleColorPalette.Gray7 + } else { + WisefySampleColorPalette.Gray5 + } + } + } + + @Composable + override fun textColor(enabled: Boolean): State { + return object : State { + override val value: Color = MaterialTheme.colors.onSurface + } + } + + @Composable + override fun trailingIconColor(enabled: Boolean, isError: Boolean): State { + return object : State { + override val value: Color = MaterialTheme.colors.primary + } + } +} + +internal class WisefySliderColors : SliderColors { + + @Composable + override fun thumbColor(enabled: Boolean): State { + return object : State { + override val value: Color = MaterialTheme.colors.primary + } + } + + @Composable + override fun tickColor(enabled: Boolean, active: Boolean): State { + return object : State { + override val value: Color = MaterialTheme.colors.primaryVariant + } + } + + @Composable + override fun trackColor(enabled: Boolean, active: Boolean): State { + return object : State { + override val value: Color = if (active) { + MaterialTheme.colors.primary + } else { + WisefySampleColorPalette.Gray4 + } + } + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/primitives/WisefySampleCornerRadii.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/primitives/WisefySampleCornerRadii.kt new file mode 100644 index 00000000..9fb8fc8c --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/primitives/WisefySampleCornerRadii.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.primitives + +import androidx.compose.ui.unit.dp + +internal object WisefySampleCornerRadii { + val Default = 16.dp +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/primitives/WisefySampleSizes.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/primitives/WisefySampleSizes.kt new file mode 100644 index 00000000..a404203e --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/primitives/WisefySampleSizes.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.primitives + +import androidx.compose.ui.unit.dp + +internal object WisefySampleSizes { + val Small = 4.dp + val Medium = 8.dp + val Large = 16.dp + val XLarge = 24.dp + val XXLarge = 32.dp + val XXXLarge = 48.dp + + private val BottomNavigationHeight = 56.dp + + val WisefySampleTopMargin = XLarge + val WisefySampleBottomMargin = BottomNavigationHeight + Large + val WisefySampleHorizontalMargins = Large +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/primitives/WisefySampleTypography.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/primitives/WisefySampleTypography.kt new file mode 100644 index 00000000..31f63667 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/primitives/WisefySampleTypography.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.primitives + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.isupatches.android.wisefy.sample.R + +private val WisefySampleFontFamily = FontFamily( + Font(R.font.rubik_regular), + Font(R.font.rubik_medium, FontWeight.W500), + Font(R.font.rubik_bold, FontWeight.Bold) +) + +private const val H1_LETTER_SPACING = -1.5 +private const val H2_LETTER_SPACING = -0.5 + +internal val WisefySampleTypography = Typography( + h1 = TextStyle( + fontWeight = FontWeight.Light, + fontSize = 96.sp, + letterSpacing = H1_LETTER_SPACING.sp + ), + h2 = TextStyle( + fontWeight = FontWeight.Light, + fontSize = 60.sp, + letterSpacing = H2_LETTER_SPACING.sp + ), + h3 = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 48.sp, + letterSpacing = 0.sp + ), + h4 = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 34.sp, + letterSpacing = 0.25.sp + ), + h5 = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + letterSpacing = 0.sp + ), + h6 = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 20.sp, + letterSpacing = 0.15.sp + ), + subtitle1 = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + letterSpacing = 0.15.sp + ), + subtitle2 = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + letterSpacing = 0.1.sp + ), + body1 = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + letterSpacing = 0.5.sp + ), + body2 = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + letterSpacing = 0.25.sp + ), + button = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + letterSpacing = 1.25.sp + ), + caption = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + letterSpacing = 0.4.sp + ), + overline = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 10.sp, + letterSpacing = 1.5.sp + ), + defaultFontFamily = WisefySampleFontFamily +) diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkFragment.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkFragment.kt deleted file mode 100644 index 9790f27b..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkFragment.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.remove - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.Manifest.permission.CHANGE_WIFI_STATE -import android.content.pm.PackageManager -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.VisibleForTesting -import com.isupatches.android.viewglu.paste -import com.isupatches.android.wisefy.removenetwork.entities.RemoveNetworkResult -import com.isupatches.android.wisefy.sample.R -import com.isupatches.android.wisefy.sample.databinding.FragmentRemoveNetworkBinding -import com.isupatches.android.wisefy.sample.internal.base.BaseFragment -import com.isupatches.android.wisefy.sample.internal.logging.WisefySampleLogger -import com.isupatches.android.wisefy.sample.internal.util.getTrimmedInput -import com.isupatches.android.wisefy.sample.internal.util.hideKeyboardFrom -import javax.inject.Inject - -@VisibleForTesting internal const val WISEFY_REMOVE_NETWORK_REQUEST_CODE = 1 - -private const val LOG_TAG = "RemoveNetworkFragment" - -internal interface RemoveNetworkView { - fun displayFailureRemovingNetwork(result: RemoveNetworkResult) - fun displayNetworkNotFountToRemove() - fun displayNetworkRemoved(result: RemoveNetworkResult) -} - -internal class RemoveNetworkFragment : BaseFragment(), RemoveNetworkView { - - @RemoveNetworkScope @Inject lateinit var presenter: RemoveNetworkPresenter - @RemoveNetworkScope @Inject lateinit var removeNetworkStore: RemoveNetworkStore - - private var binding: FragmentRemoveNetworkBinding by paste() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = FragmentRemoveNetworkBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - if (savedInstanceState == null) { - restoreUI() - } - - binding.removeNetworkBtn.setOnClickListener { - hideKeyboardFrom(binding.removeNetworkBtn) - removeNetwork() - } - } - - override fun onStart() { - super.onStart() - presenter.attachView(this) - } - - override fun onStop() { - presenter.detachView() - super.onStop() - removeNetworkStore.setLastUsedRegex(binding.networkNameEdt.getTrimmedInput()) - hideKeyboardFrom(binding.removeNetworkBtn) - } - - /* - * View helpers - */ - - private fun restoreUI() { - // Restore edit text value - binding.networkNameEdt.setText(removeNetworkStore.getLastUsedRegex()) - } - - /* - * Presenter callback overrides - */ - - override fun displayNetworkRemoved(result: RemoveNetworkResult) { - displayInfo(getString(R.string.succeeded_removing_network_args, result), R.string.remove_network_result) - } - - override fun displayNetworkNotFountToRemove() { - displayInfo(getString(R.string.network_not_fount_to_remove), R.string.remove_network_result) - } - - override fun displayFailureRemovingNetwork(result: RemoveNetworkResult) { - displayInfo(getString(R.string.failed_removing_network_args, result), R.string.remove_network_result) - } - - override fun displayWisefyAsyncError(throwable: Throwable) { - displayWisefyAsyncErrorDialog(throwable) - } - - /* - * WiseFy helpers - */ - - @Throws(SecurityException::class) - private fun removeNetwork() { - if (checkRemoveNetworkPermissions()) { - presenter.removeNetwork(binding.networkNameEdt.getTrimmedInput()) - } - } - - /* - * Permission helpers - */ - - private fun checkRemoveNetworkPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_REMOVE_NETWORK_REQUEST_CODE) && - isPermissionGranted(CHANGE_WIFI_STATE, WISEFY_REMOVE_NETWORK_REQUEST_CODE) - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - when (requestCode) { - WISEFY_REMOVE_NETWORK_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - removeNetwork() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for remove saved network are denied") - displayPermissionErrorDialog(R.string.permission_error_remove_network) - } - } - else -> { - WisefySampleLogger.wtf(LOG_TAG, "Weird permission requested, not handled") - displayPermissionErrorDialog( - getString(R.string.permission_error_unhandled_request_code_args, requestCode) - ) - } - } - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkModel.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkModel.kt deleted file mode 100644 index c57bb9ef..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkModel.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.remove - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.Manifest.permission.CHANGE_WIFI_STATE -import androidx.annotation.RequiresPermission -import com.isupatches.android.wisefy.WisefyApi -import com.isupatches.android.wisefy.callbacks.RemoveNetworkCallbacks -import com.isupatches.android.wisefy.removenetwork.entities.RemoveNetworkRequest -import com.isupatches.android.wisefy.sample.internal.scaffolding.BaseModel -import javax.inject.Inject - -internal interface RemoveNetworkModel { - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE]) - fun removeNetwork( - networkName: String, - callbacks: RemoveNetworkCallbacks? - ) -} - -@RemoveNetworkScope -internal class DefaultRemoveNetworkModel @Inject constructor( - private val wiseFy: WisefyApi -) : BaseModel(), RemoveNetworkModel { - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE]) - override fun removeNetwork(networkName: String, callbacks: RemoveNetworkCallbacks?) { - wiseFy.removeNetwork(RemoveNetworkRequest.SSID(networkName)) - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkModule.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkModule.kt deleted file mode 100644 index aaf4e6fc..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkModule.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.remove - -import dagger.Binds -import dagger.Module -import javax.inject.Scope - -@Scope -@Retention(AnnotationRetention.RUNTIME) -internal annotation class RemoveNetworkScope - -@Suppress("unused") -@Module -internal interface RemoveNetworkFragmentModule { - - @Binds - @RemoveNetworkScope - fun bindRemoveNetworkModel(impl: DefaultRemoveNetworkModel): RemoveNetworkModel - - @Binds - @RemoveNetworkScope - fun bindRemoveNetworkPresenter(impl: DefaultRemoveNetworkPresenter): RemoveNetworkPresenter - - @Binds - @RemoveNetworkScope - fun bindRemoveNetworkStore(impl: SharedPreferencesRemoveNetworkStore): RemoveNetworkStore -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkPresenter.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkPresenter.kt deleted file mode 100644 index 28593ed2..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkPresenter.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.remove - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.Manifest.permission.CHANGE_WIFI_STATE -import androidx.annotation.RequiresPermission -import com.isupatches.android.wisefy.callbacks.RemoveNetworkCallbacks -import com.isupatches.android.wisefy.removenetwork.entities.RemoveNetworkResult -import com.isupatches.android.wisefy.sample.internal.scaffolding.BasePresenter -import com.isupatches.android.wisefy.sample.internal.scaffolding.Presenter -import javax.inject.Inject - -internal interface RemoveNetworkPresenter : Presenter { - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE]) - fun removeNetwork(networkName: String) -} - -@RemoveNetworkScope -internal class DefaultRemoveNetworkPresenter @Inject constructor( - private val model: RemoveNetworkModel, -) : BasePresenter(), RemoveNetworkPresenter { - - /* - * Model call-throughs - */ - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE]) - override fun removeNetwork(networkName: String) { - model.removeNetwork( - networkName = networkName, - callbacks = object : RemoveNetworkCallbacks { - override fun onFailureRemovingNetwork(result: RemoveNetworkResult) { - doSafelyWithView { view -> - view.displayFailureRemovingNetwork(result) - } - } - - override fun onNetworkNotFoundToRemove() { - doSafelyWithView { view -> - view.displayNetworkNotFountToRemove() - } - } - - override fun onNetworkRemoved(result: RemoveNetworkResult) { - doSafelyWithView { view -> - view.displayNetworkRemoved(result) - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkStore.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkStore.kt deleted file mode 100644 index 32bf3ae5..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkStore.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.remove - -import android.content.Context -import androidx.core.content.edit -import com.isupatches.android.wisefy.sample.R -import com.isupatches.android.wisefy.sample.internal.scaffolding.BaseSharedPreferenceStore -import javax.inject.Inject - -internal interface RemoveNetworkStore { - fun clear() - - fun getLastUsedRegex(): String - - fun setLastUsedRegex(lastUsedRegex: String) -} - -@RemoveNetworkScope -internal class SharedPreferencesRemoveNetworkStore @Inject constructor( - context: Context -) : BaseSharedPreferenceStore(), RemoveNetworkStore { - - private val sharedPreferences = getSharedPreferences( - context, - R.string.preferences_remove_network_data - ) - - override fun clear() { - sharedPreferences.edit { clear() } - } - - /* - * Last used Regex - */ - - override fun getLastUsedRegex() = sharedPreferences.getLastUsedRegex() - - override fun setLastUsedRegex(lastUsedRegex: String) { - sharedPreferences.setLastUsedRegex(lastUsedRegex) - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchFragment.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchFragment.kt deleted file mode 100644 index 5286e367..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchFragment.kt +++ /dev/null @@ -1,465 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.search - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.Manifest.permission.ACCESS_WIFI_STATE -import android.content.pm.PackageManager -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.SeekBar -import androidx.annotation.VisibleForTesting -import com.isupatches.android.viewglu.paste -import com.isupatches.android.wisefy.accesspoints.entities.AccessPointData -import com.isupatches.android.wisefy.accesspoints.entities.SSIDData -import com.isupatches.android.wisefy.sample.R -import com.isupatches.android.wisefy.sample.databinding.FragmentSearchBinding -import com.isupatches.android.wisefy.sample.internal.base.BaseFragment -import com.isupatches.android.wisefy.sample.internal.entities.SearchType -import com.isupatches.android.wisefy.sample.internal.logging.WisefySampleLogger -import com.isupatches.android.wisefy.sample.internal.util.asHtmlSpanned -import com.isupatches.android.wisefy.sample.internal.util.getTrimmedInput -import com.isupatches.android.wisefy.sample.internal.util.hideKeyboardFrom -import com.isupatches.android.wisefy.savednetworks.entities.SavedNetworkData -import javax.inject.Inject -import kotlin.math.max - -@VisibleForTesting internal const val WISEFY_SEARCH_FOR_SAVED_NETWORK_REQUEST_CODE = 1 -@VisibleForTesting internal const val WISEFY_SEARCH_FOR_SAVED_NETWORKS_REQUEST_CODE = 2 -@VisibleForTesting internal const val WISEFY_SEARCH_FOR_ACCESS_POINT_REQUEST_CODE = 3 -@VisibleForTesting internal const val WISEFY_SEARCH_FOR_ACCESS_POINTS_REQUEST_CODE = 4 -@VisibleForTesting internal const val WISEFY_SEARCH_FOR_SSID_REQUEST_CODE = 5 -@VisibleForTesting internal const val WISEFY_SEARCH_FOR_SSIDS_REQUEST_CODE = 6 - -private const val TIMEOUT_MIN = 1 -private const val TIMEOUT_MAX = 60 - -private const val SEEK_MILLI_OFFSET = 1000 - -private const val LOG_TAG = "SearchFragment" - -internal interface SearchView { - fun displaySavedNetwork(savedNetwork: SavedNetworkData?) - fun displaySavedNetworkNotFound() - fun displaySavedNetworks(savedNetworks: List) - fun displayNoSavedNetworksFound() - fun displayAccessPoint(accessPoint: AccessPointData?) - fun displayAccessPointNotFound() - fun displayAccessPoints(accessPoints: List) - fun displayNoAccessPointsFound() - fun displaySSID(ssid: SSIDData?) - fun displaySSIDNotFound() - fun displaySSIDs(ssids: List) - fun displayNoSSIDsFound() -} - -@Suppress("LargeClass") -internal class SearchFragment : BaseFragment(), SearchView { - - @SearchScope @Inject lateinit var presenter: SearchPresenter - @SearchScope @Inject lateinit var searchStore: SearchStore - - private var binding: FragmentSearchBinding by paste() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = FragmentSearchBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - if (savedInstanceState == null) { - restoreUI() - } - - binding.searchTypeRdg.setOnCheckedChangeListener { _, checkedId -> - when (checkedId) { - R.id.accessPointRdb -> searchStore.setSearchType(SearchType.ACCESS_POINT) - R.id.ssidRdb -> searchStore.setSearchType(SearchType.SSID) - R.id.savedNetworkRdb -> searchStore.setSearchType(SearchType.SAVED_NETWORK) - } - updateUI() - } - binding.filterDupesRdg.setOnCheckedChangeListener { _, checkedId -> - when (checkedId) { - R.id.yesFilterDupesRdb -> searchStore.setFilterDuplicates(true) - else -> searchStore.setFilterDuplicates(false) - } - } - binding.returnFullListRdg.setOnCheckedChangeListener { _, checkedId -> - when (checkedId) { - R.id.yesFullListRdb -> searchStore.setReturnFullList(true) - else -> searchStore.setReturnFullList(false) - } - updateUI() - } - binding.searchBtn.setOnClickListener { search() } - - with(binding.timeoutSeek) { - max = TIMEOUT_MAX - setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - val timeout = max(TIMEOUT_MIN, progress) - searchStore.setTimeout(timeout) - @Suppress("SyntheticAccessor") - binding.timeoutTxt.text = getString( - R.string.timeout_after_x_seconds_args_html, - timeout - ).asHtmlSpanned() - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) { - // No-op - } - - override fun onStopTrackingTouch(seekBar: SeekBar?) { - // No-op - } - }) - } - } - - override fun onStart() { - super.onStart() - presenter.attachView(this) - } - - override fun onStop() { - presenter.detachView() - super.onStop() - searchStore.setLastUsedRegex(binding.searchRegexEdt.getTrimmedInput()) - hideKeyboardFrom(binding.searchBtn) - } - - /* - * View helpers - */ - - private fun adjustFilterDupesVisibility(visibility: Int) { - binding.filterDupesTxt.visibility = visibility - binding.filterDupesRdg.visibility = visibility - } - - private fun adjustTimeoutVisibility(visibility: Int) { - binding.timeoutSeek.visibility = visibility - binding.timeoutTxt.visibility = visibility - } - - private fun getFilterDuplicates(): Boolean { - return binding.filterDupesRdg.checkedRadioButtonId == R.id.yesFilterDupesRdb - } - - private fun getSelectedTimeout(): Int = binding.timeoutSeek.progress * SEEK_MILLI_OFFSET - - private fun restoreUI() { - // Restore edit text value - binding.searchRegexEdt.setText(searchStore.getLastUsedRegex()) - - // Restore checked state - val checkedId = when (searchStore.getSearchType()) { - SearchType.ACCESS_POINT -> { - showAccessPointUI() - R.id.accessPointRdb - } - SearchType.SSID -> { - showSSIDUI() - R.id.ssidRdb - } - SearchType.SAVED_NETWORK -> { - showSavedNetworkUI() - R.id.savedNetworkRdb - } - } - binding.searchTypeRdg.check(checkedId) - - // Restore return full list - val fullListCheckedId = if (searchStore.shouldReturnFullList()) { - R.id.yesFullListRdb - } else { - R.id.noFullListRdb - } - binding.returnFullListRdg.check(fullListCheckedId) - - // Restore filter duplicates - val filterDupesCheckedId = if (searchStore.shouldFilterDuplicates()) { - R.id.yesFilterDupesRdb - } else { - R.id.noFilterDupesRdb - } - binding.filterDupesRdg.check(filterDupesCheckedId) - - // Restore timeout - val timeout = searchStore.getTimeout() - binding.timeoutSeek.progress = timeout - binding.timeoutTxt.text = getString(R.string.timeout_after_x_seconds_args_html, timeout).asHtmlSpanned() - when (searchStore.getSearchType()) { - SearchType.SAVED_NETWORK -> adjustTimeoutVisibility(View.INVISIBLE) - else -> toggleSeekVisibility() - } - } - - private fun search() { - when (binding.searchTypeRdg.checkedRadioButtonId) { - R.id.accessPointRdb -> { - when (binding.returnFullListRdg.checkedRadioButtonId) { - R.id.yesFullListRdb -> searchForAccessPoints() - R.id.noFullListRdb -> searchForAccessPoint() - } - } - R.id.savedNetworkRdb -> { - when (binding.returnFullListRdg.checkedRadioButtonId) { - R.id.yesFullListRdb -> searchForSavedNetworks() - R.id.noFullListRdb -> searchForSavedNetwork() - } - } - R.id.ssidRdb -> { - when (binding.returnFullListRdg.checkedRadioButtonId) { - R.id.yesFullListRdb -> searchForSSIDs() - R.id.noFullListRdb -> searchForSSID() - } - } - } - } - - private fun showAccessPointUI() { - adjustFilterDupesVisibility(View.VISIBLE) - toggleSeekVisibility() - } - - private fun showSavedNetworkUI() { - adjustFilterDupesVisibility(View.INVISIBLE) - adjustTimeoutVisibility(View.INVISIBLE) - } - - private fun showSSIDUI() { - adjustFilterDupesVisibility(View.INVISIBLE) - toggleSeekVisibility() - } - - private fun toggleSeekVisibility() { - when (binding.returnFullListRdg.checkedRadioButtonId) { - R.id.yesFullListRdb -> adjustTimeoutVisibility(View.INVISIBLE) - R.id.noFullListRdb -> adjustTimeoutVisibility(View.VISIBLE) - } - } - - private fun updateUI() { - when (binding.searchTypeRdg.checkedRadioButtonId) { - R.id.accessPointRdb -> showAccessPointUI() - R.id.ssidRdb -> showSSIDUI() - R.id.savedNetworkRdb -> showSavedNetworkUI() - } - } - - /* - * WiseFy helpers - */ - - @Throws(SecurityException::class) - private fun searchForAccessPoint() { - if (checkSearchForAccessPointPermissions()) { - presenter.searchForAccessPoint( - binding.searchRegexEdt.getTrimmedInput(), - getSelectedTimeout(), - getFilterDuplicates() - ) - } - } - - @Throws(SecurityException::class) - private fun searchForAccessPoints() { - if (checkSearchForAccessPointsPermissions()) { - presenter.searchForAccessPoints(binding.searchRegexEdt.getTrimmedInput(), getFilterDuplicates()) - } - } - - @Throws(SecurityException::class) - private fun searchForSavedNetwork() { - if (checkSearchForSavedNetworkPermissions()) { - presenter.searchForSavedNetwork(binding.searchRegexEdt.getTrimmedInput()) - } - } - - @Throws(SecurityException::class) - private fun searchForSavedNetworks() { - if (checkSearchForSavedNetworksPermissions()) { - presenter.searchForSavedNetworks(binding.searchRegexEdt.getTrimmedInput()) - } - } - - @Throws(SecurityException::class) - private fun searchForSSID() { - if (checkSearchForSSIDPermissions()) { - presenter.searchForSSID(binding.searchRegexEdt.getTrimmedInput(), getSelectedTimeout()) - } - } - - @Throws(SecurityException::class) - private fun searchForSSIDs() { - if (checkSearchForSSIDsPermissions()) { - presenter.searchForSSIDs(binding.searchRegexEdt.getTrimmedInput()) - } - } - - /* - * Presenter callback overrides - */ - - override fun displaySavedNetwork(savedNetwork: SavedNetworkData?) { - displayInfoFullScreen(getString(R.string.saved_network_args, savedNetwork), R.string.search_result) - } - - override fun displaySavedNetworkNotFound() { - displayInfo(R.string.saved_network_not_found, R.string.search_result) - } - - override fun displaySavedNetworks(savedNetworks: List) { - displayInfoFullScreen(getString(R.string.saved_networks_args, savedNetworks), R.string.search_result) - } - - override fun displayNoSavedNetworksFound() { - displayInfo(R.string.no_saved_networks_found, R.string.search_result) - } - - override fun displayAccessPoint(accessPoint: AccessPointData?) { - displayInfoFullScreen(getString(R.string.access_point_args, accessPoint), R.string.search_result) - } - - override fun displayAccessPointNotFound() { - displayInfo(R.string.access_point_not_found, R.string.search_result) - } - - override fun displayAccessPoints(accessPoints: List) { - displayInfoFullScreen(getString(R.string.access_points_args, accessPoints), R.string.search_result) - } - - override fun displayNoAccessPointsFound() { - displayInfo(R.string.no_access_points_found, R.string.search_result) - } - - override fun displaySSID(ssid: SSIDData?) { - displayInfoFullScreen(getString(R.string.ssid_args, ssid), R.string.search_result) - } - - override fun displaySSIDNotFound() { - displayInfo(R.string.ssid_not_found, R.string.search_result) - } - - override fun displaySSIDs(ssids: List) { - displayInfoFullScreen(getString(R.string.ssids_args, ssids), R.string.search_result) - } - - override fun displayNoSSIDsFound() { - displayInfo(R.string.no_ssids_found, R.string.search_result) - } - - override fun displayWisefyAsyncError(throwable: Throwable) { - displayWisefyAsyncErrorDialog(throwable) - } - - /* - * Permission helpers - */ - - private fun checkSearchForAccessPointPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_SEARCH_FOR_ACCESS_POINT_REQUEST_CODE) - } - - private fun checkSearchForAccessPointsPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_SEARCH_FOR_ACCESS_POINTS_REQUEST_CODE) - } - - private fun checkSearchForSavedNetworkPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_SEARCH_FOR_SAVED_NETWORK_REQUEST_CODE) && - isPermissionGranted(ACCESS_WIFI_STATE, WISEFY_SEARCH_FOR_SAVED_NETWORKS_REQUEST_CODE) - } - - private fun checkSearchForSavedNetworksPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_SEARCH_FOR_SAVED_NETWORKS_REQUEST_CODE) && - isPermissionGranted(ACCESS_WIFI_STATE, WISEFY_SEARCH_FOR_SAVED_NETWORKS_REQUEST_CODE) - } - - private fun checkSearchForSSIDPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_SEARCH_FOR_SSID_REQUEST_CODE) - } - - private fun checkSearchForSSIDsPermissions(): Boolean { - return isPermissionGranted(ACCESS_FINE_LOCATION, WISEFY_SEARCH_FOR_SSIDS_REQUEST_CODE) - } - - @Suppress("LongMethod", "ComplexMethod") - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - when (requestCode) { - WISEFY_SEARCH_FOR_SAVED_NETWORK_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - searchForSavedNetwork() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for getting a saved network are denied") - displayPermissionErrorDialog(R.string.permission_error_search_for_saved_network) - } - } - WISEFY_SEARCH_FOR_SAVED_NETWORKS_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - searchForSavedNetworks() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for getting saved networks are denied") - displayPermissionErrorDialog(R.string.permission_error_search_for_saved_networks) - } - } - WISEFY_SEARCH_FOR_ACCESS_POINT_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - searchForAccessPoint() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for searching for an access point are denied") - displayPermissionErrorDialog(R.string.permission_error_search_for_access_point) - } - } - WISEFY_SEARCH_FOR_ACCESS_POINTS_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - searchForAccessPoints() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for searching for access points are denied") - displayPermissionErrorDialog(R.string.permission_error_search_for_access_points) - } - } - WISEFY_SEARCH_FOR_SSID_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - searchForSSID() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for searching for an SSID are denied") - displayPermissionErrorDialog(R.string.permission_error_search_for_ssid) - } - } - WISEFY_SEARCH_FOR_SSIDS_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - searchForSSIDs() - } else { - WisefySampleLogger.w(LOG_TAG, "Permissions for searching for SSIDs are denied") - displayPermissionErrorDialog(R.string.permission_error_search_for_ssids) - } - } - else -> { - WisefySampleLogger.wtf(LOG_TAG, "Weird permission requested, not handled") - displayPermissionErrorDialog( - getString(R.string.permission_error_unhandled_request_code_args, requestCode) - ) - } - } - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchModel.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchModel.kt deleted file mode 100644 index 36998c07..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchModel.kt +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.search - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.Manifest.permission.ACCESS_WIFI_STATE -import androidx.annotation.RequiresPermission -import com.isupatches.android.wisefy.WisefyApi -import com.isupatches.android.wisefy.accesspoints.entities.SearchForMultipleAccessPointsRequest -import com.isupatches.android.wisefy.accesspoints.entities.SearchForMultipleSSIDsRequest -import com.isupatches.android.wisefy.accesspoints.entities.SearchForSingleAccessPointRequest -import com.isupatches.android.wisefy.accesspoints.entities.SearchForSingleSSIDRequest -import com.isupatches.android.wisefy.callbacks.SearchForAccessPointCallbacks -import com.isupatches.android.wisefy.callbacks.SearchForAccessPointsCallbacks -import com.isupatches.android.wisefy.callbacks.SearchForSSIDCallbacks -import com.isupatches.android.wisefy.callbacks.SearchForSSIDsCallbacks -import com.isupatches.android.wisefy.callbacks.SearchForSavedNetworkCallbacks -import com.isupatches.android.wisefy.callbacks.SearchForSavedNetworksCallbacks -import com.isupatches.android.wisefy.sample.internal.scaffolding.BaseModel -import com.isupatches.android.wisefy.savednetworks.entities.SearchForSavedNetworkRequest -import javax.inject.Inject - -internal interface SearchModel { - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun searchForAccessPoint( - regexForSSID: String, - timeoutInMillis: Int, - filterDuplicates: Boolean, - callbacks: SearchForAccessPointCallbacks? - ) - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun searchForAccessPoints( - regexForSSID: String, - filterDuplicates: Boolean, - callbacks: SearchForAccessPointsCallbacks? - ) - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) - fun searchForSavedNetwork( - regexForSSID: String, - callbacks: SearchForSavedNetworkCallbacks? - ) - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) - fun searchForSavedNetworks( - regexForSSID: String, - callbacks: SearchForSavedNetworksCallbacks? - ) - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun searchForSSID( - regexForSSID: String, - timeoutInMillis: Int, - callbacks: SearchForSSIDCallbacks? - ) - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun searchForSSIDs( - regexForSSID: String, - callbacks: SearchForSSIDsCallbacks? - ) -} - -@SearchScope -internal class DefaultSearchModel @Inject constructor( - private val wisefy: WisefyApi -) : BaseModel(), SearchModel { - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun searchForAccessPoint( - regexForSSID: String, - timeoutInMillis: Int, - filterDuplicates: Boolean, - callbacks: SearchForAccessPointCallbacks? - ) { - wisefy.searchForAccessPoint( - request = SearchForSingleAccessPointRequest.SSID( - regexForSSID = regexForSSID, - timeoutInMillis = timeoutInMillis, - filterDuplicates = filterDuplicates - ), - callbacks = callbacks - ) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun searchForAccessPoints( - regexForSSID: String, - filterDuplicates: Boolean, - callbacks: SearchForAccessPointsCallbacks? - ) { - wisefy.searchForAccessPoints( - request = SearchForMultipleAccessPointsRequest.SSID( - regexForSSID = regexForSSID, - filterDuplicates = filterDuplicates - ), - callbacks = callbacks - ) - } - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) - override fun searchForSavedNetwork( - regexForSSID: String, - callbacks: SearchForSavedNetworkCallbacks? - ) { - wisefy.searchForSavedNetwork( - request = SearchForSavedNetworkRequest.SSID(regexForSSID = regexForSSID), - callbacks = callbacks - ) - } - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) - override fun searchForSavedNetworks( - regexForSSID: String, - callbacks: SearchForSavedNetworksCallbacks? - ) { - wisefy.searchForSavedNetworks( - request = SearchForSavedNetworkRequest.SSID(regexForSSID = regexForSSID), - callbacks = callbacks - ) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun searchForSSID( - regexForSSID: String, - timeoutInMillis: Int, - callbacks: SearchForSSIDCallbacks? - ) { - wisefy.searchForSSID( - request = SearchForSingleSSIDRequest.SSID( - regexForSSID = regexForSSID, - timeoutInMillis = timeoutInMillis - ), - callbacks = callbacks - ) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun searchForSSIDs( - regexForSSID: String, - callbacks: SearchForSSIDsCallbacks? - ) { - wisefy.searchForSSIDs( - request = SearchForMultipleSSIDsRequest.SSID(regexForSSID = regexForSSID), - callbacks = callbacks - ) - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchModule.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchModule.kt deleted file mode 100644 index aa9eeba6..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchModule.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.search - -import dagger.Binds -import dagger.Module -import javax.inject.Scope - -@Scope -@Retention(AnnotationRetention.RUNTIME) -internal annotation class SearchScope - -@Suppress("unused") -@Module -internal interface SearchFragmentModule { - @Binds - @SearchScope - fun bindSearchModel(impl: DefaultSearchModel): SearchModel - - @Binds - @SearchScope - fun bindSearchPresenter(impl: DefaultSearchPresenter): SearchPresenter - - @Binds - @SearchScope - fun bindSearchStore(impl: SharedPreferencesSearchStore): SearchStore -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchPresenter.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchPresenter.kt deleted file mode 100644 index 62473d46..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchPresenter.kt +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.search - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.Manifest.permission.ACCESS_WIFI_STATE -import androidx.annotation.RequiresPermission -import com.isupatches.android.wisefy.accesspoints.entities.AccessPointData -import com.isupatches.android.wisefy.accesspoints.entities.SSIDData -import com.isupatches.android.wisefy.callbacks.SearchForAccessPointCallbacks -import com.isupatches.android.wisefy.callbacks.SearchForAccessPointsCallbacks -import com.isupatches.android.wisefy.callbacks.SearchForSSIDCallbacks -import com.isupatches.android.wisefy.callbacks.SearchForSSIDsCallbacks -import com.isupatches.android.wisefy.callbacks.SearchForSavedNetworkCallbacks -import com.isupatches.android.wisefy.callbacks.SearchForSavedNetworksCallbacks -import com.isupatches.android.wisefy.sample.internal.scaffolding.BasePresenter -import com.isupatches.android.wisefy.sample.internal.scaffolding.Presenter -import com.isupatches.android.wisefy.savednetworks.entities.SavedNetworkData -import javax.inject.Inject - -internal interface SearchPresenter : Presenter { - @RequiresPermission(ACCESS_FINE_LOCATION) - fun searchForAccessPoint(regexForSSID: String, timeoutInMillis: Int, filterDuplicates: Boolean) - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun searchForAccessPoints(regexForSSID: String, filterDuplicates: Boolean) - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) - fun searchForSavedNetwork(regexForSSID: String) - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) - fun searchForSavedNetworks(regexForSSID: String) - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun searchForSSID(regexForSSID: String, timeoutInMillis: Int) - - @RequiresPermission(ACCESS_FINE_LOCATION) - fun searchForSSIDs(regexForSSID: String) -} - -@SearchScope -internal class DefaultSearchPresenter @Inject constructor( - private val model: SearchModel -) : BasePresenter(), SearchPresenter { - - /* - * Model call-throughs - */ - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun searchForAccessPoint( - regexForSSID: String, - timeoutInMillis: Int, - filterDuplicates: Boolean - ) { - model.searchForAccessPoint( - regexForSSID = regexForSSID, - timeoutInMillis = timeoutInMillis, - filterDuplicates = filterDuplicates, - callbacks = object : SearchForAccessPointCallbacks { - override fun onAccessPointFound(accessPoint: AccessPointData) { - doSafelyWithView { view -> - view.displayAccessPoint(accessPoint) - } - } - - override fun onNoAccessPointFound() { - doSafelyWithView { view -> - view.displayAccessPointNotFound() - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun searchForAccessPoints(regexForSSID: String, filterDuplicates: Boolean) { - model.searchForAccessPoints( - regexForSSID = regexForSSID, - filterDuplicates = filterDuplicates, - callbacks = object : SearchForAccessPointsCallbacks { - override fun onAccessPointsFound(accessPoints: List) { - doSafelyWithView { view -> - view.displayAccessPoints(accessPoints) - } - } - - override fun onNoAccessPointsFound() { - doSafelyWithView { view -> - view.displayNoAccessPointsFound() - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) - override fun searchForSavedNetwork(regexForSSID: String) { - model.searchForSavedNetwork( - regexForSSID = regexForSSID, - callbacks = object : SearchForSavedNetworkCallbacks { - override fun onSavedNetworkNotFound() { - doSafelyWithView { view -> - view.displaySavedNetworkNotFound() - } - } - - override fun onSavedNetworkRetrieved(savedNetwork: SavedNetworkData) { - doSafelyWithView { view -> - view.displaySavedNetwork(savedNetwork) - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } - - @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) - override fun searchForSavedNetworks(regexForSSID: String) { - model.searchForSavedNetworks( - regexForSSID = regexForSSID, - callbacks = object : SearchForSavedNetworksCallbacks { - override fun onNoSavedNetworksFound() { - doSafelyWithView { view -> - view.displayNoSavedNetworksFound() - } - } - - override fun onSavedNetworksRetrieved(savedNetworks: List) { - doSafelyWithView { view -> - view.displaySavedNetworks(savedNetworks) - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun searchForSSID(regexForSSID: String, timeoutInMillis: Int) { - model.searchForSSID( - regexForSSID = regexForSSID, - timeoutInMillis = timeoutInMillis, - callbacks = object : SearchForSSIDCallbacks { - override fun onSSIDFound(ssid: SSIDData) { - doSafelyWithView { view -> - view.displaySSID(ssid) - } - } - - override fun onSSIDNotFound() { - doSafelyWithView { view -> - view.displaySSIDNotFound() - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } - - @RequiresPermission(ACCESS_FINE_LOCATION) - override fun searchForSSIDs(regexForSSID: String) { - model.searchForSSIDs( - regexForSSID = regexForSSID, - callbacks = object : SearchForSSIDsCallbacks { - override fun onSSIDsFound(ssids: List) { - doSafelyWithView { view -> - view.displaySSIDs(ssids) - } - } - - override fun onNoSSIDsFound() { - doSafelyWithView { view -> - view.displayNoSSIDsFound() - } - } - - override fun onWisefyAsyncFailure(throwable: Throwable) { - doSafelyWithView { view -> - view.displayWisefyAsyncError(throwable) - } - } - } - ) - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchStore.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchStore.kt deleted file mode 100644 index 1e013074..00000000 --- a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchStore.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2021 Patches Klinefelter - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.isupatches.android.wisefy.sample.ui.search - -import android.content.Context -import androidx.annotation.VisibleForTesting -import androidx.core.content.edit -import com.isupatches.android.wisefy.sample.R -import com.isupatches.android.wisefy.sample.internal.entities.SearchType -import com.isupatches.android.wisefy.sample.internal.scaffolding.BaseSharedPreferenceStore -import javax.inject.Inject - -@VisibleForTesting internal const val PREF_SEARCH_TYPE = "search type" -@VisibleForTesting internal const val PREF_RETURN_FULL_LIST = "return full list" -@VisibleForTesting internal const val PREF_FILTER_DUPLICATES = "filter duplicates" -@VisibleForTesting internal const val PREF_TIMEOUT = "timeout" - -internal interface SearchStore { - fun clear() - - fun getLastUsedRegex(): String - fun getSearchType(): SearchType - fun shouldReturnFullList(): Boolean - fun shouldFilterDuplicates(): Boolean - fun getTimeout(): Int - - fun setLastUsedRegex(lastUsedRegex: String) - fun setSearchType(searchType: SearchType) - fun setReturnFullList(returnFullList: Boolean) - fun setFilterDuplicates(filterDuplicates: Boolean) - fun setTimeout(timeout: Int) -} - -@SearchScope -internal class SharedPreferencesSearchStore @Inject constructor( - context: Context -) : BaseSharedPreferenceStore(), SearchStore { - - private val sharedPreferences = getSharedPreferences( - context, - R.string.preferences_search_data - ) - - override fun clear() { - sharedPreferences.edit { clear() } - } - - /* - * Last used Regex - */ - - override fun getLastUsedRegex() = sharedPreferences.getLastUsedRegex() - - override fun setLastUsedRegex(lastUsedRegex: String) { - sharedPreferences.setLastUsedRegex(lastUsedRegex) - } - - /* - * Search type - */ - - override fun getSearchType(): SearchType = SearchType.of( - sharedPreferences.getInt(PREF_SEARCH_TYPE, SearchType.ACCESS_POINT.intVal) - ) - - override fun setSearchType(searchType: SearchType) { - sharedPreferences.edit { - putInt(PREF_SEARCH_TYPE, searchType.intVal) - } - } - - /* - * Return full list - */ - - override fun shouldReturnFullList() = sharedPreferences.getBoolean( - PREF_RETURN_FULL_LIST, - true - ) - - override fun setReturnFullList(returnFullList: Boolean) { - sharedPreferences.edit { - putBoolean(PREF_RETURN_FULL_LIST, returnFullList) - } - } - - /* - * Filter duplicates - */ - - override fun shouldFilterDuplicates() = sharedPreferences.getBoolean( - PREF_FILTER_DUPLICATES, - true - ) - - override fun setFilterDuplicates(filterDuplicates: Boolean) { - sharedPreferences.edit { - putBoolean(PREF_FILTER_DUPLICATES, filterDuplicates) - } - } - - /* - * Timeout - */ - - override fun getTimeout() = sharedPreferences.getInt(PREF_TIMEOUT, 1) - - override fun setTimeout(timeout: Int) { - sharedPreferences.edit { - putInt(PREF_TIMEOUT, timeout) - } - } -} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/ui/theme/WisefySampleTheme.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/theme/WisefySampleTheme.kt new file mode 100644 index 00000000..407f53a7 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/theme/WisefySampleTheme.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleColorPalette +import com.isupatches.android.wisefy.sample.ui.primitives.WisefySampleTypography + +private val WisefySampleDarkColors = darkColors( + primary = WisefySampleColorPalette.Primary, + primaryVariant = WisefySampleColorPalette.PrimaryDark, + onPrimary = WisefySampleColorPalette.Gray1, + secondary = WisefySampleColorPalette.Secondary, + secondaryVariant = WisefySampleColorPalette.SecondaryLight, + onSecondary = WisefySampleColorPalette.Gray10, + background = WisefySampleColorPalette.Gray10, + onBackground = WisefySampleColorPalette.Gray1, + surface = WisefySampleColorPalette.Gray9, + onSurface = WisefySampleColorPalette.Gray2, + error = WisefySampleColorPalette.ErrorDarkMode, + onError = WisefySampleColorPalette.Gray1 +) +private val WisefySampleLightColors = lightColors( + primary = WisefySampleColorPalette.Primary, + primaryVariant = WisefySampleColorPalette.PrimaryDark, + onPrimary = WisefySampleColorPalette.Gray1, + secondary = WisefySampleColorPalette.Secondary, + secondaryVariant = WisefySampleColorPalette.SecondaryLight, + onSecondary = WisefySampleColorPalette.Gray10, + background = WisefySampleColorPalette.Gray1, + onBackground = WisefySampleColorPalette.Gray10, + surface = WisefySampleColorPalette.Gray3, + onSurface = WisefySampleColorPalette.Gray9, + error = WisefySampleColorPalette.Error, + onError = WisefySampleColorPalette.Gray1 +) + +@Composable +internal fun WisefySampleTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + MaterialTheme( + colors = if (darkTheme) WisefySampleDarkColors else WisefySampleLightColors, + content = content, + typography = WisefySampleTypography + ) +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/util/ErrorMessages.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/util/ErrorMessages.kt new file mode 100644 index 00000000..154fd0ef --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/util/ErrorMessages.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.util + +internal object ErrorMessages { + + object AddNetwork { + const val WPA3_NETWORK_ADD_ON_PRE_ANDROID_Q_DEVICE = "Adding a WPA3 network is not supported until Android Q" + + " / SDK 29" + } +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/util/SdkUtil.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/util/SdkUtil.kt new file mode 100644 index 00000000..f824364f --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/util/SdkUtil.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.util + +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast +import javax.inject.Inject + +internal interface SdkUtil { + + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q) + fun isAtLeastQ(): Boolean + + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R) + fun isAtLeastR(): Boolean +} + +internal class DefaultSdkUtil @Inject constructor() : SdkUtil { + + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q) + override fun isAtLeastQ() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R) + override fun isAtLeastR() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/util/VerificationUtil.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/util/VerificationUtil.kt new file mode 100644 index 00000000..a3fd8c0d --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/util/VerificationUtil.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.util + +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + +private const val MIN_SSID_LENGTH = 2 +private const val MAX_SSID_LENGTH = 32 + +private const val MIN_PASSPHRASE_LENGTH = 8 +private const val MAX_PASSPHRASE_LENGTH = 63 + +internal fun String.validateSSID(): SSIDInputError { + val unicodeEncoder = StandardCharsets.UTF_8.newEncoder() + return when { + isBlank() -> SSIDInputError.EMPTY + length < MIN_SSID_LENGTH -> SSIDInputError.TOO_SHORT + length > MAX_SSID_LENGTH -> SSIDInputError.TOO_LONG + contains("?") || + contains("\"") || + contains("$") || + contains("[") || + contains("\\") || + contains("]") || + contains("+") -> { + SSIDInputError.INVALID_CHARACTERS + } + startsWith("!") || + startsWith("#") || + startsWith(";") -> { + SSIDInputError.INVALID_START_CHARACTERS + } + trimStart() != this || + trimEnd() != this -> { + SSIDInputError.LEADING_OR_TRAILING_SPACES + } + !unicodeEncoder.canEncode(this) -> SSIDInputError.NOT_VALID_UNICODE + else -> SSIDInputError.NONE + } +} + +internal fun String.validatePassphrase(): PassphraseInputError { + return when { + length < MIN_PASSPHRASE_LENGTH -> PassphraseInputError.TOO_SHORT + length > MAX_PASSPHRASE_LENGTH -> PassphraseInputError.TOO_LONG + !Charset.forName("US-ASCII").newEncoder().canEncode(this) -> PassphraseInputError.NOT_VALID_ASCII + else -> PassphraseInputError.NONE + } +} + +internal fun String.validateBSSID(): BSSIDInputError { + return when { + isBlank() -> BSSIDInputError.EMPTY + !matches(Regex("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$")) -> BSSIDInputError.INVALID + else -> BSSIDInputError.NONE + } +} + +internal enum class SSIDInputError { + NONE, + EMPTY, + TOO_SHORT, + TOO_LONG, + INVALID_CHARACTERS, + INVALID_START_CHARACTERS, + LEADING_OR_TRAILING_SPACES, + NOT_VALID_UNICODE +} + +internal enum class PassphraseInputError { + NONE, + TOO_SHORT, + TOO_LONG, + NOT_VALID_ASCII +} + +internal enum class BSSIDInputError { + NONE, + EMPTY, + INVALID +} diff --git a/app/src/main/java/com/isupatches/android/wisefy/sample/util/WisefyFactory.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/util/WisefyFactory.kt new file mode 100644 index 00000000..acfb9679 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/util/WisefyFactory.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Patches Barrett + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.isupatches.android.wisefy.sample.util + +import android.content.Context +import com.isupatches.android.wisefy.BuildConfig +import com.isupatches.android.wisefy.Wisefy +import com.isupatches.android.wisefy.WisefyApi +import com.isupatches.android.wisefy.sample.logging.WisefySampleLogger + +internal fun createWisefy(context: Context): WisefyApi { + return Wisefy.Brains( + context = context, + throwOnAssertions = BuildConfig.DEBUG, + logger = WisefySampleLogger + ).getSmarts() +} diff --git a/app/src/main/res/drawable/button_pressed.xml b/app/src/main/res/drawable/button_pressed.xml deleted file mode 100644 index b294c566..00000000 --- a/app/src/main/res/drawable/button_pressed.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/button_selector.xml b/app/src/main/res/drawable/button_selector.xml deleted file mode 100644 index 5e142055..00000000 --- a/app/src/main/res/drawable/button_selector.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/button_unpressed.xml b/app/src/main/res/drawable/button_unpressed.xml deleted file mode 100644 index 48a60bf6..00000000 --- a/app/src/main/res/drawable/button_unpressed.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 7c1db8e7..ba8c5ce9 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,3 +1,4 @@ + - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/dialog_base.xml b/app/src/main/res/layout/dialog_base.xml deleted file mode 100644 index e923f0c0..00000000 --- a/app/src/main/res/layout/dialog_base.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - -