diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 33e4fc47..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,84 +0,0 @@ -version: 2.1 - -orbs: - codecov: codecov/codecov@1.0.5 - -jobs: - ci_checks: - working_directory: ~/code - docker: - - image: circleci/android:api-29 - environment: - JVM_OPTS: -Xmx3200m - steps: - - checkout - - restore_cache: - key: jars-{{ checksum "build.gradle" }}-{{ checksum "wisefy/build.gradle" }}-{{ checksum "wisefysample/build.gradle" }} - - run: - name: Chmod permissions - command: sudo chmod +x ./gradlew - - run: - name: Download dependencies - command: ./gradlew androidDependencies - - run: - name: Clean build - command: ./gradlew clean assembleDebug wisefy:assembleDebugAndroidTest wisefysample:assembleDebugAndroidTest --stacktrace - - run: - name: Static code analysis - command: ./gradlew staticAnalysisSanityCheck - - run: - name: Unit tests - command: ./gradlew :wisefy:jacocoDebugUnitTest :wisefysample:jacocoDebugUnitTest --stacktrace - - run: - name: Store Service Account - command: echo $GCLOUD_SERVICE_KEY > ${HOME}/gcloud-service-key.json - - run: - name: Firebase Testing - command: | - sudo pip install -U crcmod - sudo gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json - sudo gcloud --quiet config set project ${GOOGLE_PROJECT_ID} - sudo gcloud firebase test android run \ - --type instrumentation \ - --app wisefysample/build/outputs/apk/debug/wisefysample-debug.apk \ - --test wisefy/build/outputs/apk/androidTest/debug/wisefy-debug-androidTest.apk \ - --device model=Pixel2,version=28,locale=en,orientation=portrait \ - --device model=Nexus6,version=23,locale=en,orientation=portrait \ - --device model=Nexus5,version=19,locale=en,orientation=portrait \ - --environment-variables coverage=true,coverageFile="/sdcard/coverage.ec" \ - --directories-to-pull=/sdcard \ - --results-dir=${CIRCLE_BRANCH}_${CIRCLE_BUILD_NUM} - sudo gsutil -m cp -r -U gs://${GCLOUD_BUCKET_LOCATION}/${CIRCLE_BRANCH}_${CIRCLE_BUILD_NUM}/Nexus5-19-en-portrait/artifacts/coverage.ec wisefy/build/outputs/code-coverage/connected/wisefy-Nexus5-sdk19-coverage.ec - sudo gsutil -m cp -r -U gs://${GCLOUD_BUCKET_LOCATION}/${CIRCLE_BRANCH}_${CIRCLE_BUILD_NUM}/Nexus6-23-en-portrait/artifacts/coverage.ec wisefy/build/outputs/code-coverage/connected/wisefy-Nexus6-sdk23-coverage.ec - sudo gsutil -m cp -r -U gs://${GCLOUD_BUCKET_LOCATION}/${CIRCLE_BRANCH}_${CIRCLE_BUILD_NUM}/Pixel2-28-en-portrait/artifacts/coverage.ec wisefy/build/outputs/code-coverage/connected/wisefy-Pixel2-sdk28-coverage.ec - sudo gcloud firebase test android run \ - --type instrumentation \ - --app wisefysample/build/outputs/apk/debug/wisefysample-debug.apk \ - --test wisefysample/build/outputs/apk/androidTest/debug/wisefysample-debug-androidTest.apk \ - --device model=Pixel2,version=28,locale=en,orientation=portrait \ - --environment-variables coverage=true,coverageFile="/sdcard/coverage.ec" \ - --directories-to-pull=/sdcard \ - --results-dir=${CIRCLE_BRANCH}_${CIRCLE_BUILD_NUM} - sudo gsutil -m cp -r -U gs://${GCLOUD_BUCKET_LOCATION}/${CIRCLE_BRANCH}_${CIRCLE_BUILD_NUM}/Pixel2-28-en-portrait/artifacts/coverage.ec wisefysample/build/outputs/code-coverage/connected/wisefysample-coverage.ec - sudo gsutil rm -r gs://${GCLOUD_BUCKET_LOCATION}/${CIRCLE_BRANCH}_${CIRCLE_BUILD_NUM} - - run: - name: Generate coverage report - command: ./gradlew :wisefy:jacocoDebugCombinedTestReport :wisefysample:jacocoDebugCombinedTestReport --stacktrace - - codecov/upload: - file: /reports/jacoco/*.xml - - store_artifacts: - path: wisefy/build/reports - destination: reports/wisefy - - store_artifacts: - path: wisefysample/build/reports - destination: reports/wisefysample - - save_cache: - key: jars-{{ checksum "build.gradle" }}-{{ checksum "wisefy/build.gradle" }}-{{ checksum "wisefysample/build.gradle" }} - paths: - - ~/.gradle - -workflows: - version: 2 - workflow: - jobs: - - ci_checks \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..8ef8f371 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.{kt,kts}] +max_line_length = 120 +ij_continuation_indent_size = 4 diff --git a/.gitignore b/.gitignore index b9341982..051ba5a8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /.idea/caches/* /.idea/libraries/* /.idea/codestyles/* +keystore.properties diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 00000000..7ff90dfe --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +android-wisefy \ No newline at end of file diff --git a/CHANGE.md b/CHANGE.md deleted file mode 100644 index dec0bce3..00000000 --- a/CHANGE.md +++ /dev/null @@ -1,11 +0,0 @@ -#### Change Logs - -WiseFy upon each new major release will start a brand new change log. - -[1.x Changes](/changes/1.x.md) - -[2.x Changes](/changes/2.x.md) - -[3.x Changes](/changes/3.x.md) - -[3.x Changes](/changes/4.x.md) \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/README.md b/README.md index c083e5c4..945ae1f8 100644 --- a/README.md +++ b/README.md @@ -1,177 +1,57 @@ - - -Wifi configuration and util library built for Android. - ->
*Developed by Patches 04/24/2016 - present*
->
Logo/icon created by mansya (2018)
->
Supports Android SDK levels 16-28

