├── .github └── workflows │ ├── compilation-check.yml │ └── publish.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img └── logo.png ├── permissions-avfoundation ├── build.gradle.kts └── src │ └── iosMain │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── permissions │ └── avfoundation │ └── AVCaptureDelegate.kt ├── permissions-bluetooth ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── bluetooth │ │ └── BluetoothPermissions.android.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── bluetooth │ │ └── BluetoothPermissions.kt │ └── iosMain │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── permissions │ └── bluetooth │ └── BluetoothPermissionDelegate.kt ├── permissions-camera ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── camera │ │ └── CameraPermission.android.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── camera │ │ └── CameraPermission.kt │ └── iosMain │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── permissions │ └── camera │ └── CameraPermission.ios.kt ├── permissions-compose ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── compose │ │ ├── BindEffect.android.kt │ │ └── PermissionsControllerFactory.android.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── compose │ │ ├── BindEffect.kt │ │ └── PermissionsControllerFactory.kt │ └── iosMain │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── permissions │ └── compose │ ├── BindEffect.ios.kt │ └── PermissionsControllerFactory.ios.kt ├── permissions-contacts ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── contacts │ │ └── ContactsPermission.android.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── contacts │ │ └── ContactPermission.kt │ └── iosMain │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── permissions │ └── contacts │ └── ContactsPermission.ios.kt ├── permissions-gallery ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── gallery │ │ └── GalleryPermission.android.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── gallery │ │ └── GalleryPermission.kt │ └── iosMain │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── permissions │ └── gallery │ └── GalleryPermissionDelegate.kt ├── permissions-location ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── location │ │ └── LocationPermission.android.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── location │ │ └── LocationPermission.kt │ └── iosMain │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── permissions │ └── location │ ├── LocationManagerDelegate.kt │ └── LocationPermission.ios.kt ├── permissions-microphone ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── microphone │ │ └── MicrophonePermission.android.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── microphone │ │ └── MicrophonePermission.kt │ └── iosMain │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── permissions │ └── microphone │ └── MicrophonePermission.ios.kt ├── permissions-motion ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── motion │ │ └── MotionPermission.android.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── motion │ │ └── MotionPermission.kt │ └── iosMain │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── permissions │ └── motion │ └── MotionPermission.ios.kt ├── permissions-notifications ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── notifications │ │ └── NotificationPermission.android.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── notifications │ │ └── NotificationPermission.kt │ └── iosMain │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── permissions │ └── notifications │ └── RemoteNotificationPermissionDelegate.kt ├── permissions-storage ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── storage │ │ └── StoragePermissions.android.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── storage │ │ └── StoragePermissions.kt │ └── iosMain │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── permissions │ └── storage │ ├── AlwaysGrantedDelegate.kt │ └── StoragePermissions.ios.kt ├── permissions-test ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── test │ │ └── PermissionsControllerMock.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ └── test │ │ └── PermissionsControllerMock.kt │ └── iosMain │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── permissions │ └── test │ └── PermissionsControllerMock.kt ├── permissions ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ ├── PermissionDelegate.android.kt │ │ ├── PermissionsController.kt │ │ └── PermissionsControllerImpl.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── permissions │ │ ├── DeniedExceptions.kt │ │ ├── Permission.kt │ │ ├── PermissionDelegate.kt │ │ ├── PermissionState.kt │ │ ├── PermissionsController.kt │ │ └── RequestCanceledException.kt │ └── iosMain │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── permissions │ ├── Async.kt │ ├── MainRunDispatcher.kt │ ├── PermissionDelegate.ios.kt │ ├── PermissionsController.kt │ └── ios │ ├── PermissionsController.kt │ └── PermissionsControllerProtocol.kt ├── sample ├── android-app │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── icerockdev │ │ │ └── MainActivity.kt │ │ └── res │ │ └── layout │ │ └── activity_main.xml ├── compose-android-app │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── icerockdev │ │ └── MainActivity.kt ├── gradlew ├── ios-app │ ├── Podfile │ ├── Podfile.lock │ ├── TestProj.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ ├── TestProj.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── src │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── Resources │ │ └── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ └── TestViewController.swift └── mpp-library │ ├── MultiPlatformLibrary.podspec │ ├── build.gradle.kts │ └── src │ ├── androidMain │ └── AndroidManifest.xml │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── icerockdev │ │ └── library │ │ └── SampleViewModel.kt │ └── commonTest │ └── kotlin │ └── com │ └── icerockdev │ └── library │ └── SampleViewModelTest.kt └── settings.gradle.kts /.github/workflows/compilation-check.yml: -------------------------------------------------------------------------------- 1 | name: KMP library compilation check 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - develop 8 | 9 | jobs: 10 | build: 11 | runs-on: macOS-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Set up JDK 17 16 | uses: actions/setup-java@v1 17 | with: 18 | java-version: 17 19 | - name: Build and publish local 20 | run: ./gradlew build publishToMavenLocal syncMultiPlatformLibraryDebugFrameworkIosX64 21 | - name: Install pods 22 | run: cd sample/ios-app && pod install 23 | - name: Check iOS 24 | run: cd sample/ios-app && set -o pipefail && xcodebuild -scheme TestProj -workspace TestProj.xcworkspace -configuration Debug -sdk iphonesimulator -arch x86_64 build CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | xcpretty -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version' 8 | default: '0.1.0' 9 | required: true 10 | 11 | jobs: 12 | publish: 13 | name: Publish library at mavenCentral 14 | runs-on: macOS-latest 15 | env: 16 | OSSRH_USER: ${{ secrets.OSSRH_USER }} 17 | OSSRH_KEY: ${{ secrets.OSSRH_KEY }} 18 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEYID }} 19 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 20 | SIGNING_KEY: ${{ secrets.GPG_KEY_CONTENTS }} 21 | 22 | steps: 23 | - uses: actions/checkout@v1 24 | - name: Set up JDK 17 25 | uses: actions/setup-java@v1 26 | with: 27 | java-version: 17 28 | - name: Prebuild library 29 | run: ./gradlew publishToMavenLocal 30 | - name: Publish 31 | run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository 32 | 33 | release: 34 | name: Create release 35 | needs: publish 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Create Release 39 | id: create_release 40 | uses: actions/create-release@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | commitish: ${{ github.ref }} 45 | tag_name: release/${{ github.event.inputs.version }} 46 | release_name: ${{ github.event.inputs.version }} 47 | body: "Will be filled later" 48 | draft: true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .settings 3 | .project 4 | .classpath 5 | .vscode 6 | .idea 7 | build 8 | *.iml 9 | Pods 10 | xcuserdata 11 | local.properties 12 | local.gradle -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Do’s and Don’ts 2 | 3 | * **Search tickets before you file a new one.** Add to tickets if you have new information about the issue. 4 | * **Keep tickets short but sweet.** Make sure you include all the context needed to solve the issue. Don't overdo it. Great tickets allow us to focus on solving problems instead of discussing them. 5 | * **Take care of your ticket.** When you spend time to report a ticket with care we'll enjoy fixing it for you. 6 | * **Use [GitHub-flavored Markdown](https://help.github.com/articles/markdown-basics/).** Especially put code blocks and console outputs in backticks (```` ``` ````). That increases the readability. Bonus points for applying the appropriate syntax highlighting. 7 | 8 | ## Bug Reports 9 | 10 | In short, since you are most likely a developer, provide a ticket that you _yourself_ would _like_ to receive. 11 | 12 | First check if you are using the latest library version and Kotlin version before filing a ticket. 13 | 14 | Please include steps to reproduce and _all_ other relevant information, including any other relevant dependency and version information. 15 | 16 | ## Feature Requests 17 | 18 | Please try to be precise about the proposed outcome of the feature and how it 19 | would related to existing features. 20 | 21 | 22 | ## Pull Requests 23 | 24 | We **love** pull requests! 25 | 26 | All contributions _will_ be licensed under the Apache 2 license. 27 | 28 | Code/comments should adhere to the following rules: 29 | 30 | * Names should be descriptive and concise. 31 | * Use four spaces and no tabs. 32 | * Remember that source code usually gets written once and read often: ensure 33 | the reader doesn't have to make guesses. Make sure that the purpose and inner 34 | logic are either obvious to a reasonably skilled professional, or add a 35 | comment that explains it. 36 | * Please add a detailed description. 37 | 38 | If you consistently contribute improvements and/or bug fixes, we're happy to make you a maintainer. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![moko-permissions](img/logo.png) 2 | [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) [![Download](https://img.shields.io/maven-central/v/dev.icerock.moko/permissions) ](https://repo1.maven.org/maven2/dev/icerock/moko/permissions) ![kotlin-version](https://kotlin-version.aws.icerock.dev/kotlin-version?group=dev.icerock.moko&name=permissions) 3 | 4 | # Mobile Kotlin runtime permissions multiplatform controller 5 | **moko-permissions** - Kotlin MultiPlatform library for providing runtime permissions on iOS & Android. 6 | 7 | ## Table of Contents 8 | - [Features](#features) 9 | - [Requirements](#requirements) 10 | - [Installation](#installation) 11 | - [List of supported permissions](#list-of-supported-permissions) 12 | - [Usage](#usage) 13 | - [Samples](#samples) 14 | - [Set Up Locally](#set-up-locally) 15 | - [Contributing](#contributing) 16 | - [License](#license) 17 | 18 | ## Features 19 | - **Permission** - enumeration with primary types of device permissions 20 | - **PermissionsController** - handler for runtime permission requests can be used in the common code with lifecycle safety for Android 21 | - **DeniedException** and **DeniedAlwaysException** - exceptions to handle user denial of permissions 22 | - **Compose Multiplatform** support 23 | 24 | ## Requirements 25 | - Gradle version 6.8+ 26 | - Android API 16+ 27 | - iOS version 12.0+ 28 | 29 | ## Installation 30 | root **build.gradle** 31 | ```groovy 32 | allprojects { 33 | repositories { 34 | mavenCentral() 35 | } 36 | } 37 | ``` 38 | 39 | project **build.gradle** 40 | ```groovy 41 | dependencies { 42 | commonMainApi("dev.icerock.moko:permissions:0.20.1") 43 | 44 | // specific permissions support 45 | commonMainImplementation("dev.icerock.moko:permissions-bluetooth:0.20.1") 46 | commonMainImplementation("dev.icerock.moko:permissions-camera:0.20.1") 47 | commonMainImplementation("dev.icerock.moko:permissions-contacts:0.20.1") 48 | commonMainImplementation("dev.icerock.moko:permissions-gallery:0.20.1") 49 | commonMainImplementation("dev.icerock.moko:permissions-location:0.20.1") 50 | commonMainImplementation("dev.icerock.moko:permissions-microphone:0.20.1") 51 | commonMainImplementation("dev.icerock.moko:permissions-motion:0.20.1") 52 | commonMainImplementation("dev.icerock.moko:permissions-notifications:0.20.1") 53 | commonMainImplementation("dev.icerock.moko:permissions-storage:0.20.1") 54 | 55 | // compose multiplatform 56 | commonMainApi("dev.icerock.moko:permissions-compose:0.20.1") // permissions api + compose extensions 57 | 58 | commonTestImplementation("dev.icerock.moko:permissions-test:0.20.1") 59 | } 60 | ``` 61 | 62 | ## List of supported permissions 63 | 64 | * `dev.icerock.moko:permissions-bluetooth` 65 | * Bluetooth LE: **Permission.BLUETOOTH_LE** 66 | * Bluetooth Scan: **Permission.BLUETOOTH_SCAN** 67 | * Bluetooth Connect: **Permission.BLUETOOTH_CONNECT** 68 | * Bluetooth Advertise: **Permission.BLUETOOTH_ADVERTISE** 69 | * `dev.icerock.moko:permissions-camera` 70 | * Camera: **Permission.CAMERA** 71 | * `dev.icerock.moko:permissions-contacts` 72 | * Contacts: **Permission.CONTACTS** 73 | * `dev.icerock.moko:permissions-gallery` 74 | * Gallery: **Permission.GALLERY** 75 | * `dev.icerock.moko:permissions-location` 76 | * Fine location: **Permission.LOCATION** 77 | * Coarse location: **Permission.COARSE_LOCATION** 78 | * Background location: **Permission.BACKGROUND_LOCATION** 79 | * `dev.icerock.moko:permissions-microphone` 80 | * Audio recording: **Permission.RECORD_AUDIO** 81 | * `dev.icerock.moko:permissions-motion` 82 | * Motion: **Permission.MOTION** 83 | * `dev.icerock.moko:permissions-notifications` 84 | * Remote notifications: **Permission.REMOTE_NOTIFICATION** 85 | * `dev.icerock.moko:permissions-storage` 86 | * Storage read: **Permission.STORAGE** 87 | * Storage write: **Permission.WRITE_STORAGE** 88 | 89 | ## Usage 90 | 91 | Common code: 92 | ```kotlin 93 | class ViewModel(val permissionsController: PermissionsController): ViewModel() { 94 | fun onPhotoPressed() { 95 | viewModelScope.launch { 96 | try { 97 | permissionsController.providePermission(Permission.GALLERY) 98 | // Permission has been granted successfully. 99 | } catch(deniedAlways: DeniedAlwaysException) { 100 | // Permission is always denied. 101 | } catch(denied: DeniedException) { 102 | // Permission was denied. 103 | } 104 | } 105 | } 106 | } 107 | ``` 108 | 109 | Android: 110 | ```kotlin 111 | override fun onCreate(savedInstanceState: Bundle?) { 112 | super.onCreate(savedInstanceState) 113 | 114 | val viewModel = getViewModel { 115 | // Pass the platform implementation of the permission controller to a common code. 116 | ViewModel(PermissionsController()) 117 | } 118 | 119 | // Binds the permissions controller to the activity lifecycle. 120 | viewModel.permissionsController.bind(activity) 121 | } 122 | ``` 123 | 124 | Compose: 125 | ```kotlin 126 | @Composable 127 | fun TestScreen() { 128 | val viewModel = getViewModel { 129 | // Pass the platform implementation of the permission controller to a common code. 130 | ViewModel(PermissionsController()) 131 | } 132 | 133 | // Binds the permissions controller to the LocalLifecycleOwner lifecycle. 134 | BindEffect(viewModel.permissionsController) 135 | } 136 | ``` 137 | 138 | iOS: 139 | ```swift 140 | // Just pass the platform implementation of the permission controller to a common code. 141 | let viewModel = ViewModel(permissionsController: PermissionsController()) 142 | ``` 143 | 144 | ### Compose Multiplatform 145 | ```kotlin 146 | @Composable 147 | fun Sample() { 148 | val factory: PermissionsControllerFactory = rememberPermissionsControllerFactory() 149 | val controller: PermissionsController = remember(factory) { factory.createPermissionsController() } 150 | val coroutineScope: CoroutineScope = rememberCoroutineScope() 151 | 152 | Button( 153 | onClick = { 154 | coroutineScope.launch { 155 | controller.providePermission(Permission.REMOTE_NOTIFICATION) 156 | } 157 | } 158 | ) { 159 | Text(text = "give permissions") 160 | } 161 | } 162 | ``` 163 | 164 | Or with `moko-mvvm` with correct configuration change handle on android: 165 | ```kotlin 166 | @Composable 167 | fun Sample() { 168 | val factory: PermissionsControllerFactory = rememberPermissionsControllerFactory() 169 | val viewModel: PermissionsViewModel = getViewModel( 170 | key = "permissions-screen", 171 | factory = viewModelFactory { PermissionsViewModel(factory.createPermissionsController()) } 172 | ) 173 | 174 | BindEffect(viewModel.permissionsController) 175 | 176 | Button(onClick = viewModel::onButtonClick) { 177 | Text(text = "give permissions") 178 | } 179 | } 180 | 181 | class PermissionsViewModel( 182 | val permissionsController: PermissionsController 183 | ) : ViewModel() { 184 | fun onButtonClick() { 185 | viewModelScope.launch { 186 | permissionsController.providePermission(Permission.REMOTE_NOTIFICATION) 187 | } 188 | } 189 | } 190 | ``` 191 | 192 | ## Samples 193 | More examples can be found in the [sample directory](sample). 194 | 195 | ## Set Up Locally 196 | - In [permissions directory](permissions) contains `permissions` library; 197 | - In [sample directory](sample) contains samples on android, ios & mpp-library connected to apps. 198 | 199 | ## Contributing 200 | All development (both new features and bug fixes) is performed in `develop` branch. This way `master` sources always contain sources of the most recently released version. Please send PRs with bug fixes to `develop` branch. Fixes to documentation in markdown files are an exception to this rule. They are updated directly in `master`. 201 | 202 | The `develop` branch is pushed to `master` during release. 203 | 204 | More detailed guide for contributors see in [contributing guide](CONTRIBUTING.md). 205 | 206 | ## License 207 | 208 | Copyright 2019 IceRock MAG Inc 209 | 210 | Licensed under the Apache License, Version 2.0 (the "License"); 211 | you may not use this file except in compliance with the License. 212 | You may obtain a copy of the License at 213 | 214 | http://www.apache.org/licenses/LICENSE-2.0 215 | 216 | Unless required by applicable law or agreed to in writing, software 217 | distributed under the License is distributed on an "AS IS" BASIS, 218 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 219 | See the License for the specific language governing permissions and 220 | limitations under the License. 221 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | buildscript { 6 | repositories { 7 | mavenCentral() 8 | google() 9 | gradlePluginPortal() 10 | } 11 | 12 | dependencies { 13 | classpath(libs.kotlinGradlePlugin) 14 | classpath(libs.androidGradlePlugin) 15 | classpath(libs.mokoGradlePlugin) 16 | classpath(libs.mobileMultiplatformGradlePlugin) 17 | classpath(libs.kotlinSerializationGradlePlugin) 18 | classpath(libs.composeJetBrainsGradlePlugin) 19 | classpath(libs.detektGradlePlugin) 20 | } 21 | } 22 | 23 | apply(plugin = "dev.icerock.moko.gradle.publication.nexus") 24 | val mokoVersion = libs.versions.mokoPermissionsVersion.get() 25 | allprojects { 26 | group = "dev.icerock.moko" 27 | version = mokoVersion 28 | } 29 | 30 | tasks.register("clean", Delete::class).configure { 31 | delete(rootProject.buildDir) 32 | } 33 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 2 | org.gradle.configureondemand=false 3 | org.gradle.parallel=true 4 | 5 | kotlin.code.style=official 6 | 7 | kotlin.mpp.androidSourceSetLayoutVersion=2 8 | 9 | android.useAndroidX=true 10 | 11 | moko.android.targetSdk=34 12 | moko.android.compileSdk=34 13 | moko.android.minSdk=16 14 | 15 | moko.publish.name=MOKO permissions 16 | moko.publish.description=Runtime permissions controls for mobile (android & ios) Kotlin Multiplatform development 17 | moko.publish.repo.org=icerockdev 18 | moko.publish.repo.name=moko-permissions 19 | moko.publish.license=Apache-2.0 20 | moko.publish.developers=alex009|Aleksey Mikhailov|Aleksey.Mikhailov@icerockdev.com 21 | 22 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlinVersion = "1.9.10" 3 | androidAppCompatVersion = "1.6.1" 4 | androidxCoreVersion = "1.9.0" # TODO we have two versions (1.8 + 1.9) - unify them 5 | composeMaterialVersion = "1.4.1" 6 | composeActivityVersion = "1.7.0" 7 | activityVersion = "1.7.0" 8 | materialDesignVersion = "1.8.0" 9 | androidLifecycleVersion = "2.2.0" 10 | androidCoreTestingVersion = "2.2.0" 11 | coroutinesVersion = "1.6.4" 12 | mokoMvvmVersion = "0.16.0" 13 | mokoPermissionsVersion = "0.20.1" 14 | composeJetBrainsVersion = "1.7.1" 15 | lifecycleRuntime = "2.6.1" 16 | composeUiVersion = "1.0.1" 17 | 18 | [libraries] 19 | androidxCore = { module = "androidx.core:core", version.ref = "androidxCoreVersion" } 20 | appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" } 21 | material = { module = "com.google.android.material:material", version.ref = "materialDesignVersion" } 22 | composeMaterial = { module = "androidx.compose.material:material", version.ref = "composeMaterialVersion" } 23 | composeActivity = { module = "androidx.activity:activity-compose", version.ref = "composeActivityVersion" } 24 | activity = { module = "androidx.activity:activity", version.ref = "activityVersion" } 25 | composeUi = { module = "androidx.compose.ui:ui", version.ref = "composeUiVersion" } 26 | lifecycle = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "androidLifecycleVersion" } 27 | lifecycleRuntime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntime" } 28 | coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" } 29 | mokoMvvmCore = { module = "dev.icerock.moko:mvvm-core", version.ref = "mokoMvvmVersion" } 30 | mokoMvvmTest = { module = "dev.icerock.moko:mvvm-test", version.ref = "mokoMvvmVersion" } 31 | kotlinTestJUnit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlinVersion" } 32 | androidCoreTesting = { module = "androidx.arch.core:core-testing", version.ref = "androidCoreTestingVersion" } 33 | 34 | 35 | kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinVersion" } 36 | androidGradlePlugin = { module = "com.android.tools.build:gradle", version = "7.4.2" } 37 | mokoGradlePlugin = { module = "dev.icerock.moko:moko-gradle-plugin", version = "0.5.1" } 38 | mobileMultiplatformGradlePlugin = { module = "dev.icerock:mobile-multiplatform", version = "0.14.2" } 39 | kotlinSerializationGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlinVersion" } 40 | composeJetBrainsGradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "composeJetBrainsVersion" } 41 | detektGradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version = "1.22.0" } 42 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icerockdev/moko-permissions/d6d94f77a722cfcbafbef30c8f274da633fcc3bd/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jun 10 21:09:29 CEST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icerockdev/moko-permissions/d6d94f77a722cfcbafbef30c8f274da633fcc3bd/img/logo.png -------------------------------------------------------------------------------- /permissions-avfoundation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | import dev.icerock.moko.gradle.utils.connectTargetsToSourceSet 6 | import dev.icerock.moko.gradle.utils.createMainTest 7 | import dev.icerock.moko.gradle.utils.setupDependency 8 | 9 | plugins { 10 | id("org.jetbrains.kotlin.multiplatform") 11 | id("dev.icerock.moko.gradle.publication") 12 | id("dev.icerock.moko.gradle.detekt") 13 | } 14 | 15 | kotlin { 16 | iosArm64() 17 | iosX64() 18 | iosSimulatorArm64() 19 | 20 | with(this.sourceSets) { 21 | // creation 22 | createMainTest("ios") 23 | 24 | // ios dependencies 25 | setupDependency("ios", "common") 26 | connectTargetsToSourceSet( 27 | targetNames = listOf("iosX64", "iosArm64", "iosSimulatorArm64"), 28 | sourceSetPrefix = "ios" 29 | ) 30 | } 31 | } 32 | 33 | dependencies { 34 | commonMainApi(projects.permissions) 35 | commonMainImplementation(libs.coroutines) 36 | } 37 | -------------------------------------------------------------------------------- /permissions-avfoundation/src/iosMain/kotlin/dev/icerock/moko/permissions/avfoundation/AVCaptureDelegate.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.avfoundation 6 | 7 | import dev.icerock.moko.permissions.DeniedAlwaysException 8 | import dev.icerock.moko.permissions.Permission 9 | import dev.icerock.moko.permissions.PermissionDelegate 10 | import dev.icerock.moko.permissions.PermissionState 11 | import dev.icerock.moko.permissions.mainContinuation 12 | import platform.AVFoundation.AVAuthorizationStatus 13 | import platform.AVFoundation.AVAuthorizationStatusAuthorized 14 | import platform.AVFoundation.AVAuthorizationStatusDenied 15 | import platform.AVFoundation.AVAuthorizationStatusNotDetermined 16 | import platform.AVFoundation.AVAuthorizationStatusRestricted 17 | import platform.AVFoundation.AVCaptureDevice 18 | import platform.AVFoundation.AVMediaType 19 | import platform.AVFoundation.authorizationStatusForMediaType 20 | import platform.AVFoundation.requestAccessForMediaType 21 | import kotlin.coroutines.resume 22 | import kotlin.coroutines.suspendCoroutine 23 | 24 | class AVCaptureDelegate( 25 | private val type: AVMediaType, 26 | private val permission: Permission, 27 | ) : PermissionDelegate { 28 | override suspend fun providePermission() { 29 | val status: AVAuthorizationStatus = currentAuthorizationStatus() 30 | when (status) { 31 | AVAuthorizationStatusAuthorized -> return 32 | AVAuthorizationStatusNotDetermined -> { 33 | val isGranted: Boolean = suspendCoroutine { continuation -> 34 | AVCaptureDevice.requestAccess(type) { continuation.resume(it) } 35 | } 36 | if (isGranted) { 37 | return 38 | } else { 39 | throw DeniedAlwaysException(permission) 40 | } 41 | } 42 | 43 | AVAuthorizationStatusDenied -> throw DeniedAlwaysException(permission) 44 | else -> error("unknown authorization status $status") 45 | } 46 | } 47 | 48 | override suspend fun getPermissionState(): PermissionState { 49 | val status: AVAuthorizationStatus = currentAuthorizationStatus() 50 | return when (status) { 51 | AVAuthorizationStatusAuthorized -> PermissionState.Granted 52 | AVAuthorizationStatusNotDetermined -> PermissionState.NotDetermined 53 | AVAuthorizationStatusDenied -> PermissionState.DeniedAlways 54 | AVAuthorizationStatusRestricted -> PermissionState.Granted 55 | else -> error("unknown authorization status $status") 56 | } 57 | } 58 | 59 | private fun currentAuthorizationStatus(): AVAuthorizationStatus { 60 | return AVCaptureDevice.authorizationStatusForMediaType(type) 61 | } 62 | } 63 | 64 | private fun AVCaptureDevice.Companion.requestAccess( 65 | type: AVMediaType, 66 | callback: (isGranted: Boolean) -> Unit, 67 | ) { 68 | this.requestAccessForMediaType( 69 | type, 70 | mainContinuation { isGranted: Boolean -> 71 | callback(isGranted) 72 | } 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /permissions-bluetooth/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("dev.icerock.moko.gradle.multiplatform.mobile") 7 | id("dev.icerock.moko.gradle.publication") 8 | id("dev.icerock.moko.gradle.stub.javadoc") 9 | id("dev.icerock.moko.gradle.detekt") 10 | } 11 | 12 | android { 13 | namespace = "dev.icerock.moko.permissions.bluetooth" 14 | } 15 | 16 | dependencies { 17 | commonMainApi(projects.permissions) 18 | commonMainImplementation(libs.coroutines) 19 | } 20 | -------------------------------------------------------------------------------- /permissions-bluetooth/src/androidMain/kotlin/dev/icerock/moko/permissions/bluetooth/BluetoothPermissions.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.bluetooth 6 | 7 | import android.Manifest 8 | import android.content.Context 9 | import android.os.Build 10 | import dev.icerock.moko.permissions.PermissionDelegate 11 | 12 | /** 13 | * @see https://developer.android.com/guide/topics/connectivity/bluetooth/permissions 14 | */ 15 | 16 | actual val bluetoothLEDelegate = object : PermissionDelegate { 17 | override fun getPermissionStateOverride(applicationContext: Context) = null 18 | 19 | override fun getPlatformPermission() = allBluetoothPermissions() 20 | 21 | /** 22 | * Bluetooth permissions 23 | * 24 | * @see https://developer.android.com/guide/topics/connectivity/bluetooth/permissions 25 | */ 26 | private fun allBluetoothPermissions(): List = buildSet { 27 | addAll(bluetoothConnectCompat()) 28 | addAll(bluetoothScanCompat()) 29 | addAll(bluetoothAdvertiseCompat()) 30 | }.toList() 31 | 32 | private fun bluetoothScanCompat(): List { 33 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 34 | listOf(Manifest.permission.BLUETOOTH_SCAN) 35 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 36 | listOf(Manifest.permission.ACCESS_FINE_LOCATION) 37 | } else { 38 | listOf(Manifest.permission.ACCESS_COARSE_LOCATION) 39 | } 40 | } 41 | 42 | private fun bluetoothAdvertiseCompat(): List { 43 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 44 | listOf(Manifest.permission.BLUETOOTH_ADVERTISE) 45 | } else { 46 | listOf(Manifest.permission.BLUETOOTH) 47 | } 48 | } 49 | 50 | private fun bluetoothConnectCompat(): List { 51 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 52 | listOf(Manifest.permission.BLUETOOTH_CONNECT) 53 | } else { 54 | listOf(Manifest.permission.BLUETOOTH) 55 | } 56 | } 57 | } 58 | 59 | actual val bluetoothScanDelegate = object : PermissionDelegate { 60 | override fun getPermissionStateOverride(applicationContext: Context) = null 61 | 62 | override fun getPlatformPermission() = 63 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 64 | listOf(Manifest.permission.BLUETOOTH_SCAN) 65 | } else { 66 | listOf(Manifest.permission.BLUETOOTH) 67 | } 68 | } 69 | 70 | actual val bluetoothAdvertiseDelegate = object : PermissionDelegate { 71 | override fun getPermissionStateOverride(applicationContext: Context) = null 72 | 73 | override fun getPlatformPermission() = 74 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 75 | listOf(Manifest.permission.BLUETOOTH_ADVERTISE) 76 | } else { 77 | listOf(Manifest.permission.BLUETOOTH) 78 | } 79 | } 80 | 81 | actual val bluetoothConnectDelegate = object : PermissionDelegate { 82 | override fun getPermissionStateOverride(applicationContext: Context) = null 83 | 84 | override fun getPlatformPermission() = 85 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 86 | listOf(Manifest.permission.BLUETOOTH_CONNECT) 87 | } else { 88 | listOf(Manifest.permission.BLUETOOTH) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /permissions-bluetooth/src/commonMain/kotlin/dev/icerock/moko/permissions/bluetooth/BluetoothPermissions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.bluetooth 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionDelegate 9 | 10 | internal expect val bluetoothLEDelegate: PermissionDelegate 11 | internal expect val bluetoothScanDelegate: PermissionDelegate 12 | internal expect val bluetoothAdvertiseDelegate: PermissionDelegate 13 | internal expect val bluetoothConnectDelegate: PermissionDelegate 14 | 15 | object BluetoothLEPermission : Permission { 16 | override val delegate get() = bluetoothLEDelegate 17 | } 18 | 19 | object BluetoothScanPermission : Permission { 20 | override val delegate get() = bluetoothScanDelegate 21 | } 22 | 23 | object BluetoothAdvertisePermission : Permission { 24 | override val delegate get() = bluetoothAdvertiseDelegate 25 | } 26 | 27 | object BluetoothConnectPermission : Permission { 28 | override val delegate get() = bluetoothConnectDelegate 29 | } 30 | 31 | val Permission.Companion.BLUETOOTH_LE get() = BluetoothLEPermission 32 | val Permission.Companion.BLUETOOTH_SCAN get() = BluetoothScanPermission 33 | val Permission.Companion.BLUETOOTH_ADVERTISE get() = BluetoothAdvertisePermission 34 | val Permission.Companion.BLUETOOTH_CONNECT get() = BluetoothConnectPermission 35 | -------------------------------------------------------------------------------- /permissions-bluetooth/src/iosMain/kotlin/dev/icerock/moko/permissions/bluetooth/BluetoothPermissionDelegate.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.bluetooth 6 | 7 | import dev.icerock.moko.permissions.DeniedAlwaysException 8 | import dev.icerock.moko.permissions.DeniedException 9 | import dev.icerock.moko.permissions.Permission 10 | import dev.icerock.moko.permissions.PermissionDelegate 11 | import dev.icerock.moko.permissions.PermissionState 12 | import kotlinx.cinterop.ExperimentalForeignApi 13 | import platform.CoreBluetooth.CBCentralManager 14 | import platform.CoreBluetooth.CBCentralManagerDelegateProtocol 15 | import platform.CoreBluetooth.CBManager 16 | import platform.CoreBluetooth.CBManagerAuthorization 17 | import platform.CoreBluetooth.CBManagerAuthorizationAllowedAlways 18 | import platform.CoreBluetooth.CBManagerAuthorizationDenied 19 | import platform.CoreBluetooth.CBManagerAuthorizationNotDetermined 20 | import platform.CoreBluetooth.CBManagerAuthorizationRestricted 21 | import platform.CoreBluetooth.CBManagerState 22 | import platform.CoreBluetooth.CBManagerStatePoweredOff 23 | import platform.CoreBluetooth.CBManagerStatePoweredOn 24 | import platform.CoreBluetooth.CBManagerStateResetting 25 | import platform.CoreBluetooth.CBManagerStateUnauthorized 26 | import platform.CoreBluetooth.CBManagerStateUnknown 27 | import platform.CoreBluetooth.CBManagerStateUnsupported 28 | import platform.Foundation.NSSelectorFromString 29 | import platform.darwin.NSObject 30 | import kotlin.coroutines.resume 31 | import kotlin.coroutines.suspendCoroutine 32 | 33 | internal class BluetoothPermissionDelegate( 34 | private val permission: Permission, 35 | ) : PermissionDelegate { 36 | @OptIn(ExperimentalForeignApi::class) 37 | override suspend fun providePermission() { 38 | // To maintain compatibility with iOS 12 (@see https://developer.apple.com/documentation/corebluetooth/cbmanagerauthorization) 39 | val isNotDetermined: Boolean = 40 | if (CBManager.resolveClassMethod(NSSelectorFromString("authorization"))) { 41 | CBManager.authorization == CBManagerAuthorizationNotDetermined 42 | } else { 43 | CBCentralManager().state == CBManagerStateUnknown 44 | } 45 | 46 | val state: CBManagerState = if (isNotDetermined) { 47 | suspendCoroutine { continuation -> 48 | CBCentralManager( 49 | object : NSObject(), CBCentralManagerDelegateProtocol { 50 | override fun centralManagerDidUpdateState(central: CBCentralManager) { 51 | continuation.resume(central.state) 52 | } 53 | }, 54 | null 55 | ) 56 | } 57 | } else { 58 | CBCentralManager().state 59 | } 60 | 61 | when (state) { 62 | CBManagerStatePoweredOn -> return 63 | CBManagerStateUnauthorized -> throw DeniedAlwaysException(permission) 64 | CBManagerStatePoweredOff -> 65 | throw DeniedException(permission, "Bluetooth is powered off") 66 | 67 | CBManagerStateResetting -> 68 | throw DeniedException(permission, "Bluetooth is restarting") 69 | 70 | CBManagerStateUnsupported -> 71 | throw DeniedAlwaysException(permission, "Bluetooth is not supported on this device") 72 | 73 | CBManagerStateUnknown -> 74 | error("Bluetooth state should be known at this point") 75 | 76 | else -> 77 | error("Unknown state (Permissions library should be updated) : $state") 78 | } 79 | } 80 | 81 | @OptIn(ExperimentalForeignApi::class) 82 | override suspend fun getPermissionState(): PermissionState { 83 | // To maintain compatibility with iOS 12 (@see https://developer.apple.com/documentation/corebluetooth/cbmanagerauthorization) 84 | if (CBManager.resolveClassMethod(NSSelectorFromString("authorization"))) { 85 | val state: CBManagerAuthorization = CBManager.authorization 86 | return when (state) { 87 | CBManagerAuthorizationNotDetermined -> PermissionState.NotDetermined 88 | CBManagerAuthorizationAllowedAlways, CBManagerAuthorizationRestricted -> PermissionState.Granted 89 | CBManagerAuthorizationDenied -> PermissionState.DeniedAlways 90 | else -> error("unknown state $state") 91 | } 92 | } 93 | val state: CBManagerState = CBCentralManager().state 94 | return when (state) { 95 | CBManagerStatePoweredOn -> PermissionState.Granted 96 | CBManagerStateUnauthorized, CBManagerStatePoweredOff, 97 | CBManagerStateResetting, CBManagerStateUnsupported, 98 | -> PermissionState.DeniedAlways 99 | 100 | CBManagerStateUnknown -> PermissionState.NotDetermined 101 | else -> error("unknown state $state") 102 | } 103 | } 104 | } 105 | 106 | actual val bluetoothLEDelegate: PermissionDelegate = 107 | BluetoothPermissionDelegate(BluetoothLEPermission) 108 | actual val bluetoothScanDelegate: PermissionDelegate = 109 | BluetoothPermissionDelegate(BluetoothScanPermission) 110 | actual val bluetoothAdvertiseDelegate: PermissionDelegate = 111 | BluetoothPermissionDelegate(BluetoothAdvertisePermission) 112 | actual val bluetoothConnectDelegate: PermissionDelegate = 113 | BluetoothPermissionDelegate(BluetoothConnectPermission) 114 | -------------------------------------------------------------------------------- /permissions-camera/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("dev.icerock.moko.gradle.multiplatform.mobile") 7 | id("dev.icerock.moko.gradle.publication") 8 | id("dev.icerock.moko.gradle.stub.javadoc") 9 | id("dev.icerock.moko.gradle.detekt") 10 | } 11 | 12 | android { 13 | namespace = "dev.icerock.moko.permissions.camera" 14 | } 15 | 16 | dependencies { 17 | commonMainApi(projects.permissions) 18 | iosMainImplementation(projects.permissionsAvfoundation) 19 | commonMainImplementation(libs.coroutines) 20 | } 21 | -------------------------------------------------------------------------------- /permissions-camera/src/androidMain/kotlin/dev/icerock/moko/permissions/camera/CameraPermission.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.camera 6 | 7 | import android.Manifest 8 | import android.content.Context 9 | import dev.icerock.moko.permissions.PermissionDelegate 10 | 11 | actual val cameraDelegate = object : PermissionDelegate { 12 | override fun getPermissionStateOverride(applicationContext: Context) = null 13 | override fun getPlatformPermission() = listOf(Manifest.permission.CAMERA) 14 | } 15 | -------------------------------------------------------------------------------- /permissions-camera/src/commonMain/kotlin/dev/icerock/moko/permissions/camera/CameraPermission.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.camera 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionDelegate 9 | 10 | internal expect val cameraDelegate: PermissionDelegate 11 | 12 | object CameraPermission : Permission { 13 | override val delegate get() = cameraDelegate 14 | } 15 | 16 | val Permission.Companion.CAMERA get() = CameraPermission 17 | -------------------------------------------------------------------------------- /permissions-camera/src/iosMain/kotlin/dev/icerock/moko/permissions/camera/CameraPermission.ios.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.camera 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionDelegate 9 | import dev.icerock.moko.permissions.avfoundation.AVCaptureDelegate 10 | import platform.AVFoundation.AVMediaTypeVideo 11 | 12 | actual val cameraDelegate: PermissionDelegate = AVCaptureDelegate( 13 | AVMediaTypeVideo, 14 | Permission.CAMERA 15 | ) 16 | -------------------------------------------------------------------------------- /permissions-compose/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("dev.icerock.moko.gradle.multiplatform.mobile") 7 | id("dev.icerock.moko.gradle.publication") 8 | id("dev.icerock.moko.gradle.stub.javadoc") 9 | id("dev.icerock.moko.gradle.detekt") 10 | id("org.jetbrains.compose") 11 | } 12 | 13 | android { 14 | namespace = "dev.icerock.moko.permissions.compose" 15 | 16 | defaultConfig { 17 | minSdk = 21 18 | } 19 | } 20 | 21 | dependencies { 22 | commonMainApi(projects.permissions) 23 | commonMainApi(compose.runtime) 24 | androidMainImplementation(libs.activity) 25 | androidMainImplementation(libs.composeUi) 26 | androidMainImplementation(libs.lifecycleRuntime) 27 | } 28 | -------------------------------------------------------------------------------- /permissions-compose/src/androidMain/kotlin/dev/icerock/moko/permissions/compose/BindEffect.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.compose 6 | 7 | import android.content.Context 8 | import androidx.activity.ComponentActivity 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.LaunchedEffect 11 | import androidx.compose.ui.platform.LocalContext 12 | import androidx.compose.ui.platform.LocalLifecycleOwner 13 | import androidx.lifecycle.LifecycleOwner 14 | import dev.icerock.moko.permissions.PermissionsController 15 | 16 | @Suppress("FunctionNaming") 17 | @Composable 18 | actual fun BindEffect(permissionsController: PermissionsController) { 19 | val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current 20 | val context: Context = LocalContext.current 21 | 22 | LaunchedEffect(permissionsController, lifecycleOwner, context) { 23 | val activity: ComponentActivity = checkNotNull(context as? ComponentActivity) { 24 | "$context context is not instance of ComponentActivity" 25 | } 26 | 27 | permissionsController.bind(activity) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /permissions-compose/src/androidMain/kotlin/dev/icerock/moko/permissions/compose/PermissionsControllerFactory.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.compose 6 | 7 | import android.content.Context 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.platform.LocalContext 11 | import dev.icerock.moko.permissions.PermissionsController 12 | 13 | @Composable 14 | actual fun rememberPermissionsControllerFactory(): PermissionsControllerFactory { 15 | val context: Context = LocalContext.current 16 | return remember(context) { 17 | PermissionsControllerFactory { 18 | PermissionsController(applicationContext = context.applicationContext) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /permissions-compose/src/commonMain/kotlin/dev/icerock/moko/permissions/compose/BindEffect.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.compose 6 | 7 | import androidx.compose.runtime.Composable 8 | import dev.icerock.moko.permissions.PermissionsController 9 | 10 | @Suppress("FunctionNaming") 11 | @Composable 12 | expect fun BindEffect(permissionsController: PermissionsController) 13 | -------------------------------------------------------------------------------- /permissions-compose/src/commonMain/kotlin/dev/icerock/moko/permissions/compose/PermissionsControllerFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.compose 6 | 7 | import androidx.compose.runtime.Composable 8 | import dev.icerock.moko.permissions.PermissionsController 9 | 10 | fun interface PermissionsControllerFactory { 11 | fun createPermissionsController(): PermissionsController 12 | } 13 | 14 | @Composable 15 | expect fun rememberPermissionsControllerFactory(): PermissionsControllerFactory 16 | -------------------------------------------------------------------------------- /permissions-compose/src/iosMain/kotlin/dev/icerock/moko/permissions/compose/BindEffect.ios.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.compose 6 | 7 | import androidx.compose.runtime.Composable 8 | import dev.icerock.moko.permissions.PermissionsController 9 | 10 | // on iOS side we should not do anything to prepare PermissionsController to work 11 | @Suppress("FunctionNaming") 12 | @Composable 13 | actual fun BindEffect(permissionsController: PermissionsController) = Unit 14 | -------------------------------------------------------------------------------- /permissions-compose/src/iosMain/kotlin/dev/icerock/moko/permissions/compose/PermissionsControllerFactory.ios.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.compose 6 | 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.remember 9 | 10 | @Composable 11 | actual fun rememberPermissionsControllerFactory(): PermissionsControllerFactory { 12 | return remember { 13 | PermissionsControllerFactory { 14 | dev.icerock.moko.permissions.ios.PermissionsController() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /permissions-contacts/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("dev.icerock.moko.gradle.multiplatform.mobile") 7 | id("dev.icerock.moko.gradle.publication") 8 | id("dev.icerock.moko.gradle.stub.javadoc") 9 | id("dev.icerock.moko.gradle.detekt") 10 | } 11 | 12 | android { 13 | namespace = "dev.icerock.moko.permissions.contacts" 14 | } 15 | 16 | dependencies { 17 | commonMainApi(projects.permissions) 18 | commonMainImplementation(libs.coroutines) 19 | } 20 | -------------------------------------------------------------------------------- /permissions-contacts/src/androidMain/kotlin/dev/icerock/moko/permissions/contacts/ContactsPermission.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.contacts 6 | 7 | import android.Manifest 8 | import android.content.Context 9 | import dev.icerock.moko.permissions.PermissionDelegate 10 | 11 | actual val contactsDelegate = object : PermissionDelegate { 12 | override fun getPermissionStateOverride(applicationContext: Context) = null 13 | 14 | override fun getPlatformPermission() = 15 | listOf( 16 | Manifest.permission.READ_CONTACTS, 17 | Manifest.permission.WRITE_CONTACTS 18 | ) 19 | } 20 | 21 | actual val readContactsDelegate = object : PermissionDelegate { 22 | override fun getPermissionStateOverride(applicationContext: Context) = null 23 | 24 | override fun getPlatformPermission() = 25 | listOf( 26 | Manifest.permission.READ_CONTACTS 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /permissions-contacts/src/commonMain/kotlin/dev/icerock/moko/permissions/contacts/ContactPermission.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.contacts 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionDelegate 9 | 10 | internal expect val contactsDelegate: PermissionDelegate 11 | internal expect val readContactsDelegate: PermissionDelegate 12 | 13 | /** 14 | * Permission to read and write contacts. 15 | * 16 | * On Android, declare both `READ_CONTACTS` and `WRITE_CONTACTS` permissions 17 | * in `AndroidManifest.xml` 18 | */ 19 | object ContactPermission : Permission { 20 | override val delegate get() = contactsDelegate 21 | } 22 | 23 | /** 24 | * Permission to read contacts 25 | * 26 | * On Android, declare `READ_CONTACTS` permission in `AndroidManifest.xml` 27 | * 28 | * On iOS this permission is the same with [ContactPermission] 29 | */ 30 | object ReadContactPermission : Permission { 31 | override val delegate get() = readContactsDelegate 32 | } 33 | 34 | val Permission.Companion.CONTACTS get() = ContactPermission 35 | val Permission.Companion.READ_CONTACTS get() = ReadContactPermission 36 | -------------------------------------------------------------------------------- /permissions-contacts/src/iosMain/kotlin/dev/icerock/moko/permissions/contacts/ContactsPermission.ios.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.contacts 6 | 7 | import dev.icerock.moko.permissions.DeniedAlwaysException 8 | import dev.icerock.moko.permissions.Permission 9 | import dev.icerock.moko.permissions.PermissionDelegate 10 | import dev.icerock.moko.permissions.PermissionState 11 | import platform.Contacts.CNAuthorizationStatus 12 | import platform.Contacts.CNAuthorizationStatusAuthorized 13 | import platform.Contacts.CNAuthorizationStatusDenied 14 | import platform.Contacts.CNAuthorizationStatusNotDetermined 15 | import platform.Contacts.CNAuthorizationStatusRestricted 16 | import platform.Contacts.CNContactStore 17 | import platform.Contacts.CNEntityType 18 | import kotlin.coroutines.resume 19 | import kotlin.coroutines.suspendCoroutine 20 | 21 | private class ContactsPermissionDelegate( 22 | private val permission: Permission, 23 | ) : PermissionDelegate { 24 | private val contactStore = CNContactStore() 25 | 26 | override suspend fun providePermission() { 27 | return providePermission( 28 | CNContactStore.authorizationStatusForEntityType( 29 | CNEntityType.CNEntityTypeContacts 30 | ) 31 | ) 32 | } 33 | 34 | override suspend fun getPermissionState(): PermissionState { 35 | val status: CNAuthorizationStatus = 36 | CNContactStore.authorizationStatusForEntityType(CNEntityType.CNEntityTypeContacts) 37 | return when (status) { 38 | CNAuthorizationStatusAuthorized, CNAuthorizationStatusLimited -> PermissionState.Granted 39 | 40 | CNAuthorizationStatusNotDetermined -> PermissionState.NotDetermined 41 | CNAuthorizationStatusDenied, CNAuthorizationStatusRestricted -> PermissionState.DeniedAlways 42 | else -> error("unknown contacts $status") 43 | } 44 | } 45 | 46 | private suspend fun providePermission(status: CNAuthorizationStatus) { 47 | when (status) { 48 | CNAuthorizationStatusAuthorized, 49 | CNAuthorizationStatusLimited -> return 50 | 51 | CNAuthorizationStatusNotDetermined -> { 52 | val newStatus = suspendCoroutine { continuation -> 53 | contactStore.requestAccessForEntityType(CNEntityType.CNEntityTypeContacts) { _, _ -> 54 | continuation.resume( 55 | CNContactStore.authorizationStatusForEntityType( 56 | CNEntityType.CNEntityTypeContacts 57 | ) 58 | ) 59 | } 60 | } 61 | providePermission(newStatus) 62 | } 63 | 64 | CNAuthorizationStatusDenied, CNAuthorizationStatusRestricted -> throw DeniedAlwaysException(permission) 65 | else -> error("unknown location authorization status $status") 66 | } 67 | } 68 | } 69 | 70 | // declared as constant because at now we use kotlin 1.9.10 that not know about 71 | // platform.Contacts.CNAuthorizationStatusLimited 72 | @Suppress("TopLevelPropertyNaming") 73 | private const val CNAuthorizationStatusLimited: Long = 4 74 | 75 | actual val contactsDelegate: PermissionDelegate = ContactsPermissionDelegate(ContactPermission) 76 | actual val readContactsDelegate: PermissionDelegate = ContactsPermissionDelegate(ReadContactPermission) 77 | -------------------------------------------------------------------------------- /permissions-gallery/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("dev.icerock.moko.gradle.multiplatform.mobile") 7 | id("dev.icerock.moko.gradle.publication") 8 | id("dev.icerock.moko.gradle.stub.javadoc") 9 | id("dev.icerock.moko.gradle.detekt") 10 | } 11 | 12 | android { 13 | namespace = "dev.icerock.moko.permissions.gallery" 14 | } 15 | 16 | dependencies { 17 | commonMainApi(projects.permissions) 18 | commonMainImplementation(libs.coroutines) 19 | } 20 | -------------------------------------------------------------------------------- /permissions-gallery/src/androidMain/kotlin/dev/icerock/moko/permissions/gallery/GalleryPermission.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.gallery 6 | 7 | import android.Manifest 8 | import android.content.Context 9 | import android.os.Build 10 | import dev.icerock.moko.permissions.PermissionDelegate 11 | 12 | actual val galleryDelegate = object : PermissionDelegate { 13 | override fun getPermissionStateOverride(applicationContext: Context) = null 14 | 15 | override fun getPlatformPermission() = 16 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 17 | listOf( 18 | Manifest.permission.READ_MEDIA_IMAGES, 19 | Manifest.permission.READ_MEDIA_VIDEO 20 | ) 21 | } else { 22 | listOf(Manifest.permission.READ_EXTERNAL_STORAGE) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /permissions-gallery/src/commonMain/kotlin/dev/icerock/moko/permissions/gallery/GalleryPermission.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.gallery 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionDelegate 9 | 10 | internal expect val galleryDelegate: PermissionDelegate 11 | 12 | object GalleryPermission : Permission { 13 | override val delegate get() = galleryDelegate 14 | } 15 | 16 | val Permission.Companion.GALLERY get() = GalleryPermission 17 | -------------------------------------------------------------------------------- /permissions-gallery/src/iosMain/kotlin/dev/icerock/moko/permissions/gallery/GalleryPermissionDelegate.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.gallery 6 | 7 | import dev.icerock.moko.permissions.DeniedAlwaysException 8 | import dev.icerock.moko.permissions.Permission 9 | import dev.icerock.moko.permissions.PermissionDelegate 10 | import dev.icerock.moko.permissions.PermissionState 11 | import dev.icerock.moko.permissions.mainContinuation 12 | import platform.Photos.PHAuthorizationStatus 13 | import platform.Photos.PHAuthorizationStatusAuthorized 14 | import platform.Photos.PHAuthorizationStatusDenied 15 | import platform.Photos.PHAuthorizationStatusNotDetermined 16 | import platform.Photos.PHPhotoLibrary 17 | import kotlin.coroutines.resume 18 | import kotlin.coroutines.suspendCoroutine 19 | 20 | internal class GalleryPermissionDelegate : PermissionDelegate { 21 | override suspend fun providePermission() { 22 | providePermission(PHPhotoLibrary.authorizationStatus()) 23 | } 24 | 25 | private suspend fun providePermission(status: PHAuthorizationStatus) { 26 | return when (status) { 27 | PHAuthorizationStatusAuthorized -> return 28 | PHAuthorizationStatusNotDetermined -> { 29 | val newStatus = suspendCoroutine { continuation -> 30 | requestGalleryAccess { continuation.resume(it) } 31 | } 32 | providePermission(newStatus) 33 | } 34 | 35 | PHAuthorizationStatusDenied -> throw DeniedAlwaysException(Permission.GALLERY) 36 | else -> error("unknown gallery authorization status $status") 37 | } 38 | } 39 | 40 | override suspend fun getPermissionState(): PermissionState { 41 | val status: PHAuthorizationStatus = PHPhotoLibrary.authorizationStatus() 42 | return when (status) { 43 | PHAuthorizationStatusAuthorized -> PermissionState.Granted 44 | PHAuthorizationStatusNotDetermined -> PermissionState.NotDetermined 45 | PHAuthorizationStatusDenied -> PermissionState.DeniedAlways 46 | else -> error("unknown gallery authorization status $status") 47 | } 48 | } 49 | } 50 | 51 | private fun requestGalleryAccess(callback: (PHAuthorizationStatus) -> Unit) { 52 | PHPhotoLibrary.requestAuthorization( 53 | mainContinuation { status: PHAuthorizationStatus -> 54 | callback(status) 55 | } 56 | ) 57 | } 58 | 59 | actual val galleryDelegate: PermissionDelegate = GalleryPermissionDelegate() 60 | -------------------------------------------------------------------------------- /permissions-location/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("dev.icerock.moko.gradle.multiplatform.mobile") 7 | id("dev.icerock.moko.gradle.publication") 8 | id("dev.icerock.moko.gradle.stub.javadoc") 9 | id("dev.icerock.moko.gradle.detekt") 10 | } 11 | 12 | android { 13 | namespace = "dev.icerock.moko.permissions.location" 14 | } 15 | 16 | dependencies { 17 | commonMainApi(projects.permissions) 18 | commonMainImplementation(libs.coroutines) 19 | } 20 | -------------------------------------------------------------------------------- /permissions-location/src/androidMain/kotlin/dev/icerock/moko/permissions/location/LocationPermission.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.location 6 | 7 | import android.Manifest 8 | import android.content.Context 9 | import android.os.Build 10 | import dev.icerock.moko.permissions.PermissionDelegate 11 | 12 | actual val locationDelegate = object : PermissionDelegate { 13 | override fun getPermissionStateOverride(applicationContext: Context) = null 14 | 15 | override fun getPlatformPermission() = 16 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 17 | listOf( 18 | Manifest.permission.ACCESS_FINE_LOCATION, 19 | Manifest.permission.ACCESS_COARSE_LOCATION, 20 | ) 21 | } else { 22 | listOf(Manifest.permission.ACCESS_FINE_LOCATION) 23 | } 24 | } 25 | 26 | actual val coarseLocationDelegate = object : PermissionDelegate { 27 | override fun getPermissionStateOverride(applicationContext: Context) = null 28 | 29 | override fun getPlatformPermission() = 30 | listOf(Manifest.permission.ACCESS_COARSE_LOCATION) 31 | } 32 | 33 | actual val backgroundLocationDelegate = object : PermissionDelegate { 34 | override fun getPermissionStateOverride(applicationContext: Context) = null 35 | 36 | override fun getPlatformPermission() = 37 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 38 | listOf( 39 | Manifest.permission.ACCESS_FINE_LOCATION, 40 | Manifest.permission.ACCESS_COARSE_LOCATION, 41 | Manifest.permission.ACCESS_BACKGROUND_LOCATION, 42 | ) 43 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 44 | listOf( 45 | Manifest.permission.ACCESS_FINE_LOCATION, 46 | Manifest.permission.ACCESS_BACKGROUND_LOCATION, 47 | ) 48 | } else { 49 | listOf(Manifest.permission.ACCESS_FINE_LOCATION) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /permissions-location/src/commonMain/kotlin/dev/icerock/moko/permissions/location/LocationPermission.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.location 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionDelegate 9 | 10 | internal expect val locationDelegate: PermissionDelegate 11 | internal expect val coarseLocationDelegate: PermissionDelegate 12 | internal expect val backgroundLocationDelegate: PermissionDelegate 13 | 14 | object LocationPermission : Permission { 15 | override val delegate get() = locationDelegate 16 | } 17 | 18 | object CoarseLocationPermission : Permission { 19 | override val delegate get() = coarseLocationDelegate 20 | } 21 | 22 | object BackgroundLocationPermission : Permission { 23 | override val delegate get() = backgroundLocationDelegate 24 | } 25 | 26 | val Permission.Companion.LOCATION get() = LocationPermission 27 | val Permission.Companion.COARSE_LOCATION get() = CoarseLocationPermission 28 | val Permission.Companion.BACKGROUND_LOCATION get() = BackgroundLocationPermission 29 | -------------------------------------------------------------------------------- /permissions-location/src/iosMain/kotlin/dev/icerock/moko/permissions/location/LocationManagerDelegate.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.location 6 | 7 | import platform.CoreLocation.CLAuthorizationStatus 8 | import platform.CoreLocation.CLLocationManager 9 | import platform.CoreLocation.CLLocationManagerDelegateProtocol 10 | import platform.darwin.NSObject 11 | import kotlin.experimental.ExperimentalObjCName 12 | 13 | @OptIn(ExperimentalObjCName::class) 14 | @ObjCName("LocationManagerDelegate") 15 | internal class LocationManagerDelegate : NSObject(), CLLocationManagerDelegateProtocol { 16 | private var callback: ((CLAuthorizationStatus) -> Unit)? = null 17 | 18 | private val locationManager = CLLocationManager() 19 | 20 | init { 21 | locationManager.delegate = this 22 | } 23 | 24 | fun authorizationStatus(): CLAuthorizationStatus { 25 | return locationManager.authorizationStatus 26 | } 27 | 28 | fun requestWhenInUseAuthorization(callback: (CLAuthorizationStatus) -> Unit) { 29 | this.callback = callback 30 | 31 | locationManager.requestWhenInUseAuthorization() 32 | } 33 | 34 | fun requestAlwaysAuthorization(callback: (CLAuthorizationStatus) -> Unit) { 35 | this.callback = callback 36 | 37 | locationManager.requestAlwaysAuthorization() 38 | } 39 | 40 | override fun locationManagerDidChangeAuthorization(manager: CLLocationManager) { 41 | val authorizationStatus: CLAuthorizationStatus = manager.authorizationStatus 42 | callback?.invoke(authorizationStatus) 43 | callback = null 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /permissions-location/src/iosMain/kotlin/dev/icerock/moko/permissions/location/LocationPermission.ios.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.location 6 | 7 | import dev.icerock.moko.permissions.DeniedAlwaysException 8 | import dev.icerock.moko.permissions.DeniedException 9 | import dev.icerock.moko.permissions.Permission 10 | import dev.icerock.moko.permissions.PermissionDelegate 11 | import dev.icerock.moko.permissions.PermissionState 12 | import platform.CoreLocation.CLAuthorizationStatus 13 | import platform.CoreLocation.kCLAuthorizationStatusAuthorizedAlways 14 | import platform.CoreLocation.kCLAuthorizationStatusAuthorizedWhenInUse 15 | import platform.CoreLocation.kCLAuthorizationStatusDenied 16 | import platform.CoreLocation.kCLAuthorizationStatusNotDetermined 17 | import platform.CoreLocation.kCLAuthorizationStatusRestricted 18 | import kotlin.coroutines.resume 19 | import kotlin.coroutines.suspendCoroutine 20 | 21 | private val locationManagerDelegate = LocationManagerDelegate() 22 | 23 | private class LocationPermissionDelegate( 24 | private val permission: Permission 25 | ) : PermissionDelegate { 26 | override suspend fun providePermission() { 27 | return provideLocationPermission( 28 | status = locationManagerDelegate.authorizationStatus() 29 | ) 30 | } 31 | 32 | override suspend fun getPermissionState(): PermissionState { 33 | val status: CLAuthorizationStatus = locationManagerDelegate.authorizationStatus() 34 | return when (status) { 35 | kCLAuthorizationStatusAuthorizedAlways -> PermissionState.Granted 36 | kCLAuthorizationStatusAuthorizedWhenInUse -> { 37 | when (permission) { 38 | BackgroundLocationPermission -> PermissionState.NotGranted 39 | else -> PermissionState.Granted 40 | } 41 | } 42 | 43 | kCLAuthorizationStatusNotDetermined -> PermissionState.NotDetermined 44 | kCLAuthorizationStatusDenied, 45 | kCLAuthorizationStatusRestricted -> PermissionState.DeniedAlways 46 | 47 | else -> error("unknown location authorization status $status") 48 | } 49 | } 50 | 51 | private suspend fun provideLocationPermission( 52 | status: CLAuthorizationStatus 53 | ) { 54 | when (status) { 55 | kCLAuthorizationStatusAuthorizedAlways -> Unit 56 | kCLAuthorizationStatusAuthorizedWhenInUse -> 57 | if (permission == BackgroundLocationPermission) { 58 | // if we request background permission we should receive "always" 59 | throw DeniedException(permission) 60 | } 61 | 62 | kCLAuthorizationStatusNotDetermined -> if (permission == BackgroundLocationPermission) { 63 | requestAlwaysAuthorization() 64 | } else { 65 | requestWhenInUseAuthorization() 66 | } 67 | 68 | kCLAuthorizationStatusDenied, 69 | kCLAuthorizationStatusRestricted -> throw DeniedAlwaysException(permission) 70 | 71 | else -> error("unknown location authorization status $status") 72 | } 73 | } 74 | 75 | private suspend fun requestWhenInUseAuthorization() { 76 | val newStatus = suspendCoroutine { continuation -> 77 | locationManagerDelegate.requestWhenInUseAuthorization { continuation.resume(it) } 78 | } 79 | provideLocationPermission(newStatus) 80 | } 81 | 82 | private suspend fun requestAlwaysAuthorization() { 83 | val newStatus = suspendCoroutine { continuation -> 84 | locationManagerDelegate.requestAlwaysAuthorization { continuation.resume(it) } 85 | } 86 | provideLocationPermission(newStatus) 87 | } 88 | } 89 | 90 | actual val locationDelegate: PermissionDelegate = 91 | LocationPermissionDelegate(LocationPermission) 92 | actual val coarseLocationDelegate: PermissionDelegate = 93 | LocationPermissionDelegate(CoarseLocationPermission) 94 | actual val backgroundLocationDelegate: PermissionDelegate = 95 | LocationPermissionDelegate(BackgroundLocationPermission) 96 | -------------------------------------------------------------------------------- /permissions-microphone/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("dev.icerock.moko.gradle.multiplatform.mobile") 7 | id("dev.icerock.moko.gradle.publication") 8 | id("dev.icerock.moko.gradle.stub.javadoc") 9 | id("dev.icerock.moko.gradle.detekt") 10 | } 11 | 12 | android { 13 | namespace = "dev.icerock.moko.permissions.microphone" 14 | } 15 | 16 | dependencies { 17 | commonMainApi(projects.permissions) 18 | iosMainImplementation(projects.permissionsAvfoundation) 19 | commonMainImplementation(libs.coroutines) 20 | } 21 | -------------------------------------------------------------------------------- /permissions-microphone/src/androidMain/kotlin/dev/icerock/moko/permissions/microphone/MicrophonePermission.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.microphone 6 | 7 | import android.Manifest 8 | import android.content.Context 9 | import dev.icerock.moko.permissions.PermissionDelegate 10 | 11 | actual val recordAudioDelegate = object : PermissionDelegate { 12 | override fun getPermissionStateOverride(applicationContext: Context) = null 13 | override fun getPlatformPermission() = listOf(Manifest.permission.RECORD_AUDIO) 14 | } 15 | -------------------------------------------------------------------------------- /permissions-microphone/src/commonMain/kotlin/dev/icerock/moko/permissions/microphone/MicrophonePermission.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.microphone 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionDelegate 9 | 10 | internal expect val recordAudioDelegate: PermissionDelegate 11 | 12 | object RecordAudioPermission : Permission { 13 | override val delegate get() = recordAudioDelegate 14 | } 15 | 16 | val Permission.Companion.RECORD_AUDIO get() = RecordAudioPermission 17 | -------------------------------------------------------------------------------- /permissions-microphone/src/iosMain/kotlin/dev/icerock/moko/permissions/microphone/MicrophonePermission.ios.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.microphone 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionDelegate 9 | import dev.icerock.moko.permissions.avfoundation.AVCaptureDelegate 10 | import platform.AVFoundation.AVMediaTypeAudio 11 | 12 | actual val recordAudioDelegate: PermissionDelegate = AVCaptureDelegate( 13 | AVMediaTypeAudio, 14 | Permission.RECORD_AUDIO 15 | ) 16 | -------------------------------------------------------------------------------- /permissions-motion/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("dev.icerock.moko.gradle.multiplatform.mobile") 7 | id("dev.icerock.moko.gradle.publication") 8 | id("dev.icerock.moko.gradle.stub.javadoc") 9 | id("dev.icerock.moko.gradle.detekt") 10 | } 11 | 12 | android { 13 | namespace = "dev.icerock.moko.permissions.motion" 14 | } 15 | 16 | dependencies { 17 | commonMainApi(projects.permissions) 18 | commonMainImplementation(libs.coroutines) 19 | } 20 | -------------------------------------------------------------------------------- /permissions-motion/src/androidMain/kotlin/dev/icerock/moko/permissions/motion/MotionPermission.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.motion 6 | 7 | import android.Manifest 8 | import android.content.Context 9 | import android.os.Build 10 | import dev.icerock.moko.permissions.PermissionDelegate 11 | 12 | actual val motionDelegate = object : PermissionDelegate { 13 | override fun getPermissionStateOverride(applicationContext: Context) = null 14 | 15 | override fun getPlatformPermission() = 16 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 17 | listOf( 18 | Manifest.permission.ACTIVITY_RECOGNITION, 19 | Manifest.permission.BODY_SENSORS 20 | ) 21 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { 22 | listOf(Manifest.permission.BODY_SENSORS) 23 | } else { 24 | emptyList() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /permissions-motion/src/commonMain/kotlin/dev/icerock/moko/permissions/motion/MotionPermission.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.motion 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionDelegate 9 | 10 | internal expect val motionDelegate: PermissionDelegate 11 | 12 | object MotionPermission : Permission { 13 | override val delegate get() = motionDelegate 14 | } 15 | 16 | val Permission.Companion.MOTION get() = MotionPermission 17 | -------------------------------------------------------------------------------- /permissions-motion/src/iosMain/kotlin/dev/icerock/moko/permissions/motion/MotionPermission.ios.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.motion 6 | 7 | import dev.icerock.moko.permissions.PermissionDelegate 8 | import dev.icerock.moko.permissions.PermissionState 9 | import platform.CoreMotion.CMAuthorizationStatusAuthorized 10 | import platform.CoreMotion.CMAuthorizationStatusDenied 11 | import platform.CoreMotion.CMAuthorizationStatusNotDetermined 12 | import platform.CoreMotion.CMAuthorizationStatusRestricted 13 | import platform.CoreMotion.CMMotionActivityManager 14 | import platform.Foundation.NSDate 15 | import platform.Foundation.NSOperationQueue 16 | 17 | private class MotionPermissionDelegate : PermissionDelegate { 18 | override suspend fun providePermission() { 19 | val cmActivityManager = CMMotionActivityManager() 20 | val now = NSDate() 21 | cmActivityManager.queryActivityStartingFromDate( 22 | now, 23 | now, 24 | NSOperationQueue.mainQueue() 25 | ) { _, _ -> } 26 | } 27 | 28 | @Suppress("MoveVariableDeclarationIntoWhen") 29 | override suspend fun getPermissionState(): PermissionState { 30 | val status = CMMotionActivityManager.authorizationStatus() 31 | return when (status) { 32 | CMAuthorizationStatusAuthorized, 33 | CMAuthorizationStatusRestricted, -> PermissionState.Granted 34 | CMAuthorizationStatusDenied -> PermissionState.DeniedAlways 35 | CMAuthorizationStatusNotDetermined -> PermissionState.NotDetermined 36 | else -> error("unknown motion authorization status $status") 37 | } 38 | } 39 | } 40 | 41 | actual val motionDelegate: PermissionDelegate = MotionPermissionDelegate() 42 | -------------------------------------------------------------------------------- /permissions-notifications/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("dev.icerock.moko.gradle.multiplatform.mobile") 7 | id("dev.icerock.moko.gradle.publication") 8 | id("dev.icerock.moko.gradle.stub.javadoc") 9 | id("dev.icerock.moko.gradle.detekt") 10 | } 11 | 12 | android { 13 | namespace = "dev.icerock.moko.permissions.notifications" 14 | } 15 | 16 | dependencies { 17 | commonMainApi(projects.permissions) 18 | commonMainImplementation(libs.coroutines) 19 | androidMainImplementation(libs.androidxCore) 20 | } 21 | -------------------------------------------------------------------------------- /permissions-notifications/src/androidMain/kotlin/dev/icerock/moko/permissions/notifications/NotificationPermission.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.notifications 6 | 7 | import android.Manifest 8 | import android.content.Context 9 | import android.os.Build 10 | import androidx.core.app.NotificationManagerCompat 11 | import dev.icerock.moko.permissions.PermissionDelegate 12 | import dev.icerock.moko.permissions.PermissionState 13 | 14 | actual val remoteNotificationDelegate = object : PermissionDelegate { 15 | override fun getPermissionStateOverride(applicationContext: Context): PermissionState? { 16 | if (Build.VERSION.SDK_INT !in VERSIONS_WITHOUT_NOTIFICATION_PERMISSION) return null 17 | 18 | val isNotificationsEnabled = NotificationManagerCompat.from(applicationContext) 19 | .areNotificationsEnabled() 20 | return if (isNotificationsEnabled) { 21 | PermissionState.Granted 22 | } else { 23 | PermissionState.DeniedAlways 24 | } 25 | } 26 | 27 | override fun getPlatformPermission() = 28 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 29 | listOf(Manifest.permission.POST_NOTIFICATIONS) 30 | } else { 31 | emptyList() 32 | } 33 | } 34 | 35 | private val VERSIONS_WITHOUT_NOTIFICATION_PERMISSION = 36 | Build.VERSION_CODES.KITKAT until Build.VERSION_CODES.TIRAMISU 37 | -------------------------------------------------------------------------------- /permissions-notifications/src/commonMain/kotlin/dev/icerock/moko/permissions/notifications/NotificationPermission.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.notifications 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionDelegate 9 | 10 | internal expect val remoteNotificationDelegate: PermissionDelegate 11 | 12 | object RemoteNotificationPermission : Permission { 13 | override val delegate get() = remoteNotificationDelegate 14 | } 15 | 16 | val Permission.Companion.REMOTE_NOTIFICATION get() = RemoteNotificationPermission 17 | -------------------------------------------------------------------------------- /permissions-notifications/src/iosMain/kotlin/dev/icerock/moko/permissions/notifications/RemoteNotificationPermissionDelegate.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.notifications 6 | 7 | import dev.icerock.moko.permissions.DeniedAlwaysException 8 | import dev.icerock.moko.permissions.Permission 9 | import dev.icerock.moko.permissions.PermissionDelegate 10 | import dev.icerock.moko.permissions.PermissionState 11 | import dev.icerock.moko.permissions.mainContinuation 12 | import platform.UserNotifications.UNAuthorizationOptionAlert 13 | import platform.UserNotifications.UNAuthorizationOptionBadge 14 | import platform.UserNotifications.UNAuthorizationOptionCarPlay 15 | import platform.UserNotifications.UNAuthorizationOptionSound 16 | import platform.UserNotifications.UNAuthorizationStatus 17 | import platform.UserNotifications.UNAuthorizationStatusAuthorized 18 | import platform.UserNotifications.UNAuthorizationStatusDenied 19 | import platform.UserNotifications.UNAuthorizationStatusEphemeral 20 | import platform.UserNotifications.UNAuthorizationStatusNotDetermined 21 | import platform.UserNotifications.UNAuthorizationStatusProvisional 22 | import platform.UserNotifications.UNNotificationSettings 23 | import platform.UserNotifications.UNUserNotificationCenter 24 | import kotlin.coroutines.suspendCoroutine 25 | 26 | internal class RemoteNotificationPermissionDelegate : PermissionDelegate { 27 | override suspend fun providePermission() { 28 | return provideNotificationPermission( 29 | getPermissionStatus() 30 | ) 31 | } 32 | 33 | override suspend fun getPermissionState(): PermissionState { 34 | val status: UNAuthorizationStatus = getPermissionStatus() 35 | 36 | return when (status) { 37 | UNAuthorizationStatusAuthorized, 38 | UNAuthorizationStatusProvisional, 39 | UNAuthorizationStatusEphemeral, 40 | -> PermissionState.Granted 41 | 42 | UNAuthorizationStatusNotDetermined -> PermissionState.NotDetermined 43 | UNAuthorizationStatusDenied -> PermissionState.DeniedAlways 44 | else -> error("unknown push authorization status $status") 45 | } 46 | } 47 | 48 | private suspend fun getPermissionStatus(): UNAuthorizationStatus { 49 | val currentCenter = UNUserNotificationCenter.currentNotificationCenter() 50 | return suspendCoroutine { continuation -> 51 | currentCenter.getNotificationSettingsWithCompletionHandler( 52 | mainContinuation { settings: UNNotificationSettings? -> 53 | continuation.resumeWith( 54 | Result.success( 55 | settings?.authorizationStatus ?: UNAuthorizationStatusNotDetermined 56 | ) 57 | ) 58 | } 59 | ) 60 | } 61 | } 62 | 63 | private suspend fun provideNotificationPermission( 64 | status: UNAuthorizationStatus 65 | ) { 66 | when (status) { 67 | UNAuthorizationStatusAuthorized, 68 | UNAuthorizationStatusProvisional, 69 | UNAuthorizationStatusEphemeral -> return 70 | 71 | UNAuthorizationStatusNotDetermined -> { 72 | // User has not yet chosen permission, request permission 73 | val newStatus = suspendCoroutine { continuation -> 74 | UNUserNotificationCenter.currentNotificationCenter() 75 | .requestAuthorizationWithOptions( 76 | UNAuthorizationOptionSound 77 | .or(UNAuthorizationOptionAlert) 78 | .or(UNAuthorizationOptionBadge) 79 | .or(UNAuthorizationOptionCarPlay), 80 | mainContinuation { isOk, error -> 81 | if (isOk && error == null) { 82 | continuation.resumeWith(Result.success(UNAuthorizationStatusAuthorized)) 83 | } else { 84 | continuation.resumeWith(Result.success(UNAuthorizationStatusDenied)) 85 | } 86 | } 87 | ) 88 | } 89 | provideNotificationPermission(newStatus) 90 | } 91 | 92 | UNAuthorizationStatusDenied -> throw DeniedAlwaysException(Permission.REMOTE_NOTIFICATION) 93 | else -> error("unknown notifications authorization status $status") 94 | } 95 | } 96 | } 97 | 98 | actual val remoteNotificationDelegate: PermissionDelegate = RemoteNotificationPermissionDelegate() 99 | -------------------------------------------------------------------------------- /permissions-storage/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("dev.icerock.moko.gradle.multiplatform.mobile") 7 | id("dev.icerock.moko.gradle.publication") 8 | id("dev.icerock.moko.gradle.stub.javadoc") 9 | id("dev.icerock.moko.gradle.detekt") 10 | } 11 | 12 | android { 13 | namespace = "dev.icerock.moko.permissions.storage" 14 | } 15 | 16 | dependencies { 17 | commonMainApi(projects.permissions) 18 | commonMainImplementation(libs.coroutines) 19 | } 20 | -------------------------------------------------------------------------------- /permissions-storage/src/androidMain/kotlin/dev/icerock/moko/permissions/storage/StoragePermissions.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.storage 6 | 7 | import android.Manifest 8 | import android.content.Context 9 | import android.os.Build 10 | import dev.icerock.moko.permissions.PermissionDelegate 11 | 12 | actual val storageDelegate = object : PermissionDelegate { 13 | override fun getPermissionStateOverride(applicationContext: Context) = null 14 | 15 | override fun getPlatformPermission() = 16 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 17 | listOf( 18 | Manifest.permission.READ_MEDIA_AUDIO, 19 | Manifest.permission.READ_MEDIA_IMAGES, 20 | Manifest.permission.READ_MEDIA_VIDEO 21 | ) 22 | } else { 23 | listOf(Manifest.permission.READ_EXTERNAL_STORAGE) 24 | } 25 | } 26 | 27 | actual val writeStorageDelegate = object : PermissionDelegate { 28 | override fun getPermissionStateOverride(applicationContext: Context) = null 29 | 30 | override fun getPlatformPermission() = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) 31 | } 32 | -------------------------------------------------------------------------------- /permissions-storage/src/commonMain/kotlin/dev/icerock/moko/permissions/storage/StoragePermissions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.storage 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionDelegate 9 | 10 | internal expect val storageDelegate: PermissionDelegate 11 | internal expect val writeStorageDelegate: PermissionDelegate 12 | 13 | object StoragePermission : Permission { 14 | override val delegate get() = storageDelegate 15 | } 16 | 17 | object WriteStoragePermission : Permission { 18 | override val delegate get() = writeStorageDelegate 19 | } 20 | 21 | val Permission.Companion.STORAGE get() = StoragePermission 22 | val Permission.Companion.WRITE_STORAGE get() = WriteStoragePermission 23 | -------------------------------------------------------------------------------- /permissions-storage/src/iosMain/kotlin/dev/icerock/moko/permissions/storage/AlwaysGrantedDelegate.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.storage 6 | 7 | import dev.icerock.moko.permissions.PermissionDelegate 8 | import dev.icerock.moko.permissions.PermissionState 9 | 10 | object AlwaysGrantedDelegate : PermissionDelegate { 11 | override suspend fun providePermission() = Unit 12 | override suspend fun getPermissionState() = PermissionState.Granted 13 | } 14 | -------------------------------------------------------------------------------- /permissions-storage/src/iosMain/kotlin/dev/icerock/moko/permissions/storage/StoragePermissions.ios.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.storage 6 | 7 | import dev.icerock.moko.permissions.PermissionDelegate 8 | 9 | actual val storageDelegate: PermissionDelegate = AlwaysGrantedDelegate 10 | actual val writeStorageDelegate: PermissionDelegate = AlwaysGrantedDelegate 11 | -------------------------------------------------------------------------------- /permissions-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("org.jetbrains.kotlin.multiplatform") 7 | id("dev.icerock.moko.gradle.multiplatform.mobile") 8 | id("dev.icerock.moko.gradle.publication") 9 | id("dev.icerock.moko.gradle.stub.javadoc") 10 | id("dev.icerock.moko.gradle.detekt") 11 | } 12 | 13 | android { 14 | namespace = "dev.icerock.moko.permissions.test" 15 | } 16 | 17 | dependencies { 18 | commonMainImplementation(libs.coroutines) 19 | 20 | androidMainImplementation(libs.activity) 21 | 22 | commonMainApi(projects.permissions) 23 | } 24 | -------------------------------------------------------------------------------- /permissions-test/src/androidMain/kotlin/dev/icerock/moko/permissions/test/PermissionsControllerMock.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.test 6 | 7 | import androidx.activity.ComponentActivity 8 | import dev.icerock.moko.permissions.Permission 9 | import dev.icerock.moko.permissions.PermissionsController 10 | 11 | actual abstract class PermissionsControllerMock : PermissionsController { 12 | actual abstract override suspend fun providePermission(permission: Permission) 13 | 14 | actual abstract override suspend fun isPermissionGranted(permission: Permission): Boolean 15 | 16 | override fun bind( 17 | activity: ComponentActivity 18 | ) { 19 | TODO("Not yet implemented") 20 | } 21 | 22 | actual companion object 23 | } 24 | -------------------------------------------------------------------------------- /permissions-test/src/commonMain/kotlin/dev/icerock/moko/permissions/test/PermissionsControllerMock.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.test 6 | 7 | import dev.icerock.moko.permissions.DeniedException 8 | import dev.icerock.moko.permissions.Permission 9 | import dev.icerock.moko.permissions.PermissionState 10 | import dev.icerock.moko.permissions.PermissionsController 11 | 12 | expect abstract class PermissionsControllerMock constructor() : PermissionsController { 13 | abstract override suspend fun providePermission(permission: Permission) 14 | 15 | abstract override suspend fun isPermissionGranted(permission: Permission): Boolean 16 | 17 | companion object 18 | } 19 | 20 | fun createPermissionControllerMock( 21 | allow: Set = emptySet(), 22 | granted: Set = emptySet(), 23 | ): PermissionsControllerMock = object : PermissionsControllerMock() { 24 | private val granted = mutableSetOf().apply { addAll(granted) } 25 | 26 | override suspend fun providePermission(permission: Permission) { 27 | if (allow.contains(permission)) { 28 | this.granted.add(permission) 29 | return 30 | } 31 | if (this.granted.contains(permission)) return 32 | 33 | throw DeniedException(permission, "mock block permission") 34 | } 35 | 36 | override suspend fun isPermissionGranted(permission: Permission): Boolean { 37 | return this.granted.contains(permission) 38 | } 39 | 40 | override fun openAppSettings() = Unit 41 | 42 | override suspend fun getPermissionState(permission: Permission): PermissionState { 43 | return if (isPermissionGranted(permission)) { 44 | PermissionState.Granted 45 | } else { 46 | PermissionState.NotDetermined 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /permissions-test/src/iosMain/kotlin/dev/icerock/moko/permissions/test/PermissionsControllerMock.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.test 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionsController 9 | 10 | actual abstract class PermissionsControllerMock : PermissionsController { 11 | actual abstract override suspend fun providePermission(permission: Permission) 12 | 13 | actual abstract override suspend fun isPermissionGranted(permission: Permission): Boolean 14 | 15 | actual companion object 16 | } 17 | -------------------------------------------------------------------------------- /permissions/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("dev.icerock.moko.gradle.multiplatform.mobile") 7 | id("dev.icerock.moko.gradle.publication") 8 | id("dev.icerock.moko.gradle.stub.javadoc") 9 | id("dev.icerock.moko.gradle.detekt") 10 | } 11 | 12 | android { 13 | namespace = "dev.icerock.moko.permissions" 14 | } 15 | 16 | dependencies { 17 | commonMainImplementation(libs.coroutines) 18 | androidMainImplementation(libs.activity) 19 | androidMainImplementation(libs.lifecycleRuntime) 20 | } 21 | -------------------------------------------------------------------------------- /permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionDelegate.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | @file:Suppress("MatchingDeclarationName") 6 | 7 | package dev.icerock.moko.permissions 8 | 9 | import android.content.Context 10 | 11 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 12 | actual interface PermissionDelegate { 13 | fun getPermissionStateOverride(applicationContext: Context): PermissionState? 14 | fun getPlatformPermission(): List 15 | } 16 | -------------------------------------------------------------------------------- /permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsController.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions 6 | 7 | import android.content.Context 8 | import androidx.activity.ComponentActivity 9 | 10 | actual interface PermissionsController { 11 | actual suspend fun providePermission(permission: Permission) 12 | actual suspend fun isPermissionGranted(permission: Permission): Boolean 13 | actual suspend fun getPermissionState(permission: Permission): PermissionState 14 | actual fun openAppSettings() 15 | 16 | fun bind(activity: ComponentActivity) 17 | 18 | companion object { 19 | operator fun invoke( 20 | applicationContext: Context 21 | ): PermissionsController { 22 | return PermissionsControllerImpl( 23 | applicationContext = applicationContext 24 | ) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsControllerImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions 6 | 7 | import android.app.Activity 8 | import android.content.Context 9 | import android.content.Intent 10 | import android.content.pm.PackageManager 11 | import android.net.Uri 12 | import android.provider.Settings 13 | import androidx.activity.ComponentActivity 14 | import androidx.activity.result.ActivityResultLauncher 15 | import androidx.activity.result.ActivityResultRegistryOwner 16 | import androidx.activity.result.contract.ActivityResultContracts 17 | import androidx.core.app.ActivityCompat 18 | import androidx.core.content.ContextCompat 19 | import androidx.lifecycle.Lifecycle 20 | import androidx.lifecycle.LifecycleEventObserver 21 | import androidx.lifecycle.LifecycleOwner 22 | import kotlinx.coroutines.flow.MutableSharedFlow 23 | import kotlinx.coroutines.flow.MutableStateFlow 24 | import kotlinx.coroutines.flow.filterNotNull 25 | import kotlinx.coroutines.flow.first 26 | import kotlinx.coroutines.sync.Mutex 27 | import kotlinx.coroutines.sync.withLock 28 | import kotlinx.coroutines.withTimeoutOrNull 29 | import java.util.UUID 30 | 31 | @Suppress("TooManyFunctions") 32 | class PermissionsControllerImpl( 33 | private val applicationContext: Context, 34 | ) : PermissionsController { 35 | private val activityHolder = MutableStateFlow(null) 36 | 37 | private val mutex: Mutex = Mutex() 38 | 39 | private val launcherHolder = MutableStateFlow>?>(null) 40 | 41 | private val permissionRequestResultFlow = MutableSharedFlow(extraBufferCapacity = 1) 42 | 43 | private val key = UUID.randomUUID().toString() 44 | 45 | override fun bind(activity: ComponentActivity) { 46 | unbindActivity() 47 | this.activityHolder.value = activity 48 | val activityResultRegistryOwner = activity as ActivityResultRegistryOwner 49 | 50 | val launcher = activityResultRegistryOwner.activityResultRegistry.register( 51 | key, 52 | ActivityResultContracts.RequestMultiplePermissions() 53 | ) { permissions -> 54 | val isCancelled = permissions.isEmpty() 55 | 56 | if (isCancelled) { 57 | permissionRequestResultFlow.tryEmit(PermissionRequestResult.CANCELLED) 58 | return@register 59 | } 60 | 61 | val success = permissions.values.all { it } 62 | 63 | if (success) { 64 | permissionRequestResultFlow.tryEmit(PermissionRequestResult.GRANTED) 65 | } else { 66 | if (shouldShowRequestPermissionRationale(activity, permissions.keys.first())) { 67 | permissionRequestResultFlow.tryEmit(PermissionRequestResult.DENIED) 68 | } else { 69 | permissionRequestResultFlow.tryEmit(PermissionRequestResult.DENIED_ALWAYS) 70 | } 71 | } 72 | } 73 | 74 | launcherHolder.value = launcher 75 | 76 | val observer = object : LifecycleEventObserver { 77 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 78 | if (event == Lifecycle.Event.ON_DESTROY) { 79 | unbindActivity() 80 | source.lifecycle.removeObserver(this) 81 | } 82 | } 83 | } 84 | activity.lifecycle.addObserver(observer) 85 | } 86 | 87 | override suspend fun providePermission(permission: Permission) { 88 | mutex.withLock { 89 | val launcher = awaitActivityResultLauncher() 90 | val platformPermission = permission.delegate.getPlatformPermission() 91 | launcher.launch(platformPermission.toTypedArray()) 92 | 93 | when (permissionRequestResultFlow.first()) { 94 | PermissionRequestResult.GRANTED -> Unit 95 | PermissionRequestResult.DENIED -> throw DeniedException(permission) 96 | PermissionRequestResult.DENIED_ALWAYS -> throw DeniedAlwaysException(permission) 97 | PermissionRequestResult.CANCELLED -> throw RequestCanceledException(permission) 98 | } 99 | } 100 | } 101 | 102 | private suspend fun awaitActivityResultLauncher(): ActivityResultLauncher> { 103 | val activityResultLauncher = launcherHolder.value 104 | if (activityResultLauncher != null) return activityResultLauncher 105 | 106 | return withTimeoutOrNull(AWAIT_ACTIVITY_TIMEOUT_DURATION_MS) { 107 | launcherHolder.filterNotNull().first() 108 | } ?: error( 109 | "activityResultLauncher is null, `bind` function was never called," + 110 | " consider calling permissionsController.bind(activity)" + 111 | " or BindEffect(permissionsController) in the composable function," + 112 | " check the documentation for more info: " + 113 | "https://github.com/icerockdev/moko-permissions/blob/master/README.md" 114 | ) 115 | } 116 | 117 | private suspend fun awaitActivity(): Activity { 118 | val activity = activityHolder.value 119 | if (activity != null) return activity 120 | 121 | return withTimeoutOrNull(AWAIT_ACTIVITY_TIMEOUT_DURATION_MS) { 122 | activityHolder.filterNotNull().first() 123 | } ?: error( 124 | "activity is null, `bind` function was never called," + 125 | " consider calling permissionsController.bind(activity)" + 126 | " or BindEffect(permissionsController) in the composable function," + 127 | " check the documentation for more info: " + 128 | "https://github.com/icerockdev/moko-permissions/blob/master/README.md" 129 | ) 130 | } 131 | 132 | override suspend fun isPermissionGranted(permission: Permission): Boolean { 133 | return getPermissionState(permission) == PermissionState.Granted 134 | } 135 | 136 | @Suppress("ReturnCount") 137 | override suspend fun getPermissionState(permission: Permission): PermissionState { 138 | permission.delegate.getPermissionStateOverride(applicationContext)?.let { return it } 139 | val permissions: List = permission.delegate.getPlatformPermission() 140 | val status: List = permissions.map { 141 | ContextCompat.checkSelfPermission(applicationContext, it) 142 | } 143 | val isAllGranted: Boolean = status.all { it == PackageManager.PERMISSION_GRANTED } 144 | if (isAllGranted) return PermissionState.Granted 145 | 146 | val isAllRequestRationale: Boolean = permissions.all { 147 | shouldShowRequestPermissionRationale(it) 148 | } 149 | return if (isAllRequestRationale) { 150 | PermissionState.Denied 151 | } else { 152 | PermissionState.NotGranted 153 | } 154 | } 155 | 156 | private suspend fun shouldShowRequestPermissionRationale(permission: String): Boolean { 157 | val activity = awaitActivity() 158 | return shouldShowRequestPermissionRationale(activity, permission) 159 | } 160 | 161 | private fun shouldShowRequestPermissionRationale( 162 | activity: Activity, 163 | permission: String 164 | ): Boolean { 165 | return ActivityCompat.shouldShowRequestPermissionRationale( 166 | activity, 167 | permission 168 | ) 169 | } 170 | 171 | override fun openAppSettings() { 172 | val intent = Intent().apply { 173 | action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS 174 | data = Uri.fromParts("package", applicationContext.packageName, null) 175 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 176 | } 177 | applicationContext.startActivity(intent) 178 | } 179 | 180 | private fun unbindActivity() { 181 | launcherHolder.value?.unregister() 182 | activityHolder.value = null 183 | launcherHolder.value = null 184 | } 185 | 186 | private companion object { 187 | private const val AWAIT_ACTIVITY_TIMEOUT_DURATION_MS = 2000L 188 | } 189 | } 190 | 191 | private enum class PermissionRequestResult { 192 | GRANTED, DENIED, DENIED_ALWAYS, CANCELLED 193 | } 194 | -------------------------------------------------------------------------------- /permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/DeniedExceptions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions 6 | 7 | open class DeniedException( 8 | val permission: Permission, 9 | message: String? = null 10 | ) : Exception(message) 11 | 12 | class DeniedAlwaysException( 13 | permission: Permission, 14 | message: String? = null 15 | ) : DeniedException(permission, message) 16 | -------------------------------------------------------------------------------- /permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/Permission.kt: -------------------------------------------------------------------------------- 1 | package dev.icerock.moko.permissions 2 | 3 | interface Permission { 4 | val delegate: PermissionDelegate 5 | 6 | // Extended by individual permission delegates 7 | companion object 8 | } 9 | -------------------------------------------------------------------------------- /permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/PermissionDelegate.kt: -------------------------------------------------------------------------------- 1 | package dev.icerock.moko.permissions 2 | 3 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 4 | expect interface PermissionDelegate 5 | -------------------------------------------------------------------------------- /permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/PermissionState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions 6 | 7 | enum class PermissionState { 8 | 9 | /** 10 | * Starting state for each permission. 11 | */ 12 | NotDetermined, 13 | 14 | /** 15 | * Android-only. This could mean [NotDetermined] or [DeniedAlways], but the OS doesn't 16 | * expose which of the two it is in all scenarios. 17 | */ 18 | NotGranted, 19 | 20 | Granted, 21 | 22 | /** 23 | * Android-only. 24 | */ 25 | Denied, 26 | 27 | /** 28 | * On Android only applicable to Push Notifications. 29 | */ 30 | DeniedAlways 31 | } 32 | -------------------------------------------------------------------------------- /permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/PermissionsController.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions 6 | 7 | expect interface PermissionsController { 8 | /** 9 | * Check is permission already granted and if not - request permission from user. 10 | * 11 | * @param permission what permission we want to provide 12 | * 13 | * @throws DeniedException if user decline request, but we can retry (only on Android) 14 | * @throws DeniedAlwaysException if user decline request and we can't show request again 15 | * (we should send user to settings) 16 | * @throws RequestCanceledException if user cancel request without response (only on Android) 17 | */ 18 | suspend fun providePermission(permission: Permission) 19 | 20 | /** 21 | * @return true if permission already granted. In all other cases - false. 22 | */ 23 | suspend fun isPermissionGranted(permission: Permission): Boolean 24 | 25 | /** 26 | * Returns current state of permission. Can be suspended because on 27 | * Android detection of `Denied`/`NotDetermined` requires a bound FragmentManager. 28 | * 29 | * @param permission state of what permission we want 30 | * 31 | * @return current state. On Android can't be `DeniedAlways` (except push notifications). 32 | * On iOS can't be `Denied`. 33 | * @see PermissionState for a detailed description. 34 | */ 35 | suspend fun getPermissionState(permission: Permission): PermissionState 36 | 37 | /** 38 | * Open system UI of application settings to change permissions state 39 | */ 40 | fun openAppSettings() 41 | } 42 | -------------------------------------------------------------------------------- /permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/RequestCanceledException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions 6 | 7 | class RequestCanceledException( 8 | val permission: Permission, 9 | message: String? = null 10 | ) : Exception(message) 11 | -------------------------------------------------------------------------------- /permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/Async.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions 6 | 7 | import platform.Foundation.NSThread 8 | 9 | inline fun mainContinuation( 10 | noinline block: (T1) -> Unit 11 | ): (T1) -> Unit = { arg1 -> 12 | if (NSThread.isMainThread()) { 13 | block.invoke(arg1) 14 | } else { 15 | MainRunDispatcher.run { 16 | block.invoke(arg1) 17 | } 18 | } 19 | } 20 | 21 | inline fun mainContinuation( 22 | noinline block: (T1, T2) -> Unit 23 | ): (T1, T2) -> Unit = { arg1, arg2 -> 24 | if (NSThread.isMainThread()) { 25 | block.invoke(arg1, arg2) 26 | } else { 27 | MainRunDispatcher.run { 28 | block.invoke(arg1, arg2) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/MainRunDispatcher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions 6 | 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.Runnable 9 | import platform.Foundation.NSRunLoop 10 | import platform.Foundation.performBlock 11 | import kotlin.coroutines.CoroutineContext 12 | 13 | /** 14 | * Simple object made to ensure dispatching to the main looper on iOS 15 | */ 16 | internal object MainRunDispatcher : CoroutineDispatcher() { 17 | override fun dispatch(context: CoroutineContext, block: Runnable) = 18 | NSRunLoop.mainRunLoop.performBlock { block.run() } 19 | } 20 | -------------------------------------------------------------------------------- /permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/PermissionDelegate.ios.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | @file:Suppress("MatchingDeclarationName") 6 | 7 | package dev.icerock.moko.permissions 8 | 9 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 10 | actual interface PermissionDelegate { 11 | suspend fun providePermission() 12 | suspend fun getPermissionState(): PermissionState 13 | } 14 | -------------------------------------------------------------------------------- /permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/PermissionsController.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions 6 | 7 | import dev.icerock.moko.permissions.ios.PermissionsControllerProtocol 8 | 9 | actual typealias PermissionsController = PermissionsControllerProtocol 10 | -------------------------------------------------------------------------------- /permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/ios/PermissionsController.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.ios 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionState 9 | import platform.Foundation.NSURL 10 | import platform.UIKit.UIApplication 11 | import platform.UIKit.UIApplicationOpenSettingsURLString 12 | 13 | class PermissionsController : PermissionsControllerProtocol { 14 | 15 | override suspend fun providePermission(permission: Permission) { 16 | return permission.delegate.providePermission() 17 | } 18 | 19 | override suspend fun isPermissionGranted(permission: Permission): Boolean { 20 | return permission.delegate.getPermissionState() == PermissionState.Granted 21 | } 22 | 23 | override suspend fun getPermissionState(permission: Permission): PermissionState { 24 | return permission.delegate.getPermissionState() 25 | } 26 | 27 | override fun openAppSettings() { 28 | val settingsUrl: NSURL = NSURL.URLWithString(UIApplicationOpenSettingsURLString)!! 29 | UIApplication.sharedApplication.openURL(settingsUrl, mapOf(), null) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/ios/PermissionsControllerProtocol.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.permissions.ios 6 | 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionState 9 | 10 | interface PermissionsControllerProtocol { 11 | suspend fun providePermission(permission: Permission) 12 | suspend fun isPermissionGranted(permission: Permission): Boolean 13 | suspend fun getPermissionState(permission: Permission): PermissionState 14 | fun openAppSettings() 15 | } 16 | -------------------------------------------------------------------------------- /sample/android-app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("dev.icerock.moko.gradle.android.application") 3 | id("dev.icerock.moko.gradle.detekt") 4 | } 5 | 6 | android { 7 | namespace = "com.icerockdev" 8 | 9 | defaultConfig { 10 | applicationId = "dev.icerock.moko.samples.permissions" 11 | 12 | versionCode = 1 13 | versionName = "0.1.0" 14 | } 15 | } 16 | 17 | dependencies { 18 | implementation(libs.androidxCore) 19 | implementation(libs.material) 20 | 21 | implementation(projects.sample.mppLibrary) 22 | implementation(projects.permissionsContacts) 23 | } 24 | -------------------------------------------------------------------------------- /sample/android-app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /opt/android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /sample/android-app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/android-app/src/main/java/com/icerockdev/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.icerockdev 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.widget.LinearLayout 6 | import android.widget.Toast 7 | import androidx.appcompat.app.AppCompatActivity 8 | import com.google.android.material.snackbar.Snackbar 9 | import com.icerockdev.library.SampleViewModel 10 | import dev.icerock.moko.mvvm.dispatcher.eventsDispatcherOnMain 11 | import dev.icerock.moko.mvvm.getViewModel 12 | import dev.icerock.moko.permissions.DeniedAlwaysException 13 | import dev.icerock.moko.permissions.DeniedException 14 | import dev.icerock.moko.permissions.PermissionsController 15 | 16 | class MainActivity : AppCompatActivity(), SampleViewModel.EventListener { 17 | 18 | private lateinit var viewModel: SampleViewModel 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(R.layout.activity_main) 23 | 24 | // Creates viewModel from common code. 25 | viewModel = getViewModel { 26 | SampleViewModel( 27 | eventsDispatcher = eventsDispatcherOnMain(), 28 | permissionsController = PermissionsController(applicationContext = applicationContext) 29 | ) 30 | }.also { 31 | it.permissionsController.bind(this) 32 | it.eventsDispatcher.bind(this, this) 33 | } 34 | } 35 | 36 | fun onRequestButtonClick(@Suppress("UNUSED_PARAMETER") view: View?) { 37 | // Starts permission providing process. 38 | viewModel.onRequestPermissionButtonPressed() 39 | } 40 | 41 | override fun onSuccess() { 42 | showToast("Permission successfully granted!") 43 | } 44 | 45 | override fun onDenied(exception: DeniedException) { 46 | showToast("Permission denied!") 47 | } 48 | 49 | override fun onDeniedAlways(exception: DeniedAlwaysException) { 50 | Snackbar 51 | .make( 52 | findViewById(R.id.root_view), 53 | "Permission is always denied", 54 | Snackbar.LENGTH_LONG 55 | ) 56 | .setAction("Settings") { 57 | openAppSettings() 58 | } 59 | .show() 60 | } 61 | 62 | private fun openAppSettings() { 63 | viewModel.permissionsController.openAppSettings() 64 | } 65 | 66 | private fun showToast(message: String) { 67 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /sample/android-app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 45 | 51 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /sample/ios-app/src/TestViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Aleksey Mikhailov on 23/06/2019. 3 | // Copyright © 2019 IceRock Development. All rights reserved. 4 | // 5 | 6 | import UIKit 7 | import MultiPlatformLibrary 8 | 9 | class TestViewController: UIViewController { 10 | 11 | @IBOutlet private var label: UILabel! 12 | 13 | private var viewModel: SampleViewModel! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | label.text = "wait press..." 19 | 20 | viewModel = SampleViewModel( 21 | eventsDispatcher: EventsDispatcher(listener: self), 22 | permissionsController: PermissionsController() 23 | ) 24 | } 25 | 26 | @IBAction func onPermissionPressed() { 27 | viewModel.onRequestPermissionButtonPressed() 28 | } 29 | 30 | @IBAction func onOpenSettingsPressed() { 31 | PermissionsController().openAppSettings() 32 | } 33 | } 34 | 35 | extension TestViewController: SampleViewModelEventListener { 36 | func onSuccess() { 37 | label.text = "success granted" 38 | } 39 | 40 | func onDenied(exception: DeniedException) { 41 | label.text = "denied" // on ios is impossible 42 | } 43 | 44 | func onDeniedAlways(exception: DeniedAlwaysException) { 45 | label.text = "denied always" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sample/mpp-library/MultiPlatformLibrary.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'MultiPlatformLibrary' 3 | spec.version = '0.1.0' 4 | spec.homepage = 'Link to a Kotlin/Native module homepage' 5 | spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" } 6 | spec.authors = 'IceRock Development' 7 | spec.license = '' 8 | spec.summary = 'Shared code between iOS and Android' 9 | 10 | spec.vendored_frameworks = "build/cocoapods/framework/#{spec.name}.framework" 11 | spec.libraries = "c++" 12 | spec.module_name = "#{spec.name}_umbrella" 13 | 14 | spec.ios.deployment_target = '12.0' 15 | spec.osx.deployment_target = '10.6' 16 | 17 | spec.pod_target_xcconfig = { 18 | 'KOTLIN_FRAMEWORK_BUILD_TYPE[config=*ebug]' => 'debug', 19 | 'KOTLIN_FRAMEWORK_BUILD_TYPE[config=*elease]' => 'release', 20 | 'CURENT_SDK[sdk=iphoneos*]' => 'iphoneos', 21 | 'CURENT_SDK[sdk=iphonesimulator*]' => 'iphonesimulator', 22 | } 23 | 24 | spec.script_phases = [ 25 | { 26 | :name => 'Compile Kotlin/Native', 27 | :execution_position => :before_compile, 28 | :shell_path => '/bin/sh', 29 | :script => <<-SCRIPT 30 | if [ "$KOTLIN_FRAMEWORK_BUILD_TYPE" == "debug" ]; then 31 | CONFIG="Debug" 32 | else 33 | CONFIG="Release" 34 | fi 35 | 36 | if [ "$CURENT_SDK" == "iphoneos" ]; then 37 | TARGET="Ios" 38 | ARCH="Arm64" 39 | elif [ "$CURENT_SDK" == "iphonesimulator" ]; then 40 | if [ "$ARCHS" == "arm64" ]; then 41 | TARGET="IosSimulator" 42 | ARCH="Arm64" 43 | else 44 | TARGET="Ios" 45 | ARCH="X64" 46 | fi 47 | else 48 | echo "unsupported $CURENT_SDK" 49 | exit 1 50 | fi 51 | 52 | MPP_PROJECT_ROOT="$SRCROOT/../../mpp-library" 53 | GRADLE_TASK="syncMultiPlatformLibrary${CONFIG}Framework${TARGET}${ARCH}" 54 | 55 | "$MPP_PROJECT_ROOT/../gradlew" -p "$MPP_PROJECT_ROOT" "$GRADLE_TASK" 56 | SCRIPT 57 | } 58 | ] 59 | end -------------------------------------------------------------------------------- /sample/mpp-library/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("dev.icerock.moko.gradle.multiplatform.mobile") 7 | id("dev.icerock.mobile.multiplatform.ios-framework") 8 | id("dev.icerock.moko.gradle.detekt") 9 | } 10 | 11 | android { 12 | namespace = "com.icerockdev.library" 13 | } 14 | 15 | dependencies { 16 | commonMainImplementation(libs.coroutines) 17 | 18 | commonMainApi(libs.mokoMvvmCore) 19 | commonMainApi(projects.permissions) 20 | commonMainImplementation(projects.permissionsContacts) 21 | commonMainImplementation(projects.permissionsLocation) 22 | 23 | androidMainImplementation(libs.lifecycle) 24 | 25 | commonTestImplementation(libs.mokoMvvmTest) 26 | commonTestImplementation(projects.permissionsTest) 27 | commonTestImplementation(projects.permissionsMicrophone) 28 | } 29 | 30 | framework { 31 | export(projects.permissions) 32 | export(libs.mokoMvvmCore) 33 | } 34 | -------------------------------------------------------------------------------- /sample/mpp-library/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/SampleViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.icerockdev.library 2 | 3 | import dev.icerock.moko.mvvm.dispatcher.EventsDispatcher 4 | import dev.icerock.moko.mvvm.dispatcher.EventsDispatcherOwner 5 | import dev.icerock.moko.mvvm.viewmodel.ViewModel 6 | import dev.icerock.moko.permissions.DeniedAlwaysException 7 | import dev.icerock.moko.permissions.DeniedException 8 | import dev.icerock.moko.permissions.Permission 9 | import dev.icerock.moko.permissions.PermissionState 10 | import dev.icerock.moko.permissions.PermissionsController 11 | import dev.icerock.moko.permissions.contacts.CONTACTS 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.update 14 | import kotlinx.coroutines.launch 15 | 16 | class SampleViewModel( 17 | override val eventsDispatcher: EventsDispatcher, 18 | val permissionsController: PermissionsController 19 | ) : ViewModel(), EventsDispatcherOwner { 20 | 21 | private val permissionType = Permission.CONTACTS 22 | val permissionState = MutableStateFlow(PermissionState.NotDetermined) 23 | 24 | init { 25 | viewModelScope.launch { 26 | permissionState.update { permissionsController.getPermissionState(permissionType) } 27 | println(permissionState) 28 | } 29 | } 30 | 31 | /** 32 | * An example of using [PermissionsController] in common code. 33 | */ 34 | fun onRequestPermissionButtonPressed() { 35 | requestPermission(permissionType) 36 | } 37 | 38 | private fun requestPermission(permission: Permission) { 39 | viewModelScope.launch { 40 | try { 41 | permissionsController.getPermissionState(permission) 42 | .also { println("pre provide $it") } 43 | 44 | // Calls suspend function in a coroutine to request some permission. 45 | permissionsController.providePermission(permission) 46 | // If there are no exceptions, permission has been granted successfully. 47 | 48 | eventsDispatcher.dispatchEvent { onSuccess() } 49 | } catch (deniedAlwaysException: DeniedAlwaysException) { 50 | eventsDispatcher.dispatchEvent { onDeniedAlways(deniedAlwaysException) } 51 | } catch (deniedException: DeniedException) { 52 | eventsDispatcher.dispatchEvent { onDenied(deniedException) } 53 | } finally { 54 | permissionState.update { 55 | permissionsController.getPermissionState(permission) 56 | .also { println("post provide $it") } 57 | } 58 | } 59 | } 60 | } 61 | 62 | interface EventListener { 63 | 64 | fun onSuccess() 65 | 66 | fun onDenied(exception: DeniedException) 67 | 68 | fun onDeniedAlways(exception: DeniedAlwaysException) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/SampleViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.icerockdev.library 2 | 3 | import dev.icerock.moko.mvvm.test.TestViewModelScope 4 | import dev.icerock.moko.mvvm.test.createTestEventsDispatcher 5 | import dev.icerock.moko.permissions.DeniedAlwaysException 6 | import dev.icerock.moko.permissions.DeniedException 7 | import dev.icerock.moko.permissions.Permission 8 | import dev.icerock.moko.permissions.PermissionsController 9 | import dev.icerock.moko.permissions.contacts.CONTACTS 10 | import dev.icerock.moko.permissions.test.createPermissionControllerMock 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlin.test.AfterTest 14 | import kotlin.test.BeforeTest 15 | import kotlin.test.Test 16 | import kotlin.test.assertEquals 17 | 18 | class SampleViewModelTest { 19 | @BeforeTest 20 | fun setup() { 21 | TestViewModelScope.setupViewModelScope(CoroutineScope(Dispatchers.Unconfined)) 22 | } 23 | 24 | @AfterTest 25 | fun tearDown() { 26 | TestViewModelScope.resetViewModelScope() 27 | } 28 | 29 | @Test 30 | fun `test successful permission`() { 31 | val eventsListener = EventsListenerCatcher() 32 | val controller: PermissionsController = createPermissionControllerMock( 33 | allow = setOf(Permission.CONTACTS) 34 | ) 35 | val viewModel = SampleViewModel( 36 | eventsDispatcher = createTestEventsDispatcher(eventsListener), 37 | permissionsController = controller, 38 | ) 39 | 40 | viewModel.onRequestPermissionButtonPressed() 41 | 42 | assertEquals(expected = listOf("onSuccess"), actual = eventsListener.events) 43 | } 44 | 45 | @Test 46 | fun `test already got permission`() { 47 | val eventsListener = EventsListenerCatcher() 48 | val controller: PermissionsController = createPermissionControllerMock( 49 | granted = setOf(Permission.CONTACTS) 50 | ) 51 | val viewModel = SampleViewModel( 52 | eventsDispatcher = createTestEventsDispatcher(eventsListener), 53 | permissionsController = controller, 54 | ) 55 | 56 | viewModel.onRequestPermissionButtonPressed() 57 | 58 | assertEquals(expected = listOf("onSuccess"), actual = eventsListener.events) 59 | } 60 | 61 | @Test 62 | fun `test reject permission`() { 63 | val eventsListener = EventsListenerCatcher() 64 | val controller: PermissionsController = createPermissionControllerMock( 65 | allow = emptySet() 66 | ) 67 | val viewModel = SampleViewModel( 68 | eventsDispatcher = createTestEventsDispatcher(eventsListener), 69 | permissionsController = controller, 70 | ) 71 | 72 | viewModel.onRequestPermissionButtonPressed() 73 | 74 | assertEquals( 75 | expected = listOf("onDenied(dev.icerock.moko.permissions.DeniedException: mock block permission)"), 76 | actual = eventsListener.events 77 | ) 78 | } 79 | 80 | class EventsListenerCatcher : SampleViewModel.EventListener { 81 | private val _events = mutableListOf() 82 | val events: List = _events 83 | 84 | override fun onSuccess() { 85 | _events.add("onSuccess") 86 | } 87 | 88 | override fun onDenied(exception: DeniedException) { 89 | _events.add("onDenied($exception)") 90 | } 91 | 92 | override fun onDeniedAlways(exception: DeniedAlwaysException) { 93 | _events.add("onDeniedAlways($exception)") 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 6 | 7 | dependencyResolutionManagement { 8 | repositories { 9 | mavenCentral() 10 | google() 11 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 12 | } 13 | } 14 | 15 | include(":permissions") 16 | include(":permissions-compose") 17 | include(":permissions-bluetooth") 18 | include(":permissions-camera") 19 | include(":permissions-contacts") 20 | include(":permissions-gallery") 21 | include(":permissions-location") 22 | include(":permissions-avfoundation") 23 | include(":permissions-microphone") 24 | include(":permissions-motion") 25 | include(":permissions-notifications") 26 | include(":permissions-storage") 27 | include(":permissions-test") 28 | include(":sample:android-app") 29 | include(":sample:compose-android-app") 30 | include(":sample:mpp-library") 31 | --------------------------------------------------------------------------------