Repository: permissions-dispatcher/PermissionsDispatcher Branch: master Commit: 74b532bf1fe4 Files: 185 Total size: 745.7 KB Directory structure: gitextract_y6ni10jf/ ├── .github/ │ ├── ISSUE_TEMPLATE.md │ └── workflows/ │ ├── deploy_snapshot.yml │ ├── ktx-release.yml │ ├── pull_request_ci.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── annotation/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ └── java/ │ └── permissions/ │ └── dispatcher/ │ ├── GrantableRequest.java │ ├── NeedsPermission.java │ ├── OnNeverAskAgain.java │ ├── OnPermissionDenied.java │ ├── OnShowRationale.java │ ├── PermissionRequest.java │ └── RuntimePermissions.java ├── build.gradle ├── buildSrc/ │ ├── build.gradle │ └── src/ │ └── main/ │ └── groovy/ │ └── permissions/ │ └── dispatcher/ │ ├── AarToJarDependencyPlugin.groovy │ ├── AndroidJarDependencyExtension.groovy │ └── AndroidJarDependencyPlugin.groovy ├── doc/ │ ├── java_usage.md │ ├── maxsdkversion.md │ ├── migration_guide.md │ └── special_permissions.md ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── ktx/ │ ├── README.md │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── permissions/ │ │ └── dispatcher/ │ │ └── ktx/ │ │ ├── ActivityExtensions.kt │ │ ├── Event.kt │ │ ├── FragmentExtensions.kt │ │ ├── KtxPermissionRequest.kt │ │ ├── LocationPermission.kt │ │ ├── PermissionRequestFragment.kt │ │ ├── PermissionRequestResult.kt │ │ ├── PermissionRequestType.kt │ │ ├── PermissionRequestViewModel.kt │ │ ├── PermissionsRequester.kt │ │ ├── PermissionsRequesterImpl.kt │ │ └── TypeAliases.kt │ └── test/ │ ├── java/ │ │ └── permissions/ │ │ └── dispatcher/ │ │ └── test/ │ │ ├── EventTest.kt │ │ ├── KtxPermissionRequestTest.kt │ │ ├── LocationPermissionTest.kt │ │ └── PermissionRequestViewModelTest.kt │ └── resources/ │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── ktx-sample/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── permissions/ │ │ └── dispatcher/ │ │ └── ktx/ │ │ └── sample/ │ │ ├── MainActivity.kt │ │ └── MainFragment.kt │ └── res/ │ ├── layout/ │ │ ├── activity_main.xml │ │ ├── fragment_camera.xml │ │ └── fragment_main.xml │ └── values/ │ ├── dimens.xml │ ├── strings.xml │ └── template-styles.xml ├── library/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── permissions/ │ │ └── dispatcher/ │ │ └── PermissionUtils.java │ └── test/ │ └── java/ │ └── permissions/ │ └── dispatcher/ │ └── ApiLevelTestSuite.java ├── lint/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── permissions/ │ │ └── dispatcher/ │ │ ├── CallNeedsPermissionDetector.java │ │ ├── CallOnRequestPermissionsResultDetector.java │ │ ├── NoCorrespondingNeedsPermissionDetector.java │ │ ├── NoDelegateOnResumeDetector.java │ │ └── PermissionsDispatcherIssueRegistry.java │ └── test/ │ └── java/ │ └── permissions/ │ └── dispatcher/ │ ├── CallNeedsPermissionDetectorKtTest.kt │ ├── CallNeedsPermissionDetectorTest.kt │ ├── CallOnRequestPermissionsResultDetectorKtTest.kt │ ├── CallOnRequestPermissionsResultDetectorTest.kt │ ├── NoCorrespondingNeedsPermissionDetectorKtTest.kt │ ├── NoCorrespondingNeedsPermissionDetectorTest.kt │ ├── NoDelegateOnResumeDetectorKtTest.kt │ ├── NoDelegateOnResumeDetectorTest.kt │ ├── PermissionsDispatcherIssueRegistryTest.kt │ └── Utils.kt ├── processor/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── permissions/ │ │ │ └── dispatcher/ │ │ │ └── processor/ │ │ │ ├── PermissionsProcessor.kt │ │ │ ├── ProcessorUnit.kt │ │ │ ├── RequestCodeProvider.kt │ │ │ ├── RuntimePermissionsElement.kt │ │ │ ├── exception/ │ │ │ │ ├── DuplicatedMethodNameException.kt │ │ │ │ ├── DuplicatedValueException.kt │ │ │ │ ├── MixPermissionTypeException.kt │ │ │ │ ├── NoAnnotatedMethodsException.kt │ │ │ │ ├── NoParametersAllowedException.kt │ │ │ │ ├── NoThrowsAllowedException.kt │ │ │ │ ├── PrivateMethodException.kt │ │ │ │ ├── SpecialPermissionsWithNeverAskAgainException.kt │ │ │ │ ├── WrongClassException.kt │ │ │ │ ├── WrongParametersException.kt │ │ │ │ └── WrongReturnTypeException.kt │ │ │ ├── impl/ │ │ │ │ ├── java/ │ │ │ │ │ ├── JavaActivityProcessorUnit.kt │ │ │ │ │ ├── JavaBaseProcessorUnit.kt │ │ │ │ │ ├── JavaFragmentProcessorUnit.kt │ │ │ │ │ ├── SensitivePermissionInterface.kt │ │ │ │ │ ├── SystemAlertWindowHelper.kt │ │ │ │ │ └── WriteSettingsHelper.kt │ │ │ │ └── kotlin/ │ │ │ │ ├── KotlinActivityProcessorUnit.kt │ │ │ │ ├── KotlinBaseProcessorUnit.kt │ │ │ │ ├── KotlinFragmentProcessorUnit.kt │ │ │ │ ├── SensitivePermissionInterface.kt │ │ │ │ ├── SystemAlertWindowHelper.kt │ │ │ │ └── WriteSettingsHelper.kt │ │ │ └── util/ │ │ │ ├── Constants.kt │ │ │ ├── Extensions.kt │ │ │ ├── Helpers.kt │ │ │ └── Validators.kt │ │ └── resources/ │ │ └── META-INF/ │ │ ├── gradle/ │ │ │ └── incremental.annotation.processors │ │ └── services/ │ │ └── javax.annotation.processing.Processor │ └── test/ │ ├── java/ │ │ └── permissions/ │ │ └── dispatcher/ │ │ └── processor/ │ │ ├── KtProcessorTestSuite.kt │ │ ├── ProcessorTestSuite.java │ │ ├── base/ │ │ │ ├── AndroidAwareClassLoader.java │ │ │ ├── BaseTest.java │ │ │ ├── StringEquals.java │ │ │ └── TestSuite.java │ │ ├── data/ │ │ │ └── Source.java │ │ └── util/ │ │ └── ExtensionsTest.kt │ └── resources/ │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── sample/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── kotlin/ │ │ └── permissions/ │ │ └── dispatcher/ │ │ └── sample/ │ │ ├── MainActivity.kt │ │ ├── camera/ │ │ │ ├── CameraPreview.kt │ │ │ └── CameraPreviewFragment.kt │ │ └── contacts/ │ │ └── ContactsFragment.kt │ └── res/ │ ├── layout/ │ │ ├── activity_main.xml │ │ ├── fragment_camera.xml │ │ └── fragment_contacts.xml │ └── values/ │ ├── dimens.xml │ ├── strings.xml │ └── template-styles.xml ├── settings.gradle └── test/ ├── .gitignore ├── build.gradle └── src/ ├── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── permissions/ │ └── dispatcher/ │ └── test/ │ ├── ActivityOnlyNeedsPermission.java │ ├── ActivityWithAllAnnotations.java │ ├── ActivityWithAllAnnotationsKt.kt │ ├── ActivityWithNoParameterArgumentKt.kt │ ├── ActivityWithOnNeverAskAgain.java │ ├── ActivityWithOnPermissionDenied.java │ ├── ActivityWithParametersKt.kt │ ├── ActivityWithShowRationale.java │ ├── ActivityWithSystemAlertWindow.java │ ├── ActivityWithSystemAlertWindowAllAnnotations.java │ ├── ActivityWithSystemAlertWindowKt.kt │ ├── ActivityWithSystemAlertWindowKtAllAnnotations.kt │ ├── ActivityWithWriteSetting.java │ ├── ActivityWithWriteSettingAllAnnotations.java │ ├── ActivityWithWriteSettingKt.kt │ ├── ActivityWithWriteSettingKtAllAnnotations.kt │ ├── FragmentWithAllAnnotations.java │ └── FragmentWithAllAnnotationsKt.kt └── test/ └── java/ └── permissions/ └── dispatcher/ └── test/ ├── ActivityOnlyNeedsPermissionPermissionsDispatcherTest.kt ├── ActivityWithAllAnnotationsKtPermissionsDispatcherTest.kt ├── ActivityWithAllAnnotationsPermissionsDispatcherTest.kt ├── ActivityWithNoParameterArgumentKtPermissionsDispatcherTest.kt ├── ActivityWithOnNeverAskAgainPermissionsDispatcherTest.kt ├── ActivityWithOnPermissionDeniedPermissionsDispatcherTest.kt ├── ActivityWithParametersKtPermissionsDispatcherTest.kt ├── ActivityWithShowRationalePermissionsDispatcherTest.kt ├── ActivityWithSystemAlertWindowAllAnnotationsPermissionsDispatcherTest.kt ├── ActivityWithSystemAlertWindowKtAllAnnotationsTest.kt ├── ActivityWithSystemAlertWindowKtTest.kt ├── ActivityWithSystemAlertWindowPermissionsDispatcherTest.kt ├── ActivityWithWriteSettingAllAnnotationsPermissionsDispatcherTest.kt ├── ActivityWithWriteSettingKtAllAnnotationsTest.kt ├── ActivityWithWriteSettingKtTest.kt ├── ActivityWithWriteSettingPermissionsDispatcherTest.kt ├── Extensions.kt ├── FragmentWithAllAnnotationsKtPermissionsDispatcherTest.kt └── FragmentWithAllAnnotationsPermissionsDispatcherTest.kt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ## FAQs ___ ## Overview - Describe the issue briefly ### Expected - What is the expected behavior? ### Actual - What is the actual behavior? ## Environment - Which library version are you using? - On which devices do you observe the issue? - Note any other information that might be useful ## Reproducible steps - While it's not required, it'd be perfect to add a link to a sample project where you encounter the issue ================================================ FILE: .github/workflows/deploy_snapshot.yml ================================================ name: Deploy snapshot on: pull_request: branches: - master types: [closed] jobs: build: runs-on: ubuntu-latest if: github.event.pull_request.merged == true steps: - uses: actions/checkout@v2 - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Prepare local.properties for testing run: echo "sdk.dir=$ANDROID_HOME" > local.properties - name: Build with Gradle run: ./gradlew clean build - name: Deploy snapshot run: ./gradlew uploadArchives --no-daemon --no-parallel -PSONATYPE_NEXUS_USERNAME=${{secrets.SONATYPE_NEXUS_USERNAME}} -PSONATYPE_NEXUS_PASSWORD=${{secrets.SONATYPE_NEXUS_PASSWORD}} -Psigning.keyId=${{secrets.SIGNING_KEY_ID}} -Psigning.password=${{secrets.SIGNING_PASSWORD}} -Psigning.secretKeyRingFile=$(echo ~/.gradle/secring.gpg) ================================================ FILE: .github/workflows/ktx-release.yml ================================================ name: KTX Release on: push: tags: - 'ktx-*.*.*' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Prepare local.properties for testing run: echo "sdk.dir=$ANDROID_HOME" > local.properties - name: Build with Gradle run: ./gradlew clean build - name: Decode the secret key and place the file run: | echo "${{secrets.SIGNING_SECRET_KEY_RING_FILE}}" > ~/.gradle/secring.gpg.b64 base64 -d ~/.gradle/secring.gpg.b64 > ~/.gradle/secring.gpg - name: Publish project run: ./gradlew ktx:uploadArchives --no-daemon --no-parallel -PSONATYPE_NEXUS_USERNAME=${{secrets.SONATYPE_NEXUS_USERNAME}} -PSONATYPE_NEXUS_PASSWORD=${{secrets.SONATYPE_NEXUS_PASSWORD}} -Psigning.keyId=${{secrets.SIGNING_KEY_ID}} -Psigning.password=${{secrets.SIGNING_PASSWORD}} -Psigning.secretKeyRingFile=$(echo ~/.gradle/secring.gpg) - name: Close the repository run: ./gradlew closeAndReleaseRepository ================================================ FILE: .github/workflows/pull_request_ci.yml ================================================ name: CI for pull request on: pull_request: types: [assigned, opened, synchronize, reopened] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Prepare local.properties for testing run: echo "sdk.dir=$ANDROID_HOME" > local.properties - name: Build with Gradle run: ./gradlew clean check --stacktrace ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - '[0-9]+.[0-9]+.[0-9]+' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Prepare local.properties for testing run: echo "sdk.dir=$ANDROID_HOME" > local.properties - name: Build with Gradle run: ./gradlew clean build - name: Decode the secret key and place the file run: | echo "${{secrets.SIGNING_SECRET_KEY_RING_FILE}}" > ~/.gradle/secring.gpg.b64 base64 -d ~/.gradle/secring.gpg.b64 > ~/.gradle/secring.gpg - name: Publish project run: ./gradlew uploadArchives --no-daemon --no-parallel -PSONATYPE_NEXUS_USERNAME=${{secrets.SONATYPE_NEXUS_USERNAME}} -PSONATYPE_NEXUS_PASSWORD=${{secrets.SONATYPE_NEXUS_PASSWORD}} -Psigning.keyId=${{secrets.SIGNING_KEY_ID}} -Psigning.password=${{secrets.SIGNING_PASSWORD}} -Psigning.secretKeyRingFile=$(echo ~/.gradle/secring.gpg) - name: Close the repository run: ./gradlew closeAndReleaseRepository ================================================ FILE: .gitignore ================================================ .gradle local.properties build/ .idea/ *.iml ================================================ FILE: CHANGELOG.md ================================================ # ChangeLog - 4.9.2 | ktx: 1.1.4 2022/04/04 - Fix: [fix: use ContextCompat instead of PermissionChecker](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/757) - ktx: 1.1.3 2021/11/07 - Fix: [sort permissions to secure identification](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/729) - ktx: 1.1.2 2021/08/14 - Fix: [permissionRequest proceed doesn't work due to KtxPermissionRequest.requestPermission being null](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/742) - ktx: 1.1.1 2021/08/09 - Fix: [Avoid memory leak with ViewModel](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/741) - Fix: [Support device orientation again](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/738) - Fix: [Write settings action bug fix](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/737) - 4.9.1 2021/08/09 - Fix: [Address compile error on Kotlin 1.5](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/733) - ktx: 1.0.5 2021/04/14 - Fix: [Use commitAllowingStateLoss() instead of commitNowAllowingStateLoss() in PermissionRequestFragment dismiss() to avoid exception when performing multiple transactions at same time](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/724) - ktx: 1.0.4 2021/03/16 - Fix: [Wrong behaviour of ktx library when two permissions are requested in one screen](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/717) - ktx: 1.0.3 2021/03/03 - Fix: stop depending on SNAPSHOT library module - ktx: 1.0.2 2021/03/03 - Fix: [System dialog doesn't show up, after manually disable permission from setting](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/705) - ktx: 1.0.1 2020/09/17 - Fix: [fix: wrap PermissionResult with Event](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/689) - ktx: 1.0.0 2020/08/31 - fix: [observe ViewModel only when a permission has not been granted](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/681) - feat: [Location dedicated permission methods](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/679) - 4.8.0 2020/08/31 - Feat: [Bug fix for NeedOnRequestPermissionsResult lint derector](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/680) - ktx: 1.0.0-beta1 - Update: [rewrite internal implementation](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/678) - 4.7.0 2020/03/25 - Feat: [ktx module](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/665) - Update: [Add missing dangerous permissions check in later versions](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/652) - Update: [Use the constant from `PermissionChecker`](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/658) - 4.6.0 2019/10/30 - Fix: lint CallNeedsPermission on same named function [#602](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/602) - Fix: Remove Conductor support [#644](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/644) [#620](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/620) - Fix: Java to Kotlin collections mapping [#643](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/643) - 4.5.0 2019/07/01 - Improve: Incremental annotation processing for Kotlin [#626](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/626) - Fix: lint CallNeedsPermission on same named function [#602](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/602) - Fix: java-kotlin conversion issue [#603](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/603) [#620](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/620) - Fix: Remove redundant BuildConfig file [#607](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/607) - 4.3.1 2019/04/08 - Add: Add support for internal classes [#574](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/606) - 4.3.0 2018/12/31 - Add: Conductor support [#574](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/574) - 4.2.0 2018/12/21 - Add: OnShowRationale API change with keeping backward compatibility [#569](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/569) - Update: Change maven groupId from `com.github.hotchemi` to `org.permissionsdispatcher` [#560](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/560) - Doc: Publish document site! https://permissions-dispatcher.github.io - Fix: Any is translated to java.lang.Object [#545](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/545) - Add: Support Java incremental compilation [#473](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/473) - Update: Drop Xiaomi support [#548](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/548) - 4.1.0 2018/12/07 - Fix: compile time validation for detecting OnNeverAskAgain with special permissions [#549](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/549) - Fix: Fix CallNeedsPermissionDetector to scan only annotated classes [#536](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/536) - 4.0.0 2018/10/20 - Update: [Update androidx ver to 1.0.0](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/530) - Fix: [Add NonNull annotation and use requireActivity](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/526) - Fix: [workaround to convert java String to kotlin String in argument parameter](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/509) - 4.0.0-alpha1 2018/07/12 - Remove: Remove native Fragment support[#498](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/498) - Add: Jetpack support [#494](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/494) - 3.3.2 2018/12/07 - Update: Drop Xiaomi support [#548](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/548) - 3.3.1 2018/06/25 - Internal: Update using Kotlin ver to 1.2.50 [#489](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/489) - Add: Add a lint rule for not call WithPermissionCheck inside onResume [#486](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/486) - Fix: Fix compile error when request SYSTEM_ALERT_WINDOW on SupportFragment [#482](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/482) - Fix: Fix the problem with order matter "Useless @OnShowRationale declaration [#479](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/479) - 3.2.0 2018/04/17 - Update: Address lint for Kotlin project [#460](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/460) - Add: Add JvmName annotation to generated file [#458](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/458) - Update: Deprecate android.app.Fragment [#454](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/454) - Fix: Kotlin file problem with CallOnRequestPermissionsResultDetector [#449](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/449) - 3.1.0 2017/12/18 - Fix: nullable params with Kotlin [#397](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/397) - Fix: PD does not generate kt version of SYSTEM_ALERT_WINDOW [#406](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/406) - 3.0.1 2017/09/17 - Fix: NeedsPermission annotated method with parameter doesn't work in Kotlin [#376](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/376) - Fix: CallNeedsPermission check incorrectly flags calls to methods of different class [#377](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/377) - 3.0.0 2017/09/16 - Add fully [Kotlin support](https://github.com/hotchemi/PermissionsDispatcher/blob/master/doc/kotlin_support.md)! - Allow for Deterministic, Reproducible Builds with sorted inputs [#342](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/342) - Internal: Migrate Lint Rules to UAST [363](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/363) - Rename withCheck to withPermissionCheck [#365](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/365) - Fix CallNeedsPermissionDetector to work correctly [#368](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/368) - 2.4.0 2017/05/01 - Fix `SupportV13MissingException` with newer Gradle [#279](https://github.com/hotchemi/PermissionsDispatcher/issues/279). - Now we bundle support v13 library in the library, you don't have to add v13 dependency by yourself. - If you don't need v13 remove it rxpressly. ref: [README](https://github.com/hotchemi/PermissionsDispatcher#download) - Remove a workaround in the case `targetSdkVersion < 23` [#305](https://github.com/hotchemi/PermissionsDispatcher/issues/305). - If you need the workaround please use older version. - 2.3.2 2017/03/10 - Update minSdkVersion to API level 9 [#286](https://github.com/hotchemi/PermissionsDispatcher/pull/286). - Add new compile-time check [#284](https://github.com/hotchemi/PermissionsDispatcher/pull/284). - Fix the problem with receiver of startActivityForResult [#280](https://github.com/hotchemi/PermissionsDispatcher/pull/280) - Support nested annotated class [#263](https://github.com/hotchemi/PermissionsDispatcher/pull/263) - 2.3.1 2016/12/26 - Fix a bug related to Xiaomi. [#240](https://github.com/hotchemi/PermissionsDispatcher/issues/240). - Address [#244](https://github.com/hotchemi/PermissionsDispatcher/issues/244) just in case. - 2.3.0 2016/12/14 - Start Xiaomi support. [#235](https://github.com/hotchemi/PermissionsDispatcher/pull/235). - Fix slight bug for lint check. [#230](https://github.com/hotchemi/PermissionsDispatcher/pull/230). - 2.2.1 2016/12/08 - Lint update: Migrate to Psi APIs [#227](https://github.com/hotchemi/PermissionsDispatcher/pull/227). - Make sure to support Java 1.6 [#222](https://github.com/hotchemi/PermissionsDispatcher/pull/222). - 2.2.0 2016/09/25 - Support maxSdkVersion [#204](https://github.com/hotchemi/PermissionsDispatcher/pull/204). - Add ProGuard support [#175](https://github.com/hotchemi/PermissionsDispatcher/pull/175). - Some internal cleanup. - 2.1.3 2016/05/12 - Fix [#147](https://github.com/hotchemi/PermissionsDispatcher/pull/147). - 2.1.2 2016/04/11 - Fix [#131](https://github.com/hotchemi/PermissionsDispatcher/pull/131). - Add [#122](https://github.com/hotchemi/PermissionsDispatcher/pull/122). - 2.1.1 2016/03/30 - Fix [#124](https://github.com/hotchemi/PermissionsDispatcher/issues/124). - 2.1.0 2016/03/20 - Fix [#114](https://github.com/hotchemi/PermissionsDispatcher/issues/114). - 2.0.9 2016/03/19 - Fix [#112](https://github.com/hotchemi/PermissionsDispatcher/issues/112). - 2.0.8 2016/03/09 - Fix [#107](https://github.com/hotchemi/PermissionsDispatcher/issues/107). - Fix [#109](https://github.com/hotchemi/PermissionsDispatcher/issues/109). - 2.0.7 2016/02/16 - Kotlin 1.0 support [#98](https://github.com/hotchemi/PermissionsDispatcher/pull/98). - 2.0.6 2016/02/15 - Add lint support [#75](https://github.com/hotchemi/PermissionsDispatcher/pull/75). - Update kotlin version [#91](https://github.com/hotchemi/PermissionsDispatcher/pull/91). - Performance improvement [#93](https://github.com/hotchemi/PermissionsDispatcher/pull/93). - 2.0.5 2016/01/29 - Back to support JDK 1.7. - 2.0.4 2016/01/22 - Fix [Issue #78](https://github.com/hotchemi/PermissionsDispatcher/issues/78) - 2.0.3 2016/01/18 - **This version has a bug which @OnNeverAskAgain is never called. Please don't use it.** - Add [#65](https://github.com/hotchemi/PermissionsDispatcher/pull/65). - Add [#72](https://github.com/hotchemi/PermissionsDispatcher/pull/72). - 2.0.2 2016/01/13 - Fix [#63](https://github.com/hotchemi/PermissionsDispatcher/issues/63). - 2.0.1 2015/12/04 - Add `@NeverAskAgain`. - 1.2.1 2015/09/14 - Fix #14 and #16. - 1.2.0 2015/09/07 - Add `@DeniedPermission` and `@DeniedPermissions`. - 1.1.2 2015/08/26 - Downgrade processor jdk version to 1.7. - 1.1.1 2015/08/25 - Fix some bugs. - 1.1.0 2015/08/24 - Add `@NeedsPermissions` and `@ShowRationales`. - 1.0.1 2015/08/20 - Stop calling rationale methods in the OnRequestPermissionsResult. - 1.0.0 2015/08/19 - Using support v4 compat classes. - 0.9.0 2015/08/18 - Prepare for preview 3(final release). - 0.5.0 2015/08/16 - Initial release. ================================================ FILE: LICENSE ================================================ 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. ================================================ FILE: README.md ================================================ # PermissionsDispatcher ![CI for pull request](https://github.com/permissions-dispatcher/PermissionsDispatcher/workflows/CI%20for%20pull%20request/badge.svg) [![PermissionsDispatcher](https://www.appbrain.com/stats/libraries/shield/permissions_dispatcher.svg)](https://www.appbrain.com/stats/libraries/details/permissions_dispatcher/permissionsdispatcher) - **Fully Kotlin/Java support** - [**Special permissions support**](https://github.com/hotchemi/PermissionsDispatcher/blob/master/doc/special_permissions.md) - **100% reflection-free** PermissionsDispatcher provides a simple annotation-based API to handle runtime permissions. This library lifts the burden that comes with writing a bunch of check statements whether a permission has been granted or not from you, in order to keep your code clean and safe. ## Usage - Kotlin: You can pick either of [ktx](https://github.com/permissions-dispatcher/PermissionsDispatcher/tree/master/ktx) or [kapt](https://github.com/permissions-dispatcher/PermissionsDispatcher#0-prepare-androidmanifest). - Java: [apt](https://github.com/hotchemi/PermissionsDispatcher/blob/master/doc/java_usage.md) Here's a minimum example, in which you register a `MainActivity` which requires `Manifest.permission.CAMERA`. ### 0. Prepare AndroidManifest Add the following line to `AndroidManifest.xml`: `` ### 1. Attach annotations PermissionsDispatcher introduces only a few annotations, keeping its general API concise: > NOTE: Annotated methods must not be `private`. |Annotation|Required|Description| |---|---|---| |`@RuntimePermissions`|**✓**|Register an `Activity` or `Fragment` to handle permissions| |`@NeedsPermission`|**✓**|Annotate a method which performs the action that requires one or more permissions| |`@OnShowRationale`||Annotate a method which explains why the permissions are needed. It passes in a `PermissionRequest` object which can be used to continue or abort the current permission request upon user input. If you don't specify any argument for the method compiler will generate `process${NeedsPermissionMethodName}ProcessRequest` and `cancel${NeedsPermissionMethodName}ProcessRequest`. You can use those methods in place of `PermissionRequest`(ex: with `DialogFragment`)| |`@OnPermissionDenied`||Annotate a method which is invoked if the user doesn't grant the permissions| |`@OnNeverAskAgain`||Annotate a method which is invoked if the user chose to have the device "never ask again" about a permission| ```kotlin @RuntimePermissions class MainActivity : AppCompatActivity(), View.OnClickListener { @NeedsPermission(Manifest.permission.CAMERA) fun showCamera() { supportFragmentManager.beginTransaction() .replace(R.id.sample_content_fragment, CameraPreviewFragment.newInstance()) .addToBackStack("camera") .commitAllowingStateLoss() } @OnShowRationale(Manifest.permission.CAMERA) fun showRationaleForCamera(request: PermissionRequest) { showRationaleDialog(R.string.permission_camera_rationale, request) } @OnPermissionDenied(Manifest.permission.CAMERA) fun onCameraDenied() { Toast.makeText(this, R.string.permission_camera_denied, Toast.LENGTH_SHORT).show() } @OnNeverAskAgain(Manifest.permission.CAMERA) fun onCameraNeverAskAgain() { Toast.makeText(this, R.string.permission_camera_never_askagain, Toast.LENGTH_SHORT).show() } } ``` ### 2. Delegate to generated functions Now generated functions become much more concise and intuitive than Java version! ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById(R.id.button_camera).setOnClickListener { // NOTE: delegate the permission handling to generated function showCameraWithPermissionCheck() } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) // NOTE: delegate the permission handling to generated function onRequestPermissionsResult(requestCode, grantResults) } ``` Check out the [sample](https://github.com/hotchemi/PermissionsDispatcher/tree/master/sample) for more details. ## Other features/plugins - [Getting Special Permissions](https://github.com/hotchemi/PermissionsDispatcher/blob/master/doc/special_permissions.md) - [maxSdkVersion](https://github.com/hotchemi/PermissionsDispatcher/blob/master/doc/maxsdkversion.md) - [IntelliJ plugin](https://github.com/shiraji/permissions-dispatcher-plugin) - [AndroidAnnotations plugin](https://github.com/AleksanderMielczarek/AndroidAnnotationsPermissionsDispatcherPlugin) ## Installation **NOTE:** - If you're using jCenter we've moved on to MavenCentral, see [migration guide](https://github.com/hotchemi/PermissionsDispatcher/blob/master/doc/migration_guide.md). - 4.x only supports [Jetpack](https://developer.android.com/jetpack/). If you still use appcompat 3.x is the way to go. To add PermissionsDispatcher to your project, include the following in your **app module** `build.gradle` file: `${latest.version}` is [![Download](https://maven-badges.herokuapp.com/maven-central/com.github.permissions-dispatcher/permissionsdispatcher/badge.svg)](https://search.maven.org/search?q=g:com.github.permissions-dispatcher) ```groovy dependencies { implementation "com.github.permissions-dispatcher:permissionsdispatcher:${latest.version}" annotationProcessor "com.github.permissions-dispatcher:permissionsdispatcher-processor:${latest.version}" } ``` With Kotlin: ```groovy apply plugin: 'kotlin-kapt' dependencies { implementation "com.github.permissions-dispatcher:permissionsdispatcher:${latest.version}" kapt "com.github.permissions-dispatcher:permissionsdispatcher-processor:${latest.version}" } ``` ## License ``` Copyright 2016 Shintaro Katafuchi, Marcel Schnelle, Yoshinori Isogai Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ================================================ FILE: annotation/.gitignore ================================================ /build ================================================ FILE: annotation/build.gradle ================================================ apply plugin: 'java-library' apply plugin: "com.vanniktech.maven.publish" ================================================ FILE: annotation/gradle.properties ================================================ POM_NAME = permissionsdispatcher-annotation POM_ARTIFACT_ID = permissionsdispatcher-annotation POM_PACKAGING = jar ================================================ FILE: annotation/src/main/java/permissions/dispatcher/GrantableRequest.java ================================================ package permissions.dispatcher; public interface GrantableRequest extends PermissionRequest { void grant(); } ================================================ FILE: annotation/src/main/java/permissions/dispatcher/NeedsPermission.java ================================================ package permissions.dispatcher; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Register some methods which permissions are needed. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface NeedsPermission { String[] value(); int maxSdkVersion() default 0; } ================================================ FILE: annotation/src/main/java/permissions/dispatcher/OnNeverAskAgain.java ================================================ package permissions.dispatcher; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Register some methods handling the user's choice to permanently deny permissions. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface OnNeverAskAgain { String[] value(); } ================================================ FILE: annotation/src/main/java/permissions/dispatcher/OnPermissionDenied.java ================================================ package permissions.dispatcher; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Register some methods which permissions are needed. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface OnPermissionDenied { String[] value(); } ================================================ FILE: annotation/src/main/java/permissions/dispatcher/OnShowRationale.java ================================================ package permissions.dispatcher; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Register some methods which explain why permissions are needed. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface OnShowRationale { String[] value(); } ================================================ FILE: annotation/src/main/java/permissions/dispatcher/PermissionRequest.java ================================================ package permissions.dispatcher; /** * Interface used by {@link OnShowRationale} methods to allow for continuation * or cancellation of a permission request. */ public interface PermissionRequest { void proceed(); void cancel(); } ================================================ FILE: annotation/src/main/java/permissions/dispatcher/RuntimePermissions.java ================================================ package permissions.dispatcher; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Register an Activity or Fragment to handle permissions. */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface RuntimePermissions { } ================================================ FILE: build.gradle ================================================ buildscript { repositories { mavenCentral() google() jcenter() } dependencies { classpath "com.android.tools.build:gradle:$GRADLE_PLUGIN_VERSION" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION" classpath "org.jetbrains.dokka:dokka-gradle-plugin:$DOKKA_VERSION" classpath "com.vanniktech:gradle-maven-publish-plugin:$PUBLISH_PLUGIN_VERSION" } } allprojects { repositories { mavenCentral() google() jcenter() // TODO: remove after publishing kompile-testing to Maven Central maven { url 'https://jitpack.io' } } } ================================================ FILE: buildSrc/build.gradle ================================================ apply plugin: "groovy" repositories { mavenCentral() } dependencies { implementation gradleApi() implementation localGroovy() implementation "commons-io:commons-io:2.6" } ================================================ FILE: buildSrc/src/main/groovy/permissions/dispatcher/AarToJarDependencyPlugin.groovy ================================================ package permissions.dispatcher import org.apache.commons.io.FileUtils import org.gradle.api.DefaultTask import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.file.FileCollection import org.gradle.api.tasks.Input import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import java.util.zip.ZipFile /** * Custom plugin for creating "broken" JARs from AAR files. * * It allows an annotation processor's code generation test * to resolve Android-specific classes on the classpath. * * Of course, the generated JARs aren't able to be loaded in by a ClassLoader, * since they reference Android classes absent in a pure-JVM environment. * However the generated code will compile and serve as the cornerstone * for assertions of an annotation processor that interfaces with Android APIs. * * To do this, a special configuration is created when the plugin is applied. * Any dependency specified in this configuration will be written into * a specific resource file, then loaded into a custom ClassLoader when * the unit tests are executed. Since AARs are not readable by a pure Java * runtime environment, their classes need to be extracted, and converted * into the JAR format. */ class AarToJarConversionPlugin implements Plugin { private static final CONFIGURATION_NAME = "testCompileAar" private static final TASK_NAME = "convertAarsToJar" private static final CLASSPATH_FILE_NAME = "additional-test-classpath.txt" @Override void apply(Project project) { // Custom Configuration def configuration = project.configurations.create(CONFIGURATION_NAME) // Conversion Task def conversionTask = project.tasks.create(TASK_NAME, ConvertAarToJarTask) { inputFiles = configuration jarOutputDir = project.file("$project.buildDir/tmp/converted-aars") classpathOutputDir = project.file("$project.buildDir/resources/test") } // Hook into the task dependency chain project.tasks.getByName("classes").dependsOn conversionTask } static class ConvertAarToJarTask extends DefaultTask { @Input FileCollection inputFiles @OutputDirectory File jarOutputDir @OutputDirectory File classpathOutputDir @TaskAction def convert() { def paths = [] inputFiles.each { file -> // AARs need to be converted to JAR, // other files are piped through to the output folder def destinationFile if (file.name.endsWith(".aar")) { // Classes are contained inside a file named classes.jar, // which resides in an AAR. // Extract & rename it according to the artifact in question, // then copy it over to the output def zipFile = new ZipFile(file) def classesEntry = zipFile.getEntry("classes.jar") def newFileName = file.name.replace(".aar", ".jar") def sourceInputStream = zipFile.getInputStream(classesEntry) destinationFile = new File(jarOutputDir, newFileName) FileUtils.copyInputStreamToFile(sourceInputStream, destinationFile) project.logger.info("Converted AAR '$file.name' to JAR") } else { destinationFile = new File(jarOutputDir, file.name) FileUtils.copyFile(file, destinationFile) } paths += destinationFile.toURI().toURL().toString() } def classpathFile = new File(classpathOutputDir, CLASSPATH_FILE_NAME) classpathFile.withWriter { it.write(paths.join("\n")) } } } } ================================================ FILE: buildSrc/src/main/groovy/permissions/dispatcher/AndroidJarDependencyExtension.groovy ================================================ package permissions.dispatcher import javax.annotation.Nullable class AndroidJarDependencyExtension { @Nullable String directory } ================================================ FILE: buildSrc/src/main/groovy/permissions/dispatcher/AndroidJarDependencyPlugin.groovy ================================================ package permissions.dispatcher import org.gradle.api.Plugin import org.gradle.api.Project /** * This plugin creates a convenient dependency handler * that allows a shorthand way to access an "android.jar" file * of the environment's platform. * * It also adds an extension named "androidJar" to the project, * with which the default directory to search can be overridden. * If no value is specified explicitly, the plugin performs a * lookup of the "sdk.dir" property in the user's local.properties * file, and derives the Android version to include from the * compile SDK version specified in the root-level gradle.properties. */ @SuppressWarnings("GroovyUnusedDeclaration") class AndroidJarDependencyPlugin implements Plugin { private static final EXTENSION_NAME = "androidJar" private static final SDK_DIR_PROPERTY = "sdk.dir" private static final PLATFORM_DIR_PROPERTY = "COMPILE_SDK_VERSION" private static final ANDROID_JAR_FILENAME = "android.jar" @Override void apply(Project project) { def extension = project.extensions.create(EXTENSION_NAME, AndroidJarDependencyExtension) project.dependencies.ext.androidJar = { def directory = extension.directory if (!directory) { // By default, concatenate android.jar path from build environment variables def sdkDir = loadProperties(project.rootProject.file("local.properties")) .getProperty(SDK_DIR_PROPERTY) def platformDir = loadProperties(project.rootProject.file("gradle.properties")) .getProperty(PLATFORM_DIR_PROPERTY) directory = "$sdkDir/platforms/$platformDir" } if (!new File(directory, ANDROID_JAR_FILENAME).exists()) { throw new RuntimeException("can't find android.jar in folder '$directory'!") } def dependency = project.fileTree(dir: directory, includes: [ANDROID_JAR_FILENAME]) return project.dependencies.create(dependency) } } private static Properties loadProperties(File file) { def properties = new Properties() file.withReader { properties.load(it) } return properties } } ================================================ FILE: doc/java_usage.md ================================================ ## Usage with Java Here's a minimum example, in which you register a `MainActivity` which requires `Manifest.permission.CAMERA`. ### 0. Prepare AndroidManifest Add the following line to `AndroidManifest.xml`: `` ### 1. Attach annotations PermissionsDispatcher introduces only a few annotations, keeping its general API concise: > NOTE: Annotated methods must not be `private`. |Annotation|Required|Description| |---|---|---| |`@RuntimePermissions`|**✓**|Register an `Activity` or `Fragment` to handle permissions| |`@NeedsPermission`|**✓**|Annotate a method which performs the action that requires one or more permissions| |`@OnShowRationale`||Annotate a method which explains why the permissions are needed. It passes in a `PermissionRequest` object which can be used to continue or abort the current permission request upon user input. If you don't specify any argument for the method compiler will generate `process${NeedsPermissionMethodName}ProcessRequest` and `cancel${NeedsPermissionMethodName}ProcessRequest`. You can use those methods in place of `PermissionRequest`(ex: with `DialogFragment`)| |`@OnPermissionDenied`||Annotate a method which is invoked if the user doesn't grant the permissions| |`@OnNeverAskAgain`||Annotate a method which is invoked if the user chose to have the device "never ask again" about a permission| ```java @RuntimePermissions public class MainActivity extends AppCompatActivity { @NeedsPermission(Manifest.permission.CAMERA) void showCamera() { getSupportFragmentManager().beginTransaction() .replace(R.id.sample_content_fragment, CameraPreviewFragment.newInstance()) .addToBackStack("camera") .commitAllowingStateLoss(); } @OnShowRationale(Manifest.permission.CAMERA) void showRationaleForCamera(final PermissionRequest request) { new AlertDialog.Builder(this) .setMessage(R.string.permission_camera_rationale) .setPositiveButton(R.string.button_allow, (dialog, button) -> request.proceed()) .setNegativeButton(R.string.button_deny, (dialog, button) -> request.cancel()) .show(); } @OnPermissionDenied(Manifest.permission.CAMERA) void showDeniedForCamera() { Toast.makeText(this, R.string.permission_camera_denied, Toast.LENGTH_SHORT).show(); } @OnNeverAskAgain(Manifest.permission.CAMERA) void showNeverAskForCamera() { Toast.makeText(this, R.string.permission_camera_neverask, Toast.LENGTH_SHORT).show(); } } ``` ### 2. Delegate to generated class Upon compilation, PermissionsDispatcher generates a class for `MainActivityPermissionsDispatcher`([Activity Name] + PermissionsDispatcher), which you can use to safely access these permission-protected methods. The only step you have to do is delegating the work to this helper class: ```java @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button_camera).setOnClickListener(v -> { // NOTE: delegate the permission handling to generated method MainActivityPermissionsDispatcher.showCameraWithPermissionCheck(this); }); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); // NOTE: delegate the permission handling to generated method MainActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults); } ``` Check out the [sample](https://github.com/hotchemi/PermissionsDispatcher/tree/master/sample) for more details. ================================================ FILE: doc/maxsdkversion.md ================================================ ## maxSdkVersion [<uses-permission>](https://developer.android.com/guide/topics/manifest/uses-permission-element.html) has an attribute call `maxSdkVersion`. PermissionsDispatcher support the feature as well. The following sample is for declaring `Manifest.permisison.WRITE_EXTERNAL_STORAGE` up to API level 18. ### 0. AndroidManifest Declare the permission with `maxSdkVersion` attribute ```xml ``` ### 1. Attach annotations with `maxSdkVersion` ```java @RuntimePermissions public class MainActivity extends AppCompatActivity { @NeedsPermission(value = Manifest.permission.WRITE_EXTERNAL_STORAGE, maxSdkVersion = 18) void getStorage() { } } ``` ================================================ FILE: doc/migration_guide.md ================================================ # Migration guide - [Migrating to Maven Central](#migrating-to-maven-central) - [Migrating to 4.x](#migrating-to-4x) - [Migrating to 3.x](#migrating-to-3x) - [Migrating to 2.x](#migrating-to-2x) ## Migrating to Maven Central Since Maven Central only accepts valid URL, we had no other choices but to change groupId again. ```diff dependencies { - implementation "org.permissionsdispatcher:permissionsdispatcher:${latest.version}" + implementation "com.github.permissions-dispatcher:permissionsdispatcher:${latest.version}" - annotationProcessor "org.permissionsdispatcher:permissionsdispatcher-processor:${latest.version}" + annotationProcessor "com.github.permissions-dispatcher:permissionsdispatcher-processor:${latest.version}" } ``` #### KTX NOTE: Due to accidental mistake, artifact id `permissionsdispatcher-ktx` also exists on maven central but `ktx` is the correct one. ```diff dependencies { - implementation "org.permissionsdispatcher:permissionsdispatcher-ktx:${latest.version}" + implementation "com.github.permissions-dispatcher:ktx:${latest.version}" } ``` ## Migrating to 4.x ### Change Maven groupId Issue: [#560](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/560) ```diff dependencies { - implementation "com.github.hotchemi:permissionsdispatcher:${latest.version}" + implementation "org.permissionsdispatcher:permissionsdispatcher:${latest.version}" - annotationProcessor "com.github.hotchemi:permissionsdispatcher-processor:${latest.version}" + annotationProcessor "org.permissionsdispatcher:permissionsdispatcher-processor:${latest.version}" } ``` ### Migrate to AndroidX from AppCompat Issue: [#488](https://github.com/permissions-dispatcher/PermissionsDispatcher/pull/488) From 4.x we only support [Jetpack](https://developer.android.com/jetpack/). Be sure you've gotten AndroidX migration done before upgrading the library version to 4.x. ## Migrating to 3.x ### Method name changing Issue: [#355](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/355) PermissionsDispatcher used to generate `***WithCheck` methods. Starting with 3.0, the suffix of these methods becomes `***WithPermissionCheck`. ```diff - MainActivityPermissionsDispatcher.showCameraWithCheck(this); + MainActivityPermissionsDispatcher.showCameraWithPermissionCheck(this); ``` The motivation of this change is to make generated code more declarative and easy to figure out what'd be going on under the hood. This change is especially beneficial in Kotlin, because the receiver of the generated method is a class annotated with `@RuntimePermissions`, instead of a helper class named `***PermissionsDispatcher`. ### Kotlin support Issue: [#320](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/320) Actually it's been possible to use PermissionsDispatcher with Kotlin already, because of its interoperability with Java. But to give you a more concise and idiomatic experience, we added full Kotlin support which is described in [here](kotlin_support.md). If you're already using PermissionsDispatcher with Kotlin, be aware of the following 2 changes: #### `***WithPermissionCheck` ```diff button.setOnClickListener { - MainActivityPermissionsDispatcher.showCameraWithCheck(this) + showCameraWithPermissionCheck() } ``` #### `onRequestPermissionsResult` ```diff override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) - MainActivityPermissionsDispatcher.onRequestPermissionsResult(requestCode, grantResults) + onRequestPermissionsResult(requestCode, grantResults) } ``` Those methods are defined as extension functions so now you don't have to call `***MainActivityPermissionsDispatcher` class! ## Migrating to 2.x Since the internals of PermissionsDispatcher 2 have undergone a fundamental refactoring, most notably in the switch of languages to Kotlin for our annotation processor, the exposed APIs to users of the library have been tweaked as well. This guide will help you migrate to the latest version in just a few minutes! ### Update the artifacts First of all, make sure you're pulling in the correct version of the library: #### Before ```groovy dependencies { compile "com.github.hotchemi:permissionsdispatcher:1.2.1" apt "com.github.hotchemi:permissionsdispatcher-processor:1.2.1" } ``` #### After ```groovy dependencies { compile "com.github.hotchemi:permissionsdispatcher:2.0.0" apt "com.github.hotchemi:permissionsdispatcher-processor:2.0.0" } ``` *Make sure to change both the library and processor artifacts!* ### @RuntimePermissions Your `Activity` or `Fragment` classes utilizing PermissionsDispatcher still need to be annotated with `@RuntimePermissions`, so nothing has changed there. ### @NeedsPermission While version 1.x of the library had two flavors of the `@NeedsPermission` annotation (both the singular and plural form `@NeedsPermissions`), from now on there is only one. Make sure to change your previous usage of `@NeedsPermissions` to the unified one: #### Before ```java @NeedsPermissions({ CAMERA, WRITE_EXTERNAL_STORAGE }) void setupCamera() { } ``` #### After ```java @NeedsPermission({ CAMERA, WRITE_EXTERNAL_STORAGE }) void setupCamera() { } ``` ### @OnShowRationale Both `@ShowsRationale` and its plural form `@ShowsRationales` have been removed in version 2. They have been unified in the new annotation `@OnShowRationale`, which is used to display a reasoning to your users about why you need to request permissions for specific actions. The signature of methods annotated with this new annotation require a single parameter of type `PermissionRequest`: When PermissionsDispatcher calls this method on your class, the `PermissionRequest` will provide an interface for you to either `cancel()` or `proceed()` the ongoing runtime permission process. This allows you to defer showing a system dialog, for instance if you would like to display your own explanation in a dialog beforehand. And don't worry: If you forget to follow this signature pattern, PermissionsDispatcher will remind you at compile time! This is one of the most exciting new features of PermissionsDispatcher 2: #### Before ```java @ShowsRationale({ CAMERA, WRITE_EXTERNAL_STORAGE }) void showCameraRationale() { // Can't really do much here, since the system dialog is shown immediately afterwards... Toast.makeText(...).show(); } ``` #### After ```java @OnShowRationale({ CAMERA, WRITE_EXTERNAL_STORAGE }) void showCameraRationale(final PermissionRequest request) { // E.g. show a dialog explaining why you need the permission. // Call proceed() or cancel() on the incoming request to continue or abort the current permissions process new AlertDialog.Builder(...) .setPositiveButton("OK", (dialog, which) -> request.proceed()) .setNegativeButton("Abort", (dialog, which) -> request.cancel()) .show(); } ``` ### @OnPermissionDenied The old annotations `@DeniedPermission` and `@DeniedPermissions` have been unified into the new annotation `@OnPermissionDenied`. A method annotated with this annotation will be invoked if the user declines an ongoing permission request. For developers, it's as simple as switching the annotation's name: #### Before ```java @DeniedPermission({ CAMERA, WRITE_EXTERNAL_STORAGE }) void cameraDenied() { } ``` #### After ```java @OnPermissionDenied({ CAMERA, WRITE_EXTERNAL_STORAGE }) void cameraDenied() { } ``` ### That's it! You're good to go and have successfully migrated to PermissionsDispatcher 2.x! We are striving to continue improving this library, so that the hassle of runtime permission handling becomes as convenient as possible for you. ================================================ FILE: doc/special_permissions.md ================================================ ## Special Permissions PermissionsDispatcher takes care of special permissions `Manifest.permission.SYSTEM_ALERT_WINDOW` and `Manifest.permission.WRITE_SETTINGS`. The following sample is to grant `SYSTEM_ALERT_WINDOW`. ### 0. Prepare AndroidManifest Add the following line to `AndroidManifest.xml`: `` ### 1. Attach annotations It's the same as other permissions: ```java @RuntimePermissions public class MainActivity extends AppCompatActivity { @NeedsPermission(Manifest.permission.SYSTEM_ALERT_WINDOW) void systemAlertWindow() { } @OnShowRationale(Manifest.permission.SYSTEM_ALERT_WINDOW) void systemAlertWindowOnShowRationale(final PermissionRequest request) { } @OnPermissionDenied(Manifest.permission.SYSTEM_ALERT_WINDOW) void systemAlertWindowOnPermissionDenied() { } @OnNeverAskAgain(Manifest.permission.SYSTEM_ALERT_WINDOW) void systemAlertWindowOnNeverAskAgain() { } } ``` ### 2. Delegate to generated class Unlike other permissions, special permissions require to call the delegation method at `onActivityResult`: ```java @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button_system_alert_window).setOnClickListener(v -> { // NOTE: delegate the permission handling to generated method MainActivityPermissionsDispatcher.systemAlertWindowWithPermissionCheck(this); }); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { MainActivityPermissionsDispatcher.onActivityResult(this, requestCode); } ``` ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ # Upload configuration USER = hotchemi GROUP = com.github.permissions-dispatcher VERSION_NAME = 4.10.0-SNAPSHOT POM_NAME = PermissionsDispatcher POM_DESCRIPTION = A declarative API to handle Android runtime permissions. POM_INCEPTION_YEAR = 2015 POM_URL = https://github.com/permissions-dispatcher/PermissionsDispatcher POM_SCM_URL = https://github.com/permissions-dispatcher/PermissionsDispatcher POM_SCM_CONNECTION = https://github.com/permissions-dispatcher/PermissionsDispatcher.git POM_LICENCE_NAME = The Apache Software License, Version 2.0 POM_LICENCE_URL = https://www.apache.org/licenses/LICENSE-2.0.txt POM_LICENCE_DIST = repo POM_DEVELOPER_ID = hotchemi POM_DEVELOPER_NAME = Shintaro Katafuchi POM_DEVELOPER_URL = https://github.com/hotchemi # Plugin versions DOKKA_VERSION = 1.4.20 PUBLISH_PLUGIN_VERSION = 0.18.0 GRADLE_PLUGIN_VERSION = 4.2.0 KOTLIN_VERSION = 1.5.20 KOTLIN_METADATA_VERSION = 0.3.0 ANDROIDX_LIBRARY_VERSION= 1.0.0 LIFECYCLE_VERSION = 2.2.0 ARC_TESTING_VERSION = 1.1.1 JAVAPOET_VERSION = 1.9.0 KOTLINPOET_VERSION = 1.3.0 JUNIT_VERSION = 4.12 MOCKITO_VERSION = 2.28.2 MOCKITO_KOTLIN_VERSION = 2.2.0 POWERMOCK_VERSION = 2.0.2 COMPILE_TESTING_VERSION = 0.12 LINT_VERSION = 26.3.2 ROBOLECTRIC_VERSION = 3.3.2 COMMONS_IO_VERSION = 2.6 KOMPILE_TESTING_VERSION = 0.1.4 # Android configuration COMPILE_SDK_VERSION = android-29 TARGET_SDK_VERSION = 29 MIN_SDK_VERSION = 14 SAMPLE_MIN_SDK_VERSION = 16 # Gradle parameters org.gradle.daemon = true org.gradle.jvmargs = -XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m # AndroidX android.useAndroidX = true android.enableJetifier = true ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # 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 APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # 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 nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac 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" -a "$nonstop" = "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 # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @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 set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @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= @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 Windows variants if not "%OS%" == "Windows_NT" goto win9xME_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=%* :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 ================================================ FILE: ktx/README.md ================================================ ## permissionsdispatcher-ktx `permissionsdispatcher-ktx` aims to let developers cope with runtime permissions handling in a declarative style without using annotation processing([kapt](https://kotlinlang.org/docs/reference/kapt.html)). Let's see a minimum example, in which you register a `MainActivity` which requires `Manifest.permission.CAMERA`. ### 0. Declare a permission in AndroidManifest.xml Add the following line to `AndroidManifest.xml`: `` ### 1. Define a requester with `constructPermissionsRequest` The library provides `constructPermissionsRequest` which you can construct a requester object with the given several callback functions to be called in an appropriate situation. ```kotlin /** * @param permissions the permissions [requiresPermission] requires. * @param onShowRationale the method explains why the permissions are required. * @param onPermissionDenied the method invoked if the user doesn't grant the permissions. * @param onNeverAskAgain the method invoked if the user does not deny the permissions with * "never ask again" option. * @param requiresPermission the action requires [permissions]. */ fun FragmentActivity/*(or Fragment)*/.constructPermissionsRequest( vararg permissions: String, onShowRationale: ShowRationaleFunc? = null, onPermissionDenied: Func? = null, onNeverAskAgain: Func? = null, requiresPermission: Func): PermissionsRequester ``` Here you just define `showCamera` and basically that's it! With the library you don't need to manually override `onRequestPermissionsResult`. NOTE: Be sure to construct a requester every time an activity is created to capture the callbacks appropriately. ```kotlin class MainActivity: AppCompatActivity { // constructPermissionsRequest must be invoked every time an activity is created private val showCamera = constructPermissionsRequest(Manifest.permission.CAMERA, onShowRationale = ::onCameraShowRationale, onPermissionDenied = ::onCameraDenied, onNeverAskAgain = ::onCameraNeverAskAgain) { // do something here } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById(R.id.button_camera).setOnClickListener { showCamera.launch() } } private fun onCameraDenied() { Toast.makeText(requireContext(), R.string.permission_camera_denied, Toast.LENGTH_SHORT).show() } private fun onCameraShowRationale(request: PermissionRequest) { request.proceed() } private fun onCameraNeverAskAgain() { Toast.makeText(requireContext(), R.string.permission_camera_never_ask_again, Toast.LENGTH_SHORT).show() } } ``` Check out the [sample](https://github.com/hotchemi/PermissionsDispatcher/tree/master/ktx-sample) for more details. #### Location Permissions Since the location permissions have been one of the most sensitive permission group to deal with, we provide a dedicated method `constructLocationPermissionRequest`. With the method you don't have to think of which API version you can ask [ACCESS_BACKGROUND_LOCATION](https://developer.android.com/about/versions/10/privacy/changes#app-access-device-location)(see the [issue](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/646) for more detail). ```kotlin /** * @param onShowRationale the method explains why the permissions are required. * @param onPermissionDenied the method invoked if the user doesn't grant the permissions. * @param requiresPermission the action requires [permissions]. */ fun FragmentActivity/*(or Fragment)*/.constructLocationPermissionRequest( vararg permissions: LocationPermission, onShowRationale: ShowRationaleFun? = null, onPermissionDenied: Fun? = null, onNeverAskAgain: Fun? = null, requiresPermission: Fun ): PermissionsRequester ``` #### Special Permissions The library also provides `constructWriteSettingsPermissionRequest` and `constructSystemAlertWindowPermissionRequest` to support `WRITE_SETTINGS` and `SYSTEM_ALERT_WINDOW` that requires exceptional handling. ```kotlin /** * @param onShowRationale the method explains why the permissions are required. * @param onPermissionDenied the method invoked if the user doesn't grant the permissions. * @param requiresPermission the action requires [permissions]. */ fun FragmentActivity/*(or Fragment)*/.constructWriteSettingsPermissionRequest( onShowRationale: ShowRationaleFunc? = null, onPermissionDenied: Func? = null, requiresPermission: Func): PermissionsRequester /** * @param onShowRationale the method explains why the permissions are required. * @param onPermissionDenied the method invoked if the user doesn't grant the permissions. * @param requiresPermission the action requires [permissions]. */ fun FragmentActivity/*(or Fragment)*/.constructSystemAlertWindowPermissionRequest( onShowRationale: ShowRationaleFunc? = null, onPermissionDenied: Func? = null, requiresPermission: Func): PermissionsRequester ``` ## Installation `${latest.version}` is [![Download](https://maven-badges.herokuapp.com/maven-central/com.github.permissions-dispatcher/ktx/badge.svg)](https://search.maven.org/artifact/com.github.permissions-dispatcher/ktx/1.0.2/aar) ```groovy dependencies { implementation "com.github.permissions-dispatcher:ktx:${latest.version}" } ``` ================================================ FILE: ktx/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'org.jetbrains.dokka' apply plugin: "com.vanniktech.maven.publish" android { compileSdkVersion COMPILE_SDK_VERSION defaultConfig { minSdkVersion MIN_SDK_VERSION targetSdkVersion TARGET_SDK_VERSION } libraryVariants.all { it.generateBuildConfigProvider.configure { enabled = false } } } dependencies { api "com.github.permissions-dispatcher:permissionsdispatcher:4.8.0" implementation "androidx.core:core:$ANDROIDX_LIBRARY_VERSION" implementation "androidx.appcompat:appcompat:$ANDROIDX_LIBRARY_VERSION" implementation "androidx.fragment:fragment:$ANDROIDX_LIBRARY_VERSION" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VERSION" implementation "androidx.collection:collection:$ANDROIDX_LIBRARY_VERSION" implementation "org.jetbrains.kotlin:kotlin-stdlib:$KOTLIN_VERSION" testImplementation "junit:junit:$JUNIT_VERSION" testImplementation "androidx.test:core:1.3.0-rc01" testImplementation "androidx.test.ext:junit:1.1.2-rc01" testImplementation "androidx.test:runner:1.3.0-rc01" testImplementation "android.arch.core:core-testing:$ARC_TESTING_VERSION" testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$MOCKITO_KOTLIN_VERSION" } ================================================ FILE: ktx/gradle.properties ================================================ POM_NAME = ktx POM_ARTIFACT_ID = ktx POM_PACKAGING = aar VERSION_NAME = 1.2.0-SNAPSHOT ================================================ FILE: ktx/src/main/AndroidManifest.xml ================================================ ================================================ FILE: ktx/src/main/java/permissions/dispatcher/ktx/ActivityExtensions.kt ================================================ package permissions.dispatcher.ktx import android.annotation.SuppressLint import android.provider.Settings import androidx.fragment.app.FragmentActivity /** * Constructs a request for ordinary permissions that require a grant from the user. * Be sure to invoke the method when an activity is created to capture the valid callbacks. * * @param permissions the permissions [requiresPermission] requires. * @param onShowRationale the method explains why the permissions are required. * @param onPermissionDenied the method invoked if the user doesn't grant the permissions. * @param onNeverAskAgain the method invoked if the user does not deny the permissions with * "never ask again" option. * @param requiresPermission the action requires [permissions]. * @see PermissionsRequester */ fun FragmentActivity.constructPermissionsRequest( vararg permissions: String, onShowRationale: ShowRationaleFun? = null, onPermissionDenied: Fun? = null, onNeverAskAgain: Fun? = null, requiresPermission: Fun ): PermissionsRequester = PermissionsRequesterImpl( permissions = permissions, activity = this, onShowRationale = onShowRationale, onPermissionDenied = onPermissionDenied, onNeverAskAgain = onNeverAskAgain, requiresPermission = requiresPermission, permissionRequestType = PermissionRequestType.Normal ) /** * Constructs a request for location permissions that require a grant from the user. * Be sure to invoke the method when an activity is created to capture the valid callbacks. * * @param onShowRationale the method explains why the permissions are required. * @param onPermissionDenied the method invoked if the user doesn't grant the permissions. * @param requiresPermission the action requires [permissions]. * @see PermissionsRequester */ fun FragmentActivity.constructLocationPermissionRequest( vararg permissions: LocationPermission, onShowRationale: ShowRationaleFun? = null, onPermissionDenied: Fun? = null, onNeverAskAgain: Fun? = null, requiresPermission: Fun ): PermissionsRequester = PermissionsRequesterImpl( permissions = permissions.filterByApiLevel(), activity = this, onShowRationale = onShowRationale, onPermissionDenied = onPermissionDenied, onNeverAskAgain = onNeverAskAgain, requiresPermission = requiresPermission, permissionRequestType = PermissionRequestType.Normal ) /** * Constructs a request for [android.Manifest.permission.WRITE_SETTINGS]. * Be sure to invoke the method when an activity is created to capture the valid callbacks. * * @param onShowRationale the method explains why the permissions are required. * @param onPermissionDenied the method invoked if the user doesn't grant the permissions. * @param requiresPermission the action requires [permissions]. * @see PermissionsRequester */ @SuppressLint("InlinedApi") fun FragmentActivity.constructWriteSettingsPermissionRequest( onShowRationale: ShowRationaleFun? = null, onPermissionDenied: Fun? = null, requiresPermission: Fun ): PermissionsRequester = PermissionsRequesterImpl( permissions = arrayOf(Settings.ACTION_MANAGE_WRITE_SETTINGS), activity = this, onShowRationale = onShowRationale, onPermissionDenied = onPermissionDenied, onNeverAskAgain = null, requiresPermission = requiresPermission, permissionRequestType = PermissionRequestType.WriteSettings ) /** * Constructs a request for [android.Manifest.permission.SYSTEM_ALERT_WINDOW]. * Be sure to invoke the method when an activity is created to capture the valid callbacks. * * @param onShowRationale the method explains why the permissions are required. * @param onPermissionDenied the method invoked if the user doesn't grant the permissions. * @param requiresPermission the action requires [permissions]. * @see PermissionsRequester */ @SuppressLint("InlinedApi") fun FragmentActivity.constructSystemAlertWindowPermissionRequest( onShowRationale: ShowRationaleFun? = null, onPermissionDenied: Fun? = null, requiresPermission: Fun ): PermissionsRequester = PermissionsRequesterImpl( permissions = arrayOf(Settings.ACTION_MANAGE_OVERLAY_PERMISSION), activity = this, onShowRationale = onShowRationale, onPermissionDenied = onPermissionDenied, onNeverAskAgain = null, requiresPermission = requiresPermission, permissionRequestType = PermissionRequestType.SystemAlertWindow ) ================================================ FILE: ktx/src/main/java/permissions/dispatcher/ktx/Event.kt ================================================ package permissions.dispatcher.ktx import androidx.annotation.AnyThread /** * Used as a wrapper for data that is exposed via a LiveData that represents an event. * ref: https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150 */ @AnyThread class Event(private val content: T) { @Volatile private var hasBeenHandled = false /** * Returns the content and prevents its use again. */ fun getContentIfNotHandled(): T? = if (hasBeenHandled) { null } else { hasBeenHandled = true content } } ================================================ FILE: ktx/src/main/java/permissions/dispatcher/ktx/FragmentExtensions.kt ================================================ package permissions.dispatcher.ktx import android.annotation.SuppressLint import android.provider.Settings import androidx.fragment.app.Fragment /** * Constructs a request for ordinary permissions that require a grant from the user. * Be sure to invoke the method when an activity is created to capture the valid callbacks. * * @param permissions the permissions [requiresPermission] requires. * @param onShowRationale the method explains why the permissions are required. * @param onPermissionDenied the method invoked if the user doesn't grant the permissions. * @param onNeverAskAgain the method invoked if the user does not deny the permissions with * "never ask again" option. * @param requiresPermission the action requires [permissions]. * @see PermissionsRequester */ fun Fragment.constructPermissionsRequest( vararg permissions: String, onShowRationale: ShowRationaleFun? = null, onPermissionDenied: Fun? = null, onNeverAskAgain: Fun? = null, requiresPermission: Fun ): PermissionsRequester = PermissionsRequesterImpl( permissions = permissions, activity = requireActivity(), onShowRationale = onShowRationale, onPermissionDenied = onPermissionDenied, onNeverAskAgain = onNeverAskAgain, requiresPermission = requiresPermission, permissionRequestType = PermissionRequestType.Normal ) /** * Constructs a request for location permissions that require a grant from the user. * Be sure to invoke the method when an activity is created to capture the valid callbacks. * * @param onShowRationale the method explains why the permissions are required. * @param onPermissionDenied the method invoked if the user doesn't grant the permissions. * @param requiresPermission the action requires [permissions]. * @see PermissionsRequester */ fun Fragment.constructLocationPermissionRequest( vararg permissions: LocationPermission, onShowRationale: ShowRationaleFun? = null, onPermissionDenied: Fun? = null, onNeverAskAgain: Fun? = null, requiresPermission: Fun ): PermissionsRequester = PermissionsRequesterImpl( permissions = permissions.filterByApiLevel(), activity = requireActivity(), onShowRationale = onShowRationale, onPermissionDenied = onPermissionDenied, onNeverAskAgain = onNeverAskAgain, requiresPermission = requiresPermission, permissionRequestType = PermissionRequestType.Normal ) /** * Constructs a request for [android.Manifest.permission.WRITE_SETTINGS]. * Be sure to invoke the method when an activity is created to capture the valid callbacks. * * @param onShowRationale the method explains why the permissions are required. * @param onPermissionDenied the method invoked if the user doesn't grant the permissions. * @param requiresPermission the action requires [permissions]. * @see PermissionsRequester */ @SuppressLint("InlinedApi") fun Fragment.constructWriteSettingsPermissionRequest( onShowRationale: ShowRationaleFun? = null, onPermissionDenied: Fun? = null, requiresPermission: Fun ): PermissionsRequester = PermissionsRequesterImpl( permissions = arrayOf(Settings.ACTION_MANAGE_WRITE_SETTINGS), activity = requireActivity(), onShowRationale = onShowRationale, onPermissionDenied = onPermissionDenied, onNeverAskAgain = null, requiresPermission = requiresPermission, permissionRequestType = PermissionRequestType.WriteSettings ) /** * Constructs a request for [android.Manifest.permission.SYSTEM_ALERT_WINDOW]. * Be sure to invoke the method when an activity is created to capture the valid callbacks. * * @param onShowRationale the method explains why the permissions are required. * @param onPermissionDenied the method invoked if the user doesn't grant the permissions. * @param requiresPermission the action requires [permissions]. * @see PermissionsRequester */ @SuppressLint("InlinedApi") fun Fragment.constructSystemAlertWindowPermissionRequest( onShowRationale: ShowRationaleFun? = null, onPermissionDenied: Fun? = null, requiresPermission: Fun ): PermissionsRequester = PermissionsRequesterImpl( permissions = arrayOf(Settings.ACTION_MANAGE_OVERLAY_PERMISSION), activity = requireActivity(), onShowRationale = onShowRationale, onPermissionDenied = onPermissionDenied, onNeverAskAgain = null, requiresPermission = requiresPermission, permissionRequestType = PermissionRequestType.SystemAlertWindow ) ================================================ FILE: ktx/src/main/java/permissions/dispatcher/ktx/KtxPermissionRequest.kt ================================================ package permissions.dispatcher.ktx import permissions.dispatcher.PermissionRequest import java.lang.ref.WeakReference internal class KtxPermissionRequest( private val requestPermission: WeakReference, private val permissionDenied: WeakReference? ) : PermissionRequest { override fun proceed() { requestPermission.get()?.invoke() } override fun cancel() { permissionDenied?.get()?.invoke() } companion object { fun create(onPermissionDenied: Fun?, requestPermission: Fun) = KtxPermissionRequest( requestPermission = WeakReference(requestPermission), permissionDenied = onPermissionDenied?.let { WeakReference(it) } ) } } ================================================ FILE: ktx/src/main/java/permissions/dispatcher/ktx/LocationPermission.kt ================================================ package permissions.dispatcher.ktx import android.Manifest import android.annotation.SuppressLint import android.os.Build /** * An enum which represents location permission types. * * **See:** [Android Developers](https://developer.android.com/training/location/permissions) */ enum class LocationPermission(internal val permission: String, internal val apiLevel: Int) { FINE(Manifest.permission.ACCESS_FINE_LOCATION, 1), COARSE(Manifest.permission.ACCESS_COARSE_LOCATION, 1), @SuppressLint("InlinedApi") BACKGROUND(Manifest.permission.ACCESS_BACKGROUND_LOCATION, 29) } internal fun Array.filterByApiLevel(sdkVer: Int = Build.VERSION.SDK_INT) = asSequence().filter { it.apiLevel <= sdkVer }.map { it.permission }.toList().toTypedArray() ================================================ FILE: ktx/src/main/java/permissions/dispatcher/ktx/PermissionRequestFragment.kt ================================================ package permissions.dispatcher.ktx import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import permissions.dispatcher.PermissionUtils import permissions.dispatcher.PermissionUtils.verifyPermissions import java.util.* internal sealed class PermissionRequestFragment : Fragment() { protected val requestCode = Random().nextInt(1000) protected lateinit var viewModel: PermissionRequestViewModel override fun onAttach(context: Context?) { super.onAttach(context) retainInstance = true viewModel = ViewModelProvider(requireActivity()).get(PermissionRequestViewModel::class.java) } protected fun dismiss() = fragmentManager?.beginTransaction()?.remove(this)?.commitAllowingStateLoss() internal class NormalRequestPermissionFragment : PermissionRequestFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val permissions = arguments?.getStringArray(BUNDLE_PERMISSIONS_KEY) ?: return requestPermissions(permissions, requestCode) } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == this.requestCode) { // https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/729 val key = permissions.sortedArray().contentToString() if (verifyPermissions(*grantResults)) { viewModel.postPermissionRequestResult(key, PermissionResult.GRANTED) } else { if (!PermissionUtils.shouldShowRequestPermissionRationale(this, *permissions)) { viewModel.postPermissionRequestResult( key, PermissionResult.DENIED_AND_DISABLED ) } else { viewModel.postPermissionRequestResult(key, PermissionResult.DENIED) } } } dismiss() } companion object { const val BUNDLE_PERMISSIONS_KEY = "key:permissions" fun newInstance(permissions: Array) = NormalRequestPermissionFragment().apply { arguments = Bundle().apply { putStringArray(BUNDLE_PERMISSIONS_KEY, permissions) } } } } internal class SpecialRequestPermissionFragment : PermissionRequestFragment() { private lateinit var action: String override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) action = arguments?.getString(BUNDLE_ACTION_KEY) ?: return val packageName = context?.packageName ?: return val uri = Uri.parse("package:$packageName") startActivityForResult(Intent(action, uri), requestCode) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == this.requestCode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(activity) ) { viewModel.postPermissionRequestResult(action, PermissionResult.GRANTED) } else { viewModel.postPermissionRequestResult(action, PermissionResult.DENIED) } } dismiss() } companion object { const val BUNDLE_ACTION_KEY = "key:action" fun newInstance(action: String) = SpecialRequestPermissionFragment().apply { arguments = Bundle().apply { putString(BUNDLE_ACTION_KEY, action) } } } } } ================================================ FILE: ktx/src/main/java/permissions/dispatcher/ktx/PermissionRequestResult.kt ================================================ package permissions.dispatcher.ktx internal enum class PermissionResult { GRANTED, DENIED, DENIED_AND_DISABLED } ================================================ FILE: ktx/src/main/java/permissions/dispatcher/ktx/PermissionRequestType.kt ================================================ package permissions.dispatcher.ktx import android.content.Context import android.os.Build import android.provider.Settings import androidx.annotation.RequiresApi import permissions.dispatcher.PermissionUtils.hasSelfPermissions internal sealed class PermissionRequestType { object Normal : PermissionRequestType() { override fun checkPermissions(context: Context, permissions: Array): Boolean = hasSelfPermissions(context, *permissions) override fun fragment(permissions: Array): PermissionRequestFragment = PermissionRequestFragment.NormalRequestPermissionFragment.newInstance( permissions ) } object SystemAlertWindow : PermissionRequestType() { override fun checkPermissions(context: Context, permissions: Array): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(context) @RequiresApi(Build.VERSION_CODES.M) override fun fragment(permissions: Array): PermissionRequestFragment = PermissionRequestFragment.SpecialRequestPermissionFragment.newInstance( Settings.ACTION_MANAGE_OVERLAY_PERMISSION ) } object WriteSettings : PermissionRequestType() { override fun checkPermissions(context: Context, permissions: Array): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.System.canWrite(context) @RequiresApi(Build.VERSION_CODES.M) override fun fragment(permissions: Array): PermissionRequestFragment = PermissionRequestFragment.SpecialRequestPermissionFragment.newInstance( Settings.ACTION_MANAGE_WRITE_SETTINGS ) } abstract fun checkPermissions(context: Context, permissions: Array): Boolean abstract fun fragment(permissions: Array): PermissionRequestFragment } ================================================ FILE: ktx/src/main/java/permissions/dispatcher/ktx/PermissionRequestViewModel.kt ================================================ package permissions.dispatcher.ktx import androidx.annotation.MainThread import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import java.lang.ref.WeakReference @MainThread internal class PermissionRequestViewModel : ViewModel() { private val permissionRequestResult = MutableLiveData>>() get() { field.value ?: run { field.value = mutableMapOf() } return field } fun postPermissionRequestResult(key: String, value: PermissionResult) { permissionRequestResult.value?.set(key, Event(value)) permissionRequestResult.notifyObserver() } inline fun observe( owner: LifecycleOwner, key: String, requiresPermission: WeakReference, onPermissionDenied: WeakReference?, onNeverAskAgain: WeakReference? ) { permissionRequestResult.observe(owner, { when (it[key]?.getContentIfNotHandled()) { PermissionResult.GRANTED -> requiresPermission.get()?.invoke() PermissionResult.DENIED -> onPermissionDenied?.get()?.invoke() PermissionResult.DENIED_AND_DISABLED -> onNeverAskAgain?.get()?.invoke() else -> Unit } }) } fun removeObservers(owner: LifecycleOwner) = permissionRequestResult.removeObservers(owner) private fun MutableLiveData.notifyObserver() { this.value = this.value } } ================================================ FILE: ktx/src/main/java/permissions/dispatcher/ktx/PermissionsRequester.kt ================================================ package permissions.dispatcher.ktx /** * An intermediate class that is able to launch permissions request process as appropriate. * [launch] method kicks off the actual process. */ interface PermissionsRequester { fun launch() } ================================================ FILE: ktx/src/main/java/permissions/dispatcher/ktx/PermissionsRequesterImpl.kt ================================================ package permissions.dispatcher.ktx import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ViewModelProvider import permissions.dispatcher.PermissionUtils.shouldShowRequestPermissionRationale import java.lang.ref.WeakReference internal class PermissionsRequesterImpl( private val permissions: Array, private val activity: FragmentActivity, private val onShowRationale: ShowRationaleFun?, private val onPermissionDenied: Fun?, private val requiresPermission: Fun, onNeverAskAgain: Fun?, private val permissionRequestType: PermissionRequestType ) : PermissionsRequester { private val viewModel = ViewModelProvider(activity).get(PermissionRequestViewModel::class.java) private val requestFun: Fun = { activity.supportFragmentManager .beginTransaction() .replace(android.R.id.content, permissionRequestType.fragment(permissions)) .commitAllowingStateLoss() } init { viewModel.observe( activity, // https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/729 permissions.sortedArray().contentToString(), WeakReference(requiresPermission), WeakReference(onPermissionDenied), WeakReference(onNeverAskAgain) ) } override fun launch() { if (permissionRequestType.checkPermissions(activity, permissions)) { viewModel.removeObservers(activity) requiresPermission() } else { if (shouldShowRequestPermissionRationale(activity, *permissions) && onShowRationale != null) { onShowRationale.invoke(KtxPermissionRequest.create(onPermissionDenied, requestFun)) } else { requestFun.invoke() } } } } ================================================ FILE: ktx/src/main/java/permissions/dispatcher/ktx/TypeAliases.kt ================================================ package permissions.dispatcher.ktx import permissions.dispatcher.PermissionRequest internal typealias Fun = () -> Unit internal typealias ShowRationaleFun = (PermissionRequest) -> Unit ================================================ FILE: ktx/src/test/java/permissions/dispatcher/test/EventTest.kt ================================================ package permissions.dispatcher.test import junit.framework.Assert.assertEquals import junit.framework.Assert.assertNull import org.junit.Test import permissions.dispatcher.ktx.Event class EventTest { @Test fun `getContentIfNotHandled returns null from the second access`() { val testInstance = Event(1) assertEquals(1, testInstance.getContentIfNotHandled()) assertNull(testInstance.getContentIfNotHandled()) assertNull(testInstance.getContentIfNotHandled()) } } ================================================ FILE: ktx/src/test/java/permissions/dispatcher/test/KtxPermissionRequestTest.kt ================================================ package permissions.dispatcher.test import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import org.junit.Before import org.junit.Test import org.mockito.Mockito.never import permissions.dispatcher.ktx.* class KtxPermissionRequestTest { private lateinit var onPermissionDenied: Fun private lateinit var requiresPermission: Fun @Before fun setUp() { onPermissionDenied = mock() requiresPermission = mock() } @Test fun `PermissionRequest#proceed invokes requiresPermission`() { val request = KtxPermissionRequest.create(onPermissionDenied, requiresPermission) request.proceed() verify(onPermissionDenied, never()).invoke() verify(requiresPermission).invoke() } @Test fun `PermissionRequest#cancel invokes onPermissionDenied`() { val request = KtxPermissionRequest.create(onPermissionDenied, requiresPermission) request.cancel() verify(onPermissionDenied).invoke() verify(requiresPermission, never()).invoke() } } ================================================ FILE: ktx/src/test/java/permissions/dispatcher/test/LocationPermissionTest.kt ================================================ package permissions.dispatcher.test import org.junit.Assert.* import org.junit.Test import permissions.dispatcher.ktx.LocationPermission import permissions.dispatcher.ktx.filterByApiLevel class LocationPermissionTest { @Test fun `BACKGRGROUND should be excluded when sdkVer is less than 29`() { val permissions = LocationPermission.values() .filterByApiLevel(sdkVer = LocationPermission.BACKGROUND.apiLevel - 1) assertEquals(2, permissions.size) assertFalse(permissions.contains(LocationPermission.BACKGROUND.permission)) } @Test fun `BACKGRGROUND should be included when sdkVer equals to 29`() { val permissions = LocationPermission.values() .filterByApiLevel(sdkVer = LocationPermission.BACKGROUND.apiLevel) assertEquals(3, permissions.size) assertTrue(permissions.contains(LocationPermission.BACKGROUND.permission)) } @Test fun `BACKGRGROUND should be included when sdkVer is more than 29`() { val permissions = LocationPermission.values() .filterByApiLevel(sdkVer = LocationPermission.BACKGROUND.apiLevel + 1) assertEquals(3, permissions.size) assertTrue(permissions.contains(LocationPermission.BACKGROUND.permission)) } } ================================================ FILE: ktx/src/test/java/permissions/dispatcher/test/PermissionRequestViewModelTest.kt ================================================ package permissions.dispatcher.test import android.Manifest import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import permissions.dispatcher.ktx.Fun import permissions.dispatcher.ktx.PermissionRequestViewModel import permissions.dispatcher.ktx.PermissionResult import java.lang.ref.WeakReference class PermissionRequestViewModelTest { @Rule @JvmField val instantTaskExecutorRule = InstantTaskExecutorRule() private val permission = arrayOf(Manifest.permission.CAMERA).contentToString() private lateinit var viewModel: PermissionRequestViewModel private lateinit var lifecycleOwner: LifecycleOwner private lateinit var requiresPermission: Fun private lateinit var onPermissionDenied: Fun private lateinit var onNeverAskAgain: Fun @Before fun setup() { viewModel = PermissionRequestViewModel() lifecycleOwner = mock() val lifecycle = LifecycleRegistry(mock()) lifecycle.markState(Lifecycle.State.RESUMED) whenever(lifecycleOwner.lifecycle).thenReturn(lifecycle) onPermissionDenied = mock() requiresPermission = mock() onNeverAskAgain = mock() } @After fun cleanup() { viewModel.removeObservers(lifecycleOwner) } @Test fun `GRANTED emits requiresPermission`() { viewModel.observe( lifecycleOwner, permission, WeakReference(requiresPermission), WeakReference(onPermissionDenied), WeakReference(onNeverAskAgain) ) viewModel.postPermissionRequestResult(permission, PermissionResult.GRANTED) verify(requiresPermission).invoke() verify(onPermissionDenied, never()).invoke() verify(onNeverAskAgain, never()).invoke() } @Test fun `DENIED emits onPermissionDenied`() { viewModel.observe( lifecycleOwner, permission, WeakReference(requiresPermission), WeakReference(onPermissionDenied), WeakReference(onNeverAskAgain) ) viewModel.postPermissionRequestResult(permission, PermissionResult.DENIED) verify(requiresPermission, never()).invoke() verify(onPermissionDenied).invoke() verify(onNeverAskAgain, never()).invoke() } @Test fun `DENIED_AND_DISABLED emits onNeverAskAgain`() { viewModel.observe( lifecycleOwner, permission, WeakReference(requiresPermission), WeakReference(onPermissionDenied), WeakReference(onNeverAskAgain) ) viewModel.postPermissionRequestResult(permission, PermissionResult.DENIED_AND_DISABLED) verify(requiresPermission, never()).invoke() verify(onPermissionDenied, never()).invoke() verify(onNeverAskAgain).invoke() } } ================================================ FILE: ktx/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker ================================================ mock-maker-inline ================================================ FILE: ktx-sample/.gitignore ================================================ /build ================================================ FILE: ktx-sample/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { compileSdkVersion COMPILE_SDK_VERSION defaultConfig { applicationId "permissions.dispatcher.sample" targetSdkVersion TARGET_SDK_VERSION minSdkVersion SAMPLE_MIN_SDK_VERSION versionCode 1 versionName "1.0" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$KOTLIN_VERSION" implementation "androidx.appcompat:appcompat:$ANDROIDX_LIBRARY_VERSION" implementation project(':ktx') } ================================================ FILE: ktx-sample/src/main/AndroidManifest.xml ================================================ ================================================ FILE: ktx-sample/src/main/java/permissions/dispatcher/ktx/sample/MainActivity.kt ================================================ package permissions.dispatcher.ktx.sample import android.os.Bundle import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) if (savedInstanceState == null) { val fragment = MainFragment() supportFragmentManager.beginTransaction() .replace(R.id.container, fragment) .commitNowAllowingStateLoss() } } } ================================================ FILE: ktx-sample/src/main/java/permissions/dispatcher/ktx/sample/MainFragment.kt ================================================ package permissions.dispatcher.ktx.sample import android.Manifest import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Environment import android.provider.MediaStore import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.Toast import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import permissions.dispatcher.PermissionRequest import permissions.dispatcher.ktx.PermissionsRequester import permissions.dispatcher.ktx.constructPermissionsRequest class MainFragment : Fragment() { private lateinit var permissionsRequester: PermissionsRequester private lateinit var fileManagerRequester: PermissionsRequester override fun onAttach(context: Context?) { super.onAttach(context) permissionsRequester = constructPermissionsRequest( Manifest.permission.CAMERA, onShowRationale = ::onCameraShowRationale, onPermissionDenied = ::onCameraDenied, onNeverAskAgain = ::onCameraNeverAskAgain, requiresPermission = ::openCamera ) fileManagerRequester = constructPermissionsRequest( Manifest.permission.READ_EXTERNAL_STORAGE, requiresPermission = ::openFileManager ) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = inflater.inflate(R.layout.fragment_main, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.findViewById