- -[![Build Status](https://travis-ci.org/isuPatches/WiseFy.svg?branch=master)](https://travis-ci.org/isuPatches/WiseFy) [ ![Download](https://api.bintray.com/packages/isupatches/Maven/wisefy/images/download.svg) ](https://bintray.com/isupatches/Maven/wisefy/_latestVersion) [![codecov](https://codecov.io/gh/isuPatches/WiseFy/branch/4.x/graph/badge.svg)](https://codecov.io/gh/isuPatches/WiseFy) - -[![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) - -## What's New in 4.x - -- Android P Support - - New isDeviceRoaming logic -- New logic for SDK 23 and above - - WiseFySearch rewritten to remove deprecated APIs from Android OS - - WiseFyConnection rewritten to remove deprecated APIs from Android OS - - Ability to use legacy search logic - - Ability to use legacy connection logic -- Update to Android X -- Improved annotations for required permissions -- Update to gradle 5.x -- Static analysis tools updated -- WEP is now deprecated due to security and other issues with this network type and will be phased out -- Better naming for some saved network functions - - GetSavedNetworkCallbacks renamed SearchForSavedNetworkCallbacks - - Added SearchForSavedNetworksCallbacks - - getSavedNetwork(regex: String?): List? refactored to searchForSavedNetwork(regexForSSID: String?): WifiConfiguration? - - getSavedNetwork(regexForSSID: String?, callbacks: GetSavedNetworkCallbacks?) refactored to searchForSavedNetwork(regexForSSID: String?, callbacks: SearchForSavedNetworkCallbacks?) - - getSavedNetworks(regexForSSID: String?, callbacks: GetSavedNetworksCallbacks?) refactored to searchForSavedNetworks(regexForSSID: String?, callbacks: SearchForSavedNetworksCallbacks?) -- Moved from TravisCI to CircleCI -- Instrumentation tests are now run on Google's Firebase TestLab -- Removal of Checkstyle and FindBugs since project is no longer Java -- Removal of GCM support due to GCM being sunset -- New [sample app](/wisefysample) included as part of the repo - - This replaces the previous permissions example -- Crash fixes for: - - Async api with null current network - - Async api with null current network info - - Async api with null nearby access points -- Fix for searching when empty list returned from OS -- More tests -- Removal of some generic variable names from documentation for clarity - -Previous updates: -- [What's New in 3.x](/changes/whatsnew/3.x.md) -- [What's New in 2.x](/changes/whatsnew/2.x.md) - -## Adding to your project - -Make sure you have one of the following repositories accessible: - -```groovy - repositories { - jcenter() - } -``` - -```groovy - repositories { - mavenCentral() - } -``` - -```groovy - repositories { - maven { - url "http://dl.bintray.com/isupatches/Maven" - } - } -``` - -Then add it as a dependency (please see https://github.com/isuPatches/WiseFy/releases for the latest version): - -For Gradle: - -```groovy - implementation 'com.isupatches:wisefy:' -``` - -For Maven: - -```xml - - com.isupatches - wisefy - LATEST_VERSION - pom - -``` - -You may also download the @aar from the releases page and import it into your project manually. - -## Getting An Instance - -WiseFy is constructed with the builder pattern that allows you access to the synchronous and asynchronous APIs. - -*NOTE* The context passed in must be non-null. - -To grab a default instance: - -_With Kotlin_ +## Wisefy -```kotlin -val wisefy = WiseFy.Brains(activity!!).getSmarts() -``` - -_With Java_ - -```java -WiseFy wisefy = new WiseFy.Brains(getActivity()).getSmarts(); -``` +## Installation -To grab an instance with logging enabled: - -_With Kotlin_ +There is a new package for 5.0. Please use: ```kotlin -val wisefy = WiseFy.Brains(activity!!).logging(true).getSmarts() -``` - -_With Java_ - -```java -WiseFy wisefy = new WiseFy.Brains(getActivity()).logging(true).getSmarts(); +implementation("com.isupatches.android:wisefy:5.0.0-RC1") ``` -By default, legacy logic is disabled on devices with SDK 23 or higher. If you want to use or test against the legacy search or connection logic, please see: [Using Legacy Classes And Logic](/documentation/using_legacy_classes_and_logic.md). +## 5.0 Rewrite -## Cleanup +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. -Since the Async API of WiseFy is run on a background thread, it is necessary to make sure it is exited and cleanup up properly. +I hope you enjoy the rewrite and please create an issue if you see anything odd or have questions! -To stop the WiseFy thread and nullify it along with it's handler please call: +### Highlights -_With Kotlin_ +- Android P, Android Q, and Android R are now supported +- 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 + * 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 +- Updated names for callbacks +- Kotlin first mentality (but willing to support Java as first class too!) +- WPA3 networks now supported +- wisefysample renamed to app -```kotlin -wisefy.dump() -``` - -_With Java_ - -```java -wisefy.dump(); -``` +### Work Remaining Before Official Release -## Permissions +- Documentation added +- Unit and instrumentation tests added back -For the sake of transparency and because you're probably curious as to what permissions this library adds to your app, here are the additional expected permissions: - -```xml - - - - - - -``` +### New Structure - * NOTE * +- WiseFy API -> WiseFy -> Internal util -> Delegate -> OS API and OS API implementation -If access points or SSIDs are not being returned on >= 6.x devices but there are visible networks, it's most likely because you haven't asked in your application for the `Manifest.permission.ACCESS_FINE_LOCATION` permission which is what allows us to see the access points nearby. This permission request will not be added to the WiseFy library to reduce package bloat and so users can determine their own UI/UX. +### Deprecations -Please check [the sample app](/wisefysample) for an example of how to request permissions for WiseFy. +- disableWifi() and disableWifi(callbacks: DisableWifiCallbacks?) +- enableWifi() and enableWifi(callbacks: EnableWifiCallbacks?) +- calculateBars(rssiLevel: Int, targetNumberOfBars: Int) -## Usage +### Known Android Q Problems -Please check [the documentation markdown directory](/documentation) for usage examples and details about both the synchronous and asynchronous API. +- 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 ## License ## -Copyright 2019 Patches Klinefelter +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 diff --git a/wisefysample/.gitignore b/app/.gitignore similarity index 100% rename from wisefysample/.gitignore rename to app/.gitignore diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..9b2e9513 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,126 @@ +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.dagger +import com.isupatches.android.wisefy.build.debug +import com.isupatches.android.wisefy.build.navigation +import com.isupatches.android.wisefy.build.release +import java.util.Properties + +plugins { + id("com.android.application") + id("kotlin-android") + id("kotlin-kapt") +} + +val keystoreProperties: Properties = Properties() +val keystoreFile: File = rootProject.file("keystore.properties") +if (keystoreFile.exists()) { + keystoreFile.inputStream().use { keystoreProperties.load(it) } +} + +android { + compileSdkVersion(BuildVersions.COMPILE_SDK) + buildToolsVersion = BuildVersions.BUILD_TOOLS + + defaultConfig { + applicationId = "com.isupatches.android.wisefy.sample" + + minSdk = BuildVersions.MIN_SDK + targetSdk = BuildVersions.TARGET_SDK + + versionCode = BuildVersions.MODULE_VERSION_CODE + versionName = BuildVersions.MODULE_VERSION_NAME + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + signingConfigs { + getByName("debug") { + storeFile = file(keystoreProperties.getProperty("app.debug.keystore_location")) + keyAlias = keystoreProperties.getProperty("app.debug.key_alias") + storePassword = System.getenv("APP_DEBUG_PASSWORD") ?: keystoreProperties.getProperty("app.debug.password") + keyPassword = System.getenv("APP_DEBUG_PASSWORD") ?: keystoreProperties.getProperty("app.debug.password") + } + + create("release") { + storeFile = file(keystoreProperties.getProperty("app.release.keystore_location")) + keyAlias = keystoreProperties.getProperty("app.release.key_alias") + storePassword = keystoreProperties.getProperty("app.release.password") + keyPassword = keystoreProperties.getProperty("app.release.password") + } + } + + buildTypes { + debug { + applicationIdSuffix = ".debug" + isTestCoverageEnabled = true + isMinifyEnabled = false + isShrinkResources = false + proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules-debug.pro") + testProguardFile(file("proguard-rules-test.pro")) + signingConfig = signingConfigs.getByName("debug") + } + + release { + isTestCoverageEnabled = false + isMinifyEnabled = true + isShrinkResources = true + proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules-release.pro") + signingConfig = signingConfigs.getByName("release") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "${JavaVersion.VERSION_1_8}" + } + + buildFeatures { + viewBinding = true + } + + jacoco { + version = Versions.JACOCO + } + + lintOptions { + isCheckAllWarnings = true + isShowAll = true + isExplainIssues = true + isAbortOnError = true + isWarningsAsErrors = true + disable("UnusedIds") + } +} + +dependencies { + /* + * Toggle these to test release binary vs. source code + */ + implementation(project(":wisefy")) +// implementation("com.isupatches.android:wisefy:5.0.0-RC1") { +// isChanging = true +// } + + implementation(Dependencies.VIEWGLU) + + // AndroidX + implementation(Dependencies.AndroidX.APPCOMPAT) + implementation(Dependencies.AndroidX.CONSTRAINT_LAYOUT) + implementation(Dependencies.AndroidX.CORE_KTX) + navigation() + + // Koltin + implementation(Dependencies.Kotlin.STD_LIB) + + // Google + implementation(Dependencies.Google.MATERIAL) + + // Dependency Injection + dagger() +} diff --git a/app/proguard-rules-common.pro b/app/proguard-rules-common.pro new file mode 100644 index 00000000..b7062e79 --- /dev/null +++ b/app/proguard-rules-common.pro @@ -0,0 +1,15 @@ +-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 new file mode 100644 index 00000000..5ca67fd6 --- /dev/null +++ b/app/proguard-rules-debug.pro @@ -0,0 +1,7 @@ +-include proguard-rules-common.pro + +-dontobfuscate + +-keepattributes SourceFile, LineNumberTable + +-keep class com.isupatches.android.wisefy.sample.DebugMainApplication { *; } diff --git a/wisefysample/proguard-rules-release.pro b/app/proguard-rules-release.pro similarity index 100% rename from wisefysample/proguard-rules-release.pro rename to app/proguard-rules-release.pro diff --git a/wisefysample/proguard-test-rules.pro b/app/proguard-test-rules.pro similarity index 100% rename from wisefysample/proguard-test-rules.pro rename to app/proguard-test-rules.pro diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..809412e8 --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/wisefysample/src/debug/java/com/isupatches/wisefysample/DebugMainApplication.kt b/app/src/debug/java/com/isupatches/android/wisefy/sample/DebugMainApplication.kt similarity index 83% rename from wisefysample/src/debug/java/com/isupatches/wisefysample/DebugMainApplication.kt rename to app/src/debug/java/com/isupatches/android/wisefy/sample/DebugMainApplication.kt index fbf18130..a866892d 100644 --- a/wisefysample/src/debug/java/com/isupatches/wisefysample/DebugMainApplication.kt +++ b/app/src/debug/java/com/isupatches/android/wisefy/sample/DebugMainApplication.kt @@ -1,4 +1,4 @@ -package com.isupatches.wisefysample +package com.isupatches.android.wisefy.sample internal open class DebugMainApplication : MainApplication() { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..6c5f905c --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 00000000..d06f4724 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/MainApplication.kt @@ -0,0 +1,70 @@ +/* + * 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 + +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 + +@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 + } + } +} 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 new file mode 100644 index 00000000..a9470c52 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/base/BaseActivity.kt @@ -0,0 +1,30 @@ +/* + * 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 new file mode 100644 index 00000000..a5f6f644 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/base/BaseDialogFragment.kt @@ -0,0 +1,33 @@ +/* + * 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 new file mode 100644 index 00000000..689da0cf --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/base/BaseFragment.kt @@ -0,0 +1,113 @@ +/* + * 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 new file mode 100644 index 00000000..f08bd4bb --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/di/ScreenBindingsModule.kt @@ -0,0 +1,34 @@ +/* + * 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/wisefysample/src/main/java/com/isupatches/wisefysample/internal/models/NetworkType.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/entities/NetworkType.kt similarity index 85% rename from wisefysample/src/main/java/com/isupatches/wisefysample/internal/models/NetworkType.kt rename to app/src/main/java/com/isupatches/android/wisefy/sample/internal/entities/NetworkType.kt index 068509f4..b7351744 100644 --- a/wisefysample/src/main/java/com/isupatches/wisefysample/internal/models/NetworkType.kt +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/entities/NetworkType.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Patches Klinefelter + * 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. @@ -13,20 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.isupatches.wisefysample.internal.models +package com.isupatches.android.wisefy.sample.internal.entities internal enum class NetworkType(val intVal: Int) { OPEN(0), - WEP(1), - WPA2(2); + WPA2(1), + WPA3(2); companion object { fun of(intVal: Int): NetworkType { return when (intVal) { OPEN.intVal -> OPEN - WEP.intVal -> WEP WPA2.intVal -> WPA2 + WPA3.intVal -> WPA3 else -> throw IllegalArgumentException("Invalid NetworkType, intVal: $intVal") } } diff --git a/wisefysample/src/main/java/com/isupatches/wisefysample/internal/models/SearchType.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/entities/SearchType.kt similarity index 91% rename from wisefysample/src/main/java/com/isupatches/wisefysample/internal/models/SearchType.kt rename to app/src/main/java/com/isupatches/android/wisefy/sample/internal/entities/SearchType.kt index 1281e84a..f4e933d0 100644 --- a/wisefysample/src/main/java/com/isupatches/wisefysample/internal/models/SearchType.kt +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/entities/SearchType.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Patches Klinefelter + * 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.isupatches.wisefysample.internal.models +package com.isupatches.android.wisefy.sample.internal.entities internal enum class SearchType(val intVal: Int) { ACCESS_POINT(0), 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/internal/logging/WisefySampleLogger.kt new file mode 100644 index 00000000..d6b66e0c --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/logging/WisefySampleLogger.kt @@ -0,0 +1,90 @@ +/* + * 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.logging + +import android.util.Log +import com.isupatches.android.wisefy.logging.WisefyLogger +import com.isupatches.android.wisefy.sample.BuildConfig +import java.util.Locale + +private const val LOG_TAG = "WisefySample" + +internal object WisefySampleLogger : WisefyLogger { + + override fun i(tag: String, message: String, vararg args: Any): Int { + if (BuildConfig.DEBUG) { + return Log.i(LOG_TAG, createMessage(tag, message, *args)) + } + return 0 + } + + override fun v(tag: String, message: String, vararg args: Any): Int { + if (BuildConfig.DEBUG) { + return Log.v(LOG_TAG, createMessage(tag, message, *args)) + } + return 0 + } + + override fun d(tag: String, message: String, vararg args: Any): Int { + if (BuildConfig.DEBUG) { + return Log.d(LOG_TAG, createMessage(tag, message, *args)) + } + return 0 + } + + override fun w(tag: String, message: String, vararg args: Any): Int { + if (BuildConfig.DEBUG) { + return Log.w(LOG_TAG, createMessage(tag, message, *args)) + } + return 0 + } + + override fun e(tag: String, message: String, vararg args: Any): Int { + if (BuildConfig.DEBUG) { + return Log.e(LOG_TAG, createMessage(tag, message, *args)) + } + return 0 + } + + override fun e(tag: String, throwable: Throwable, message: String, vararg args: Any): Int { + if (BuildConfig.DEBUG) { + return Log.e(LOG_TAG, createMessage(tag, message, *args), throwable) + } + return 0 + } + + override fun wtf(tag: String, message: String, vararg args: Any): Int { + if (BuildConfig.DEBUG) { + return Log.wtf(LOG_TAG, createMessage(tag, message, *args)) + } + return 0 + } + + override fun wtf(tag: String, throwable: Throwable, message: String, vararg args: Any): Int { + if (BuildConfig.DEBUG) { + Log.wtf(LOG_TAG, createMessage(tag, message, *args), throwable) + } + return 0 + } + + /* + * Private Helpers + */ + + private fun createMessage(tag: String, message: String, vararg args: Any): String { + return "$tag - ${message.format(Locale.US, *args)}" + } +} 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 new file mode 100644 index 00000000..b7b62b59 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BaseModel.kt @@ -0,0 +1,20 @@ +/* + * 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 new file mode 100644 index 00000000..7b4cedea --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BasePresenter.kt @@ -0,0 +1,62 @@ +/* + * 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 new file mode 100644 index 00000000..92183a6e --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BaseStore.kt @@ -0,0 +1,42 @@ +/* + * 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 new file mode 100644 index 00000000..1445c678 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/scaffolding/BaseView.kt @@ -0,0 +1,24 @@ +/* + * 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 new file mode 100644 index 00000000..9e7eafdd --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/BundleUtil.kt @@ -0,0 +1,24 @@ +/* + * 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/wisefysample/src/main/java/com/isupatches/wisefysample/internal/util/EditTextUtil.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/EditTextUtil.kt similarity index 87% rename from wisefysample/src/main/java/com/isupatches/wisefysample/internal/util/EditTextUtil.kt rename to app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/EditTextUtil.kt index bd8542c6..368138d8 100644 --- a/wisefysample/src/main/java/com/isupatches/wisefysample/internal/util/EditTextUtil.kt +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/EditTextUtil.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Patches Klinefelter + * 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.isupatches.wisefysample.internal.util +package com.isupatches.android.wisefy.sample.internal.util import android.widget.EditText diff --git a/wisefysample/src/main/java/com/isupatches/wisefysample/internal/util/HtmlUtil.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/HtmlUtil.kt similarity index 78% rename from wisefysample/src/main/java/com/isupatches/wisefysample/internal/util/HtmlUtil.kt rename to app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/HtmlUtil.kt index 274417fc..09930bd5 100644 --- a/wisefysample/src/main/java/com/isupatches/wisefysample/internal/util/HtmlUtil.kt +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/HtmlUtil.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Patches Klinefelter + * 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. @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.isupatches.wisefysample.internal.util +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 = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { +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/wisefysample/src/main/java/com/isupatches/wisefysample/internal/util/KeyboardUtil.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/KeyboardUtil.kt similarity index 84% rename from wisefysample/src/main/java/com/isupatches/wisefysample/internal/util/KeyboardUtil.kt rename to app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/KeyboardUtil.kt index a5d26d03..3ca8f3f3 100644 --- a/wisefysample/src/main/java/com/isupatches/wisefysample/internal/util/KeyboardUtil.kt +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/KeyboardUtil.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Patches Klinefelter + * 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. @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.isupatches.wisefysample.internal.util +package com.isupatches.android.wisefy.sample.internal.util import android.app.Activity import android.view.View import android.view.inputmethod.InputMethodManager -import com.isupatches.wisefysample.internal.base.BaseFragment +import com.isupatches.android.wisefy.sample.internal.base.BaseFragment internal fun BaseFragment.hideKeyboardFrom(view: View) { activity?.let { diff --git a/wisefysample/src/main/java/com/isupatches/wisefysample/internal/util/PermissionsUtil.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/PermissionsUtil.kt similarity index 91% rename from wisefysample/src/main/java/com/isupatches/wisefysample/internal/util/PermissionsUtil.kt rename to app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/PermissionsUtil.kt index ddd38f97..aa034689 100644 --- a/wisefysample/src/main/java/com/isupatches/wisefysample/internal/util/PermissionsUtil.kt +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/PermissionsUtil.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Patches Klinefelter + * 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.isupatches.wisefysample.internal.util +package com.isupatches.android.wisefy.sample.internal.util import android.content.Context import android.content.pm.PackageManager 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 new file mode 100644 index 00000000..2e896cff --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/SdkUtil.kt @@ -0,0 +1,29 @@ +/* + * 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 new file mode 100644 index 00000000..7698aaaa --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/internal/util/WiseFyFactory.kt @@ -0,0 +1,25 @@ +/* + * 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/ui/add/AddNetworkFragment.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkFragment.kt new file mode 100644 index 00000000..68c963fe --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkFragment.kt @@ -0,0 +1,298 @@ +/* + * 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 new file mode 100644 index 00000000..90b9d04a --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkModel.kt @@ -0,0 +1,178 @@ +/* + * 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.OpenNetworkData +import com.isupatches.android.wisefy.addnetwork.entities.WPA2NetworkData +import com.isupatches.android.wisefy.addnetwork.entities.WPA3NetworkData +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( + data = OpenNetworkData.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( + data = OpenNetworkData.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( + data = WPA2NetworkData.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( + data = WPA2NetworkData.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( + data = WPA3NetworkData.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( + data = WPA3NetworkData.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 new file mode 100644 index 00000000..b2b7f8a0 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkModule.kt @@ -0,0 +1,40 @@ +/* + * 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 new file mode 100644 index 00000000..958b7eec --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkPresenter.kt @@ -0,0 +1,164 @@ +/* + * 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/wisefysample/src/main/java/com/isupatches/wisefysample/internal/preferences/AddNetworkStore.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkStore.kt similarity index 78% rename from wisefysample/src/main/java/com/isupatches/wisefysample/internal/preferences/AddNetworkStore.kt rename to app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkStore.kt index 1e42e575..3366ddd3 100644 --- a/wisefysample/src/main/java/com/isupatches/wisefysample/internal/preferences/AddNetworkStore.kt +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/add/AddNetworkStore.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Patches Klinefelter + * 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. @@ -13,12 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.isupatches.wisefysample.internal.preferences +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.wisefysample.internal.models.NetworkType +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" @@ -36,9 +40,15 @@ internal interface AddNetworkStore { fun setLastUsedNetworkPassword(lastUsedNetworkPassword: String) } -internal class SharedPreferencesAddNetworkStore( - private val sharedPreferences: SharedPreferences -) : AddNetworkStore { +@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() } 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 new file mode 100644 index 00000000..cb96adc4 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/dialogs/BaseNoticeDialogFragment.kt @@ -0,0 +1,30 @@ +/* + * 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 new file mode 100644 index 00000000..ef2160ec --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/dialogs/FullScreenNoticeDialogFragment.kt @@ -0,0 +1,71 @@ +/* + * 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 new file mode 100644 index 00000000..79a30e0c --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/dialogs/NoticeDialogFragment.kt @@ -0,0 +1,62 @@ +/* + * 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 new file mode 100644 index 00000000..54c03544 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/main/MainActivity.kt @@ -0,0 +1,113 @@ +/* + * 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 new file mode 100644 index 00000000..55a4f39f --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/main/MainFragment.kt @@ -0,0 +1,38 @@ +/* + * 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 new file mode 100644 index 00000000..8c861ee4 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscFragment.kt @@ -0,0 +1,296 @@ +/* + * 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.networkinfo.entities.CurrentNetworkData +import com.isupatches.android.wisefy.networkinfo.entities.CurrentNetworkInfoData +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: Int) + fun displayFailureRetrievingFrequency() + fun displayIP(ip: String) + 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: Int) { + displayInfo(getString(R.string.frequency_args, frequency), R.string.wisefy_action_result) + } + + override fun displayFailureRetrievingFrequency() { + displayInfo(R.string.failure_retrieving_frequency, R.string.wisefy_action_result) + } + + override fun displayIP(ip: String) { + displayInfo(getString(R.string.ip_args, 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 new file mode 100644 index 00000000..4d8bd90f --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscModel.kt @@ -0,0 +1,100 @@ +/* + * 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.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(true, 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 new file mode 100644 index 00000000..1d19bed2 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscModule.kt @@ -0,0 +1,31 @@ +/* + * 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 new file mode 100644 index 00000000..0932a723 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/misc/MiscPresenter.kt @@ -0,0 +1,268 @@ +/* + * 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.networkinfo.entities.CurrentNetworkData +import com.isupatches.android.wisefy.networkinfo.entities.CurrentNetworkInfoData +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: Int) { + 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: String) { + 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/remove/RemoveNetworkFragment.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkFragment.kt new file mode 100644 index 00000000..9790f27b --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkFragment.kt @@ -0,0 +1,150 @@ +/* + * 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 new file mode 100644 index 00000000..0f4fc2bd --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkModel.kt @@ -0,0 +1,44 @@ +/* + * 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.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(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 new file mode 100644 index 00000000..aaf4e6fc --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkModule.kt @@ -0,0 +1,41 @@ +/* + * 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 new file mode 100644 index 00000000..28593ed2 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkPresenter.kt @@ -0,0 +1,73 @@ +/* + * 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 new file mode 100644 index 00000000..32bf3ae5 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/remove/RemoveNetworkStore.kt @@ -0,0 +1,55 @@ +/* + * 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 new file mode 100644 index 00000000..9a6168f4 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchFragment.kt @@ -0,0 +1,464 @@ +/* + * 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.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: String?) + 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: String?) { + 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 new file mode 100644 index 00000000..c8f06e52 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchModel.kt @@ -0,0 +1,152 @@ +/* + * 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.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 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( + regexForSSID = regexForSSID, + timeoutInMillis = timeoutInMillis, + filterDuplicates = filterDuplicates, + callbacks = callbacks + ) + } + + @RequiresPermission(ACCESS_FINE_LOCATION) + override fun searchForAccessPoints( + regexForSSID: String, + filterDuplicates: Boolean, + callbacks: SearchForAccessPointsCallbacks? + ) { + wisefy.searchForAccessPoints( + regexForSSID = regexForSSID, + filterDuplicates = filterDuplicates, + callbacks = callbacks + ) + } + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) + override fun searchForSavedNetwork( + regexForSSID: String, + callbacks: SearchForSavedNetworkCallbacks? + ) { + wisefy.searchForSavedNetwork( + regexForSSID = regexForSSID, + callbacks = callbacks + ) + } + + @RequiresPermission(allOf = [ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE]) + override fun searchForSavedNetworks( + regexForSSID: String, + callbacks: SearchForSavedNetworksCallbacks? + ) { + wisefy.searchForSavedNetworks( + regexForSSID = regexForSSID, + callbacks = callbacks + ) + } + + @RequiresPermission(ACCESS_FINE_LOCATION) + override fun searchForSSID( + regexForSSID: String, + timeoutInMillis: Int, + callbacks: SearchForSSIDCallbacks? + ) { + wisefy.searchForSSID( + regexForSSID = regexForSSID, + timeoutInMillis = timeoutInMillis, + callbacks = callbacks + ) + } + + @RequiresPermission(ACCESS_FINE_LOCATION) + override fun searchForSSIDs( + regexForSSID: String, + callbacks: SearchForSSIDsCallbacks? + ) { + wisefy.searchForSSIDs( + 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 new file mode 100644 index 00000000..aa9eeba6 --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchModule.kt @@ -0,0 +1,40 @@ +/* + * 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 new file mode 100644 index 00000000..4c62bc2d --- /dev/null +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchPresenter.kt @@ -0,0 +1,225 @@ +/* + * 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.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: String) { + 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/wisefysample/src/main/java/com/isupatches/wisefysample/internal/preferences/SearchStore.kt b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchStore.kt similarity index 82% rename from wisefysample/src/main/java/com/isupatches/wisefysample/internal/preferences/SearchStore.kt rename to app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchStore.kt index 82dff2de..1e013074 100644 --- a/wisefysample/src/main/java/com/isupatches/wisefysample/internal/preferences/SearchStore.kt +++ b/app/src/main/java/com/isupatches/android/wisefy/sample/ui/search/SearchStore.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Patches Klinefelter + * 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. @@ -13,12 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.isupatches.wisefysample.internal.preferences +package com.isupatches.android.wisefy.sample.ui.search -import android.content.SharedPreferences +import android.content.Context import androidx.annotation.VisibleForTesting import androidx.core.content.edit -import com.isupatches.wisefysample.internal.models.SearchType +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" @@ -41,9 +44,15 @@ internal interface SearchStore { fun setTimeout(timeout: Int) } -internal class SharedPreferencesSearchStore( - private val sharedPreferences: SharedPreferences -) : SearchStore { +@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() } diff --git a/wisefysample/src/main/res/drawable/button_pressed.xml b/app/src/main/res/drawable/button_pressed.xml similarity index 100% rename from wisefysample/src/main/res/drawable/button_pressed.xml rename to app/src/main/res/drawable/button_pressed.xml diff --git a/wisefysample/src/main/res/drawable/button_selector.xml b/app/src/main/res/drawable/button_selector.xml similarity index 100% rename from wisefysample/src/main/res/drawable/button_selector.xml rename to app/src/main/res/drawable/button_selector.xml diff --git a/wisefysample/src/main/res/drawable/button_unpressed.xml b/app/src/main/res/drawable/button_unpressed.xml similarity index 100% rename from wisefysample/src/main/res/drawable/button_unpressed.xml rename to app/src/main/res/drawable/button_unpressed.xml diff --git a/wisefysample/src/main/res/drawable/ic_add_circle.xml b/app/src/main/res/drawable/ic_add_circle.xml similarity index 100% rename from wisefysample/src/main/res/drawable/ic_add_circle.xml rename to app/src/main/res/drawable/ic_add_circle.xml diff --git a/wisefysample/src/main/res/drawable/ic_apps.xml b/app/src/main/res/drawable/ic_apps.xml similarity index 100% rename from wisefysample/src/main/res/drawable/ic_apps.xml rename to app/src/main/res/drawable/ic_apps.xml diff --git a/wisefysample/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml similarity index 100% rename from wisefysample/src/main/res/drawable/ic_home.xml rename to app/src/main/res/drawable/ic_home.xml diff --git a/wisefysample/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from wisefysample/src/main/res/drawable/ic_launcher_background.xml rename to app/src/main/res/drawable/ic_launcher_background.xml diff --git a/wisefysample/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from wisefysample/src/main/res/drawable/ic_launcher_foreground.xml rename to app/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/wisefysample/src/main/res/drawable/ic_logo.xml b/app/src/main/res/drawable/ic_logo.xml similarity index 100% rename from wisefysample/src/main/res/drawable/ic_logo.xml rename to app/src/main/res/drawable/ic_logo.xml diff --git a/wisefysample/src/main/res/drawable/ic_remove_circle.xml b/app/src/main/res/drawable/ic_remove_circle.xml similarity index 100% rename from wisefysample/src/main/res/drawable/ic_remove_circle.xml rename to app/src/main/res/drawable/ic_remove_circle.xml diff --git a/wisefysample/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml similarity index 100% rename from wisefysample/src/main/res/drawable/ic_search.xml rename to app/src/main/res/drawable/ic_search.xml diff --git a/wisefysample/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml similarity index 85% rename from wisefysample/src/main/res/layout/activity_main.xml rename to app/src/main/res/layout/activity_main.xml index 68cf1f08..86f627cb 100644 --- a/wisefysample/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -24,14 +24,18 @@ - + app:layout_constraintTop_toBottomOf="@id/toolbar" + /> + + + + + + + + + + + + + + + + + + + + + + + + + + +