From c5da2879be45f5caf272bc6f9aa25307fc046f3a Mon Sep 17 00:00:00 2001 From: Aidan Follestad Date: Sat, 18 Jan 2020 20:26:09 -0800 Subject: [PATCH] 3.0.0-RC1 --- README.md | 74 +++++++++++++++-- build.gradle | 4 +- {library => core}/.gitignore | 0 {library => core}/build.gradle | 10 +-- .../src/main/AndroidManifest.xml | 0 .../main/java/com/afollestad/assent/Assent.kt | 14 +--- .../com/afollestad/assent/AssentInActivity.kt | 1 + .../com/afollestad/assent/AssentInFragment.kt | 5 +- .../com/afollestad/assent/AssentResult.kt | 0 .../java/com/afollestad/assent/Permissions.kt | 8 +- .../com/afollestad/assent/internal/Assent.kt | 12 +-- .../afollestad/assent/internal/Extensions.kt | 2 +- .../afollestad/assent/internal/Lifecycle.kt | 0 .../com/afollestad/assent/internal/Logger.kt | 43 ++++++++++ .../assent/internal/PendingRequest.kt | 0 .../assent/internal/PermissionFragment.kt | 13 ++- .../com/afollestad/assent/internal/Queue.kt | 3 +- .../assent/rationale/ConfirmCallback.kt | 4 +- .../assent/rationale/RationaleHandler.kt | 23 ++--- .../assent/rationale/ShouldShowRationale.kt | 0 .../afollestad/assent/AssentInActivityTest.kt | 0 .../afollestad/assent/AssentInFragmentTest.kt | 0 .../assent/testutil/AssertableCallback.kt | 0 .../assent/testutil/MockResponseQueue.kt | 0 .../assent/testutil/NoManifestTestRunner.kt | 0 coroutines/.gitignore | 1 + coroutines/build.gradle | 18 ++++ coroutines/src/main/AndroidManifest.xml | 4 + .../coroutines/AssentCoroutinesInActivity.kt | 70 ++++++++++++++++ .../coroutines/AssentCoroutinesInFragment.kt | 68 +++++++++++++++ .../assent/coroutines/CheckOnMainThread.kt | 16 ++-- dependencies.gradle | 21 ++--- gradle/android_application_config.gradle | 8 ++ gradle/android_bintray_config.gradle | 48 +++++++++++ gradle/android_common_config.gradle | 36 ++++++++ gradle/android_library_config.gradle | 4 + gradle/spotless_plugin_config.gradle | 18 ++++ gradle/versions_plugin_config.gradle | 14 ++++ rationales/.gitignore | 1 + rationales/build.gradle | 17 ++++ rationales/src/main/AndroidManifest.xml | 4 + .../rationale/AlertDialogRationaleHandler.kt | 6 +- .../rationale/SnackBarRationaleHandler.kt | 6 +- .../assent/rationale/MockRequestResponder.kt | 0 .../rationale/MockShouldShowRationale.kt | 0 .../assent/rationale/RationaleHandlerTest.kt | 0 .../assent/testutil/AssertableCallback.kt | 42 ++++++++++ .../assent/testutil/MockResponseQueue.kt | 81 ++++++++++++++++++ sample/build.gradle | 9 +- sample/src/main/AndroidManifest.xml | 1 - .../afollestad/assentsample/MainActivity.kt | 83 ++++++++++++------- .../java/com/afollestad/assentsample/Util.kt | 42 ++++++++++ .../fragment/ExampleChildFragment.kt | 21 ++++- .../assentsample/fragment/ExampleFragment.kt | 4 +- sample/src/main/res/values/ids.xml | 4 + settings.gradle | 2 +- 56 files changed, 724 insertions(+), 141 deletions(-) rename {library => core}/.gitignore (100%) rename {library => core}/build.gradle (61%) rename {library => core}/src/main/AndroidManifest.xml (100%) rename {library => core}/src/main/java/com/afollestad/assent/Assent.kt (91%) rename {library => core}/src/main/java/com/afollestad/assent/AssentInActivity.kt (97%) rename {library => core}/src/main/java/com/afollestad/assent/AssentInFragment.kt (94%) rename {library => core}/src/main/java/com/afollestad/assent/AssentResult.kt (100%) rename {library => core}/src/main/java/com/afollestad/assent/Permissions.kt (92%) rename {library => core}/src/main/java/com/afollestad/assent/internal/Assent.kt (91%) rename {library => core}/src/main/java/com/afollestad/assent/internal/Extensions.kt (96%) rename {library => core}/src/main/java/com/afollestad/assent/internal/Lifecycle.kt (100%) create mode 100644 core/src/main/java/com/afollestad/assent/internal/Logger.kt rename {library => core}/src/main/java/com/afollestad/assent/internal/PendingRequest.kt (100%) rename {library => core}/src/main/java/com/afollestad/assent/internal/PermissionFragment.kt (93%) rename {library => core}/src/main/java/com/afollestad/assent/internal/Queue.kt (91%) rename {library => core}/src/main/java/com/afollestad/assent/rationale/ConfirmCallback.kt (87%) rename {library => core}/src/main/java/com/afollestad/assent/rationale/RationaleHandler.kt (88%) rename {library => core}/src/main/java/com/afollestad/assent/rationale/ShouldShowRationale.kt (100%) rename {library => core}/src/test/java/com/afollestad/assent/AssentInActivityTest.kt (100%) rename {library => core}/src/test/java/com/afollestad/assent/AssentInFragmentTest.kt (100%) rename {library => core}/src/test/java/com/afollestad/assent/testutil/AssertableCallback.kt (100%) rename {library => core}/src/test/java/com/afollestad/assent/testutil/MockResponseQueue.kt (100%) rename {library => core}/src/test/java/com/afollestad/assent/testutil/NoManifestTestRunner.kt (100%) create mode 100644 coroutines/.gitignore create mode 100644 coroutines/build.gradle create mode 100644 coroutines/src/main/AndroidManifest.xml create mode 100644 coroutines/src/main/java/com/afollestad/assent/coroutines/AssentCoroutinesInActivity.kt create mode 100644 coroutines/src/main/java/com/afollestad/assent/coroutines/AssentCoroutinesInFragment.kt rename sample/src/main/java/com/afollestad/assentsample/SampleApp.kt => coroutines/src/main/java/com/afollestad/assent/coroutines/CheckOnMainThread.kt (69%) create mode 100644 gradle/android_application_config.gradle create mode 100644 gradle/android_bintray_config.gradle create mode 100644 gradle/android_common_config.gradle create mode 100644 gradle/android_library_config.gradle create mode 100644 gradle/spotless_plugin_config.gradle create mode 100644 gradle/versions_plugin_config.gradle create mode 100644 rationales/.gitignore create mode 100644 rationales/build.gradle create mode 100644 rationales/src/main/AndroidManifest.xml rename {library => rationales}/src/main/java/com/afollestad/assent/rationale/AlertDialogRationaleHandler.kt (94%) rename {library => rationales}/src/main/java/com/afollestad/assent/rationale/SnackBarRationaleHandler.kt (93%) rename {library => rationales}/src/test/java/com/afollestad/assent/rationale/MockRequestResponder.kt (100%) rename {library => rationales}/src/test/java/com/afollestad/assent/rationale/MockShouldShowRationale.kt (100%) rename {library => rationales}/src/test/java/com/afollestad/assent/rationale/RationaleHandlerTest.kt (100%) create mode 100644 rationales/src/test/java/com/afollestad/assent/testutil/AssertableCallback.kt create mode 100644 rationales/src/test/java/com/afollestad/assent/testutil/MockResponseQueue.kt create mode 100644 sample/src/main/res/values/ids.xml diff --git a/README.md b/README.md index a397266..6686193 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Assent is designed to make Android's runtime permissions easier and take less co -[ ![jCenter](https://api.bintray.com/packages/drummer-aidan/maven/assent/images/download.svg) ](https://bintray.com/drummer-aidan/maven/assent/_latestVersion) +[ ![jCenter](https://api.bintray.com/packages/drummer-aidan/maven/assent%3Acore/images/download.svg) ](https://bintray.com/drummer-aidan/maven/assent%3Acore/_latestVersion) [![Build Status](https://travis-ci.org/afollestad/assent.svg)](https://travis-ci.org/afollestad/assent) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/f1a2334c4c0349699760391bb71f763e)](https://www.codacy.com/app/drummeraidan_50/assent?utm_source=github.com&utm_medium=referral&utm_content=afollestad/assent&utm_campaign=Badge_Grade) [![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg?style=flat-square)](https://www.apache.org/licenses/LICENSE-2.0.html) @@ -14,25 +14,26 @@ Assent is designed to make Android's runtime permissions easier and take less co 1. [Gradle Dependency](#gradle-dependency) 2. [The Basics](#the-basics) 3. [Using Results](#using-results) -4. [Under the Hood Extras](#under-the-hood-extras) +4. [Request Debouncing](#request-debouncing) 5. [Rationales](#rationales) +6. [Coroutines](#coroutines) --- -## Gradle Dependency +## Core Add this to your module's `build.gradle` file: ```gradle dependencies { - implementation 'com.afollestad:assent:2.3.1' + implementation 'com.afollestad.assent:core:3.0.0-RC1' } ``` --- -## The Basics +### The Basics Runtime permissions on Android are completely reliant on the UI the user is in. Permission requests go in and out of Activities and Fragments. This library provides its functionality as Kotlin @@ -71,7 +72,7 @@ both.** --- -## Using Results +### Using Results `AssentResult` is provided in request callbacks. It has a few useful fields and methods: @@ -93,7 +94,7 @@ val permissionDenied: Boolean = result.isAllDenied(WRITE_EXTERNAL_STORAGE) --- -## Under the Hood Extras +### Request Debouncing If you were to do this... @@ -119,6 +120,19 @@ askForPermissions(CALL_PHONE) { _ -> } ## Rationales +[ ![jCenter](https://api.bintray.com/packages/drummer-aidan/maven/assent%3Arationales/images/download.svg) ](https://bintray.com/drummer-aidan/maven/assent%3Arationales/_latestVersion) + +Add this to your module's `build.gradle` file: + +```gradle +dependencies { + + implementation 'com.afollestad.assent:rationales:3.0.0-RC1' +} +``` + +--- + Google recommends showing rationales for permissions when it may not be obvious to the user why you need them. @@ -142,3 +156,49 @@ askForPermissions( // Use result } ``` + +--- + +## Coroutines + +[ ![jCenter](https://api.bintray.com/packages/drummer-aidan/maven/assent%3Acoroutines/images/download.svg) ](https://bintray.com/drummer-aidan/maven/assent%3Acoroutines/_latestVersion) + +Add this to your module's `build.gradle` file: + +```gradle +dependencies { + + implementation 'com.afollestad.assent:coroutines:3.0.0-RC1' +} +``` + +--- + +Kotlin coroutines enable Assent to work without callbacks. If you do not know the basics of +coroutines, you should research them first. + +First, `awaitPermissionsResult(...)` is the coroutines equivalent to `askForPermissions(...)`: + +```kotlin +// Launch a coroutine in some scope... +launch { + val result: AssentResult = awaitPermissionsResult( + READ_CONTACTS, WRITE_EXTERNAL_STORAGE, READ_SMS, + rationaleHandler = rationaleHandler + ) + // Use the result... +} +``` + +And second, `awaitPermissionsGranted(...)` is the coroutines equivalent to `runWithPermissions(...)`: + +```kotlin +// Launch a coroutine in some scope... +launch { + waitPermissionsGranted( + READ_CONTACTS, WRITE_EXTERNAL_STORAGE, READ_SMS, + rationaleHandler = rationaleHandler + ) + // All three permissions were granted... +} +``` diff --git a/build.gradle b/build.gradle index f7504b9..a4a4f0f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,4 @@ apply from: rootProject.file("gradle/versions_plugin_config.gradle") -apply from: rootProject.file("gradle/nebula_lint_config.gradle") buildscript { apply from: rootProject.file("dependencies.gradle") @@ -13,7 +12,6 @@ buildscript { classpath deps.gradle_plugins.android classpath deps.gradle_plugins.bintray_release classpath deps.gradle_plugins.kotlin - classpath deps.gradle_plugins.nebula_lint classpath deps.gradle_plugins.spotless classpath deps.gradle_plugins.versions } @@ -29,7 +27,7 @@ allprojects { subprojects { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { - freeCompilerArgs += ['-module-name', project.path.replace('/', '.').replace(':', '_')] + freeCompilerArgs += ['-module-name', project.path.substring(1).replace(':', '')] } } } diff --git a/library/.gitignore b/core/.gitignore similarity index 100% rename from library/.gitignore rename to core/.gitignore diff --git a/library/build.gradle b/core/build.gradle similarity index 61% rename from library/build.gradle rename to core/build.gradle index f79e902..5fc9aac 100644 --- a/library/build.gradle +++ b/core/build.gradle @@ -1,17 +1,17 @@ -ext.module_group = "com.afollestad" -ext.module_name = "assent" +ext.module_group = "com.afollestad.assent" +ext.module_name = "core" apply from: rootProject.file("gradle/android_library_config.gradle") dependencies { - implementation deps.androidx.annotations - implementation deps.androidx.app_compat + api deps.androidx.app_compat + compileOnly deps.androidx.annotations implementation deps.kotlin.stdlib8 - implementation deps.debug.timber testImplementation deps.kotlin.test.mockito testImplementation deps.test.junit + testImplementation deps.test.mockito_inline testImplementation deps.test.robolectric testImplementation deps.test.truth } diff --git a/library/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml similarity index 100% rename from library/src/main/AndroidManifest.xml rename to core/src/main/AndroidManifest.xml diff --git a/library/src/main/java/com/afollestad/assent/Assent.kt b/core/src/main/java/com/afollestad/assent/Assent.kt similarity index 91% rename from library/src/main/java/com/afollestad/assent/Assent.kt rename to core/src/main/java/com/afollestad/assent/Assent.kt index 67b28ad..f2f1dfc 100644 --- a/library/src/main/java/com/afollestad/assent/Assent.kt +++ b/core/src/main/java/com/afollestad/assent/Assent.kt @@ -19,13 +19,12 @@ import android.content.Context import android.content.pm.PackageManager.PERMISSION_GRANTED import androidx.annotation.CheckResult import androidx.core.content.ContextCompat -import com.afollestad.assent.internal.Assent.Companion.LOCK import com.afollestad.assent.internal.Assent.Companion.get import com.afollestad.assent.internal.PendingRequest import com.afollestad.assent.internal.PermissionFragment import com.afollestad.assent.internal.equalsPermissions +import com.afollestad.assent.internal.log import com.afollestad.assent.rationale.RationaleHandler -import timber.log.Timber /** * Returns true if ALL given [permissions] have been granted. @@ -44,7 +43,7 @@ internal fun T.startPermissionRequest( requestCode: Int = 20, rationaleHandler: RationaleHandler? = null, callback: Callback -) = synchronized(LOCK) { +) { log("askForPermissions(${permissions.joinToString()})") if (rationaleHandler != null) { @@ -59,7 +58,7 @@ internal fun T.startPermissionRequest( // Request matches permissions, append a callback log("Callback appended to existing matching request") currentRequest.callbacks.add(callback) - return@synchronized + return } // Create a new pending request since none exist for these permissions @@ -83,10 +82,3 @@ internal fun T.startPermissionRequest( get().requestQueue += newPendingRequest } } - -internal fun Any.log(message: String) { - Timber.tag("Assent-${name()}") - Timber.d(message) -} - -private fun Any.name() = this::class.java.simpleName diff --git a/library/src/main/java/com/afollestad/assent/AssentInActivity.kt b/core/src/main/java/com/afollestad/assent/AssentInActivity.kt similarity index 97% rename from library/src/main/java/com/afollestad/assent/AssentInActivity.kt rename to core/src/main/java/com/afollestad/assent/AssentInActivity.kt index 7afeb45..ad1b55d 100644 --- a/library/src/main/java/com/afollestad/assent/AssentInActivity.kt +++ b/core/src/main/java/com/afollestad/assent/AssentInActivity.kt @@ -19,6 +19,7 @@ package com.afollestad.assent import android.app.Activity import com.afollestad.assent.internal.Assent.Companion.ensureFragment +import com.afollestad.assent.internal.log import com.afollestad.assent.rationale.RationaleHandler typealias Callback = (result: AssentResult) -> Unit diff --git a/library/src/main/java/com/afollestad/assent/AssentInFragment.kt b/core/src/main/java/com/afollestad/assent/AssentInFragment.kt similarity index 94% rename from library/src/main/java/com/afollestad/assent/AssentInFragment.kt rename to core/src/main/java/com/afollestad/assent/AssentInFragment.kt index a4a40e6..19d63e1 100644 --- a/library/src/main/java/com/afollestad/assent/AssentInFragment.kt +++ b/core/src/main/java/com/afollestad/assent/AssentInFragment.kt @@ -20,15 +20,14 @@ package com.afollestad.assent import androidx.annotation.CheckResult import androidx.fragment.app.Fragment import com.afollestad.assent.internal.Assent.Companion.ensureFragment +import com.afollestad.assent.internal.log import com.afollestad.assent.rationale.RationaleHandler /** * Returns true if ALL given [permissions] have been granted. */ @CheckResult fun Fragment.isAllGranted(vararg permissions: Permission) = - activity?.isAllGranted(*permissions) ?: throw IllegalStateException( - "Fragment's Activity is null." - ) + activity?.isAllGranted(*permissions) ?: error("Fragment's Activity is null.") /** * Performs a permission request, asking for all given [permissions], and diff --git a/library/src/main/java/com/afollestad/assent/AssentResult.kt b/core/src/main/java/com/afollestad/assent/AssentResult.kt similarity index 100% rename from library/src/main/java/com/afollestad/assent/AssentResult.kt rename to core/src/main/java/com/afollestad/assent/AssentResult.kt diff --git a/library/src/main/java/com/afollestad/assent/Permissions.kt b/core/src/main/java/com/afollestad/assent/Permissions.kt similarity index 92% rename from library/src/main/java/com/afollestad/assent/Permissions.kt rename to core/src/main/java/com/afollestad/assent/Permissions.kt index a6d7917..982ec4a 100644 --- a/library/src/main/java/com/afollestad/assent/Permissions.kt +++ b/core/src/main/java/com/afollestad/assent/Permissions.kt @@ -46,7 +46,6 @@ enum class Permission(val value: String) { WRITE_CALL_LOG(Manifest.permission.WRITE_CALL_LOG), ADD_VOICEMAIL(Manifest.permission.ADD_VOICEMAIL), USE_SIP(Manifest.permission.USE_SIP), - PROCESS_OUTGOING_CALLS(Manifest.permission.PROCESS_OUTGOING_CALLS), BODY_SENSORS(Manifest.permission.BODY_SENSORS), @@ -59,7 +58,12 @@ enum class Permission(val value: String) { READ_EXTERNAL_STORAGE(Manifest.permission.READ_EXTERNAL_STORAGE), WRITE_EXTERNAL_STORAGE(Manifest.permission.WRITE_EXTERNAL_STORAGE), - SYSTEM_ALERT_WINDOW(Manifest.permission.SYSTEM_ALERT_WINDOW); + SYSTEM_ALERT_WINDOW(Manifest.permission.SYSTEM_ALERT_WINDOW), + + /** @deprecated */ + @Suppress("DEPRECATION") + @Deprecated("Manifest.permission.PROCESS_OUTGOING_CALLS is deprecated.") + PROCESS_OUTGOING_CALLS(Manifest.permission.PROCESS_OUTGOING_CALLS); companion object { @JvmStatic fun parse(raw: String): Permission { diff --git a/library/src/main/java/com/afollestad/assent/internal/Assent.kt b/core/src/main/java/com/afollestad/assent/internal/Assent.kt similarity index 91% rename from library/src/main/java/com/afollestad/assent/internal/Assent.kt rename to core/src/main/java/com/afollestad/assent/internal/Assent.kt index 8572be1..4e52846 100644 --- a/library/src/main/java/com/afollestad/assent/internal/Assent.kt +++ b/core/src/main/java/com/afollestad/assent/internal/Assent.kt @@ -20,7 +20,6 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.PRIVATE import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity -import timber.log.Timber internal class Assent { @@ -29,8 +28,6 @@ internal class Assent { internal var permissionFragment: PermissionFragment? = null companion object { - - val LOCK = Any() var instance: Assent? = null @VisibleForTesting(otherwise = PRIVATE) @@ -63,7 +60,7 @@ internal class Assent { log("Re-using PermissionFragment for Context") permissionFragment } - return permissionFragment ?: throw IllegalStateException() + return permissionFragment ?: error("impossible!") } fun ensureFragment(context: Fragment): PermissionFragment = with(get()) { @@ -76,7 +73,7 @@ internal class Assent { log("Re-using PermissionFragment for parent Fragment") permissionFragment } - return permissionFragment ?: throw IllegalStateException() + return permissionFragment ?: error("impossible!") } fun forgetFragment() = with(get()) { @@ -86,8 +83,3 @@ internal class Assent { } } } - -private fun log(message: String) { - Timber.tag("AssentData") - Timber.d(message) -} diff --git a/library/src/main/java/com/afollestad/assent/internal/Extensions.kt b/core/src/main/java/com/afollestad/assent/internal/Extensions.kt similarity index 96% rename from library/src/main/java/com/afollestad/assent/internal/Extensions.kt rename to core/src/main/java/com/afollestad/assent/internal/Extensions.kt index 8e851b1..d7f8e7d 100644 --- a/library/src/main/java/com/afollestad/assent/internal/Extensions.kt +++ b/core/src/main/java/com/afollestad/assent/internal/Extensions.kt @@ -81,7 +81,7 @@ internal fun FragmentActivity.transact(action: FragmentTransaction.(Context) -> internal fun Fragment.transact(action: FragmentTransaction.(Context) -> Unit) { childFragmentManager.beginTransaction() .apply { - action(activity ?: throw IllegalStateException("Fragment's activity is null.")) + action(activity ?: error("Fragment's activity is null.")) commit() } childFragmentManager.executePendingTransactions() diff --git a/library/src/main/java/com/afollestad/assent/internal/Lifecycle.kt b/core/src/main/java/com/afollestad/assent/internal/Lifecycle.kt similarity index 100% rename from library/src/main/java/com/afollestad/assent/internal/Lifecycle.kt rename to core/src/main/java/com/afollestad/assent/internal/Lifecycle.kt diff --git a/core/src/main/java/com/afollestad/assent/internal/Logger.kt b/core/src/main/java/com/afollestad/assent/internal/Logger.kt new file mode 100644 index 0000000..5b5abc8 --- /dev/null +++ b/core/src/main/java/com/afollestad/assent/internal/Logger.kt @@ -0,0 +1,43 @@ +/** + * Designed and developed by Aidan Follestad (@afollestad) + * + * 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.afollestad.assent.internal + +import android.util.Log +import com.afollestad.assent.BuildConfig + +internal fun Any.log( + message: String, + vararg args: Any? +) { + if (BuildConfig.DEBUG) { + try { + Log.d(this::class.java.simpleName, message.format(*args)) + } catch (_: Exception) { + } + } +} + +internal fun Any.warn( + message: String, + vararg args: Any? +) { + if (BuildConfig.DEBUG) { + try { + Log.w(this::class.java.simpleName, message.format(*args)) + } catch (_: Exception) { + } + } +} diff --git a/library/src/main/java/com/afollestad/assent/internal/PendingRequest.kt b/core/src/main/java/com/afollestad/assent/internal/PendingRequest.kt similarity index 100% rename from library/src/main/java/com/afollestad/assent/internal/PendingRequest.kt rename to core/src/main/java/com/afollestad/assent/internal/PendingRequest.kt diff --git a/library/src/main/java/com/afollestad/assent/internal/PermissionFragment.kt b/core/src/main/java/com/afollestad/assent/internal/PermissionFragment.kt similarity index 93% rename from library/src/main/java/com/afollestad/assent/internal/PermissionFragment.kt rename to core/src/main/java/com/afollestad/assent/internal/PermissionFragment.kt index 85e6702..dbbaba9 100644 --- a/library/src/main/java/com/afollestad/assent/internal/PermissionFragment.kt +++ b/core/src/main/java/com/afollestad/assent/internal/PermissionFragment.kt @@ -21,20 +21,17 @@ import com.afollestad.assent.AssentResult import com.afollestad.assent.internal.Assent.Companion.ensureFragment import com.afollestad.assent.internal.Assent.Companion.forgetFragment import com.afollestad.assent.internal.Assent.Companion.get -import timber.log.Timber -import timber.log.Timber.d as log -import timber.log.Timber.w as warn /** @author Aidan Follestad (afollestad) */ class PermissionFragment : Fragment() { override fun onAttach(context: Context) { super.onAttach(context) - Timber.d("onAttach($context)") + log("onAttach($context)") } override fun onDetach() { - Timber.d("onDetach()") + log("onDetach()") super.onDetach() } @@ -75,7 +72,7 @@ class PermissionFragment : Fragment() { internal fun Fragment.onPermissionsResponse( permissions: Array, grantResults: IntArray -) = synchronized(Assent.LOCK) { +) { log( "onPermissionsResponse(\n\tpermissions = %s,\n\tgrantResults = %s\n))", permissions.joinToString(), @@ -85,7 +82,7 @@ internal fun Fragment.onPermissionsResponse( val currentRequest = get().currentPendingRequest if (currentRequest == null) { warn("response() called but there's no current pending request.") - return@synchronized + return } if (currentRequest.permissions.equalsStrings(permissions)) { @@ -101,7 +98,7 @@ internal fun Fragment.onPermissionsResponse( warn( "onPermissionsResponse() called with a result that doesn't match the current pending request." ) - return@synchronized + return } if (get().requestQueue.isNotEmpty()) { diff --git a/library/src/main/java/com/afollestad/assent/internal/Queue.kt b/core/src/main/java/com/afollestad/assent/internal/Queue.kt similarity index 91% rename from library/src/main/java/com/afollestad/assent/internal/Queue.kt rename to core/src/main/java/com/afollestad/assent/internal/Queue.kt index f2c2187..3cdf890 100644 --- a/library/src/main/java/com/afollestad/assent/internal/Queue.kt +++ b/core/src/main/java/com/afollestad/assent/internal/Queue.kt @@ -27,8 +27,7 @@ internal class Queue { } fun pop(): T = synchronized(lock) { - val result = data.firstOrNull() - ?: throw IllegalStateException("Queue is empty, cannot pop.") + val result = data.firstOrNull() ?: error("Queue is empty, cannot pop.") data.removeAt(0) return result } diff --git a/library/src/main/java/com/afollestad/assent/rationale/ConfirmCallback.kt b/core/src/main/java/com/afollestad/assent/rationale/ConfirmCallback.kt similarity index 87% rename from library/src/main/java/com/afollestad/assent/rationale/ConfirmCallback.kt rename to core/src/main/java/com/afollestad/assent/rationale/ConfirmCallback.kt index cc85320..4dfa91d 100644 --- a/library/src/main/java/com/afollestad/assent/rationale/ConfirmCallback.kt +++ b/core/src/main/java/com/afollestad/assent/rationale/ConfirmCallback.kt @@ -15,7 +15,7 @@ */ package com.afollestad.assent.rationale -import timber.log.Timber +import com.afollestad.assent.internal.warn /** @author Aidan Follestad (@afollestad) */ class ConfirmCallback( @@ -23,7 +23,7 @@ class ConfirmCallback( ) { operator fun invoke(isConfirmed: Boolean) { if (action == null) { - Timber.w("Confirm callback invoked more than once, ignored after first invocation.") + warn("Confirm callback invoked more than once, ignored after first invocation.") } action?.invoke(isConfirmed) action = null diff --git a/library/src/main/java/com/afollestad/assent/rationale/RationaleHandler.kt b/core/src/main/java/com/afollestad/assent/rationale/RationaleHandler.kt similarity index 88% rename from library/src/main/java/com/afollestad/assent/rationale/RationaleHandler.kt rename to core/src/main/java/com/afollestad/assent/rationale/RationaleHandler.kt index 2b8b72b..e1e04f4 100644 --- a/library/src/main/java/com/afollestad/assent/rationale/RationaleHandler.kt +++ b/core/src/main/java/com/afollestad/assent/rationale/RationaleHandler.kt @@ -25,10 +25,10 @@ import androidx.lifecycle.Lifecycle.Event.ON_DESTROY import com.afollestad.assent.AssentResult import com.afollestad.assent.Callback import com.afollestad.assent.Permission +import com.afollestad.assent.internal.log import com.afollestad.assent.internal.maybeObserveLifecycle import com.afollestad.assent.plus import kotlin.properties.Delegates.notNull -import timber.log.Timber typealias Requester = (Array, Int, RationaleHandler?, Callback) -> Unit @@ -48,10 +48,7 @@ abstract class RationaleHandler( private var rationalePermissionsResult: AssentResult? = null private var owner: Any = context - @CheckResult internal fun withOwner(owner: Any): RationaleHandler { - this.owner = owner - return this - } + @CheckResult internal fun withOwner(owner: Any) = apply { this.owner = owner } fun onPermission( permission: Permission, @@ -77,13 +74,13 @@ abstract class RationaleHandler( .toMutableSet() val simplePermissions = permissions.filterNot { showRationale.check(it) } - Timber.d( + log( "Found %d permissions that DO require a rationale: %s", remainingRationalePermissions.size, remainingRationalePermissions.joinToString() ) if (simplePermissions.isEmpty()) { - Timber.d("No simple permissions to request") + log("No simple permissions to request") requestRationalePermissions() return } @@ -104,19 +101,19 @@ abstract class RationaleHandler( private fun requestRationalePermissions() { val nextInQueue = remainingRationalePermissions.firstOrNull() ?: return finish() - Timber.d("Showing rationale for permission %s", nextInQueue) + log("Showing rationale for permission %s", nextInQueue) owner.maybeObserveLifecycle(ON_DESTROY) { onDestroy() } showRationale(nextInQueue, getMessageFor(nextInQueue), ConfirmCallback { confirmed -> if (confirmed) { - Timber.d("Got rationale confirm signal for permission %s", nextInQueue) + log("Got rationale confirm signal for permission %s", nextInQueue) requester(arrayOf(nextInQueue), requestCode, null) { rationalePermissionsResult += it remainingRationalePermissions.remove(nextInQueue) requestRationalePermissions() } } else { - Timber.d("Got rationale deny signal for permission %s", nextInQueue) + log("Got rationale deny signal for permission %s", nextInQueue) rationalePermissionsResult += AssentResult( listOf(nextInQueue), intArrayOf(PERMISSION_DENIED) @@ -129,7 +126,7 @@ abstract class RationaleHandler( } private fun finish() { - Timber.d("finish()") + log("finish()") val simpleResult = simplePermissionsResult val rationaleResult = rationalePermissionsResult when { @@ -142,8 +139,6 @@ abstract class RationaleHandler( } private fun getMessageFor(permission: Permission): CharSequence { - return messages[permission] ?: throw IllegalStateException( - "No message provided for $permission" - ) + return messages[permission] ?: error("No message provided for $permission") } } diff --git a/library/src/main/java/com/afollestad/assent/rationale/ShouldShowRationale.kt b/core/src/main/java/com/afollestad/assent/rationale/ShouldShowRationale.kt similarity index 100% rename from library/src/main/java/com/afollestad/assent/rationale/ShouldShowRationale.kt rename to core/src/main/java/com/afollestad/assent/rationale/ShouldShowRationale.kt diff --git a/library/src/test/java/com/afollestad/assent/AssentInActivityTest.kt b/core/src/test/java/com/afollestad/assent/AssentInActivityTest.kt similarity index 100% rename from library/src/test/java/com/afollestad/assent/AssentInActivityTest.kt rename to core/src/test/java/com/afollestad/assent/AssentInActivityTest.kt diff --git a/library/src/test/java/com/afollestad/assent/AssentInFragmentTest.kt b/core/src/test/java/com/afollestad/assent/AssentInFragmentTest.kt similarity index 100% rename from library/src/test/java/com/afollestad/assent/AssentInFragmentTest.kt rename to core/src/test/java/com/afollestad/assent/AssentInFragmentTest.kt diff --git a/library/src/test/java/com/afollestad/assent/testutil/AssertableCallback.kt b/core/src/test/java/com/afollestad/assent/testutil/AssertableCallback.kt similarity index 100% rename from library/src/test/java/com/afollestad/assent/testutil/AssertableCallback.kt rename to core/src/test/java/com/afollestad/assent/testutil/AssertableCallback.kt diff --git a/library/src/test/java/com/afollestad/assent/testutil/MockResponseQueue.kt b/core/src/test/java/com/afollestad/assent/testutil/MockResponseQueue.kt similarity index 100% rename from library/src/test/java/com/afollestad/assent/testutil/MockResponseQueue.kt rename to core/src/test/java/com/afollestad/assent/testutil/MockResponseQueue.kt diff --git a/library/src/test/java/com/afollestad/assent/testutil/NoManifestTestRunner.kt b/core/src/test/java/com/afollestad/assent/testutil/NoManifestTestRunner.kt similarity index 100% rename from library/src/test/java/com/afollestad/assent/testutil/NoManifestTestRunner.kt rename to core/src/test/java/com/afollestad/assent/testutil/NoManifestTestRunner.kt diff --git a/coroutines/.gitignore b/coroutines/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/coroutines/.gitignore @@ -0,0 +1 @@ +/build diff --git a/coroutines/build.gradle b/coroutines/build.gradle new file mode 100644 index 0000000..12f7208 --- /dev/null +++ b/coroutines/build.gradle @@ -0,0 +1,18 @@ +ext.module_group = "com.afollestad.assent" +ext.module_name = "coroutines" + +apply from: rootProject.file("gradle/android_library_config.gradle") + +dependencies { + api project(":core") + api deps.kotlin.coroutines.android + api deps.kotlin.coroutines.core + + compileOnly deps.androidx.annotations + implementation deps.kotlin.stdlib8 + + testImplementation deps.kotlin.test.mockito + testImplementation deps.test.junit + testImplementation deps.test.robolectric + testImplementation deps.test.truth +} diff --git a/coroutines/src/main/AndroidManifest.xml b/coroutines/src/main/AndroidManifest.xml new file mode 100644 index 0000000..43cc897 --- /dev/null +++ b/coroutines/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/coroutines/src/main/java/com/afollestad/assent/coroutines/AssentCoroutinesInActivity.kt b/coroutines/src/main/java/com/afollestad/assent/coroutines/AssentCoroutinesInActivity.kt new file mode 100644 index 0000000..c3cd2b1 --- /dev/null +++ b/coroutines/src/main/java/com/afollestad/assent/coroutines/AssentCoroutinesInActivity.kt @@ -0,0 +1,70 @@ +/** + * Designed and developed by Aidan Follestad (@afollestad) + * + * 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. + */ +@file:Suppress("unused") + +package com.afollestad.assent.coroutines + +import android.app.Activity +import com.afollestad.assent.AssentResult +import com.afollestad.assent.Permission +import com.afollestad.assent.askForPermissions +import com.afollestad.assent.rationale.RationaleHandler +import com.afollestad.assent.runWithPermissions +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +/** + * Performs a permission request, asking for all given [permissions], + * and returning the result. + */ +suspend fun Activity.awaitPermissionsResult( + vararg permissions: Permission, + requestCode: Int = 20, + rationaleHandler: RationaleHandler? = null +): AssentResult { + checkMainThread() + return suspendCoroutine { continuation -> + askForPermissions( + permissions = *permissions, + requestCode = requestCode, + rationaleHandler = rationaleHandler + ) { result -> + continuation.resume(result) + } + } +} + +/** + * Like [awaitPermissionsResult], but only returns if all given + * permissions are granted. So be warned, this method will wait + * indefinitely if permissions are not all granted. + */ +suspend fun Activity.awaitPermissionsGranted( + vararg permissions: Permission, + requestCode: Int = 40, + rationaleHandler: RationaleHandler? = null +): AssentResult { + checkMainThread() + return suspendCoroutine { continuation -> + runWithPermissions( + permissions = *permissions, + requestCode = requestCode, + rationaleHandler = rationaleHandler + ) { result -> + continuation.resume(result) + } + } +} diff --git a/coroutines/src/main/java/com/afollestad/assent/coroutines/AssentCoroutinesInFragment.kt b/coroutines/src/main/java/com/afollestad/assent/coroutines/AssentCoroutinesInFragment.kt new file mode 100644 index 0000000..acdd134 --- /dev/null +++ b/coroutines/src/main/java/com/afollestad/assent/coroutines/AssentCoroutinesInFragment.kt @@ -0,0 +1,68 @@ +/** + * Designed and developed by Aidan Follestad (@afollestad) + * + * 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.afollestad.assent.coroutines + +import androidx.fragment.app.Fragment +import com.afollestad.assent.AssentResult +import com.afollestad.assent.Permission +import com.afollestad.assent.askForPermissions +import com.afollestad.assent.rationale.RationaleHandler +import com.afollestad.assent.runWithPermissions +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +/** + * Performs a permission request, asking for all given [permissions], + * and returning the result. + */ +suspend fun Fragment.awaitPermissionsResult( + vararg permissions: Permission, + requestCode: Int = 60, + rationaleHandler: RationaleHandler? = null +): AssentResult { + checkMainThread() + return suspendCoroutine { continuation -> + askForPermissions( + permissions = *permissions, + requestCode = requestCode, + rationaleHandler = rationaleHandler + ) { result -> + continuation.resume(result) + } + } +} + +/** + * Like [awaitPermissionsResult], but only returns if all given + * permissions are granted. So be warned, this method will wait + * indefinitely if permissions are not all granted. + */ +suspend fun Fragment.awaitPermissionsGranted( + vararg permissions: Permission, + requestCode: Int = 80, + rationaleHandler: RationaleHandler? = null +): AssentResult { + checkMainThread() + return suspendCoroutine { continuation -> + runWithPermissions( + permissions = *permissions, + requestCode = requestCode, + rationaleHandler = rationaleHandler + ) { result -> + continuation.resume(result) + } + } +} diff --git a/sample/src/main/java/com/afollestad/assentsample/SampleApp.kt b/coroutines/src/main/java/com/afollestad/assent/coroutines/CheckOnMainThread.kt similarity index 69% rename from sample/src/main/java/com/afollestad/assentsample/SampleApp.kt rename to coroutines/src/main/java/com/afollestad/assent/coroutines/CheckOnMainThread.kt index 2a4b3c6..1c555f0 100644 --- a/sample/src/main/java/com/afollestad/assentsample/SampleApp.kt +++ b/coroutines/src/main/java/com/afollestad/assent/coroutines/CheckOnMainThread.kt @@ -13,16 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.afollestad.assentsample +package com.afollestad.assent.coroutines -import android.app.Application -import timber.log.Timber -import timber.log.Timber.DebugTree +import android.os.Looper +import java.lang.Thread.currentThread -class SampleApp : Application() { - - override fun onCreate() { - super.onCreate() - Timber.plant(DebugTree()) +internal fun checkMainThread() = + check(Looper.myLooper() == Looper.getMainLooper()) { + "Expected to be called on the main thread but was ${currentThread().name}" } -} diff --git a/dependencies.gradle b/dependencies.gradle index 6c01f69..cfe9fa9 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -2,8 +2,8 @@ ext.versions = [ min_sdk: 14, compile_sdk: 29, build_tools: "29.0.0", - publish_version: "2.3.1", - publish_version_code: 20 + publish_version: "3.0.0-RC1", + publish_version_code: 21 ] ext.deps = [ @@ -12,39 +12,32 @@ ext.deps = [ spotless: "com.diffplug.spotless:spotless-plugin-gradle:3.27.1", versions: "com.github.ben-manes:gradle-versions-plugin:0.27.0", bintray_release: "com.novoda:bintray-release:0.9.2", - kotlin: "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61", - nebula_lint: "com.netflix.nebula:gradle-lint-plugin:16.0.2" + kotlin: "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61" ], androidx: [ annotations: "androidx.annotation:annotation:1.1.0", app_compat: "androidx.appcompat:appcompat:1.1.0", - test: [ - core: "androidx.test:core:1.1.0" - ] + material: "com.google.android.material:material:1.0.0" ], - material: "com.google.android.material:material:1.0.0", - kotlin: [ stdlib8: "org.jetbrains.kotlin:kotlin-stdlib-jdk8", coroutines: [ core: "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2", - android: "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2" + android: "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2", + flow_binding: "io.github.reactivecircus.flowbinding:flowbinding-android:0.8.0" ], test: [ mockito: "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0" ] ], - debug: [ - timber: "com.jakewharton.timber:timber:4.7.1" - ], - test: [ junit: "junit:junit:4.13", robolectric: "org.robolectric:robolectric:4.3.1", mockito_core: "org.mockito:mockito-core:3.2.4", + mockito_inline: "org.mockito:mockito-inline:3.2.4", truth: "com.google.truth:truth:1.0", ] ] diff --git a/gradle/android_application_config.gradle b/gradle/android_application_config.gradle new file mode 100644 index 0000000..03b2e97 --- /dev/null +++ b/gradle/android_application_config.gradle @@ -0,0 +1,8 @@ +apply plugin: "com.android.application" +apply from: rootProject.file("gradle/android_common_config.gradle") + +if (module_package_id == null) { + throw new IllegalStateException("module_package_id is missing!") +} + +android.defaultConfig.applicationId = module_package_id.replace('-', '') diff --git a/gradle/android_bintray_config.gradle b/gradle/android_bintray_config.gradle new file mode 100644 index 0000000..c336f95 --- /dev/null +++ b/gradle/android_bintray_config.gradle @@ -0,0 +1,48 @@ +if (!project.rootProject.file('local.properties').exists()) { + logger.warn("local.properties not found. Skipping Bintray Release setup.") + return +} +apply plugin: "com.novoda.bintray-release" + +def getBintrayUserAndKey() { + Properties properties = new Properties() + properties.load(project.rootProject.file("local.properties").newDataInputStream()) + return [ + properties.getProperty("bintray.user"), + properties.getProperty("bintray.apikey") + ] +} + +if (versions == null || versions.publish_version == null) { + throw new IllegalStateException("Unable to reference publish_version!") +} else if (module_group == null || module_name == null) { + throw new IllegalStateException("Must provide module_group and module_name!") +} + +task checkBintrayConfig { + doLast { + def (user, key) = getBintrayUserAndKey() + if (user == null || user.isEmpty() || + key == null || key.isEmpty()) { + throw new IllegalStateException("Must specify Bintray user/API key in your local.properties.") + } + } +} + +afterEvaluate { + bintrayUpload.dependsOn checkBintrayConfig +} + +def (user, key) = getBintrayUserAndKey() +publish { + bintrayUser = user + bintrayKey = key + userOrg = "drummer-aidan" + groupId = module_group + artifactId = module_name + uploadName = "assent:$module_name" + publishVersion = versions.publish_version + desc = "Android permission requests made easy and compact." + website = "https://github.com/afollestad/assent" + dryRun = false +} diff --git a/gradle/android_common_config.gradle b/gradle/android_common_config.gradle new file mode 100644 index 0000000..b267b50 --- /dev/null +++ b/gradle/android_common_config.gradle @@ -0,0 +1,36 @@ +ext.module_package_id = "${module_group}.${module_name}" +logger.info("Package ID: $module_package_id") + +apply plugin: "kotlin-android" +apply from: rootProject.file("dependencies.gradle") +apply from: rootProject.file("gradle/spotless_plugin_config.gradle") + +if (!project.hasProperty("min_sdk")) { + ext.min_sdk = versions.min_sdk +} else { + logger.warn("Using minSdk: $min_sdk") +} + +android { + compileSdkVersion versions.compile_sdk + buildToolsVersion versions.build_tools + + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } + + defaultConfig { + minSdkVersion min_sdk + targetSdkVersion versions.compile_sdk + versionCode versions.publish_version_code + versionName versions.publish_version + } + + sourceSets { + main.res.srcDirs = [ + "src/main/res", + "src/main/res-public" + ] + } +} diff --git a/gradle/android_library_config.gradle b/gradle/android_library_config.gradle new file mode 100644 index 0000000..0c5e24b --- /dev/null +++ b/gradle/android_library_config.gradle @@ -0,0 +1,4 @@ +apply plugin: "com.android.library" + +apply from: rootProject.file("gradle/android_common_config.gradle") +apply from: rootProject.file("gradle/android_bintray_config.gradle") diff --git a/gradle/spotless_plugin_config.gradle b/gradle/spotless_plugin_config.gradle new file mode 100644 index 0000000..61a3ca7 --- /dev/null +++ b/gradle/spotless_plugin_config.gradle @@ -0,0 +1,18 @@ +apply plugin: "com.diffplug.gradle.spotless" + +spotless { + java { + target "**/*.java" + trimTrailingWhitespace() + removeUnusedImports() + googleJavaFormat() + endWithNewline() + } + kotlin { + target "**/*.kt" + ktlint().userData(["indent_size": "2", "continuation_indent_size": "2"]) + licenseHeaderFile rootProject.file("spotless.license.kt") + trimTrailingWhitespace() + endWithNewline() + } +} diff --git a/gradle/versions_plugin_config.gradle b/gradle/versions_plugin_config.gradle new file mode 100644 index 0000000..1062c5f --- /dev/null +++ b/gradle/versions_plugin_config.gradle @@ -0,0 +1,14 @@ +apply plugin: "com.github.ben-manes.versions" + +dependencyUpdates.resolutionStrategy { + componentSelection { rules -> + rules.all { ComponentSelection selection -> + boolean rejected = ['alpha', 'beta', 'rc', 'cr', 'm'].any { qualifier -> + selection.candidate.version ==~ /(?i).*[.-]${qualifier}[.\d-]*/ + } + if (rejected) { + selection.reject('Not stable') + } + } + } +} diff --git a/rationales/.gitignore b/rationales/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/rationales/.gitignore @@ -0,0 +1 @@ +/build diff --git a/rationales/build.gradle b/rationales/build.gradle new file mode 100644 index 0000000..46b8d07 --- /dev/null +++ b/rationales/build.gradle @@ -0,0 +1,17 @@ +ext.module_group = "com.afollestad.assent" +ext.module_name = "rationales" + +apply from: rootProject.file("gradle/android_library_config.gradle") + +dependencies { + api project(":core") + api deps.androidx.material + + compileOnly deps.androidx.annotations + implementation deps.kotlin.stdlib8 + + testImplementation deps.kotlin.test.mockito + testImplementation deps.test.junit + testImplementation deps.test.mockito_inline + testImplementation deps.test.truth +} diff --git a/rationales/src/main/AndroidManifest.xml b/rationales/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a350743 --- /dev/null +++ b/rationales/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/library/src/main/java/com/afollestad/assent/rationale/AlertDialogRationaleHandler.kt b/rationales/src/main/java/com/afollestad/assent/rationale/AlertDialogRationaleHandler.kt similarity index 94% rename from library/src/main/java/com/afollestad/assent/rationale/AlertDialogRationaleHandler.kt rename to rationales/src/main/java/com/afollestad/assent/rationale/AlertDialogRationaleHandler.kt index 7aedc58..b8a1188 100644 --- a/library/src/main/java/com/afollestad/assent/rationale/AlertDialogRationaleHandler.kt +++ b/rationales/src/main/java/com/afollestad/assent/rationale/AlertDialogRationaleHandler.kt @@ -41,10 +41,10 @@ internal class DialogRationaleHandler( .setMessage(message) .setPositiveButton(android.R.string.ok) { dialog, _ -> (dialog as AlertDialog).setOnDismissListener(null) - confirm(true) + confirm(isConfirmed = true) } .setOnDismissListener { - confirm(false) + confirm(isConfirmed = false) } .show() } @@ -61,7 +61,7 @@ fun Fragment.createDialogRationale( ): RationaleHandler { return DialogRationaleHandler( dialogTitle = dialogTitle, - context = activity ?: throw IllegalStateException("Fragment not attached"), + context = activity ?: error("Fragment not attached"), requester = ::askForPermissions ).apply(block) } diff --git a/library/src/main/java/com/afollestad/assent/rationale/SnackBarRationaleHandler.kt b/rationales/src/main/java/com/afollestad/assent/rationale/SnackBarRationaleHandler.kt similarity index 93% rename from library/src/main/java/com/afollestad/assent/rationale/SnackBarRationaleHandler.kt rename to rationales/src/main/java/com/afollestad/assent/rationale/SnackBarRationaleHandler.kt index 2b3a283..823e049 100644 --- a/library/src/main/java/com/afollestad/assent/rationale/SnackBarRationaleHandler.kt +++ b/rationales/src/main/java/com/afollestad/assent/rationale/SnackBarRationaleHandler.kt @@ -39,13 +39,13 @@ internal class SnackBarRationaleHandler( override fun onDismissed( transientBottomBar: Snackbar?, event: Int - ) = confirm(false) + ) = confirm(isConfirmed = false) } Snackbar.make(root, message, Snackbar.LENGTH_INDEFINITE) .apply { setAction(android.R.string.ok) { removeCallback(dismissListener) - confirm(true) + confirm(isConfirmed = true) } addCallback(dismissListener) show() @@ -61,7 +61,7 @@ fun Fragment.createSnackBarRationale( ): RationaleHandler { return SnackBarRationaleHandler( root = root, - context = activity ?: throw IllegalStateException("Fragment not attached"), + context = activity ?: error("Fragment not attached"), requester = ::askForPermissions ).apply(block) } diff --git a/library/src/test/java/com/afollestad/assent/rationale/MockRequestResponder.kt b/rationales/src/test/java/com/afollestad/assent/rationale/MockRequestResponder.kt similarity index 100% rename from library/src/test/java/com/afollestad/assent/rationale/MockRequestResponder.kt rename to rationales/src/test/java/com/afollestad/assent/rationale/MockRequestResponder.kt diff --git a/library/src/test/java/com/afollestad/assent/rationale/MockShouldShowRationale.kt b/rationales/src/test/java/com/afollestad/assent/rationale/MockShouldShowRationale.kt similarity index 100% rename from library/src/test/java/com/afollestad/assent/rationale/MockShouldShowRationale.kt rename to rationales/src/test/java/com/afollestad/assent/rationale/MockShouldShowRationale.kt diff --git a/library/src/test/java/com/afollestad/assent/rationale/RationaleHandlerTest.kt b/rationales/src/test/java/com/afollestad/assent/rationale/RationaleHandlerTest.kt similarity index 100% rename from library/src/test/java/com/afollestad/assent/rationale/RationaleHandlerTest.kt rename to rationales/src/test/java/com/afollestad/assent/rationale/RationaleHandlerTest.kt diff --git a/rationales/src/test/java/com/afollestad/assent/testutil/AssertableCallback.kt b/rationales/src/test/java/com/afollestad/assent/testutil/AssertableCallback.kt new file mode 100644 index 0000000..ea3df21 --- /dev/null +++ b/rationales/src/test/java/com/afollestad/assent/testutil/AssertableCallback.kt @@ -0,0 +1,42 @@ +/** + * Designed and developed by Aidan Follestad (@afollestad) + * + * 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.afollestad.assent.testutil + +import com.afollestad.assent.AssentResult +import com.afollestad.assent.Callback +import com.google.common.truth.Truth.assertThat + +class AssertableCallback { + private var results = mutableListOf() + + val consumer: Callback = { + results.add(it) + } + + fun assertInvokes(vararg expected: AssentResult) { + if (results.isEmpty()) { + throw AssertionError("The callback was not invoked") + } + assertThat(results).isEqualTo(expected.toMutableList()) + results.clear() + } + + fun assertDoesNotInvoke() { + if (results.isNotEmpty()) { + throw AssertionError("The callback was invoked") + } + } +} diff --git a/rationales/src/test/java/com/afollestad/assent/testutil/MockResponseQueue.kt b/rationales/src/test/java/com/afollestad/assent/testutil/MockResponseQueue.kt new file mode 100644 index 0000000..99d4563 --- /dev/null +++ b/rationales/src/test/java/com/afollestad/assent/testutil/MockResponseQueue.kt @@ -0,0 +1,81 @@ +/** + * Designed and developed by Aidan Follestad (@afollestad) + * + * 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. + */ +@file:Suppress("MemberVisibilityCanBePrivate") + +package com.afollestad.assent.testutil + +import android.content.pm.PackageManager +import com.afollestad.assent.Permission +import com.afollestad.assent.internal.PermissionFragment + +data class QueuedRequest( + val permissions: Array, + val requestCode: Int +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as QueuedRequest + + if (!permissions.contentEquals(other.permissions)) return false + if (requestCode != other.requestCode) return false + + return true + } + + override fun hashCode(): Int { + var result = permissions.contentHashCode() + result = 31 * result + requestCode + return result + } +} + +class MockResponseQueue( + private val allowedPermissions: Set, + private val permissionFragment: PermissionFragment +) { + private val queue = mutableListOf() + + fun handle( + permissions: Array, + requestCode: Int + ) { + queue.add(QueuedRequest(permissions, requestCode)) + } + + fun respondToOne(which: QueuedRequest? = null) = with(which ?: queue.first()) { + val grantResults = IntArray(permissions.size) { index -> + val parsedPermission = Permission.parse(permissions[index]) + if (allowedPermissions.contains(parsedPermission)) { + PackageManager.PERMISSION_GRANTED + } else { + PackageManager.PERMISSION_DENIED + } + } + permissionFragment.onRequestPermissionsResult(requestCode, permissions, grantResults) + queue.remove(this) + } + + fun respondToAll() { + if (queue.isEmpty()) { + throw AssertionError("No requests to respond to") + } + while (queue.isNotEmpty()) { + respondToOne() + } + } +} diff --git a/sample/build.gradle b/sample/build.gradle index 6c22592..bb871d9 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,13 +1,14 @@ ext.module_group = "com.afollestad" ext.module_name = "assentsample" +ext.min_sdk = 21 apply from: rootProject.file("gradle/android_application_config.gradle") dependencies { - implementation project(':library') + implementation project(":core") + implementation project(":coroutines") + implementation project(":rationales") - implementation deps.androidx.annotations - implementation deps.androidx.app_compat implementation deps.kotlin.stdlib8 - implementation deps.debug.timber + implementation deps.kotlin.coroutines.flow_binding } diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 73d4090..9bf70e7 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -8,7 +8,6 @@ (R.id.rootView) } - private val requestPermissionButton: View by lazy { - findViewById(R.id.requestPermissionButton) + private val requestPermissionButton by lazy { + findViewById(R.id.requestPermissionButton) } - private val statusText: TextView by lazy { - findViewById(R.id.statusText) + private val statusText by lazy { + findViewById(R.id.statusText) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - fun performRequest() { - val rationaleHandler = createSnackBarRationale(rootView) { - onPermission(READ_CONTACTS, "Test rationale #1, please accept!") - onPermission(WRITE_EXTERNAL_STORAGE, "Test rationale #1, please accept!") - onPermission(READ_SMS, "Test rationale #3, please accept!") - } - askForPermissions( - READ_CONTACTS, WRITE_EXTERNAL_STORAGE, READ_SMS, - rationaleHandler = rationaleHandler - ) { result -> - val statusRes = when { - result.isAllGranted(READ_CONTACTS, WRITE_EXTERNAL_STORAGE, READ_SMS) -> - R.string.all_granted - result.isAllDenied(READ_CONTACTS, WRITE_EXTERNAL_STORAGE, READ_SMS) -> - R.string.none_granted - else -> R.string.some_granted - } - statusText.setText(statusRes) - } + val rationaleHandler = createSnackBarRationale(rootView) { + onPermission(READ_CONTACTS, "Test rationale #1, please accept!") + onPermission(WRITE_EXTERNAL_STORAGE, "Test rationale #1, please accept!") + onPermission(READ_SMS, "Test rationale #3, please accept!") } - requestPermissionButton.setOnClickListener { performRequest() } + requestPermissionButton.clicks() + .debounce(200L) + .onEach { + val result = awaitPermissionsResult( + READ_CONTACTS, WRITE_EXTERNAL_STORAGE, READ_SMS, + rationaleHandler = rationaleHandler + ) + + val statusRes = when { + result.isAllGranted(READ_CONTACTS, WRITE_EXTERNAL_STORAGE, READ_SMS) -> + R.string.all_granted + result.isAllDenied(READ_CONTACTS, WRITE_EXTERNAL_STORAGE, READ_SMS) -> + R.string.none_granted + else -> R.string.some_granted + } + statusText.setText(statusRes) + } + .launchIn(rootView.viewScope) + + rootView.viewScope.launch { + awaitPermissionsGranted(WRITE_EXTERNAL_STORAGE) + toast("External storage permission is granted!") + } } override fun onResume() { super.onResume() - if (isAllGranted(READ_CONTACTS, WRITE_EXTERNAL_STORAGE, READ_SMS)) { - statusText.setText(R.string.all_granted) - } else { - statusText.setText(R.string.none_granted) - } + statusText.setText( + if (isAllGranted(READ_CONTACTS, WRITE_EXTERNAL_STORAGE, READ_SMS)) { + R.string.all_granted + } else { + R.string.none_granted + } + ) } override fun onCreateOptionsMenu(menu: Menu): Boolean { diff --git a/sample/src/main/java/com/afollestad/assentsample/Util.kt b/sample/src/main/java/com/afollestad/assentsample/Util.kt index 73bc5ba..a97a91f 100644 --- a/sample/src/main/java/com/afollestad/assentsample/Util.kt +++ b/sample/src/main/java/com/afollestad/assentsample/Util.kt @@ -18,10 +18,26 @@ package com.afollestad.assentsample import android.app.Activity import android.content.Context import android.content.Intent +import android.view.View +import android.widget.Toast +import android.widget.Toast.LENGTH_SHORT import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel + +private var toast: Toast? = null + +fun Activity.toast(message: String) { + toast?.cancel() + toast = Toast.makeText(this, message, LENGTH_SHORT) + .apply { show() } +} inline fun Context.startActivity() { startActivity(Intent(this, T::class.java)) @@ -41,3 +57,29 @@ fun FragmentActivity.transact(block: FragmentTransaction.() -> Unit) { fun Fragment.transact(block: FragmentTransaction.() -> Unit) { childFragmentManager.transact(block) } + +val View.viewScope: CoroutineScope + get() { + val scope: CoroutineScope? = getTag(R.id.tag_view_coroutine_scope) as? CoroutineScope + if (scope != null) { + return scope + } + return LifecycleCoroutineScope(this, SupervisorJob() + Dispatchers.Main) + } + +internal class LifecycleCoroutineScope( + private var view: View?, + context: CoroutineContext +) : CoroutineScope { + init { + view!!.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) = Unit + override fun onViewDetachedFromWindow(v: View) { + coroutineContext.cancel() + view = null + } + }) + } + + override val coroutineContext: CoroutineContext = context +} diff --git a/sample/src/main/java/com/afollestad/assentsample/fragment/ExampleChildFragment.kt b/sample/src/main/java/com/afollestad/assentsample/fragment/ExampleChildFragment.kt index 0f5ed84..13b2ead 100644 --- a/sample/src/main/java/com/afollestad/assentsample/fragment/ExampleChildFragment.kt +++ b/sample/src/main/java/com/afollestad/assentsample/fragment/ExampleChildFragment.kt @@ -1,3 +1,18 @@ +/** + * Designed and developed by Aidan Follestad (@afollestad) + * + * 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.afollestad.assentsample.fragment import android.annotation.SuppressLint @@ -14,8 +29,8 @@ import com.afollestad.assentsample.R.layout /** @author Aidan Follestad (afollestad) */ class ExampleChildFragment : Fragment() { - private val requestPermissionButtonChild: View by lazy { - view!!.findViewById(R.id.requestPermissionButtonChild) + private val requestPermissionButtonChild by lazy { + view!!.findViewById(R.id.requestPermissionButtonChild) } @SuppressLint("SetTextI18n") @@ -36,4 +51,4 @@ class ExampleChildFragment : Fragment() { } } } -} \ No newline at end of file +} diff --git a/sample/src/main/java/com/afollestad/assentsample/fragment/ExampleFragment.kt b/sample/src/main/java/com/afollestad/assentsample/fragment/ExampleFragment.kt index 8fc8743..9963a66 100644 --- a/sample/src/main/java/com/afollestad/assentsample/fragment/ExampleFragment.kt +++ b/sample/src/main/java/com/afollestad/assentsample/fragment/ExampleFragment.kt @@ -29,8 +29,8 @@ import com.afollestad.assentsample.transact /** @author Aidan Follestad (afollestad) */ class ExampleFragment : Fragment() { - private val requestPermissionButtonMain: View by lazy { - view!!.findViewById(R.id.requestPermissionButtonMain) + private val requestPermissionButtonMain by lazy { + view!!.findViewById(R.id.requestPermissionButtonMain) } @SuppressLint("SetTextI18n") diff --git a/sample/src/main/res/values/ids.xml b/sample/src/main/res/values/ids.xml new file mode 100644 index 0000000..8dfcf53 --- /dev/null +++ b/sample/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 52baf7e..ef849fd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':sample', ':library' +include ':sample', ':core', ':rationales', ':coroutines'