diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d4d299 --- /dev/null +++ b/.gitignore @@ -0,0 +1,241 @@ + +# Created by https://www.gitignore.io/api/macos,android,androidstudio +# Edit at https://www.gitignore.io/?templates=macos,android,androidstudio + +### Android ### +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/caches +.idea/codeStyles +# Android Studio 3 in .gitignore file. +.idea/caches/build_file_checksums.ser +.idea/modules.xml +.idea/sonarIssues.xml +.idea/vcs.xml +.idea/runConfigurations.xml +.idea/sonarlint/ + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +### Android Patch ### +gen-external-apklibs +output.json + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files + +# Files for the ART/Dalvik VM + +# Java class files + +# Generated files + +# Gradle files +.gradle + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse + +# Log Files + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +*.ipr +*~ +*.swp + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later + +# NDK +obj/ + +# IntelliJ IDEA +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/scopes/scope_settings.xml +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) +hs_err_pid* + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +### macOS ### +# General +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.TemporaryItems +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### SonarQube ### +# SonarQube ignore files. +# +# https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner +# Sonar Scanner working directories +.sonar/ +.scannerwork/ + +# http://www.sonarlint.org/commandline/ +# SonarLint working directories, configuration files (including credentials) +.sonarlint/ + + +# End of https://www.gitignore.io/api/macos,android,androidstudio \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..55f3a52 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# StartaskPermissions +[![JitPack](https://jitpack.io/v/illiashenkoo/startask-permissions.svg)](https://jitpack.io/#illiashenkoo/startask-permissions) + +StartaskPermissions is a library that helps to handle runtime permissions on Android, entirely written using Kotlin language. + +![](https://raw.githubusercontent.com/illiashenkoo/startask-permissions/master/images/logo.png) +## Using in your projects + +### ![Gradle](https://raw.githubusercontent.com/illiashenkoo/startask-permissions/master/images/ic_gradle.png) Gradle + +The library is published to [JitPack](https://jitpack.io/#illiashenkoo/startask-permissions) repository. + +1. Add the JitPack repository to your root build.gradle at the end of repositories. +```groovy +allprojects { + repositories { + //... + maven { url 'https://jitpack.io' } + } +} +``` + +2. Add the dependency + +`${latest.version}` is [![](https://jitpack.io/v/illiashenkoo/startask-permissions.svg)](https://jitpack.io/#illiashenkoo/startask-permissions) + +```groovy +dependencies { + implementation "com.github.illiashenkoo:startaskpermissions:${latest.version}" +} +``` + +### ![Kotlin](https://raw.githubusercontent.com/illiashenkoo/startask-permissions/master/images/ic_kotlin.png) Usage with Kotlin + +1. Add the following line to AndroidManifest.xml: +```xml + +``` + +2. Create a `Permission` object +``` kotlin +private val permission: Permission by lazy { + Permission.Builder(Manifest.permission.CAMERA) + .setRequestCode(MY_PERMISSIONS_REQUEST_CODE) + .build() +} +``` + +3. Check and request permission if needed + +``` kotlin +permission.check(this) + .onGranted { + // All requested permissions are granted + }.onShowRationale { + // Provide an explanation if the user has already denied that permission request + } +``` + +4. Delegate the permission handling to library +``` kotlin +override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + permission.onRequestPermissionsResult(this, requestCode, grantResults) + .onGranted { + // All requested permissions are granted + }.onDenied { + // Oops, some of the permissions are denied + }.onNeverAskAgain { + // Oops, some of the permissions are denied + // User chose "never ask again" about a permission + } +} +``` + +[Look at the examples of using the library](https://github.com/illiashenkoo/startask-permissions/blob/master/sample/src/main/java/net/codecision/startask/permissions/sample/PermissionsActivity.kt) + +## License + +[Apache License 2.0](https://github.com/illiashenkoo/startask-permissions/blob/master/LICENSE) + +## Contacts + +[Oleg Illiashenko](mailto:illiashenkoo.dev@gmail.com) + +## Contributions and releases + +All development (both new features and bug fixes) is performed in `develop` branch. +This way `master` sources always contain sources of the most recently released version. +Please send PRs with bug fixes to `develop` branch. +Fixes to documentation in markdown files are an exception to this rule. They are updated directly in `master`. + +The `develop` branch is pushed to `master` during release. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..e356252 --- /dev/null +++ b/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + jcenter() + google() + mavenCentral() + maven { url 'https://jitpack.io' } + } + + dependencies { + apply from: 'dependencies.gradle' + classpath buildPlugins.gradle + classpath buildPlugins.kotlin + classpath buildPlugins.dcendents + classpath buildPlugins.dokka + } +} + +allprojects { + repositories { + google() + jcenter() + maven { url "https://maven.google.com" } + maven { url 'https://jitpack.io' } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/dependencies.gradle b/dependencies.gradle new file mode 100644 index 0000000..1a573bf --- /dev/null +++ b/dependencies.gradle @@ -0,0 +1,106 @@ +ext { + + /*====================================== MAIN ======================================*/ + + android = [ + compileSdkVersion: 28, + minSdkVersion : 16, + targetSdkVersion : 28, + buildToolsVersion: "28.0.3", + ] + + libVersion = "1.0.0" + libGroup = "com.github.illiashenkoo" + + /*================================= BUILD PLUGINS ==================================*/ + + buildVersions = [ + gradle : '3.4.1', + kotlin : '1.3.31', + dcendents: '2.1', + dokka : '0.9.18', + ] + + buildPlugins = [ + gradle : "com.android.tools.build:gradle:$buildVersions.gradle", + kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$buildVersions.kotlin", + dcendents: "com.github.dcendents:android-maven-gradle-plugin:$buildVersions.dcendents", + dokka : "org.jetbrains.dokka:dokka-android-gradle-plugin:$buildVersions.dokka", + ] + + /*================================== DEPENDENCIES ==================================*/ + + def androidx_appcompat = '1.0.2' + def androidx_fragment = '1.0.0' + def junit_version = '4.12' + def mockito_version = '2.7.22' + def robolectric_version = '4.2.1' + + def libs = [ + kotlin_stdlib : "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$buildVersions.kotlin", + + androidx_appcompat: "androidx.appcompat:appcompat:$androidx_appcompat", + androidx_fragment : "androidx.fragment:fragment:$androidx_fragment", + + ] + + def testLibs = [ + jnuit : "junit:junit:$junit_version", + mockito : "org.mockito:mockito-core:$mockito_version", + robolectric: "org.robolectric:robolectric:$robolectric_version" + ] + + /*============================== MODULE DEPENDENCIES ==============================*/ + + def modules = [ + library: ":startask-permissions", + ] + + libraryModuleDependencies = [ + [configuration: "implementation", dependency: libs.androidx_appcompat], + [configuration: "implementation", dependency: libs.androidx_fragment], + + [configuration: "implementation", dependency: libs.kotlin_stdlib], + + [configuration: "testImplementation", dependency: testLibs.jnuit], + [configuration: "testImplementation", dependency: testLibs.mockito], + [configuration: "testImplementation", dependency: testLibs.robolectric], + ] + + sampleModuleDependencies = [ + [configuration: "implementation", dependency: libs.androidx_appcompat], + + [configuration: "implementation", dependency: libs.kotlin_stdlib], + + [configuration: "implementation", dependency: project(modules.library)] + ] + + /*==================================== PLUGINS =====================================*/ + + def plugins = [ + android_library : 'com.android.library', + android_application : 'com.android.application', + kotlin_android : 'kotlin-android', + kotlin_android_extensions: 'kotlin-android-extensions', + kotlin_kapt : 'kotlin-kapt', + android_maven : 'com.github.dcendents.android-maven', + dokka_android : 'org.jetbrains.dokka-android', + jacoco : 'jacoco', + + ] + + sampleModulePlugins = [ + plugins.android_application, + plugins.kotlin_android, + plugins.kotlin_android_extensions, + plugins.kotlin_kapt, + ] + + libraryModulePlugins = [ + plugins.android_library, + plugins.kotlin_android, + plugins.android_maven, + plugins.dokka_android, + ] + +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..0c8a62f --- /dev/null +++ b/gradle.properties @@ -0,0 +1,10 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +android.enableUnitTestBinaryResources=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..13372ae Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d757f3d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/images/ic_gradle.png b/images/ic_gradle.png new file mode 100644 index 0000000..eee0735 Binary files /dev/null and b/images/ic_gradle.png differ diff --git a/images/ic_kotlin.png b/images/ic_kotlin.png new file mode 100644 index 0000000..f92beca Binary files /dev/null and b/images/ic_kotlin.png differ diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000..ccce39f Binary files /dev/null and b/images/logo.png differ diff --git a/images/repository-open-graph-template.png b/images/repository-open-graph-template.png new file mode 100644 index 0000000..d03dffd Binary files /dev/null and b/images/repository-open-graph-template.png differ diff --git a/library/.gitignore b/library/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/library/.gitignore @@ -0,0 +1 @@ +/build diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..49ae954 --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,46 @@ +version = rootProject.libVersion + +rootProject.libraryModulePlugins.each { + apply plugin: it +} + +group=rootProject.libGroup + +android { + def ext = rootProject.extensions.ext + + compileSdkVersion ext.android.compileSdkVersion + buildToolsVersion ext.android.buildToolsVersion + + defaultConfig { + minSdkVersion ext.android.minSdkVersion + targetSdkVersion ext.android.targetSdkVersion + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } + + dokka { + outputFormat = 'html' + outputDirectory = "$buildDir/javadoc" + skipDeprecated = true + reportUndocumented = true + } +} + +dependencies { + rootProject.libraryModuleDependencies.each { + add(it.configuration, it.dependency, it.options) + } +} \ No newline at end of file diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/library/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..cf04164 --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/library/src/main/java/net/codecision/startask/permissions/Permission.kt b/library/src/main/java/net/codecision/startask/permissions/Permission.kt new file mode 100644 index 0000000..a577ccb --- /dev/null +++ b/library/src/main/java/net/codecision/startask/permissions/Permission.kt @@ -0,0 +1,165 @@ +package net.codecision.startask.permissions + +import android.app.Activity +import androidx.fragment.app.Fragment +import net.codecision.startask.permissions.model.PermissionCheckResult +import net.codecision.startask.permissions.model.PermissionRequestResult + + +class Permission private constructor( + private val requestCode: Int, + private val shouldShowRationale: Boolean, + private val shouldRequestAutomatically: Boolean, + private val permissions: Array +) { + + /** + * Checks whether your app has a given permission and whether the app op + * that corresponds to this permission is allowed. + * + * @param activity: Activity for accessing resources. + * @return The permission check result {@link PermissionCheckResult} + */ + fun check(activity: Activity): PermissionCheckResult { + return PermissionUtils.checkPermissions( + activity, + permissions, + requestCode, + shouldShowRationale, + shouldRequestAutomatically + ) + } + + /** + * Checks whether your app has a given permission and whether the app op + * that corresponds to this permission is allowed. + * + * @param fragment: Fragment for accessing resources. + * @return The permission check result {@link PermissionCheckResult} + */ + fun check(fragment: Fragment): PermissionCheckResult { + return PermissionUtils.checkPermissions( + fragment, + permissions, + requestCode, + shouldShowRationale, + shouldRequestAutomatically + ) + } + + /** + * Requests permissions to be granted to this application. + * + * @param activity: Activity for accessing resources. + */ + fun request(activity: Activity) { + return PermissionUtils.requestPermissions(activity, permissions, requestCode) + } + + /** + * Requests permissions to be granted to this application. + * + * @param fragment: Fragment for accessing resources. + */ + fun request(fragment: Fragment) { + return PermissionUtils.requestPermissions(fragment, permissions, requestCode) + } + + /** + * Checks whether your app has a given permission + * + * @param activity: Activity for accessing resources. + * @return The permission check result which is either {true or false}. + */ + fun isGranted(activity: Activity): Boolean { + return PermissionUtils.isGranted(activity, permissions) + } + + /** + * Checks whether your app has a given permission + * + * @param fragment: Fragment for accessing resources. + * @return The permission check result which is either {true or false}. + */ + fun isGranted(fragment: Fragment): Boolean { + return PermissionUtils.isGranted(fragment, permissions) + } + + /** + * Forwarding the result from requesting permissions. + * + * @param fragment: Fragment for accessing resources. + * @param requestCode + * @param grantResults + */ + fun onRequestPermissionsResult( + fragment: Fragment, + requestCode: Int, + grantResults: IntArray + ): PermissionRequestResult { + return if (requestCode == this.requestCode) { + PermissionUtils.onRequestPermissionsResult(fragment, grantResults, permissions) + } else { + PermissionRequestResult.getIncorrectCode() + } + } + + /** + * Forwarding the result from requesting permissions. + * + * @param activity: Activity for accessing resources. + * @param requestCode + * @param grantResults + */ + fun onRequestPermissionsResult( + activity: Activity, + requestCode: Int, + grantResults: IntArray + ): PermissionRequestResult { + return if (requestCode == this.requestCode) { + PermissionUtils.onRequestPermissionsResult(activity, grantResults, permissions) + } else { + PermissionRequestResult.getIncorrectCode() + } + } + + class Builder(private vararg val permissions: String) { + + private var requestCode: Int = PERMISSIONS_REQUEST_CODE + + private var shouldShowRationale = true + + private var shouldRequestAutomatically = true + + fun setRequestCode(requestCode: Int): Builder { + this.requestCode = requestCode + return this + } + + fun setShouldShowRationale(shouldShowRationale: Boolean): Builder { + this.shouldShowRationale = shouldShowRationale + return this + } + + fun setShouldRequestAutomatically(shouldRequestAutomatically: Boolean): Builder { + this.shouldRequestAutomatically = shouldRequestAutomatically + return this + } + + fun build(): Permission { + return if (permissions.isNotEmpty()) { + Permission(requestCode, shouldShowRationale, shouldRequestAutomatically, permissions) + } else { + throw IllegalArgumentException("Require one or more permission!") + } + } + + } + + companion object { + + const val PERMISSIONS_REQUEST_CODE = 43 + + } + +} \ No newline at end of file diff --git a/library/src/main/java/net/codecision/startask/permissions/PermissionUtils.kt b/library/src/main/java/net/codecision/startask/permissions/PermissionUtils.kt new file mode 100644 index 0000000..2fd0a06 --- /dev/null +++ b/library/src/main/java/net/codecision/startask/permissions/PermissionUtils.kt @@ -0,0 +1,166 @@ +package net.codecision.startask.permissions + +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.annotation.VisibleForTesting +import androidx.core.app.ActivityCompat +import androidx.core.content.PermissionChecker +import androidx.fragment.app.Fragment +import net.codecision.startask.permissions.model.PermissionCheckResult +import net.codecision.startask.permissions.model.PermissionRequestResult + +class PermissionUtils { + + companion object { + + fun checkPermissions( + activity: Activity, + permissions: Array, + requestCode: Int, + shouldShowRationale: Boolean, + shouldRequestAutomatically: Boolean + ): PermissionCheckResult { + return if (isGranted(activity, permissions)) { + PermissionCheckResult.getGranted() + } else { + doOnDenied(activity, permissions, requestCode, shouldShowRationale, shouldRequestAutomatically) + } + } + + fun checkPermissions( + fragment: Fragment, + permissions: Array, + requestCode: Int, + shouldShowRationale: Boolean, + shouldRequestAutomatically: Boolean): PermissionCheckResult { + return if (isGranted(fragment, permissions)) { + PermissionCheckResult.getGranted() + } else { + doOnDenied(fragment, permissions, requestCode, shouldShowRationale, shouldRequestAutomatically) + } + } + + fun isGranted(activity: Activity, permissions: Array) = isGranted(activity as Context, permissions) + + fun isGranted(fragment: Fragment, permissions: Array) = isGranted(fragment.requireContext(), permissions) + + fun requestPermissions(activity: Activity, permissions: Array, requestCode: Int) { + ActivityCompat.requestPermissions(activity, permissions, requestCode) + } + + fun requestPermissions(fragment: Fragment, permissions: Array, requestCode: Int) { + fragment.requestPermissions(permissions, requestCode) + } + + fun onRequestPermissionsResult( + fragment: Fragment, + grantResults: IntArray, + permissions: Array + ): PermissionRequestResult { + return if (verifyPermissionsResult(grantResults)) { + PermissionRequestResult.getGranted() + } else { + if (shouldShowRequestPermissionRationale(fragment, permissions)) { + PermissionRequestResult.getDenied() + } else { + PermissionRequestResult.getNeverAskAgain() + } + } + } + + fun onRequestPermissionsResult( + activity: Activity, + grantResults: IntArray, + permissions: Array + ): PermissionRequestResult { + return if (verifyPermissionsResult(grantResults)) { + PermissionRequestResult.getGranted() + } else { + if (shouldShowRequestPermissionRationale(activity, permissions)) { + PermissionRequestResult.getDenied() + } else { + PermissionRequestResult.getNeverAskAgain() + } + } + } + + private fun doOnDenied( + activity: Activity, + permissions: Array, + requestCode: Int, + shouldShowRationale: Boolean, + shouldRequestAutomatically: Boolean + ): PermissionCheckResult { + return if (shouldShowRationale && shouldShowRequestPermissionRationale(activity, permissions)) { + PermissionCheckResult.getShowRationale() + } else { + if (shouldRequestAutomatically) { + requestPermissions(activity, permissions, requestCode) + } + PermissionCheckResult.getRequiredRequest() + } + } + + private fun doOnDenied( + fragment: Fragment, + permissions: Array, + requestCode: Int, + shouldShowRationale: Boolean, + shouldRequestAutomatically: Boolean + ): PermissionCheckResult { + return if (shouldShowRationale && shouldShowRequestPermissionRationale(fragment, permissions)) { + PermissionCheckResult.getShowRationale() + } else { + if (shouldRequestAutomatically) { + requestPermissions(fragment, permissions, requestCode) + } + PermissionCheckResult.getRequiredRequest() + } + } + + @VisibleForTesting + fun verifyPermissionsResult(grantResults: IntArray): Boolean { + return grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED } + } + + private fun shouldShowRequestPermissionRationale(activity: Activity, permissions: Array): Boolean { + return permissions.any { ActivityCompat.shouldShowRequestPermissionRationale(activity, it) } + } + + private fun shouldShowRequestPermissionRationale(fragment: Fragment, permissions: Array): Boolean { + return permissions.any { fragment.shouldShowRequestPermissionRationale(it) } + } + + private fun isGranted(context: Context, permissions: Array): Boolean { + return isLessMarshmallow() || hasSelfPermissions(context, permissions) + } + + private fun hasSelfPermissions(context: Context, permissions: Array): Boolean { + return permissions.all { hasSelfPermission(context, it) } + } + + private fun hasSelfPermission(context: Context, permission: String): Boolean { + return try { + checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED + } catch (exception: RuntimeException) { + exception.printStackTrace() + false + } + } + + @VisibleForTesting + @Throws(RuntimeException::class) + fun checkSelfPermission(context: Context, permission: String): Int { + return PermissionChecker.checkSelfPermission(context, permission) + } + + @VisibleForTesting + fun isLessMarshmallow(): Boolean { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.M + } + + } +} + diff --git a/library/src/main/java/net/codecision/startask/permissions/model/PermissionCheckResult.kt b/library/src/main/java/net/codecision/startask/permissions/model/PermissionCheckResult.kt new file mode 100644 index 0000000..37c3d23 --- /dev/null +++ b/library/src/main/java/net/codecision/startask/permissions/model/PermissionCheckResult.kt @@ -0,0 +1,47 @@ +package net.codecision.startask.permissions.model + +class PermissionCheckResult private constructor( + val result: Int +) { + + inline fun onGranted(action: () -> Unit): PermissionCheckResult { + if (result == GRANTED_RESULT) { + action() + } + return this + } + + inline fun onShowRationale(action: () -> Unit): PermissionCheckResult { + if (result == SHOW_RATIONALE_RESULT) { + action() + } + return this + } + + inline fun onRequiredRequest(action: () -> Unit): PermissionCheckResult { + if (result == REQUIRED_REQUEST_RESULT) { + action() + } + return this + } + + companion object { + + /** Result: The permission is granted. */ + const val GRANTED_RESULT = 0 + + /** Result: The permission is show rationale. */ + const val SHOW_RATIONALE_RESULT = 1 + + /** Result: The permission need request. */ + const val REQUIRED_REQUEST_RESULT = 3 + + fun getGranted() = PermissionCheckResult(GRANTED_RESULT) + + fun getShowRationale() = PermissionCheckResult(SHOW_RATIONALE_RESULT) + + fun getRequiredRequest() = PermissionCheckResult(REQUIRED_REQUEST_RESULT) + + } + +} diff --git a/library/src/main/java/net/codecision/startask/permissions/model/PermissionRequestResult.kt b/library/src/main/java/net/codecision/startask/permissions/model/PermissionRequestResult.kt new file mode 100644 index 0000000..2b435cd --- /dev/null +++ b/library/src/main/java/net/codecision/startask/permissions/model/PermissionRequestResult.kt @@ -0,0 +1,51 @@ +package net.codecision.startask.permissions.model + +class PermissionRequestResult private constructor( + val result: Int +) { + + inline fun onGranted(action: () -> Unit): PermissionRequestResult { + if (result == GRANTED_RESULT) { + action() + } + return this + } + + inline fun onDenied(action: () -> Unit): PermissionRequestResult { + if (result == DENIED_RESULT) { + action() + } + return this + } + + inline fun onNeverAskAgain(action: () -> Unit): PermissionRequestResult { + if (result == NEVER_ASK_AGAIN_RESULT) { + action() + } + return this + } + + companion object { + + /** Result: The permission is granted. */ + const val GRANTED_RESULT = 0 + + /** Result: The permission is denied. */ + const val DENIED_RESULT = 1 + + /** Result: The permission is never ask again. */ + const val NEVER_ASK_AGAIN_RESULT = 2 + + private const val INCORRECT_CODE_RESULT = -1 + + fun getGranted() = PermissionRequestResult(GRANTED_RESULT) + + fun getDenied() = PermissionRequestResult(DENIED_RESULT) + + fun getNeverAskAgain() = PermissionRequestResult(NEVER_ASK_AGAIN_RESULT) + + fun getIncorrectCode() = PermissionRequestResult(INCORRECT_CODE_RESULT) + + } + +} \ No newline at end of file diff --git a/library/src/test/java/net/codecision/startask/permissions/BaseLibTest.kt b/library/src/test/java/net/codecision/startask/permissions/BaseLibTest.kt new file mode 100644 index 0000000..b33df2b --- /dev/null +++ b/library/src/test/java/net/codecision/startask/permissions/BaseLibTest.kt @@ -0,0 +1,41 @@ +package net.codecision.startask.permissions + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import java.lang.reflect.Field +import java.lang.reflect.Modifier + +open class BaseLibTest { + + @Throws(NoSuchFieldException::class, IllegalAccessException::class, IllegalArgumentException::class) + protected fun setFinalStatic(field: Field, newValue: Any) { + field.isAccessible = true + + val modifiersField = Field::class.java.getDeclaredField("modifiers") + modifiersField.isAccessible = true + modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv()) + + field.set(null, newValue) + } + + protected inline fun d(message: () -> String) { + println(message()) + } + + protected fun getSdkVersionField(): Field { + return Build.VERSION::class.java.getField(SDK_VERSION_FIELD) + } + + companion object { + const val SDK_VERSION_FIELD = "SDK_INT" + + const val GRANTED = PackageManager.PERMISSION_GRANTED + const val DENIED = PackageManager.PERMISSION_DENIED + + const val CAMERA_PERMISSION = Manifest.permission.CAMERA + + const val PERMISSIONS_REQUEST_CODE = 99 + } + +} \ No newline at end of file diff --git a/library/src/test/java/net/codecision/startask/permissions/PermissionCheckResultTest.kt b/library/src/test/java/net/codecision/startask/permissions/PermissionCheckResultTest.kt new file mode 100644 index 0000000..3b41ddf --- /dev/null +++ b/library/src/test/java/net/codecision/startask/permissions/PermissionCheckResultTest.kt @@ -0,0 +1,69 @@ +package net.codecision.startask.permissions + +import net.codecision.startask.permissions.model.PermissionCheckResult +import org.junit.Assert.assertEquals +import org.junit.Test + +class PermissionCheckResultTest : BaseLibTest() { + + @Test + fun getGranted_Result_Granted() { + val permissionCheckResult = PermissionCheckResult.getGranted() + assertEquals(PermissionCheckResult.GRANTED_RESULT, permissionCheckResult.result) + } + + @Test + fun getGranted_Result_ShowRationale() { + val permissionCheckResult = PermissionCheckResult.getShowRationale() + assertEquals(PermissionCheckResult.SHOW_RATIONALE_RESULT, permissionCheckResult.result) + } + + @Test + fun getGranted_Result_RequiredRequest() { + val permissionCheckResult = PermissionCheckResult.getRequiredRequest() + assertEquals(PermissionCheckResult.REQUIRED_REQUEST_RESULT, permissionCheckResult.result) + } + + @Test + fun onGranted_Granted_True() { + PermissionCheckResult.getGranted() + .onGranted { + assert(true) + } + .onShowRationale { + assert(false) + } + .onRequiredRequest { + assert(false) + } + } + + @Test + fun onGranted_ShowRationale_True() { + PermissionCheckResult.getShowRationale() + .onGranted { + assert(false) + } + .onShowRationale { + assert(true) + } + .onRequiredRequest { + assert(false) + } + } + + @Test + fun onGranted_RequiredRequest_True() { + PermissionCheckResult.getRequiredRequest() + .onGranted { + assert(false) + } + .onShowRationale { + assert(false) + } + .onRequiredRequest { + assert(true) + } + } + +} \ No newline at end of file diff --git a/library/src/test/java/net/codecision/startask/permissions/PermissionRequestResultTest.kt b/library/src/test/java/net/codecision/startask/permissions/PermissionRequestResultTest.kt new file mode 100644 index 0000000..c062e01 --- /dev/null +++ b/library/src/test/java/net/codecision/startask/permissions/PermissionRequestResultTest.kt @@ -0,0 +1,71 @@ +package net.codecision.startask.permissions + +import net.codecision.startask.permissions.model.PermissionRequestResult +import org.junit.Assert +import org.junit.Test + +class PermissionRequestResultTest : BaseLibTest() { + + + @Test + fun getGranted_Result_Granted() { + val permissionCheckResult = PermissionRequestResult.getGranted() + Assert.assertEquals(PermissionRequestResult.GRANTED_RESULT, permissionCheckResult.result) + } + + @Test + fun getGranted_Result_Denied() { + val permissionCheckResult = PermissionRequestResult.getDenied() + Assert.assertEquals(PermissionRequestResult.DENIED_RESULT, permissionCheckResult.result) + } + + @Test + fun getGranted_Result_NeverAskAgain() { + val permissionCheckResult = PermissionRequestResult.getNeverAskAgain() + Assert.assertEquals(PermissionRequestResult.NEVER_ASK_AGAIN_RESULT, permissionCheckResult.result) + } + + @Test + fun getGranted_Result_IncorrectCode() { + val permissionCheckResult = PermissionRequestResult.getIncorrectCode() + Assert.assertEquals(-1, permissionCheckResult.result) + } + + @Test + fun onGranted_Granted_True() { + PermissionRequestResult.getGranted() + .onGranted { + assert(true) + }.onDenied { + assert(false) + }.onNeverAskAgain { + assert(false) + } + } + + @Test + fun onGranted_Denied_True() { + PermissionRequestResult.getDenied() + .onGranted { + assert(false) + }.onDenied { + assert(true) + }.onNeverAskAgain { + assert(false) + } + } + + @Test + fun onGranted_NeverAskAgain_True() { + PermissionRequestResult.getNeverAskAgain() + .onGranted { + assert(false) + }.onDenied { + assert(false) + }.onNeverAskAgain { + assert(true) + } + } + + +} \ No newline at end of file diff --git a/library/src/test/java/net/codecision/startask/permissions/PermissionTest.kt b/library/src/test/java/net/codecision/startask/permissions/PermissionTest.kt new file mode 100644 index 0000000..2bddc21 --- /dev/null +++ b/library/src/test/java/net/codecision/startask/permissions/PermissionTest.kt @@ -0,0 +1,71 @@ +package net.codecision.startask.permissions + +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.content.PermissionChecker +import net.codecision.startask.permissions.model.PermissionCheckResult +import net.codecision.startask.permissions.model.PermissionRequestResult +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.M]) +class PermissionTest : BaseLibTest() { + + lateinit var activity: Activity + + @Before + fun setUp() { + val activityController = Robolectric.buildActivity(Activity::class.java) + activity = Mockito.spy(activityController.setup().get()) + } + + @Test + @Throws(Exception::class) + fun checkActivity_NotNull_True() { + Assert.assertNotNull(activity) + } + + @Test + fun checkActivity_Granted_GrantedResult() { + Mockito.`when`(PermissionUtils.checkSelfPermission(getContext(), CAMERA_PERMISSION)) + .thenReturn(PermissionChecker.PERMISSION_GRANTED) + + val permissionCheckResult = Permission.Builder(CAMERA_PERMISSION) + .build() + .check(activity) + assertEquals(PermissionCheckResult.GRANTED_RESULT, permissionCheckResult.result) + } + + @Test + fun isGranted_Granted_True() { + Mockito.`when`(PermissionUtils.checkSelfPermission(getContext(), CAMERA_PERMISSION)) + .thenReturn(PermissionChecker.PERMISSION_GRANTED) + + assert(Permission.Builder(CAMERA_PERMISSION).build().isGranted(activity)) + } + + @Test + fun onRequestPermissionsResult_Granted() { + val permissionRequestResult = Permission.Builder(CAMERA_PERMISSION) + .build() + .onRequestPermissionsResult( + activity, + Permission.PERMISSIONS_REQUEST_CODE, + IntArray(1) { PackageManager.PERMISSION_GRANTED } + ) + assertEquals(PermissionRequestResult.GRANTED_RESULT, permissionRequestResult.result) + } + + private fun getContext() = activity as Context + +} \ No newline at end of file diff --git a/library/src/test/java/net/codecision/startask/permissions/PermissionUtilsTest.kt b/library/src/test/java/net/codecision/startask/permissions/PermissionUtilsTest.kt new file mode 100644 index 0000000..5aaee17 --- /dev/null +++ b/library/src/test/java/net/codecision/startask/permissions/PermissionUtilsTest.kt @@ -0,0 +1,176 @@ +package net.codecision.startask.permissions + +import android.app.Activity +import android.content.Context +import android.os.Build +import androidx.core.app.ActivityCompat +import androidx.core.content.PermissionChecker +import net.codecision.startask.permissions.model.PermissionCheckResult +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.`when` +import org.mockito.Mockito.spy +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.M]) +class PermissionUtilsTest : BaseLibTest() { + + lateinit var activity: Activity + + @Before + fun setUp() { + val activityController = Robolectric.buildActivity(Activity::class.java) + activity = spy(activityController.setup().get()) + } + + @Test + @Throws(Exception::class) + fun checkActivity_NotNull_True() { + assertNotNull(activity) + } + + @Test + fun checkIsLessMarshmallow_22_True() { + changeSdkVersion(Build.VERSION_CODES.LOLLIPOP_MR1) + assert(PermissionUtils.isLessMarshmallow()) + } + + @Test + fun checkIsLessMarshmallow_23_False() { + changeSdkVersion(Build.VERSION_CODES.M) + assertFalse(PermissionUtils.isLessMarshmallow()) + } + + @Test + fun checkIsLessMarshmallow_24_False() { + changeSdkVersion(Build.VERSION_CODES.N) + assertFalse(PermissionUtils.isLessMarshmallow()) + } + + @Test + fun isGranted_Granted_True() { + `when`(PermissionUtils.checkSelfPermission(getContext(), CAMERA_PERMISSION)) + .thenReturn(PermissionChecker.PERMISSION_GRANTED) + assert(PermissionUtils.isGranted(activity, arrayOf(CAMERA_PERMISSION))) + } + + @Test + fun isGranted_Denied_False() { + `when`(PermissionUtils.checkSelfPermission(getContext(), CAMERA_PERMISSION)) + .thenReturn(PermissionChecker.PERMISSION_DENIED) + assertFalse(PermissionUtils.isGranted(activity, arrayOf(CAMERA_PERMISSION))) + } + + @Test + fun isGranted_Exception_False() { + `when`(PermissionUtils.checkSelfPermission(getContext(), CAMERA_PERMISSION)) + .thenThrow(RuntimeException::class.java) + assertFalse(PermissionUtils.isGranted(activity, arrayOf(CAMERA_PERMISSION))) + } + + @Test + fun checkPermissions_GrantedShowRationale_GrantedResult() { + `when`(PermissionUtils.checkSelfPermission(getContext(), CAMERA_PERMISSION)) + .thenReturn(PermissionChecker.PERMISSION_GRANTED) + + val permissionCheckResult = PermissionUtils.checkPermissions( + activity, + arrayOf(CAMERA_PERMISSION), + PERMISSIONS_REQUEST_CODE, + true, + true + ) + + assertEquals(PermissionCheckResult.GRANTED_RESULT, permissionCheckResult.result) + } + + @Test + fun checkPermissions_GrantedNotShowRationale_GrantedResult() { + `when`(PermissionUtils.checkSelfPermission(getContext(), CAMERA_PERMISSION)) + .thenReturn(PermissionChecker.PERMISSION_GRANTED) + + val permissionCheckResult = PermissionUtils.checkPermissions( + activity, + arrayOf(CAMERA_PERMISSION), + PERMISSIONS_REQUEST_CODE, + false, + true + ) + + assertEquals(PermissionCheckResult.GRANTED_RESULT, permissionCheckResult.result) + } + + @Test + fun checkPermissions_DeniedShowRationale_True() { + `when`(PermissionUtils.checkSelfPermission(getContext(), CAMERA_PERMISSION)) + .thenReturn(PermissionChecker.PERMISSION_DENIED) + + `when`(ActivityCompat.shouldShowRequestPermissionRationale(activity, CAMERA_PERMISSION)) + .thenReturn(true) + + val permissionCheckResult = PermissionUtils.checkPermissions( + activity, + arrayOf(CAMERA_PERMISSION), + PERMISSIONS_REQUEST_CODE, + true, + true + ) + + assertEquals(PermissionCheckResult.SHOW_RATIONALE_RESULT, permissionCheckResult.result) + } + + @Test + fun checkPermissions_DeniedNotShowRationale_True() { + `when`(PermissionUtils.checkSelfPermission(getContext(), CAMERA_PERMISSION)) + .thenReturn(PermissionChecker.PERMISSION_DENIED) + + `when`(ActivityCompat.shouldShowRequestPermissionRationale(activity, CAMERA_PERMISSION)) + .thenReturn(true) + + val permissionCheckResult = PermissionUtils.checkPermissions( + activity, + arrayOf(CAMERA_PERMISSION), + PERMISSIONS_REQUEST_CODE, + false, + true + ) + + assertEquals(PermissionCheckResult.REQUIRED_REQUEST_RESULT, permissionCheckResult.result) + } + + @Test + fun verifyPermissionsResult_GrantedOne_True() { + assertTrue(PermissionUtils.verifyPermissionsResult(IntArray(1) { GRANTED })) + } + + @Test + fun verifyPermissionsResult_GrantedTwo_True() { + assertTrue(PermissionUtils.verifyPermissionsResult(IntArray(2) { GRANTED; GRANTED })) + } + + @Test + fun verifyPermissionsResult_GrantedZero_False() { + assertFalse(PermissionUtils.verifyPermissionsResult(IntArray(0))) + } + + @Test + fun verifyPermissionsResult_Denied_False() { + assertFalse(PermissionUtils.verifyPermissionsResult(IntArray(1) { DENIED })) + } + + @Test + fun verifyPermissionsResult_GrantedDenied_False() { + assertFalse(PermissionUtils.verifyPermissionsResult(IntArray(2) { GRANTED; DENIED })) + } + + private fun getContext() = activity as Context + + private fun changeSdkVersion(sdkVersion: Int) = setFinalStatic(getSdkVersionField(), sdkVersion) + +} \ No newline at end of file diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 0000000..818680c --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,30 @@ +rootProject.sampleModulePlugins.each { + apply plugin: it +} + +android { + def ext = rootProject.extensions.ext + compileSdkVersion ext.android.compileSdkVersion + defaultConfig { + applicationId "net.codecision.startask.permissions.sample" + minSdkVersion ext.android.minSdkVersion + targetSdkVersion ext.android.targetSdkVersion + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + rootProject.sampleModuleDependencies.each { + add(it.configuration, it.dependency, it.options) + } +} + +repositories { + mavenCentral() +} diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/sample/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..67d02cb --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/java/net/codecision/startask/permissions/sample/PermissionsActivity.kt b/sample/src/main/java/net/codecision/startask/permissions/sample/PermissionsActivity.kt new file mode 100644 index 0000000..a638c96 --- /dev/null +++ b/sample/src/main/java/net/codecision/startask/permissions/sample/PermissionsActivity.kt @@ -0,0 +1,84 @@ +package net.codecision.startask.permissions.sample + +import android.Manifest +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import kotlinx.android.synthetic.main.activity_permissions.* +import net.codecision.startask.permissions.Permission +import net.codecision.startask.permissions.sample.utils.ktx.setSingleClickListener + +class PermissionsActivity : AppCompatActivity() { + + private val cameraPermission: Permission by lazy { + Permission.Builder(Manifest.permission.CAMERA) + .setRequestCode(MY_PERMISSIONS_REQUEST_CODE) + .build() + } + + @SuppressLint("SetTextI18n") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_permissions) + + initView() + initListeners() + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + onRequestLocationPermissionResult(requestCode, grantResults) + } + + @SuppressLint("SetTextI18n") + private fun checkLocationPermission() { + cameraPermission.check(this) + .onGranted { + statusView.text = "Granted!" + }.onShowRationale { + showRationaleDialog() + } + } + + @SuppressLint("SetTextI18n") + private fun onRequestLocationPermissionResult(requestCode: Int, grantResults: IntArray) { + cameraPermission.onRequestPermissionsResult(this, requestCode, grantResults) + .onGranted { + statusView.text = "Granted!" + }.onDenied { + statusView.text = "Denied!" + }.onNeverAskAgain { + statusView.text = "NeverAskAgain!" + } + } + + private fun showRationaleDialog() { + AlertDialog.Builder(this) + .setTitle("Camera permission") + .setMessage("Allow app to use your camera to take photos and record videos.") + .setPositiveButton("Allow") { _, _ -> + cameraPermission.request(this) + } + .setNegativeButton("Deny") { _, _ -> + + } + .create() + .show() + } + + private fun initView() { + statusView.text = "Unknown!" + } + + private fun initListeners() { + checkButton.setSingleClickListener { + checkLocationPermission() + } + } + + companion object { + const val MY_PERMISSIONS_REQUEST_CODE = 99 + } + +} \ No newline at end of file diff --git a/sample/src/main/java/net/codecision/startask/permissions/sample/utils/ClickController.kt b/sample/src/main/java/net/codecision/startask/permissions/sample/utils/ClickController.kt new file mode 100644 index 0000000..b7d0ddc --- /dev/null +++ b/sample/src/main/java/net/codecision/startask/permissions/sample/utils/ClickController.kt @@ -0,0 +1,24 @@ +package net.codecision.startask.permissions.sample.utils + +object ClickController { + + private const val MIN_CLICK_INTERVAL_MS = 600L + + private var lastClickTime: Long = 0L + + fun isClickAllowed(): Boolean { + val currentTime = getCurrentTimeMillis() + val isClickAllowed = isClickAllowed(currentTime) + + if (isClickAllowed) { + lastClickTime = currentTime + } + + return isClickAllowed + } + + private fun isClickAllowed(currentTime: Long): Boolean = (currentTime - lastClickTime) > MIN_CLICK_INTERVAL_MS + + private fun getCurrentTimeMillis(): Long = System.currentTimeMillis() + +} \ No newline at end of file diff --git a/sample/src/main/java/net/codecision/startask/permissions/sample/utils/ktx/ViewKtx.kt b/sample/src/main/java/net/codecision/startask/permissions/sample/utils/ktx/ViewKtx.kt new file mode 100644 index 0000000..462b033 --- /dev/null +++ b/sample/src/main/java/net/codecision/startask/permissions/sample/utils/ktx/ViewKtx.kt @@ -0,0 +1,12 @@ +package net.codecision.startask.permissions.sample.utils.ktx + +import android.view.View +import net.codecision.startask.permissions.sample.utils.ClickController + +fun View.setSingleClickListener(listener: (v: View) -> Unit) { + this.setOnClickListener { + if (ClickController.isClickAllowed()) { + listener(this) + } + } +} \ No newline at end of file diff --git a/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/sample/src/main/res/drawable/ic_launcher_background.xml b/sample/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/sample/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..1c9c6af --- /dev/null +++ b/sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,14 @@ + + + +