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
+[](https://jitpack.io/#illiashenkoo/startask-permissions)
+
+StartaskPermissions is a library that helps to handle runtime permissions on Android, entirely written using Kotlin language.
+
+
+## Using in your projects
+
+###  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/#illiashenkoo/startask-permissions)
+
+```groovy
+dependencies {
+ implementation "com.github.illiashenkoo:startaskpermissions:${latest.version}"
+}
+```
+
+###  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 @@
+
+
+
+
+
diff --git a/sample/src/main/res/layout/activity_permissions.xml b/sample/src/main/res/layout/activity_permissions.xml
new file mode 100644
index 0000000..b31f96e
--- /dev/null
+++ b/sample/src/main/res/layout/activity_permissions.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a2f5908
Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1b52399
Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher.png b/sample/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..ff10afd
Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..115a4c7
Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..dcd3cd8
Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..459ca60
Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..8ca12fe
Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8e19b41
Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b824ebd
Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..4c19a13
Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3ab3e9c
--- /dev/null
+++ b/sample/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml
new file mode 100644
index 0000000..18e2c6c
--- /dev/null
+++ b/sample/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+ Startask Permissions
+
+ Check Camera Permission!
+
diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml
new file mode 100644
index 0000000..5885930
--- /dev/null
+++ b/sample/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..a683d69
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,3 @@
+include 'library'
+project(':library').name = 'startask-permissions'
+include ':sample'
\ No newline at end of file