Showing preview only (217K chars total). Download the full file or copy to clipboard to get everything.
Repository: PatilShreyas/permission-flow-android
Branch: main
Commit: 257d9243bd3b
Files: 85
Total size: 190.6 KB
Directory structure:
gitextract_5sp6w6t0/
├── .github/
│ ├── CODEOWNERS
│ ├── FUNDING.yml
│ ├── dependabot.yml
│ └── workflows/
│ ├── build.yml
│ └── release.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── dev/
│ │ └── shreyaspatil/
│ │ └── permissionFlow/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── dev/
│ │ │ └── shreyaspatil/
│ │ │ └── permissionFlow/
│ │ │ └── example/
│ │ │ ├── data/
│ │ │ │ ├── ContactRepository.kt
│ │ │ │ ├── impl/
│ │ │ │ │ └── AndroidDefaultContactRepository.kt
│ │ │ │ └── model/
│ │ │ │ └── Contact.kt
│ │ │ └── ui/
│ │ │ ├── MainActivity.kt
│ │ │ ├── composePermission/
│ │ │ │ └── ComposePermissionActivity.kt
│ │ │ ├── contacts/
│ │ │ │ ├── ContactsActivity.kt
│ │ │ │ ├── ContactsUiEvents.kt
│ │ │ │ └── ContactsViewModel.kt
│ │ │ ├── fragment/
│ │ │ │ ├── ExampleFragment.kt
│ │ │ │ └── ExampleFragmentActivity.kt
│ │ │ └── multipermission/
│ │ │ └── MultiPermissionActivity.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ └── ic_launcher_background.xml
│ │ ├── drawable-v24/
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── layout/
│ │ │ ├── activity_contacts.xml
│ │ │ ├── activity_fragment_example.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── activity_multipermission.xml
│ │ │ └── view_fragment_example.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── values/
│ │ │ ├── colors.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ ├── values-night/
│ │ │ └── themes.xml
│ │ └── xml/
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test/
│ └── java/
│ └── dev/
│ └── shreyaspatil/
│ └── permissionFlow/
│ └── ExampleUnitTest.kt
├── build.gradle
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── permission-flow/
│ ├── .gitignore
│ ├── build.gradle
│ ├── consumer-rules.pro
│ ├── gradle.properties
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── dev/
│ │ └── shreyaspatil/
│ │ └── permissionFlow/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── dev/
│ │ └── shreyaspatil/
│ │ └── permissionFlow/
│ │ ├── PermissionFlow.kt
│ │ ├── State.kt
│ │ ├── contract/
│ │ │ └── RequestPermissionsContract.kt
│ │ ├── impl/
│ │ │ └── PermissionFlowImpl.kt
│ │ ├── initializer/
│ │ │ └── PermissionFlowInitializer.kt
│ │ ├── internal/
│ │ │ └── ApplicationStateMonitor.kt
│ │ ├── utils/
│ │ │ ├── ActivityResultLauncherExt.kt
│ │ │ ├── PermissionResultLauncher.kt
│ │ │ └── stateFlow/
│ │ │ └── CombinedStateFlow.kt
│ │ └── watchmen/
│ │ └── PermissionWatchmen.kt
│ └── test/
│ └── java/
│ └── dev/
│ └── shreyaspatil/
│ └── permissionFlow/
│ ├── MultiplePermissionStateTest.kt
│ ├── PermissionFlowTest.kt
│ ├── contract/
│ │ └── RequestPermissionsContractTest.kt
│ ├── impl/
│ │ └── PermissionFlowImplTest.kt
│ ├── initializer/
│ │ └── PermissionFlowInitializerTest.kt
│ ├── internal/
│ │ └── ApplicationStateMonitorTest.kt
│ ├── utils/
│ │ ├── ActivityResultLauncherExtTest.kt
│ │ ├── PermissionResultLauncherTest.kt
│ │ └── stateFlow/
│ │ └── CombinedStateFlowTest.kt
│ └── watchmen/
│ └── PermissionWatchmenTest.kt
├── permission-flow-compose/
│ ├── .gitignore
│ ├── build.gradle
│ ├── consumer-rules.pro
│ ├── gradle.properties
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── dev/
│ └── shreyaspatil/
│ └── permissionflow/
│ └── compose/
│ └── PermissionFlow.kt
├── settings.gradle
└── spotless/
└── copyright.kt
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CODEOWNERS
================================================
* @PatilShreyas
================================================
FILE: .github/FUNDING.yml
================================================
github: #
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: PatilShreyas
otechie: # Replace with a single Otechie username
custom: ['https://www.paypal.me/PatilShreyas99/', 'https://github.com/sponsors/PatilShreyas/']
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: "[Update]"
================================================
FILE: .github/workflows/build.yml
================================================
name: Build
on: [push, pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: 17
distribution: temurin
cache: gradle
- name: Grant Permission to Execute
run: chmod +x gradlew
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: build koverXmlReport --stacktrace
- name: Upload Coverage report to CodeCov
uses: codecov/codecov-action@v2
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: permission-flow/build/coverageReport/report.xml
flags: unittests
fail_ci_if_error: true
verbose: true
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
workflow_dispatch:
inputs:
versionName:
description: 'Version Name'
required: true
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: 17
distribution: temurin
cache: gradle
- name: Grant Permission to Execute Gradle
run: chmod +x gradlew
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: build dokkaHtmlMultiModule koverHtmlReport
- name: Publish Library
run: |
echo "Publishing library 🚀"
./gradlew publishAndReleaseToMavenCentral --no-configuration-cache
env:
ORG_GRADLE_PROJECT_VERSION_NAME: ${{ github.event.inputs.versionName }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_PASSWORD }}
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
- name: Create and push tag
run: |
git config --global user.email "shreyaspatilg@gmail.com"
git config --global user.name "$GITHUB_ACTOR"
git tag -a $TAG -m "Release $TAG"
git push origin $TAG
env:
TAG: v${{ github.event.inputs.versionName }}
- name: Create Release on GitHub
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ github.event.inputs.versionName }}
release_name: v${{ github.event.inputs.versionName }}
draft: true
prerelease: false
- name: Gather API Documentation and Coverage Report
run: |
mkdir gh-pages
mv README.md gh-pages/README.md
mv build/docs gh-pages/docs
mv permission-flow/build/coverageReport/html gh-pages/coverageReport
- name: Publish Documentation and Coverage Report
uses: JamesIves/github-pages-deploy-action@v4.3.3
with:
branch: gh-pages
folder: gh-pages
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/
.DS_Store
/build
*/build/
/captures
.externalNativeBuild
.cxx
local.properties
.idea
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
================================================
FILE: CONTRIBUTING.md
================================================
## Feeling Awesome! Thanks for thinking about this.
You can contribute us by filing issues, bugs and PRs. You can also take a look at active issues and fix them.
If you want to discuss on something then feel free to present your opinions, views or any other relevant comment on [discussions](https://github.com/PatilShreyas/permission-flow-android/discussions).
### Code contribution
- Open issue regarding proposed change.
- If your proposed change is approved, Fork this repo and do changes.
- Open PR against latest *development* branch. Add nice description in PR.
- You're done!
### Code contribution checklist
- New code addition/deletion should not break existing flow of a system.
- All tests should be passed.
- Verify `./gradlew build` is passing before raising a PR.
- Reformat code with Spotless `./gradlew spotlessApply` before raising a PR.
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 Shreyas Patil
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# Permission Flow for Android
Know about real-time state of a Android app Permissions with Kotlin Flow APIs. _Made with ❤️ for
Android Developers_.
[](https://github.com/PatilShreyas/permission-flow-android/actions/workflows/build.yml)
[](https://github.com/PatilShreyas/permission-flow-android/actions/workflows/release.yml)
[](https://codecov.io/gh/PatilShreyas/permission-flow-android)
[](https://search.maven.org/artifact/dev.shreyaspatil.permission-flow/permission-flow-android)
[](LICENSE)
[](https://patilshreyas.github.io/permission-flow-android/docs/)
[](https://patilshreyas.github.io/permission-flow-android/coverageReport/)
## 💡Introduction
In big projects, app is generally divided in several modules and in such cases, if any individual
module is just a data module (_not having UI_) and need to know state of a permission, it's not
that easy. This library provides a way to know state of a permission throughout the app and
from any layer of the application safely.
_For example, you can listen for state of contacts permission in class where you'll instantly show
list of contacts when permission is granted._
It's a simple and easy to use library. Just Plug and Play.
## 🚀 Implementation
You can check [/app](/app) directory which includes example application for demonstration.
### 1. Gradle setup
In `build.gradle` of app module, include this dependency
```gradle
dependencies {
implementation "dev.shreyaspatil.permission-flow:permission-flow-android:$version"
// For using in Jetpack Compose
implementation "dev.shreyaspatil.permission-flow:permission-flow-compose:$version"
}
```
_You can find latest version and changelogs in the [releases](https://github.com/PatilShreyas/permission-flow-android/releases)_.
### 2. Observing a Permission State
#### 2.1 Observing Permission with `StateFlow`
A permission state can be subscribed by retrieving `StateFlow<PermissionState>` or `StateFlow<MultiplePermissionState>` as follows:
```kotlin
val permissionFlow = PermissionFlow.getInstance()
// Observe state of single permission
suspend fun observePermission() {
permissionFlow.getPermissionState(Manifest.permission.READ_CONTACTS).collect { state ->
if (state.isGranted) {
// Permission granted, access contacts.
} else if (state.isRationaleRequired == true) {
// Permission denied, but can be requested again
} else {
// Permission denied, and can't be requested again
}
}
}
// Observe state of multiple permissions
suspend fun observeMultiplePermissions() {
permissionFlow.getMultiplePermissionState(
Manifest.permission.READ_CONTACTS,
Manifest.permission.READ_SMS
).collect { state ->
// All permission states
val allPermissions = state.permissions
// Check whether all permissions are granted
val allGranted = state.allGranted
// List of granted permissions
val grantedPermissions = state.grantedPermissions
// List of denied permissions
val deniedPermissions = state.deniedPermissions
// List of permissions requiring rationale
val permissionsRequiringRationale = state.permissionsRequiringRationale
}
}
```
#### 2.2 Observing permissions in Jetpack Compose
State of a permission and state of multiple permissions can also be observed in Jetpack Compose application as follows:
```kotlin
@Composable
fun ExampleSinglePermission() {
val state by rememberPermissionState(Manifest.permission.CAMERA)
if (state.isGranted) {
// Permission granted
} else if (state.isRationaleRequired == true) {
// Permission denied, but can be requested again
} else {
// Permission denied, and can't be requested again
}
}
@Composable
fun ExampleMultiplePermission() {
val state by rememberMultiplePermissionState(
Manifest.permission.CAMERA,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.READ_CONTACTS
)
if (state.allGranted) {
// Render something
}
val grantedPermissions = state.grantedPermissions
// Do something with `grantedPermissions`
val deniedPermissions = state.deniedPermissions
// Do something with `deniedPermissions`
val permissionsRequiringRationale = state.permissionsRequiringRationale
// Do something with `permissionsRequiringRationale`
}
```
### 3. Requesting permission with PermissionFlow
It's necessary to use utilities provided by this library to request permissions so that whenever permission state
changes, this library takes care of notifying respective flows.
#### 3.1 Request permission from Activity / Fragment
Use [`registerForPermissionFlowRequestsResult()`](https://patilshreyas.github.io/permission-flow-android/docs/permission-flow/dev.shreyaspatil.permissionFlow.utils/register-for-permission-flow-requests-result.html) method to get `ActivityResultLauncher`
and use `launch()` method to request for permission.
```kotlin
class ContactsActivity : AppCompatActivity() {
private val permissionLauncher = registerForPermissionFlowRequestsResult()
private fun askContactsPermission() {
permissionLauncher.launch(Manifest.permission.READ_CONTACTS, ...)
}
}
```
#### 3.2 Request permission in Jetpack Compose
Use [`rememberPermissionFlowRequestLauncher()`](https://patilshreyas.github.io/permission-flow-android/docs/permission-flow-compose/dev.shreyaspatil.permissionflow.compose/remember-permission-flow-request-launcher.html) method to get `ManagedActivityResultLauncher`
and use `launch()` method to request for permission.
```kotlin
@Composable
fun Example() {
val permissionLauncher = rememberPermissionFlowRequestLauncher()
Button(onClick = { permissionLauncher.launch(android.Manifest.permission.CAMERA, ...) }) {
Text("Request Permissions")
}
}
```
### 4. Manually notifying permission state changes ⚠️
If you're not using `ActivityResultLauncher` APIs provided by this library then
you will ***not receive permission state change updates***. But there's a provision by which
you can help this library to know about permission state changes.
Use [`PermissionFlow#notifyPermissionsChanged()`](https://patilshreyas.github.io/permission-flow-android/docs/permission-flow/dev.shreyaspatil.permissionFlow/-permission-flow/notify-permissions-changed.html) to notify the permission state changes
from your manual implementations.
For example:
```kotlin
class MyActivity: AppCompatActivity() {
private val permissionFlow = PermissionFlow.getInstance()
private val permissionLauncher = registerForActivityResult(RequestPermission()) { isGranted ->
permissionFlow.notifyPermissionsChanged(android.Manifest.permission.READ_CONTACTS)
}
}
```
### 5. Manually Start / Stop Listening ⚠️
This library starts processing things lazily whenever `getPermissionState()` or `getMultiplePermissionState()` is called
for the first time. But this can be controlled with these methods:
```kotlin
fun doSomething() {
// Stops listening to the state changes of permissions throughout the application.
// This means the state of permission retrieved with [getMultiplePermissionState] method will not
// be updated after stopping listening.
permissionFlow.stopListening()
// Starts listening the changes of state of permissions after stopping listening
permissionFlow.startListening()
}
```
### 6. What about Initialization?
This library automatically gets initialized with the App Startup library.
If you want to provide own coroutine dispatcher
#### 6.1 Initialize **PermissionFlow** as follows (For example, in `Application` class)
```kotlin
class MyApplication: Application() {
override fun onCreate() {
super.onCreate()
val permissionDispatcher = Executors.newFixedThreadPool(3).asCoroutineDispatcher()
PermissionFlow.init(this, permissionDispatcher)
}
}
```
#### 6.2 Disable PermissionFlowInitializer in AndroidManifest.xml
Disable auto initialization of library with default configuration using this:
```xml
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="dev.shreyaspatil.permissionFlow.initializer.PermissionFlowInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
```
## 📄 API Documentation
[Visit the API documentation](https://patilshreyas.github.io/permission-flow-android/docs/) of this library to get more information in detail. This documentation is generated using [Dokka](https://github.com/Kotlin/dokka).
## 📊 Test coverage report
[Check the Test Coverage Report](https://patilshreyas.github.io/permission-flow-android/coverageReport/) of this library. This is generated using [Kover](https://github.com/Kotlin/kotlinx-kover).
---
## 🙋♂️ Contribute
Read [contribution guidelines](CONTRIBUTING.md) for more information regarding contribution.
## 💬 Discuss?
Have any questions, doubts or want to present your opinions, views? You're always welcome. You can [start discussions](https://github.com/PatilShreyas/permission-flow-android/discussions).
## 📝 License
```
Copyright 2022 Shreyas Patil
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/README.md
================================================
# Example
https://github.com/PatilShreyas/permission-flow-android/assets/19620536/5ea5ac2c-24a6-4d16-87f8-1b5a0b8e708e
================================================
FILE: app/build.gradle
================================================
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.compose'
}
kotlin {
jvmToolchain(17)
}
android {
compileSdk 35
defaultConfig {
applicationId "dev.shreyaspatil.permissionFlow.example"
minSdk 21
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
viewBinding true
compose true
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
namespace 'dev.shreyaspatil.permissionFlow.example'
}
dependencies {
// Library
implementation(project(":permission-flow"))
implementation(project(":permission-flow-compose"))
// Android
implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.fragment:fragment-ktx:1.8.6'
implementation "androidx.appcompat:appcompat:$appCompatVersion"
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
// Jetpack Compose
implementation platform("androidx.compose:compose-bom:$composeBomVersion")
implementation "androidx.activity:activity-compose:$activityVersion"
implementation "androidx.compose.material:material"
implementation "androidx.compose.runtime:runtime"
implementation "androidx.compose.animation:animation"
implementation "androidx.compose.ui:ui-tooling"
// Lifecycle
def lifecycleVersion = '2.8.7'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
// Testing
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/src/androidTest/java/dev/shreyaspatil/permissionFlow/ExampleInstrumentedTest.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("dev.shreyaspatil.permiduct", appContext.packageName)
}
}
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PermissionFlow"
tools:targetApi="31">
<activity
android:name=".ui.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ui.composePermission.ComposePermissionActivity" />
<activity android:name=".ui.multipermission.MultiPermissionActivity" />
<activity android:name=".ui.contacts.ContactsActivity" />
<activity android:name=".ui.fragment.ExampleFragmentActivity" />
</application>
</manifest>
================================================
FILE: app/src/main/java/dev/shreyaspatil/permissionFlow/example/data/ContactRepository.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.example.data
import dev.shreyaspatil.permissionFlow.example.data.model.Contact
import kotlinx.coroutines.flow.Flow
interface ContactRepository {
val allContacts: Flow<List<Contact>>
}
================================================
FILE: app/src/main/java/dev/shreyaspatil/permissionFlow/example/data/impl/AndroidDefaultContactRepository.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.example.data.impl
import android.Manifest
import android.content.ContentResolver
import android.database.Cursor
import android.provider.ContactsContract
import androidx.core.database.getStringOrNull
import dev.shreyaspatil.permissionFlow.PermissionFlow
import dev.shreyaspatil.permissionFlow.example.data.ContactRepository
import dev.shreyaspatil.permissionFlow.example.data.model.Contact
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.withContext
class AndroidDefaultContactRepository(
private val contentResolver: ContentResolver,
private val permissionFlow: PermissionFlow = PermissionFlow.getInstance(),
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : ContactRepository {
override val allContacts: Flow<List<Contact>> =
permissionFlow.getPermissionState(Manifest.permission.READ_CONTACTS).transform { state ->
if (state.isGranted) {
emit(getContacts())
} else {
emit(emptyList())
}
}
private suspend fun getContacts(): List<Contact> =
withContext(ioDispatcher) {
buildList {
var cursor: Cursor? = null
try {
val projection =
arrayOf(
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.PHOTO_URI,
)
val order = "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} ASC"
cursor =
contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
projection,
null,
null,
order,
)
if (cursor != null) {
while (cursor.moveToNext()) {
val contactId =
cursor.getStringOrNull(
cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.CONTACT_ID),
)
val name =
cursor.getStringOrNull(
cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME),
)
val number =
cursor.getStringOrNull(
cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.NUMBER),
)
if (contactId != null && name != null && number != null) {
add(Contact(contactId, name, number))
}
}
}
} finally {
cursor?.close()
}
}
}
}
================================================
FILE: app/src/main/java/dev/shreyaspatil/permissionFlow/example/data/model/Contact.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.example.data.model
data class Contact(val id: String, val name: String, val number: String)
================================================
FILE: app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/MainActivity.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.example.ui
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.appcompat.app.AppCompatActivity
import dev.shreyaspatil.permissionFlow.example.databinding.ActivityMainBinding
import dev.shreyaspatil.permissionFlow.example.ui.composePermission.ComposePermissionActivity
import dev.shreyaspatil.permissionFlow.example.ui.contacts.ContactsActivity
import dev.shreyaspatil.permissionFlow.example.ui.fragment.ExampleFragmentActivity
import dev.shreyaspatil.permissionFlow.example.ui.multipermission.MultiPermissionActivity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
with(binding) {
buttonContacts.setOnClickListener { launchScreen<ContactsActivity>() }
buttonMultiPermission.setOnClickListener { launchScreen<MultiPermissionActivity>() }
buttonComposeSample.setOnClickListener { launchScreen<ComposePermissionActivity>() }
buttonFragmentSample.setOnClickListener { launchScreen<ExampleFragmentActivity>() }
}
}
private inline fun <reified S : ComponentActivity> launchScreen() {
startActivity(Intent(this, S::class.java))
}
}
================================================
FILE: app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/composePermission/ComposePermissionActivity.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.example.ui.composePermission
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import dev.shreyaspatil.permissionflow.compose.rememberMultiplePermissionState
import dev.shreyaspatil.permissionflow.compose.rememberPermissionFlowRequestLauncher
/**
* The example activity which demonstrates the usage of PermissionFlow APIs in the Jetpack Compose
*/
class ComposePermissionActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { MainScreen() }
}
}
private val permissions =
arrayOf(
android.Manifest.permission.CAMERA,
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.READ_CALL_LOG,
android.Manifest.permission.READ_CONTACTS,
android.Manifest.permission.READ_PHONE_STATE,
)
@Composable
fun MainScreen() {
val permissionLauncher = rememberPermissionFlowRequestLauncher()
val state by rememberMultiplePermissionState(*permissions)
// or use `rememberPermissionState()` to get the state of a single permission
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = { permissionLauncher.launch(permissions) }) { Text("Request Permissions") }
Column(modifier = Modifier.background(Color.Green).padding(16.dp)) {
Text(text = "Granted Permissions:")
Text(text = state.grantedPermissions.joinToString())
}
Column(modifier = Modifier.background(Color.Red).padding(16.dp)) {
Text(text = "Denied Permissions:")
Text(text = state.deniedPermissions.joinToString())
}
}
}
================================================
FILE: app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/contacts/ContactsActivity.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.example.ui.contacts
import android.Manifest
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import dev.shreyaspatil.permissionFlow.example.databinding.ActivityContactsBinding
import dev.shreyaspatil.permissionFlow.utils.launch
import dev.shreyaspatil.permissionFlow.utils.registerForPermissionFlowRequestsResult
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
class ContactsActivity : AppCompatActivity() {
private lateinit var binding: ActivityContactsBinding
private val viewModel by
viewModels<ContactsViewModel> { ContactsViewModel.FactoryProvider(contentResolver).get() }
private val permissionLauncher = registerForPermissionFlowRequestsResult()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityContactsBinding.inflate(layoutInflater)
setContentView(binding.root)
observeStates()
}
private fun render(state: ContactsUiEvents) {
when (state) {
ContactsUiEvents.ContactPermissionGranted -> {
binding.permissionStatusText.apply {
text = "Contacts Permission Granted!"
setOnClickListener(null)
}
}
ContactsUiEvents.ContactPermissionNotGranted -> {
binding.permissionStatusText.apply {
text = "Click here to ask for contacts permission"
setOnClickListener { askContactsPermission() }
}
}
is ContactsUiEvents.ContactsAvailable -> {
binding.contactsDataText.text =
state.contacts.joinToString("\n") { "${it.id}. ${it.name} (${it.number})" }
}
is ContactsUiEvents.Failure -> {
Toast.makeText(this, state.error, Toast.LENGTH_SHORT).show()
}
}
}
private fun askContactsPermission() {
permissionLauncher.launch(Manifest.permission.READ_CONTACTS)
}
private fun observeStates() {
viewModel.state.flowWithLifecycle(lifecycle).onEach { render(it) }.launchIn(lifecycleScope)
}
}
================================================
FILE: app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/contacts/ContactsUiEvents.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.example.ui.contacts
import dev.shreyaspatil.permissionFlow.example.data.model.Contact
sealed class ContactsUiEvents {
object ContactPermissionNotGranted : ContactsUiEvents()
object ContactPermissionGranted : ContactsUiEvents()
data class ContactsAvailable(val contacts: List<Contact>) : ContactsUiEvents()
data class Failure(val error: String) : ContactsUiEvents()
}
================================================
FILE: app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/contacts/ContactsViewModel.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.example.ui.contacts
import android.Manifest
import android.content.ContentResolver
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.ViewModelInitializer
import dev.shreyaspatil.permissionFlow.PermissionFlow
import dev.shreyaspatil.permissionFlow.example.data.ContactRepository
import dev.shreyaspatil.permissionFlow.example.data.impl.AndroidDefaultContactRepository
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.BUFFERED
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
class ContactsViewModel(
private val repository: ContactRepository,
private val permissionFlow: PermissionFlow = PermissionFlow.getInstance(),
) : ViewModel() {
private val uiEvents = Channel<ContactsUiEvents>(capacity = BUFFERED)
val state: Flow<ContactsUiEvents> = uiEvents.receiveAsFlow()
init {
observeContacts()
observeContactsPermission()
}
private fun observeContacts() {
viewModelScope.launch {
repository.allContacts
.catch { setNextState(ContactsUiEvents.Failure(it.message ?: "Error occurred")) }
.collect { contacts -> setNextState(ContactsUiEvents.ContactsAvailable(contacts)) }
}
}
private fun observeContactsPermission() {
viewModelScope.launch {
permissionFlow.getPermissionState(Manifest.permission.READ_CONTACTS).collect { state ->
if (state.isGranted) {
setNextState(ContactsUiEvents.ContactPermissionGranted)
} else {
setNextState(ContactsUiEvents.ContactPermissionNotGranted)
}
}
}
}
private fun setNextState(nextState: ContactsUiEvents) {
uiEvents.trySend(nextState)
}
class FactoryProvider(private val contentResolver: ContentResolver) {
fun get(): ViewModelProvider.Factory {
val initializer =
ViewModelInitializer(ContactsViewModel::class.java) {
ContactsViewModel(AndroidDefaultContactRepository(contentResolver))
}
return ViewModelProvider.Factory.from(initializer)
}
}
}
================================================
FILE: app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/fragment/ExampleFragment.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.example.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import dev.shreyaspatil.permissionFlow.PermissionFlow
import dev.shreyaspatil.permissionFlow.example.databinding.ViewFragmentExampleBinding
import dev.shreyaspatil.permissionFlow.utils.registerForPermissionFlowRequestsResult
import kotlinx.coroutines.launch
@Suppress("ktlint:standard:property-naming")
class ExampleFragment : Fragment() {
private var _binding: ViewFragmentExampleBinding? = null
private val binding: ViewFragmentExampleBinding
get() = _binding!!
private val permissions =
arrayOf(
android.Manifest.permission.CAMERA,
android.Manifest.permission.READ_CALL_LOG,
android.Manifest.permission.READ_CONTACTS,
android.Manifest.permission.READ_PHONE_STATE,
)
private val permissionFlow = PermissionFlow.getInstance()
private val permissionLauncher = registerForPermissionFlowRequestsResult()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = ViewFragmentExampleBinding.inflate(inflater, null, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
observePermissions()
}
private fun initView() {
binding.button.setOnClickListener { permissionLauncher.launch(permissions) }
}
private fun observePermissions() {
viewLifecycleOwner.lifecycleScope.launch {
permissionFlow.getMultiplePermissionState(*permissions).collect {
val grantedPermissionsText =
it.grantedPermissions.joinToString(
separator = "\n",
prefix = "Granted Permissions:\n",
)
val deniedPermissionsText =
it.deniedPermissions.joinToString(
separator = "\n",
prefix = "Denied Permissions:\n",
)
binding.grantedPermissionsText.text = grantedPermissionsText
binding.deniedPermissionsText.text = deniedPermissionsText
binding.button.isVisible = !it.allGranted
}
}
}
}
================================================
FILE: app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/fragment/ExampleFragmentActivity.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.example.ui.fragment
import android.os.Bundle
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import dev.shreyaspatil.permissionFlow.example.R
class ExampleFragmentActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_fragment_example)
findViewById<FrameLayout>(R.id.frameLayout).let { frameLayout ->
supportFragmentManager
.beginTransaction()
.replace(frameLayout.id, ExampleFragment())
.commit()
}
}
}
================================================
FILE: app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/multipermission/MultiPermissionActivity.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.example.ui.multipermission
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import dev.shreyaspatil.permissionFlow.MultiplePermissionState
import dev.shreyaspatil.permissionFlow.PermissionFlow
import dev.shreyaspatil.permissionFlow.example.databinding.ActivityMultipermissionBinding
import dev.shreyaspatil.permissionFlow.utils.registerForPermissionFlowRequestsResult
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
/**
* This example activity shows how to use [PermissionFlow] to request multiple permissions at once
* and observe multiple permissions at once.
*/
class MultiPermissionActivity : AppCompatActivity() {
private lateinit var binding: ActivityMultipermissionBinding
private val permissionFlow = PermissionFlow.getInstance()
private val permissionLauncher = registerForPermissionFlowRequestsResult()
private val permissions =
arrayOf(
android.Manifest.permission.CAMERA,
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.READ_CALL_LOG,
android.Manifest.permission.READ_CONTACTS,
android.Manifest.permission.READ_PHONE_STATE,
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMultipermissionBinding.inflate(layoutInflater)
setContentView(binding.root)
initViews()
observePermissions()
}
private fun initViews() {
binding.buttonAskPermission.setOnClickListener { permissionLauncher.launch(permissions) }
}
private fun observePermissions() {
permissionFlow
.getMultiplePermissionState(
*permissions) // or use `getPermissionState()` for observing a single permission
.flowWithLifecycle(lifecycle)
.onEach { onPermissionStateChange(it) }
.launchIn(lifecycleScope)
}
private fun onPermissionStateChange(state: MultiplePermissionState) {
if (state.allGranted) {
Toast.makeText(this, "All permissions are granted!", Toast.LENGTH_SHORT).show()
}
binding.textViewGrantedPermissions.text =
"Granted permissions: ${state.grantedPermissions.joinToStringByNewLine()}"
binding.textViewDeniedPermissions.text =
"Denied permissions: ${state.deniedPermissions.joinToStringByNewLine()}"
}
private fun List<String>.joinToStringByNewLine(): String {
return joinToString(prefix = "\n", postfix = "\n", separator = ",\n")
}
}
================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
================================================
FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
================================================
FILE: app/src/main/res/layout/activity_contacts.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="dev.shreyaspatil.permissionFlow.example.ui.contacts.ContactsActivity">
<TextView
android:id="@+id/permissionStatusText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textColor="@android:color/black"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Permission Granted" />
<TextView
android:id="@+id/contactsDataText"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/permissionStatusText"
tools:text="Contacts Data" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/activity_fragment_example.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/frameLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<Button
android:id="@+id/buttonContacts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Observing Contact Permission"
tools:ignore="HardcodedText" />
<Button
android:id="@+id/buttonMultiPermission"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Observing Multiple Permissions"
tools:ignore="HardcodedText" />
<Button
android:id="@+id/buttonComposeSample"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Observing permissions in Compose"
tools:ignore="HardcodedText" />
<Button
android:id="@+id/buttonFragmentSample"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Fragment Example"
tools:ignore="HardcodedText" />
</LinearLayout>
================================================
FILE: app/src/main/res/layout/activity_multipermission.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/buttonAskPermission"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Request Permissions"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/textViewGrantedPermissions"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:textColor="@android:color/holo_green_dark"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/buttonAskPermission"
tools:text="Granted Permissions" />
<TextView
android:id="@+id/textViewDeniedPermissions"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textColor="@android:color/holo_red_dark"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textViewGrantedPermissions"
tools:text="Denied Permisisons" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/view_fragment_example.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="Request Permissions"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/grantedPermissionsText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="Granted Permissions:"
android:textColor="#009688"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button" />
<TextView
android:id="@+id/deniedPermissionsText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="Denied Permissions:"
android:textColor="#E91E63"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/grantedPermissionsText" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
================================================
FILE: app/src/main/res/values/strings.xml
================================================
<resources>
<string name="app_name">PermissionFlow</string>
</resources>
================================================
FILE: app/src/main/res/values/themes.xml
================================================
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.PermissionFlow" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
================================================
FILE: app/src/main/res/values-night/themes.xml
================================================
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.PermissionFlow" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
================================================
FILE: app/src/main/res/xml/backup_rules.xml
================================================
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
================================================
FILE: app/src/main/res/xml/data_extraction_rules.xml
================================================
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
================================================
FILE: app/src/test/java/dev/shreyaspatil/permissionFlow/ExampleUnitTest.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
agpVersion = '8.9.0'
mavenPublishVersion = '0.31.0'
kotlinVersion = '2.1.10'
dokkaVersion = '2.0.0'
composeBomVersion = '2025.02.00'
activityVersion = '1.10.1'
coroutinesVersion = '1.10.0'
appCompatVersion = '1.7.0'
jUnitVersion = '4.13.2'
robolectricVersion = "4.12.2"
mockkVersion = '1.13.17'
turbineVersion = '1.1.0'
androidxCoreTestingVersion = '1.5.0'
spotlessVersion = '6.25.0'
koverVersion = "0.5.1"
startupVersion = "1.2.0"
}
}
plugins {
id 'com.android.application' version "$agpVersion" apply false
id 'com.android.library' version "$agpVersion" apply false
id 'org.jetbrains.kotlin.android' version "$kotlinVersion" apply false
id 'org.jetbrains.kotlin.plugin.compose' version "$kotlinVersion" apply false
id 'org.jetbrains.dokka' version "$dokkaVersion"
id 'com.diffplug.spotless' version "$spotlessVersion" apply false
id("org.jetbrains.kotlinx.kover") version "$koverVersion" apply false
id("com.vanniktech.maven.publish") version "$mavenPublishVersion" apply false
}
subprojects {
apply plugin: 'com.diffplug.spotless'
spotless {
kotlin {
target '**/*.kt'
targetExclude("$buildDir/**/*.kt")
targetExclude('bin/**/*.kt')
ktfmt().dropboxStyle()
licenseHeaderFile rootProject.file('spotless/copyright.kt')
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
apply plugin: 'org.jetbrains.dokka'
tasks.dokkaHtmlMultiModule.configure {
outputDirectory.set(project.mkdir("build/docs"))
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Sat Mar 08 22:38:14 IST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: gradle.properties
================================================
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.configureondemand=true
android.useAndroidX=true
android.nonFinalResIds=false
android.nonTransitiveRClass=true
kotlin.code.style=official
# Library configuration
SONATYPE_HOST=DEFAULT
RELEASE_SIGNING_ENABLED=true
SONATYPE_AUTOMATIC_RELEASE=true
GROUP=dev.shreyaspatil.permission-flow
VERSION_NAME=2.1.0
POM_INCEPTION_YEAR=2022
POM_URL=https://github.com/PatilShreyas/permission-flow-android/
POM_LICENSE_NAME=The Apache Software License, Version 2.0
POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENSE_DIST=repo
POM_SCM_URL=https://github.com/PatilShreyas/permission-flow-android/
POM_SCM_CONNECTION=scm:git:git://github.com/PatilShreyas/permission-flow-android.git
POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/PatilShreyas/permission-flow-android.git
POM_DEVELOPER_ID=PatilShreyas
POM_DEVELOPER_NAME=Shreyas Patil
POM_DEVELOPER_URL=https://github.com/PatilShreyas/
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: permission-flow/.gitignore
================================================
/build
================================================
FILE: permission-flow/build.gradle
================================================
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlinx.kover'
id 'com.vanniktech.maven.publish'
id 'org.jetbrains.dokka'
}
kotlin {
jvmToolchain(17)
}
android {
compileSdk 35
defaultConfig {
minSdk 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
buildConfig false
}
testOptions {
unitTests.all {
if (name == "testDebugUnitTest") {
kover {
disabled = false
binaryReportFile.set(file("$buildDir/coverageReport/coverageReport.bin"))
}
}
}
}
namespace 'dev.shreyaspatil.permissionFlow'
}
dependencies {
// Android
implementation "androidx.appcompat:appcompat:$appCompatVersion"
implementation "androidx.startup:startup-runtime:$startupVersion"
// Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
// Testing
testImplementation "junit:junit:$jUnitVersion"
testImplementation "androidx.test:core:$androidxCoreTestingVersion"
testImplementation("org.robolectric:robolectric:$robolectricVersion")
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
testImplementation "app.cash.turbine:turbine:$turbineVersion"
testImplementation "io.mockk:mockk:$mockkVersion"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
}
tasks.koverHtmlReport {
enabled = true
htmlReportDir.set(layout.buildDirectory.dir("coverageReport/html"))
}
tasks.koverXmlReport {
enabled = true
xmlReportFile.set(layout.buildDirectory.file("coverageReport/report.xml"))
}
dokkaHtml.configure {
dokkaSourceSets {
named("main") {
noAndroidSdkLink.set(false)
}
}
}
================================================
FILE: permission-flow/consumer-rules.pro
================================================
================================================
FILE: permission-flow/gradle.properties
================================================
POM_ARTIFACT_ID=permission-flow-android
POM_NAME=Permission Flow for Android
POM_DESCRIPTION=Know about real-time state of a Android app Permissions with Kotlin Flow APIs.
================================================
FILE: permission-flow/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: permission-flow/src/androidTest/java/dev/shreyaspatil/permissionFlow/ExampleInstrumentedTest.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("dev.shreyaspatil.permiduct.test", appContext.packageName)
}
}
================================================
FILE: permission-flow/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="dev.shreyaspatil.permissionFlow.initializer.PermissionFlowInitializer"
android:value="androidx.startup" />
</provider>
</application>
</manifest>
================================================
FILE: permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/PermissionFlow.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow
import android.content.Context
import dev.shreyaspatil.permissionFlow.PermissionFlow.Companion.getInstance
import dev.shreyaspatil.permissionFlow.PermissionFlow.Companion.init
import dev.shreyaspatil.permissionFlow.impl.PermissionFlowImpl
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.newFixedThreadPoolContext
/**
* A utility class which provides a functionality for observing state of a permission (whether it's
* granted or not) with reactive coroutine stream i.e. [StateFlow].
*
* This takes care of listening to permission state change from any screen throughout the
* application so that you can listen to permission in any layer of architecture within app.
*
* To retrieve the instance, use [getInstance] method but make sure to initialize it with [init]
* method before retrieving instance. Otherwise, it'll throw [IllegalStateException]
*
* Example usage:
*
* **1. Initialization**
*
* ```
* class MyApplication: Application() {
* override fun onCreate() {
* super.onCreate()
* PermissionFlow.init(this)
* }
* }
* ```
*
* **2. Observing permission**
*
* ```
* val permissionFlow = PermissionFlow.getInstance()
*
* fun observeContactsPermission() {
* coroutineScope.launch {
* permissionFlow.getPermissionState(android.Manifest.permission.READ_CONTACTS)
* .collect { state ->
* if (state.isGranted) {
* // Do something
* } else {
* if (state.isRationaleRequired) {
* // Do something
* }
* }
* }
* }
* }
* ```
*
* **3. Launching permission**
*
* ```
* class MyActivity: AppCompatActivity() {
* private val permissionLauncher = registerForPermissionFlowRequestsResult()
*
* fun askContactPermission() {
* permissionLauncher.launch(android.Manifest.permission.READ_CONTACTS)
* }
* }
* ```
*
* This utility tries to listen to permission state change which may not happen within a application
* (_e.g. user trying to allow permission from app settings_), but doesn't guarantee that you'll
* always get a updated state at the accurate instant.
*/
interface PermissionFlow {
/**
* Returns [StateFlow] for a given [permission]
*
* @param permission Unique permission identity (for e.g.
* [android.Manifest.permission.READ_CONTACTS])
*
* Example:
* ```
* permissionFlow.getPermissionState(android.Manifest.permission.READ_CONTACTS)
* .collect { state ->
* if (state.isGranted) {
* // Do something
* } else {
* if (state.isRationaleRequired) {
* // Do something
* }
* }
* }
* ```
*/
fun getPermissionState(permission: String): StateFlow<PermissionState>
/**
* Returns [StateFlow] of a combining state for [permissions]
*
* @param permissions List of permissions (for e.g. [android.Manifest.permission.READ_CONTACTS],
* [android.Manifest.permission.READ_SMS])
*
* Example:
* ```
* permissionFlow.getMultiplePermissionState(
* android.Manifest.permission.READ_CONTACTS,
* android.Manifest.permission.READ_SMS
* ).collect { state ->
* // All permission states
* val allPermissions = state.permissions
*
* // Check whether all permissions are granted
* val allGranted = state.allGranted
*
* // List of granted permissions
* val grantedPermissions = state.grantedPermissions
*
* // List of denied permissions
* val deniedPermissions = state.deniedPermissions
* }
* ```
*/
fun getMultiplePermissionState(vararg permissions: String): StateFlow<MultiplePermissionState>
/**
* This helps to check if specified [permissions] are changed and it verifies it and updates the
* state of permissions which are being observed via [getMultiplePermissionState] method.
*
* This can be useful when you are not using result launcher which is provided with this library
* and manually handling permission request and want to update the state of permission in this
* library so that flows which are being observed should get an updated state.
*
* If [stopListening] is called earlier and hasn't started listening again, notifying permission
* doesn't work. Its new state is automatically calculated after starting listening to states
* again by calling [startListening] method.
*
* Example usage:
*
* In this example, we are not using result launcher provided by this library. So we are
* manually notifying library about state change of a permission.
*
* ```
* class MyActivity: AppCompatActivity() {
* private val permissionFlow = PermissionFlow.getInstance()
* private val permissionLauncher = registerForActivityResult(RequestPermission()) { isGranted ->
* permissionFlow.notifyPermissionsChanged(android.Manifest.permission.READ_CONTACTS)
* }
* }
* ```
*
* @param permissions List of permissions
*/
fun notifyPermissionsChanged(vararg permissions: String)
/**
* Starts listening the changes of state of permissions.
*
* Ideally it automatically starts listening eagerly when application is started and created via
* [dev.shreyaspatil.permissionFlow.initializer.PermissionFlowInitializer]. If initializer is
* disabled, then starts listening lazily when [getPermissionState] [getPermissionEvent] or
* [getMultiplePermissionState] method is used for the first time. But this can be used to start
* to listen again after stopping listening with [stopListening].
*/
fun startListening()
/**
* Stops listening to the state changes of permissions throughout the application. This means
* the state of permission retrieved with [getMultiplePermissionState] method will not be
* updated after stopping listening. To start to listen again, use [startListening] method.
*/
fun stopListening()
/**
* Companion of [PermissionFlow] to provide initialization of [PermissionFlow] as well as
* getting instance.
*/
companion object {
@OptIn(DelicateCoroutinesApi::class)
private val DEFAULT_DISPATCHER = newFixedThreadPoolContext(2, "PermissionFlow")
/**
* Initializes this [PermissionFlow] instance with specified arguments.
*
* @param context The Android's [Context]. Application context is recommended.
* @param dispatcher Coroutine dispatcher to be used in the [PermissionFlow]. By default, it
* utilizes dispatcher having fixed two number of threads.
*/
@JvmStatic
@JvmOverloads
fun init(context: Context, dispatcher: CoroutineDispatcher = DEFAULT_DISPATCHER) {
PermissionFlowImpl.init(context, dispatcher)
}
/**
* Returns an instance with default implementation of [PermissionFlow].
*
* @return Instance of [PermissionFlow].
* @throws IllegalStateException If method [init] is not called before using this method.
*/
@JvmStatic
fun getInstance(): PermissionFlow =
PermissionFlowImpl.instance
?: error(
"Failed to create instance of PermissionFlow. Did you forget to call `PermissionFlow.init(context)`?")
}
}
================================================
FILE: permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/State.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow
/**
* State model of a permission
*
* @property permission Name of a permission
* @property isGranted State of a permission whether it's granted or not
* @property isRationaleRequired Whether to show rationale for a permission or not.
*/
data class PermissionState(
val permission: String,
val isGranted: Boolean,
val isRationaleRequired: Boolean?,
)
/**
* State model for multiple permissions
*
* @property permissions List of state of multiple permissions
*/
data class MultiplePermissionState(val permissions: List<PermissionState>) {
/** Returns true if all permissions are granted by user */
val allGranted by lazy { permissions.all { it.isGranted } }
/** List of permissions which are granted by user */
val grantedPermissions by lazy { permissions.filter { it.isGranted }.map { it.permission } }
/** List of permissions which are denied / not granted by user */
val deniedPermissions by lazy { permissions.filter { !it.isGranted }.map { it.permission } }
/** List of permissions which are required to show rationale */
val permissionsRequiringRationale by lazy {
permissions.filter { it.isRationaleRequired == true }.map { it.permission }
}
}
================================================
FILE: permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/contract/RequestPermissionsContract.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.contract
import android.content.Context
import android.content.Intent
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
import dev.shreyaspatil.permissionFlow.PermissionFlow
/**
* An [ActivityResultContract] which delegates request and response to
* [ActivityResultContracts.RequestMultiplePermissions] and silently notifies [PermissionFlow]
* regarding state change of a permissions which are requested through [ActivityResultLauncher].
*
* Refer to [ComponentActivity.registerForPermissionFlowRequestsResult] for actual usage.
*/
class RequestPermissionsContract(
private val contract: RequestMultiplePermissions = RequestMultiplePermissions(),
private val permissionFlow: PermissionFlow = PermissionFlow.getInstance(),
) : ActivityResultContract<Array<String>, Map<String, Boolean>>() {
override fun createIntent(context: Context, input: Array<String>): Intent {
return contract.createIntent(context, input)
}
override fun parseResult(resultCode: Int, intent: Intent?): Map<String, Boolean> {
return contract.parseResult(resultCode, intent).also {
val permissions = it.keys.toList().toTypedArray()
permissionFlow.notifyPermissionsChanged(*permissions)
}
}
}
================================================
FILE: permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/impl/PermissionFlowImpl.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.impl
import android.app.Application
import android.content.Context
import androidx.annotation.VisibleForTesting
import dev.shreyaspatil.permissionFlow.MultiplePermissionState
import dev.shreyaspatil.permissionFlow.PermissionFlow
import dev.shreyaspatil.permissionFlow.PermissionState
import dev.shreyaspatil.permissionFlow.internal.ApplicationStateMonitor
import dev.shreyaspatil.permissionFlow.watchmen.PermissionWatchmen
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.StateFlow
/** Default implementation of a [PermissionFlow] */
internal class PermissionFlowImpl
@VisibleForTesting
constructor(
private val watchmen: PermissionWatchmen,
) : PermissionFlow {
override fun getPermissionState(permission: String): StateFlow<PermissionState> {
return watchmen.watchState(permission)
}
override fun getMultiplePermissionState(
vararg permissions: String
): StateFlow<MultiplePermissionState> {
return watchmen.watchMultipleState(permissions.toList().toTypedArray())
}
override fun notifyPermissionsChanged(vararg permissions: String) {
watchmen.notifyPermissionsChanged(permissions.toList().toTypedArray())
}
override fun startListening() {
watchmen.wakeUp()
}
override fun stopListening() {
watchmen.sleep()
}
internal companion object {
@Volatile
var instance: PermissionFlowImpl? = null
private set
@Synchronized
fun init(context: Context, dispatcher: CoroutineDispatcher) {
if (instance == null) {
val monitor = ApplicationStateMonitor(context.applicationContext as Application)
val watchmen =
PermissionWatchmen(
appStateMonitor = monitor,
dispatcher = dispatcher,
)
instance = PermissionFlowImpl(watchmen)
}
}
}
}
================================================
FILE: permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/initializer/PermissionFlowInitializer.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.initializer
import android.content.Context
import androidx.startup.Initializer
import dev.shreyaspatil.permissionFlow.PermissionFlow
/** Initializes [PermissionFlow] instance on app startup. */
class PermissionFlowInitializer : Initializer<Unit> {
override fun create(context: Context) {
PermissionFlow.init(context)
PermissionFlow.getInstance().startListening()
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
================================================
FILE: permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/internal/ApplicationStateMonitor.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.internal
import android.app.Activity
import android.app.Application
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import dev.shreyaspatil.permissionFlow.PermissionState
import java.lang.ref.WeakReference
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
/**
* Monitors the state of the application and provides information about the info and state of
* application.
*/
internal class ApplicationStateMonitor(private val application: Application) {
private var currentActivity: WeakReference<Activity>? = null
/** Returns the current state of the permission. */
fun getPermissionState(permission: String): PermissionState {
val isGranted = isPermissionGranted(permission)
val isRationaleRequired = shouldShowPermissionRationale(permission)
return PermissionState(permission, isGranted, isRationaleRequired)
}
/** Returns whether the permission should show rationale or not. */
private fun shouldShowPermissionRationale(permission: String): Boolean? {
val activity = currentActivity?.get() ?: return null
return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
/** Returns whether the permission is granted or not. */
private fun isPermissionGranted(permission: String): Boolean {
return ContextCompat.checkSelfPermission(
application,
permission,
) == PackageManager.PERMISSION_GRANTED
}
/**
* A flow which gives callback whenever any activity is started withing application (without
* configuration change) or any activity is resumed after being in multi-window or
* picture-in-picture mode.
*/
val activityForegroundEvents
get() = callbackFlow {
val callback =
object : Application.ActivityLifecycleCallbacks {
private var isActivityChangingConfigurations: Boolean? = null
private var wasInMultiWindowMode: Boolean? = null
private var wasInPictureInPictureMode: Boolean? = null
override fun onActivityPreCreated(
activity: Activity,
savedInstanceState: Bundle?,
) {
currentActivity = WeakReference(activity)
}
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?,
) {
if (currentActivity?.get() != activity) {
currentActivity = WeakReference(activity)
}
}
/**
* Whenever activity receives onStart() lifecycle callback, emit foreground
* event only when activity hasn't changed configurations.
*/
override fun onActivityStarted(activity: Activity) {
if (isActivityChangingConfigurations == false) {
trySend(Unit)
}
}
override fun onActivityStopped(activity: Activity) {
isActivityChangingConfigurations = activity.isChangingConfigurations
}
/**
* Whenever application is resized after being in in PiP or multi-window mode,
* or exits from these modes, onResumed() lifecycle callback is triggered.
*
* Here we assume that user has changed permission from app settings after being
* in PiP or multi-window mode. So whenever these modes are exited, emit
* foreground event.
*/
override fun onActivityResumed(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (isActivityResumedAfterMultiWindowOrPiPMode(activity)) {
trySend(Unit)
}
wasInMultiWindowMode = activity.isInMultiWindowMode
wasInPictureInPictureMode = activity.isInPictureInPictureMode
}
}
/**
* Whenever application is launched in PiP or multi-window mode, onPaused()
* lifecycle callback is triggered.
*/
override fun onActivityPaused(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
wasInMultiWindowMode = activity.isInMultiWindowMode
wasInPictureInPictureMode = activity.isInPictureInPictureMode
}
}
override fun onActivitySaveInstanceState(
activity: Activity,
outState: Bundle,
) {}
override fun onActivityDestroyed(activity: Activity) {
if (activity == currentActivity?.get()) {
currentActivity?.clear()
}
}
/**
* Returns whether [activity] was previously in multi-window mode or PiP mode.
*/
@RequiresApi(Build.VERSION_CODES.N)
private fun isActivityResumedAfterMultiWindowOrPiPMode(activity: Activity) =
(wasInMultiWindowMode == true && !activity.isInMultiWindowMode) ||
(wasInPictureInPictureMode == true &&
!activity.isInPictureInPictureMode)
}
application.registerActivityLifecycleCallbacks(callback)
awaitClose {
// Cleanup
application.unregisterActivityLifecycleCallbacks(callback)
}
}
@VisibleForTesting fun getCurrentActivityReference() = currentActivity
}
================================================
FILE: permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/utils/ActivityResultLauncherExt.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.utils
import androidx.activity.result.ActivityResultLauncher
/**
* A short-hand utility for launching multiple requests with variable arguments support.
*
* @param input Input string
*/
fun ActivityResultLauncher<Array<String>>.launch(vararg input: String) =
launch(input.toList().toTypedArray())
================================================
FILE: permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/utils/PermissionResultLauncher.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:JvmName("PermissionResultLauncher")
package dev.shreyaspatil.permissionFlow.utils
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistry
import androidx.fragment.app.Fragment
import dev.shreyaspatil.permissionFlow.PermissionFlow
import dev.shreyaspatil.permissionFlow.contract.RequestPermissionsContract
/**
* Returns a [ActivityResultLauncher] for this Activity which internally notifies [PermissionFlow]
* about the state change whenever permission state is changed with this launcher.
*
* Usage:
* ```
* class MyActivity: AppCompatActivity() {
* private val permissionLauncher = registerForPermissionFlowRequestsResult()
*
* fun askContactPermission() {
* permissionLauncher.launch(android.Manifest.permission.READ_CONTACTS)
* }
* }
* ```
*
* @param requestPermissionsContract A contract specifying permission request and result.
* @param activityResultRegistry Activity result registry. By default it uses Activity's Result
* registry.
* @param callback Callback of a permission state change.
*/
@JvmOverloads
fun ComponentActivity.registerForPermissionFlowRequestsResult(
requestPermissionsContract: RequestPermissionsContract = RequestPermissionsContract(),
activityResultRegistry: ActivityResultRegistry = this.activityResultRegistry,
callback: ActivityResultCallback<Map<String, Boolean>> = emptyCallback(),
): ActivityResultLauncher<Array<String>> =
registerForActivityResult(
requestPermissionsContract,
activityResultRegistry,
callback,
)
/**
* Returns a [ActivityResultLauncher] for this Fragment which internally notifies [PermissionFlow]
* about the state change whenever permission state is changed with this launcher.
*
* Usage:
* ```
* class MyFragment: Fragment() {
* private val permissionLauncher = registerForPermissionFlowRequestsResult()
*
* fun askContactPermission() {
* permissionLauncher.launch(android.Manifest.permission.READ_CONTACTS)
* }
* }
* ```
*
* @param requestPermissionsContract A contract specifying permission request and result. registry.
* @param callback Callback of a permission state change.
*/
@JvmOverloads
fun Fragment.registerForPermissionFlowRequestsResult(
requestPermissionsContract: RequestPermissionsContract = RequestPermissionsContract(),
callback: ActivityResultCallback<Map<String, Boolean>> = emptyCallback(),
): ActivityResultLauncher<Array<String>> =
registerForActivityResult(
requestPermissionsContract,
callback,
)
/**
* Returns a [ActivityResultLauncher] for this Fragment which internally notifies [PermissionFlow]
* about the state change whenever permission state is changed with this launcher.
*
* Usage:
* ```
* class MyFragment: Fragment() {
* private val permissionLauncher = registerForPermissionFlowRequestsResult()
*
* fun askContactPermission() {
* permissionLauncher.launch(android.Manifest.permission.READ_CONTACTS)
* }
* }
* ```
*
* @param requestPermissionsContract A contract specifying permission request and result.
* @param activityResultRegistry Activity result registry. By default it uses Activity's Result
* registry.
* @param callback Callback of a permission state change.
*/
@JvmOverloads
fun Fragment.registerForPermissionFlowRequestsResult(
requestPermissionsContract: RequestPermissionsContract = RequestPermissionsContract(),
activityResultRegistry: ActivityResultRegistry,
callback: ActivityResultCallback<Map<String, Boolean>> = emptyCallback(),
): ActivityResultLauncher<Array<String>> =
registerForActivityResult(
requestPermissionsContract,
activityResultRegistry,
callback,
)
private fun <T> emptyCallback() = ActivityResultCallback<T> {}
================================================
FILE: permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/utils/stateFlow/CombinedStateFlow.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.utils.stateFlow
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
/**
* [StateFlow] which delegates [flow] to use it as StateFlow and uses [getValue] to calculate value
* at the instant.
*/
private class CombinedStateFlow<T>(
private val getValue: () -> T,
private val flow: Flow<T>,
) : StateFlow<T> {
override val replayCache: List<T>
get() = listOf(value)
override val value: T
get() = getValue()
override suspend fun collect(collector: FlowCollector<T>): Nothing = coroutineScope {
flow.stateIn(this).collect(collector)
}
}
/** Returns implementation of [CombinedStateFlow] */
internal fun <T> combineStates(
getValue: () -> T,
flow: Flow<T>,
): StateFlow<T> = CombinedStateFlow(getValue, flow)
/** Combines multiple [StateFlow]s and transforms them into another [StateFlow] */
internal inline fun <reified T, R> combineStates(
vararg stateFlows: StateFlow<T>,
crossinline transform: (Array<T>) -> R,
): StateFlow<R> =
combineStates(
getValue = { transform(stateFlows.map { it.value }.toTypedArray()) },
flow = combine(*stateFlows) { transform(it) },
)
================================================
FILE: permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/watchmen/PermissionWatchmen.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.watchmen
import dev.shreyaspatil.permissionFlow.MultiplePermissionState
import dev.shreyaspatil.permissionFlow.PermissionState
import dev.shreyaspatil.permissionFlow.internal.ApplicationStateMonitor
import dev.shreyaspatil.permissionFlow.utils.stateFlow.combineStates
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
/** A watchmen which keeps watching state changes of permissions and events of permissions. */
@Suppress("unused")
internal class PermissionWatchmen(
private val appStateMonitor: ApplicationStateMonitor,
dispatcher: CoroutineDispatcher,
) {
private val watchmenScope =
CoroutineScope(
dispatcher + SupervisorJob() + CoroutineName("PermissionWatchmen"),
)
private var watchEventsJob: Job? = null
private var watchActivityEventJob: Job? = null
/** A in-memory store for storing permission and its state holder i.e. [StateFlow] */
private val permissionFlows = mutableMapOf<String, PermissionStateFlowDelegate>()
private val permissionEvents = MutableSharedFlow<PermissionState>()
fun watchState(permission: String): StateFlow<PermissionState> {
// Wakeup watchmen if sleeping
wakeUp()
return getOrCreatePermissionStateFlow(permission)
}
fun watchMultipleState(permissions: Array<String>): StateFlow<MultiplePermissionState> {
// Wakeup watchmen if sleeping
wakeUp()
val permissionStates =
permissions.distinct().map { getOrCreatePermissionStateFlow(it) }.toTypedArray()
return combineStates(*permissionStates) { MultiplePermissionState(it.toList()) }
}
fun notifyPermissionsChanged(permissions: Array<String>) {
watchmenScope.launch {
permissions.forEach { permission ->
permissionEvents.emit(appStateMonitor.getPermissionState(permission))
}
}
}
@Synchronized
fun wakeUp() {
watchPermissionEvents()
watchActivities()
notifyAllPermissionsChanged()
}
@Synchronized
fun sleep() {
watchmenScope.coroutineContext.cancelChildren()
}
/**
* First finds for existing flow (if available) otherwise creates a new [MutableStateFlow] for
* [permission] and returns a read-only [StateFlow] for a [permission].
*/
@Synchronized
private fun getOrCreatePermissionStateFlow(permission: String): StateFlow<PermissionState> {
return permissionFlows
.getOrPut(permission) {
PermissionStateFlowDelegate(appStateMonitor.getPermissionState(permission))
}
.state
}
/** Watches for the permission events and updates appropriate state holders of permission */
private fun watchPermissionEvents() {
if (watchEventsJob != null && watchEventsJob?.isActive == true) return
watchEventsJob =
watchmenScope.launch {
permissionEvents.collect { permissionFlows[it.permission]?.setState(it) }
}
}
/**
* Watches for activity foreground events (to detect whether user has changed permission by
* going in settings) and recalculates state of the permissions which are currently being
* observed.
*/
private fun watchActivities() {
if (watchActivityEventJob != null && watchActivityEventJob?.isActive == true) return
watchActivityEventJob =
appStateMonitor.activityForegroundEvents
.onEach {
// Since this is not priority task, we want to yield current thread for other
// tasks for the watchmen.
yield()
notifyAllPermissionsChanged()
}
.launchIn(watchmenScope)
}
private fun notifyAllPermissionsChanged() {
if (permissionFlows.isEmpty()) return
notifyPermissionsChanged(permissionFlows.keys.toTypedArray())
}
/** A delegate for [MutableStateFlow] which creates flow for holding state of a permission. */
private class PermissionStateFlowDelegate(initialState: PermissionState) {
private val _state = MutableStateFlow(initialState)
val state = _state.asStateFlow()
fun setState(newState: PermissionState) {
_state.value = newState
}
}
}
================================================
FILE: permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/MultiplePermissionStateTest.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class MultiplePermissionStateTest {
@Test
fun allGranted_shouldReturnTrue_whenAllPermissionsAreGranted() {
// When: Multiple permission state having all permissions granted
val permissions =
multiplePermissionState(
grantedPermission("A"),
grantedPermission("B"),
grantedPermission("C"),
)
// Then: All permissions should be granted
assertTrue(permissions.allGranted)
}
@Test
fun allGranted_shouldReturnFalse_whenAllPermissionsAreNotGranted() {
// When: Multiple permission state having some permissions as not granted
val permissions =
multiplePermissionState(
grantedPermission("A"),
deniedPermission("B"),
grantedPermission("C"),
)
// Then: All permissions should NOT be granted
assertFalse(permissions.allGranted)
}
@Test
fun grantedPermissions_shouldReturnListOfGrantedPermissions() {
// When: Multiple permission state
val permissions =
multiplePermissionState(
grantedPermission("A"),
deniedPermission("B"),
grantedPermission("C"),
deniedPermission("D"),
)
// Then: Permissions A and C should be present in granted permissions list
assertEquals(permissions.grantedPermissions, listOf("A", "C"))
}
@Test
fun deniedPermissions_shouldReturnListOfDeniedPermissions() {
// When: Multiple permission state
val permissions =
multiplePermissionState(
grantedPermission("A"),
deniedPermission("B"),
grantedPermission("C"),
deniedPermission("D"),
)
// Then: Permissions B and D should be present in granted permissions list
assertEquals(permissions.deniedPermissions, listOf("B", "D"))
}
@Test
fun permissionsRequiringRationale_shouldReturnListOfDeniedPermissionsRequiringRationale() {
// When: Multiple permission state
val permissions =
multiplePermissionState(
grantedPermission("A"),
deniedPermission("B"),
grantedPermission("C"),
deniedPermissionRequiringRationale("D"),
deniedPermissionRequiringRationale("E"),
)
// Then: Permissions D and E should be present in permissions requiring rationale list
assertEquals(permissions.permissionsRequiringRationale, listOf("D", "E"))
}
private fun multiplePermissionState(vararg permissionState: PermissionState) =
MultiplePermissionState(permissionState.toList())
private fun grantedPermission(permission: String) =
PermissionState(permission = permission, isGranted = true, isRationaleRequired = false)
private fun deniedPermission(permission: String) =
PermissionState(permission = permission, isGranted = false, isRationaleRequired = null)
private fun deniedPermissionRequiringRationale(permission: String) =
PermissionState(permission = permission, isGranted = false, isRationaleRequired = true)
}
================================================
FILE: permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/PermissionFlowTest.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow
import android.app.Application
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class PermissionFlowTest {
@Test
fun testGetInstanceWithoutInit_shouldThrowException() {
val result = runCatching { PermissionFlow.getInstance() }
assertTrue(result.isFailure)
val expectedErrorMessage =
"Failed to create instance of PermissionFlow. Did you forget to call `PermissionFlow.init(context)`?"
assertEquals(expectedErrorMessage, result.exceptionOrNull()!!.message)
}
@Test
fun testGetInstanceWithInit_shouldBeSingleInstanceAlways() {
// Init for the first time
initPermissionFlow()
// Get instance 1
val instance1 = PermissionFlow.getInstance()
// Init for the second time
initPermissionFlow()
// Get instance 2
val instance2 = PermissionFlow.getInstance()
// Both instances should be the same
assert(instance1 === instance2)
}
private fun initPermissionFlow() {
PermissionFlow.init(
mockk { every { applicationContext } returns mockk<Application>() },
)
}
}
================================================
FILE: permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/contract/RequestPermissionsContractTest.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.contract
import android.content.Context
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
import dev.shreyaspatil.permissionFlow.PermissionFlow
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class RequestPermissionsContractTest {
private lateinit var requestMultiplePermissions: RequestMultiplePermissions
private lateinit var permissionFlow: PermissionFlow
private lateinit var context: Context
private lateinit var contract: RequestPermissionsContract
@Before
fun setUp() {
requestMultiplePermissions = mockk()
permissionFlow = mockk(relaxUnitFun = true)
context = mockk()
contract = RequestPermissionsContract(requestMultiplePermissions, permissionFlow)
}
@Test
fun testCreateIntent() {
// Given: List of permissions
val permissions = arrayOf("A", "B", "C", "D")
every { requestMultiplePermissions.createIntent(any(), any()) } returns mockk()
// When: Intent is created with permissions
contract.createIntent(context, permissions)
// Then: The context and permissions should be delegated to original contract
verify(exactly = 1) { requestMultiplePermissions.createIntent(context, permissions) }
}
@Test
fun testParseResult() {
// Given: Result to be parsed
val expectedResult = mapOf("A" to true, "B" to false, "C" to true)
every { requestMultiplePermissions.parseResult(any(), any()) } returns expectedResult
// When: The result is parsed
val actualResult = contract.parseResult(0, null)
// Then: Correct result should be returned
assertEquals(expectedResult, actualResult)
// Then: PermissionFlow should be notified with updated permissions
verify(exactly = 1) { permissionFlow.notifyPermissionsChanged("A", "B", "C") }
}
}
================================================
FILE: permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/impl/PermissionFlowImplTest.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.impl
import dev.shreyaspatil.permissionFlow.MultiplePermissionState
import dev.shreyaspatil.permissionFlow.PermissionState
import dev.shreyaspatil.permissionFlow.watchmen.PermissionWatchmen
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class PermissionFlowImplTest {
private lateinit var watchmen: PermissionWatchmen
private lateinit var permissionFlow: PermissionFlowImpl
@Before
fun setUp() {
watchmen = mockk(relaxUnitFun = true)
permissionFlow = PermissionFlowImpl(watchmen)
}
@Test
fun testGetPermissionState() {
// Given: Permission flow
val expectedFlow = MutableStateFlow(PermissionState("A", true, false))
every { watchmen.watchState("A") } returns expectedFlow
// When: Flow for any permission is retrieved
val actualFlow = permissionFlow.getPermissionState("A")
// Then: Correct flow should be returned
assertEquals(expectedFlow, actualFlow)
}
@Test
fun testGetMultiplePermissionState() {
// Given: Permission flow
val expectedFlow = MutableStateFlow(MultiplePermissionState(emptyList()))
every { watchmen.watchMultipleState(arrayOf("A", "B")) } returns expectedFlow
// When: Flow for multiple permissions is retrieved
val actualFlow = permissionFlow.getMultiplePermissionState("A", "B")
// Then: Correct flow should be returned
assertEquals(expectedFlow, actualFlow)
}
@Test
fun testNotifyPermissionsChanged() {
// Given: Permissions whose state to be notified
val permissions = arrayOf("A", "B", "C")
// When: Permission state changes are notified
permissionFlow.notifyPermissionsChanged(*permissions)
// Then: Watchmen should be get notified about permission changes
verify(exactly = 1) { watchmen.notifyPermissionsChanged(permissions) }
}
@Test
fun testStartListening() {
// When: Starts listening
permissionFlow.startListening()
// Then: Watchmen should wake up
verify(exactly = 1) { watchmen.wakeUp() }
}
@Test
fun testStopListening() {
// When: Stops listening
permissionFlow.stopListening()
// Then: Watchmen should sleep
verify(exactly = 1) { watchmen.sleep() }
}
}
================================================
FILE: permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/initializer/PermissionFlowInitializerTest.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.initializer
import android.content.Context
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import dev.shreyaspatil.permissionFlow.PermissionFlow
import io.mockk.clearAllMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.verifySequence
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.P])
class PermissionFlowInitializerTest {
private lateinit var initializer: PermissionFlowInitializer
private lateinit var permissionFlow: PermissionFlow
@Before
fun setUp() {
permissionFlow = mockk(relaxUnitFun = true)
mockkObject(PermissionFlow)
every { PermissionFlow.getInstance() } returns permissionFlow
initializer = PermissionFlowInitializer()
}
@After
fun tearDown() {
clearAllMocks()
}
@Test
fun testInitializer() {
// Given: A application context providing context
val context = ApplicationProvider.getApplicationContext<Context>()
// When: Initializer is created
initializer.create(context = context)
// Then: Permission flow should be initialized
verifySequence {
PermissionFlow.init(context)
PermissionFlow.getInstance()
permissionFlow.startListening()
}
}
@Test
fun testInitializerDependencies_shouldBeEmpty() {
Assert.assertTrue(initializer.dependencies().isEmpty())
}
}
================================================
FILE: permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/internal/ApplicationStateMonitorTest.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.internal
import android.app.Activity
import android.app.Application
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import app.cash.turbine.test
import dev.shreyaspatil.permissionFlow.PermissionState
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.verify
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.P])
class ApplicationStateMonitorTest {
private lateinit var monitor: ApplicationStateMonitor
private lateinit var application: Application
private val callbackSlot = slot<Application.ActivityLifecycleCallbacks>()
private val lifecycleCallbacks
get() = callbackSlot.captured
@Before
fun setUp() {
application =
mockk(relaxUnitFun = true) {
every { registerActivityLifecycleCallbacks(capture(callbackSlot)) } just Runs
}
monitor = ApplicationStateMonitor(application)
}
@Test
fun getPermissionState_returnGrantedPermissionState_andCurrentActivityNotPresent() {
// Given: No current activity
val permission = "A"
mockPermissions(permission to true)
val expectedPermissionState =
PermissionState(permission = permission, isGranted = true, isRationaleRequired = null)
// When: Permission state is retrieved
val actualPermissionState = monitor.getPermissionState(permission)
// Then: Correct permission state should be returned
assertEquals(expectedPermissionState, actualPermissionState)
}
@Test
fun getPermissionState_returnDeniedPermissionState_andCurrentActivityNotPresent() {
// Given: No current activity
val permission = "A"
mockPermissions(permission to false)
val expectedPermissionState =
PermissionState(permission = permission, isGranted = false, isRationaleRequired = null)
// When: Permission state is retrieved
val actualPermissionState = monitor.getPermissionState(permission)
// Then: Correct permission state should be returned
assertEquals(expectedPermissionState, actualPermissionState)
}
@Test
fun getPermissionState_returnGrantedAndValidRationale_whenCurrentActivityIsPresent() = runTest {
monitor.activityForegroundEvents.test {
// Given: current activity
val activity = activity()
lifecycleCallbacks.onActivityCreated(activity, null)
val permission = "A"
mockPermissions(permission to true)
mockPermissionRationale(permission to false)
val expectedPermissionState =
PermissionState(
permission = permission, isGranted = true, isRationaleRequired = false)
// When: Permission state is retrieved
val actualPermissionState = monitor.getPermissionState(permission)
// Then: Correct permission state should be returned
assertEquals(expectedPermissionState, actualPermissionState)
}
}
@Test
fun getPermissionState_returnDeniedAndValidRationale_whenCurrentActivityIsPresent() = runTest {
monitor.activityForegroundEvents.test {
// Given: current activity
val activity = activity()
lifecycleCallbacks.onActivityCreated(activity, null)
val permission = "A"
mockPermissions(permission to false)
mockPermissionRationale(permission to true)
val expectedPermissionState =
PermissionState(
permission = permission, isGranted = false, isRationaleRequired = true)
// When: Permission state is retrieved
val actualPermissionState = monitor.getPermissionState(permission)
// Then: Correct permission state should be returned
assertEquals(expectedPermissionState, actualPermissionState)
}
}
@Test
fun getPermissionDeniedState_returnValidRationale_whenCurrentActivityIsPresent() = runTest {
monitor.activityForegroundEvents.test {
// Given: current activity
val activity = activity()
lifecycleCallbacks.onActivityCreated(activity, null)
val permission = "A"
mockPermissions(permission to false)
mockPermissionRationale(permission to true)
val expectedPermissionState =
PermissionState(
permission = permission, isGranted = false, isRationaleRequired = true)
// When: Permission state is retrieved
val actualPermissionState = monitor.getPermissionState(permission)
// Then: Correct permission state should be returned
assertEquals(expectedPermissionState, actualPermissionState)
}
}
@Test
fun testRegisterUnregisterCallback() = runTest {
monitor.activityForegroundEvents.test {
verify(exactly = 1) {
application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
}
cancelAndIgnoreRemainingEvents()
}
verify(exactly = 1) { application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks) }
}
@Test
@Config(sdk = [Build.VERSION_CODES.Q])
fun shouldSetCurrentActivity_whenActivityIsPreCreated() = runTest {
monitor.activityForegroundEvents.test {
// Given: Activity not created
assertNull(monitor.getCurrentActivityReference())
// When: Activity is created
val activity = activity()
lifecycleCallbacks.onActivityPreCreated(activity, null)
// Then: Activity should be set
assertEquals(activity, monitor.getCurrentActivityReference()?.get())
}
}
@Test
fun shouldSetCurrentActivity_whenActivityIsCreated() = runTest {
monitor.activityForegroundEvents.test {
// Given: Activity not created
assertNull(monitor.getCurrentActivityReference())
// When: Activity is created
val activity = activity()
lifecycleCallbacks.onActivityCreated(activity, null)
// Then: Activity should be set
assertEquals(activity, monitor.getCurrentActivityReference()?.get())
}
}
@Test
fun shouldSetCurrentActivity_whenAnotherActivityIsCreated() = runTest {
monitor.activityForegroundEvents.test {
// Given: Activity not created
assertNull(monitor.getCurrentActivityReference())
// When: Activity is created
val activity1 = activity()
val activity2 = activity()
lifecycleCallbacks.onActivityCreated(activity1, null)
lifecycleCallbacks.onActivityCreated(activity2, null)
// Then: Latest created activity should be set as current
assertEquals(activity2, monitor.getCurrentActivityReference()?.get())
}
}
@Test
@Config(sdk = [Build.VERSION_CODES.Q])
fun shouldSetCurrentActivity_whenAnotherActivityIsCreatedInApi29() = runTest {
monitor.activityForegroundEvents.test {
// Given: Activity not created
assertNull(monitor.getCurrentActivityReference())
// When: Activity is created
val activity = activity()
lifecycleCallbacks.onActivityPreCreated(activity, null)
val previousRef = monitor.getCurrentActivityReference()
lifecycleCallbacks.onActivityCreated(activity, null)
val afterRef = monitor.getCurrentActivityReference()
// Then: Reference should be same
assertEquals(previousRef, afterRef)
}
}
@Test
fun shouldClearCurrentActivity_whenActivityIsDestroyed() = runTest {
monitor.activityForegroundEvents.test {
// Given: Activity created
val activity = activity()
lifecycleCallbacks.onActivityCreated(activity, null)
// When: Activity is destroyed
lifecycleCallbacks.onActivityDestroyed(activity)
// Then: Activity should be cleared
assertNull(monitor.getCurrentActivityReference()?.get())
}
}
@Test
fun shouldNotClearCurrentActivity_whenAnotherActivityIsDestroyed() = runTest {
monitor.activityForegroundEvents.test {
// Given: Activity created
val activity1 = activity()
val activity2 = activity()
// When: One activity is created and other is destroyed
lifecycleCallbacks.onActivityCreated(activity1, null)
lifecycleCallbacks.onActivityDestroyed(activity2)
// Then: Activity1 should be present
assertEquals(activity1, monitor.getCurrentActivityReference()?.get())
}
}
@Test
fun shouldNotEmitEvent_whenActivityIsStartedWithConfigChanges() = runTest {
monitor.activityForegroundEvents.test {
// Before onStart(), onStop() should be called first
lifecycleCallbacks.onActivityStopped(activity(isChangingConfigurations = true))
lifecycleCallbacks.onActivityStarted(activity())
// Event should not get emitted
expectNoEvents()
}
}
@Test
fun shouldEmitEvent_whenActivityIsStarted_andNoConfigChanges() = runTest {
monitor.activityForegroundEvents.test {
// Before onStart(), onStop() should be called first
lifecycleCallbacks.onActivityStopped(activity(isChangingConfigurations = false))
lifecycleCallbacks.onActivityStarted(activity())
// Event should get emitted
awaitItem()
}
}
@Test
@Config(sdk = [Build.VERSION_CODES.N])
fun shouldEmitEvent_whenActivityIsResumedAfterExitingFromInMultiWindowMode() = runTest {
monitor.activityForegroundEvents.test {
// Before onStart(), onStop() should be called first
lifecycleCallbacks.onActivityPaused(activity(isInMultiWindowMode = true))
lifecycleCallbacks.onActivityResumed(activity(isInMultiWindowMode = false))
// Event should get emitted
awaitItem()
}
}
@Test
@Config(sdk = [Build.VERSION_CODES.M])
fun shouldNotEmitEvent_whenActivityIsResumedAfterPaused_onAndroidM() = runTest {
monitor.activityForegroundEvents.test {
// Before onStart(), onStop() should be called first
lifecycleCallbacks.onActivityPaused(mockk())
lifecycleCallbacks.onActivityResumed(mockk())
// Event should get emitted
expectNoEvents()
}
}
@Test
@Config(sdk = [Build.VERSION_CODES.N])
fun shouldNotEmitEvent_whenActivityIsResumedButStillInMultiWindowMode() = runTest {
monitor.activityForegroundEvents.test {
// Before onStart(), onStop() should be called first
lifecycleCallbacks.onActivityPaused(activity(isInMultiWindowMode = true))
lifecycleCallbacks.onActivityResumed(activity(isInMultiWindowMode = true))
// Event should not get emitted
expectNoEvents()
}
}
@Test
@Config(sdk = [Build.VERSION_CODES.N])
fun shouldEmitEvent_whenActivityIsResumedAfterExitingFromPiPMode() = runTest {
monitor.activityForegroundEvents.test {
// Before onStart(), onStop() should be called first
lifecycleCallbacks.onActivityPaused(activity(isInPictureInPictureMode = true))
lifecycleCallbacks.onActivityResumed(activity(isInPictureInPictureMode = false))
// Event should get emitted
awaitItem()
}
}
@Test
@Config(sdk = [Build.VERSION_CODES.N])
fun shouldNotEmitEvent_whenActivityIsResumedButStillInPiPMode() = runTest {
monitor.activityForegroundEvents.test {
// Before onStart(), onStop() should be called first
lifecycleCallbacks.onActivityPaused(activity(isInPictureInPictureMode = true))
lifecycleCallbacks.onActivityResumed(activity(isInPictureInPictureMode = true))
// Event should not get emitted
expectNoEvents()
}
}
/** Activity factory function */
private fun activity(
isChangingConfigurations: Boolean = false,
isInMultiWindowMode: Boolean = false,
isInPictureInPictureMode: Boolean = false,
): Activity = mockk {
every { isChangingConfigurations() } returns isChangingConfigurations
every { isInMultiWindowMode() } returns isInMultiWindowMode
every { isInPictureInPictureMode() } returns isInPictureInPictureMode
}
/** Mocks permission state i.e. granted / denied. */
private fun mockPermissions(vararg permissionStates: Pair<String, Boolean>) {
mockkStatic(ContextCompat::checkSelfPermission)
permissionStates.forEach { (permission, isGranted) ->
every { ContextCompat.checkSelfPermission(any(), permission) } returns
if (isGranted) {
PackageManager.PERMISSION_GRANTED
} else {
PackageManager.PERMISSION_DENIED
}
}
}
/** Mocks permission rationale state i.e. should shown or not */
private fun mockPermissionRationale(vararg permissionStates: Pair<String, Boolean>) {
mockkStatic(ActivityCompat::shouldShowRequestPermissionRationale)
permissionStates.forEach { (permission, shouldShow) ->
every { ActivityCompat.shouldShowRequestPermissionRationale(any(), permission) } returns
shouldShow
}
}
}
================================================
FILE: permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/utils/ActivityResultLauncherExtTest.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.utils
import androidx.activity.result.ActivityResultLauncher
import io.mockk.mockk
import io.mockk.verify
import org.junit.Test
class ActivityResultLauncherExtTest {
@Test
fun testLaunch() {
val activityResultLauncher: ActivityResultLauncher<Array<String>> =
mockk(
relaxUnitFun = true,
)
activityResultLauncher.launch("A", "B", "C")
verify { activityResultLauncher.launch(arrayOf("A", "B", "C")) }
}
}
================================================
FILE: permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/utils/PermissionResultLauncherTest.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.utils
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.contract.ActivityResultContract
import androidx.fragment.app.Fragment
import dev.shreyaspatil.permissionFlow.contract.RequestPermissionsContract
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Test
class PermissionResultLauncherTest {
@Test
fun test_Activity_registerForPermissionFlowRequestsResult_default() {
val activityProvidedResultRegistry = mockk<ActivityResultRegistry>()
val activity =
mockk<ComponentActivity> {
every {
registerForActivityResult(
any<ActivityResultContract<Array<String>, Map<String, Boolean>>>(),
any(),
any<ActivityResultCallback<Map<String, Boolean>>>(),
)
} returns mockk<ActivityResultLauncher<Array<String>>>()
every { activityResultRegistry } returns activityProvidedResultRegistry
}
activity.registerForPermissionFlowRequestsResult(mockk())
verify(exactly = 1) {
activity.registerForActivityResult(
any<RequestPermissionsContract>(), activityProvidedResultRegistry, any())
}
}
@Test
fun test_Activity_registerForPermissionFlowRequestsResult_withProvidedActivityResultRegistry() {
val activityResultRegistry = mockk<ActivityResultRegistry>()
val activity =
mockk<ComponentActivity> {
every {
registerForActivityResult(
any<ActivityResultContract<Array<String>, Map<String, Boolean>>>(),
any(),
any<ActivityResultCallback<Map<String, Boolean>>>(),
)
} returns mockk<ActivityResultLauncher<Array<String>>>()
}
activity.registerForPermissionFlowRequestsResult(
mockk(), activityResultRegistry = activityResultRegistry)
verify(exactly = 1) {
activity.registerForActivityResult(
any<RequestPermissionsContract>(), activityResultRegistry, any())
}
}
@Test
fun test_Fragment_registerForPermissionFlowRequestsResult() {
val fragment =
mockk<Fragment> {
every {
registerForActivityResult(
any<ActivityResultContract<Array<String>, Map<String, Boolean>>>(),
any(),
any<ActivityResultCallback<Map<String, Boolean>>>(),
)
} returns mockk<ActivityResultLauncher<Array<String>>>()
}
fragment.registerForPermissionFlowRequestsResult(mockk(), mockk(), mockk())
verify(exactly = 1) {
fragment.registerForActivityResult(any<RequestPermissionsContract>(), any(), any())
}
}
@Test
fun test_Fragment_registerForPermissionFlowRequestsResult_withoutActivityRegistry() {
val fragment =
mockk<Fragment> {
every {
registerForActivityResult(
any<ActivityResultContract<Array<String>, Map<String, Boolean>>>(),
any<ActivityResultCallback<Map<String, Boolean>>>(),
)
} returns mockk<ActivityResultLauncher<Array<String>>>()
}
val requestPermissionsContract = mockk<RequestPermissionsContract>()
val callback = mockk<ActivityResultCallback<Map<String, Boolean>>>()
fragment.registerForPermissionFlowRequestsResult(
requestPermissionsContract = requestPermissionsContract,
callback = callback,
)
verify(exactly = 1) {
fragment.registerForActivityResult(requestPermissionsContract, callback)
}
}
}
================================================
FILE: permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/utils/stateFlow/CombinedStateFlowTest.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.utils.stateFlow
import app.cash.turbine.test
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
class CombinedStateFlowTest {
@Test
fun combineStates_shouldReturnValidValueInitially() {
// Given: Individual state flows
val intState = MutableStateFlow(0)
val stringState = MutableStateFlow("")
val booleanState = MutableStateFlow(true)
// When: State flows are combined
val state =
combineStates(intState, stringState, booleanState) { (s1, s2, s3) ->
TestState(s1 as Int, s2 as String, s3 as Boolean)
}
// Then: Combined state should have valid initial value
val expectedState = TestState(intValue = 0, stringValue = "", booleanValue = true)
assertEquals(expectedState, state.value)
}
@Test
fun combineStates_shouldUpdateValue_whenIndividualStateIsUpdated() {
// Given: Individual state flows combined into another state
val intState = MutableStateFlow(0)
val stringState = MutableStateFlow("")
val booleanState = MutableStateFlow(true)
val state =
combineStates(intState, stringState, booleanState) { (s1, s2, s3) ->
TestState(s1 as Int, s2 as String, s3 as Boolean)
}
// When: Individual states are updated
intState.value = 10
stringState.value = "Test"
booleanState.value = false
// Then: Combined state should have valid updated value
val expectedState = TestState(intValue = 10, stringValue = "Test", booleanValue = false)
assertEquals(expectedState, state.value)
}
@Test
fun combineStates_shouldReturnRecentValueAsCache() {
// Given: Individual state flows combined into another state
val intState = MutableStateFlow(0)
val stringState = MutableStateFlow("")
val booleanState = MutableStateFlow(true)
val state =
combineStates(intState, stringState, booleanState) { (s1, s2, s3) ->
TestState(s1 as Int, s2 as String, s3 as Boolean)
}
// When: Individual states are updated
intState.value = 10
stringState.value = "Test"
booleanState.value = false
// Then: Combined state should have valid updated value
val expectedCacheItem = TestState(intValue = 10, stringValue = "Test", booleanValue = false)
assertEquals(listOf(expectedCacheItem), state.replayCache)
}
@Test
fun combineStates_shouldEmitInCollector_whenIndividualStateIsUpdated() = runTest {
// Given: Individual state flows combined into another state
val intState = MutableStateFlow(0)
val stringState = MutableStateFlow("")
val booleanState = MutableStateFlow(true)
val state =
combineStates(intState, stringState, booleanState) { (s1, s2, s3) ->
TestState(s1 as Int, s2 as String, s3 as Boolean)
}
// When: Individual states are collected
state.test {
// Then: Valid initial state should be emitted
assertEquals(
TestState(intValue = 0, stringValue = "", booleanValue = true),
awaitItem(),
)
// Then: Updated state should be emitted on updating individual state
intState.value = 10
stringState.value = "Test"
booleanState.value = false
// Since we updated three values, test on third event.
// This is because of StateFlow's conflated behaviour
// So intentionally receive events twice with `awaitItem()`
awaitItem()
awaitItem()
assertEquals(
TestState(intValue = 10, stringValue = "Test", booleanValue = false),
awaitItem(),
)
cancelAndIgnoreRemainingEvents()
}
}
/**
* Sample class used to test combining different StateFlows into transformed StateFlow of this
* class.
*/
private data class TestState(
val intValue: Int,
val stringValue: String,
val booleanValue: Boolean,
)
}
================================================
FILE: permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/watchmen/PermissionWatchmenTest.kt
================================================
/**
* Copyright 2022 Shreyas Patil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.shreyaspatil.permissionFlow.watchmen
import dev.shreyaspatil.permissionFlow.PermissionState
import dev.shreyaspatil.permissionFlow.internal.ApplicationStateMonitor
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class PermissionWatchmenTest {
private val dispatcher = StandardTestDispatcher()
private lateinit var applicationStateMonitor: ApplicationStateMonitor
private lateinit var watchmen: PermissionWatchmen
private lateinit var foregroundEvents: MutableSharedFlow<Unit>
@Before
fun setUp() {
foregroundEvents =
MutableSharedFlow(
extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
applicationStateMonitor =
mockk(relaxed = true) { every { activityForegroundEvents } returns foregroundEvents }
watchmen = PermissionWatchmen(applicationStateMonitor, dispatcher)
}
@Test
fun shouldWakeUpAndReturnFlow_whenWatchPermissionForTheFirstTime() {
// Given: Permission
val permission = "permission"
mockPermissions(permission to true)
// When: Starts watching permission for the first time.
val flow = watchmen.watchState(permission)
// Then: StateFlow should be returned with valid value i.e. true (Granted).
assertTrue(flow.value.isGranted)
// Then: Should start watching activity foreground events.
dispatcher.scheduler.runCurrent()
verify(exactly = 1) { applicationStateMonitor.activityForegroundEvents }
assertEquals(1, foregroundEvents.subscriptionCount.value)
}
@Test
fun shouldWakeUpAndReturnFlow_whenWatchMultiplePermissionForTheFirstTime() {
// Given: Multiple Permission
val permission1 = "permission-1"
val permission2 = "permission-2"
mockPermissions(permission1 to true, permission2 to false)
// When: Starts watching multiple permission for the first time.
val flow = watchmen.watchMultipleState(arrayOf(permission1, permission2))
// Then: StateFlow should be returned with valid value i.e. true (Granted).
assertTrue(flow.value.permissions[0].isGranted)
assertFalse(flow.value.permissions[1].isGranted)
// Then: Should start watching activity foreground events.
dispatcher.scheduler.runCurrent()
verify(exactly = 1) { applicationStateMonitor.activityForegroundEvents }
assertEquals(1, foregroundEvents.subscriptionCount.value)
}
@Test
fun shouldReturnFilteredMultiplePermissionState_whenDuplicatePermissionsAreWatched() {
// Given: Multiple Permission
val permission1 = "permission-1"
val permission2 = "permission-2"
mockPermissions(permission1 to true, permission2 to false)
// When: Starts watching multiple permission having list of repeated permissions
val flow = watchmen.watchMultipleState(arrayOf(permission1, permission1, permission2))
// Then: State should only contain list having two items
assertEquals(flow.value.permissions.size, 2)
assertEquals(flow.value.permissions.map { it.permission }, listOf(permission1, permission2))
}
@Test
fun shouldReturnSameInstance_whenWatchingPermissionMoreThanOnce() {
// Given: A permission to be observed
val permission = "permission"
mockPermissions(permission to true)
// When: A permission state is watched more than once
val flow1 = watchmen.watchState(permission)
val flow2 = watchmen.watchState(permission)
// Then: Same instance should be returned
assert(flow1 === flow2)
}
@Test
fun shouldUpdateFlowState_whenPermissionChangesAreNotified() {
// Given: Watching a permission flow
val permission = "permission"
mockPermissions(permission to true)
val state = watchmen.watchState(permission)
// When: Change in state is notified for the same permission
mockPermissions(permission to false)
watchmen.notifyPermissionsChanged(permissions = arrayOf(permission))
// Then: Current value of flow should be false i.e. Not granted
dispatcher.scheduler.runCurrent()
assertFalse(state.value.isGranted)
}
@Test
fun shouldUpdateMultiplePermissionFlowState_whenPermissionChangesAreNotified() {
// Given: Watching multiple permission state
val permission1 = "permission-1"
val permission2 = "permission-2"
mockPermissions(permission1 to true, permission2 to false)
val flow = watchmen.watchMultipleState(arrayOf(permission1, permission2))
// When: Change in state is notified for the these permission
mockPermissions(permission2 to true)
watchmen.notifyPermissionsChanged(permissions = arrayOf(permission2))
// Then: All permissions should be granted
dispatcher.scheduler.runCurrent()
assertTrue(flow.value.allGranted)
}
@Test
fun shouldUpdatePermissionFlowState_whenWatchmenWakesAfterSleeping() {
// Given: Watching a permission
val permission = "permission"
mockPermissions(permission to true)
val flow = watchmen.watchState(permission)
// When: Watchmen sleeps, permission state changes and watchmen wakes after that
watchmen.sleep()
mockPermissions(permission to false)
watchmen.wakeUp()
// Then: Permission state should be get updated
dispatcher.scheduler.advanceUntilIdle()
assertFalse(flow.value.isGranted)
}
@Test
fun shouldUpdateMultiplePermissionFlowState_whenWatchmenWakesAfterSleeping() {
// Given: Watching multiple permissions
val permission1 = "permission-1"
val permission2 = "permission-2"
mockPermissions(permission1 to true, permission2 to false)
val flow = watchmen.watchMultipleState(arrayOf(permission1, permission2))
// When: Watchmen sleeps, permission state changes and watchmen wakes after that
watchmen.sleep()
mockPermissions(permission1 to true, permission2 to true)
watchmen.wakeUp()
// Then: Permission state should be get updated
dispatcher.scheduler.advanceUntilIdle()
assertTrue(flow.value.permissions[1].isGranted)
}
@Test
fun shouldNotUpdateFlowState_whenPermissionChangesAreNotifiedAndWatchmenIsSleeping() {
// Given: Watching a permission flow and watchmen is sleeping
val permission = "permission"
mockPermissions(permission to true)
val flow = watchmen.watchState(permission)
watchmen.sleep()
// When: Change in state is notified for the same permission
mockPermissions(permission to false)
watchmen.notifyPermissionsChanged(permissions = arrayOf(permission))
// Then: Current value of flow should not be changed i.e. it should remain as Granted
dispatcher.scheduler.runCurrent()
assertTrue(flow.value.isGranted)
}
@Test
fun shouldNotUpdateMultipleFlowState_whenPermissionChangesAreNotifiedAndWatchmenIsSleeping() {
// Given: Watching a permission flow and watchmen is sleeping
val permission1 = "permission-1"
val permission2 = "permission-2"
mockPermissions(permission1 to true, permission2 to false)
val flow = watchmen.watchMultipleState(arrayOf(permission1, permission2))
watchmen.sleep()
// When: Change in state is notified for the same permission
mockPermissions(permission2 to true)
watchmen.notifyPermissionsChanged(permissions = arrayOf(permission2))
// Then: Current value of flow should not be changed i.e. it should remain as Granted
dispatcher.scheduler.runCurrent()
assertFalse(flow.value.permissions[1].isGranted)
}
@Test
fun shouldStartObservingActivity_whenWakingUp() {
// When: Request watchmen to wake-up
watchmen.wakeUp()
// Then: Should start watching activity foreground events
dispatcher.scheduler.runCurrent()
verify(exactly = 1) { applicationStateMonitor.activityForegroundEvents }
assertEquals(1, foregroundEvents.subscriptionCount.value)
}
@Test
fun shouldStartObservingActivityOnceOnce_whenWakingUpMultipleTimes() {
// When: Request watchmen to wake-up twice
watchmen.wakeUp()
dispatcher.scheduler.runCurrent()
watchmen.wakeUp()
dispatcher.scheduler.runCurrent()
// Then: Should start watching activity foreground events only once
verify(exactly = 1) { applicationStateMonitor.activityForegroundEvents }
assertEquals(1, foregroundEvents.subscriptionCount.value)
}
@Test
fun shouldStopObservingActivityEvent_whenSleeping() {
// When: Requests watchmen to sleep after being waking up
watchmen.wakeUp()
dispatcher.scheduler.runCurrent()
watchmen.sleep()
// Then: Should stop watching activity foreground events
dispatcher.scheduler.runCurrent()
verify(exactly = 1) { applicationStateMonitor.activityForegroundEvents }
// Then: Subscription should be removed
assertEquals(0, foregroundEvents.subscriptionCount.value)
}
@Test
fun shouldNotifyAllPermissionChanges_whenActivityForegroundEventIsReceived() = runTest {
// Given: Watching permissions
mockPermissions("permission-1" to false, "permission-2" to false, "permission-3" to false)
val permissionFlow1 = watchmen.watchState("permission-1")
val permissionFlow2 = watchmen.watchState("permission-2")
val permissionFlow3 = watchmen.watchState("permission-3")
advanceUntilIdle()
// When: Permission state is changed and activity foreground event is received
mockPermissions("permission-1" to true, "permission-2" to true, "permission-3" to true)
// and When: Application is in foreground
foregroundEvents.tryEmit(Unit)
advanceUntilIdle()
// Then: Permission state for all active flows should be get updated after debounce time.
assertTrue(permissionFlow1.value.isGranted)
assertTrue(permissionFlow2.value.isGranted)
assertTrue(permissionFlow3.value.isGranted)
}
/** Mocks permission state i.e. granted / denied. */
private fun mockPermissions(vararg permissionStates: Pair<String, Boolean>) {
permissionStates.forEach { (permission, isGranted) ->
every { applicationStateMonitor.getPermissionState(permission) } returns
PermissionState(
permission = permission, isGranted = isGranted, isRationaleRequired = false)
}
}
private fun runTest(testBody: suspend TestScope.(
gitextract_5sp6w6t0/
├── .github/
│ ├── CODEOWNERS
│ ├── FUNDING.yml
│ ├── dependabot.yml
│ └── workflows/
│ ├── build.yml
│ └── release.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── dev/
│ │ └── shreyaspatil/
│ │ └── permissionFlow/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── dev/
│ │ │ └── shreyaspatil/
│ │ │ └── permissionFlow/
│ │ │ └── example/
│ │ │ ├── data/
│ │ │ │ ├── ContactRepository.kt
│ │ │ │ ├── impl/
│ │ │ │ │ └── AndroidDefaultContactRepository.kt
│ │ │ │ └── model/
│ │ │ │ └── Contact.kt
│ │ │ └── ui/
│ │ │ ├── MainActivity.kt
│ │ │ ├── composePermission/
│ │ │ │ └── ComposePermissionActivity.kt
│ │ │ ├── contacts/
│ │ │ │ ├── ContactsActivity.kt
│ │ │ │ ├── ContactsUiEvents.kt
│ │ │ │ └── ContactsViewModel.kt
│ │ │ ├── fragment/
│ │ │ │ ├── ExampleFragment.kt
│ │ │ │ └── ExampleFragmentActivity.kt
│ │ │ └── multipermission/
│ │ │ └── MultiPermissionActivity.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ └── ic_launcher_background.xml
│ │ ├── drawable-v24/
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── layout/
│ │ │ ├── activity_contacts.xml
│ │ │ ├── activity_fragment_example.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── activity_multipermission.xml
│ │ │ └── view_fragment_example.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── values/
│ │ │ ├── colors.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ ├── values-night/
│ │ │ └── themes.xml
│ │ └── xml/
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test/
│ └── java/
│ └── dev/
│ └── shreyaspatil/
│ └── permissionFlow/
│ └── ExampleUnitTest.kt
├── build.gradle
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── permission-flow/
│ ├── .gitignore
│ ├── build.gradle
│ ├── consumer-rules.pro
│ ├── gradle.properties
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── dev/
│ │ └── shreyaspatil/
│ │ └── permissionFlow/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── dev/
│ │ └── shreyaspatil/
│ │ └── permissionFlow/
│ │ ├── PermissionFlow.kt
│ │ ├── State.kt
│ │ ├── contract/
│ │ │ └── RequestPermissionsContract.kt
│ │ ├── impl/
│ │ │ └── PermissionFlowImpl.kt
│ │ ├── initializer/
│ │ │ └── PermissionFlowInitializer.kt
│ │ ├── internal/
│ │ │ └── ApplicationStateMonitor.kt
│ │ ├── utils/
│ │ │ ├── ActivityResultLauncherExt.kt
│ │ │ ├── PermissionResultLauncher.kt
│ │ │ └── stateFlow/
│ │ │ └── CombinedStateFlow.kt
│ │ └── watchmen/
│ │ └── PermissionWatchmen.kt
│ └── test/
│ └── java/
│ └── dev/
│ └── shreyaspatil/
│ └── permissionFlow/
│ ├── MultiplePermissionStateTest.kt
│ ├── PermissionFlowTest.kt
│ ├── contract/
│ │ └── RequestPermissionsContractTest.kt
│ ├── impl/
│ │ └── PermissionFlowImplTest.kt
│ ├── initializer/
│ │ └── PermissionFlowInitializerTest.kt
│ ├── internal/
│ │ └── ApplicationStateMonitorTest.kt
│ ├── utils/
│ │ ├── ActivityResultLauncherExtTest.kt
│ │ ├── PermissionResultLauncherTest.kt
│ │ └── stateFlow/
│ │ └── CombinedStateFlowTest.kt
│ └── watchmen/
│ └── PermissionWatchmenTest.kt
├── permission-flow-compose/
│ ├── .gitignore
│ ├── build.gradle
│ ├── consumer-rules.pro
│ ├── gradle.properties
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── dev/
│ └── shreyaspatil/
│ └── permissionflow/
│ └── compose/
│ └── PermissionFlow.kt
├── settings.gradle
└── spotless/
└── copyright.kt
Condensed preview — 85 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (209K chars).
[
{
"path": ".github/CODEOWNERS",
"chars": 17,
"preview": "\n* @PatilShreyas\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 573,
"preview": "github: #\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective use"
},
{
"path": ".github/dependabot.yml",
"chars": 484,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/workflows/build.yml",
"chars": 847,
"preview": "name: Build\non: [push, pull_request]\n\njobs:\n build:\n name: Build\n runs-on: ubuntu-latest\n\n steps:\n - uses"
},
{
"path": ".github/workflows/release.yml",
"chars": 2400,
"preview": "name: Release\non:\n workflow_dispatch:\n inputs:\n versionName:\n description: 'Version Name'\n requir"
},
{
"path": ".gitignore",
"chars": 125,
"preview": "*.iml\n.gradle\n/local.properties\n/.idea/\n.DS_Store\n/build\n*/build/\n/captures\n.externalNativeBuild\n.cxx\nlocal.properties\n."
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 5202,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
},
{
"path": "CONTRIBUTING.md",
"chars": 862,
"preview": "## Feeling Awesome! Thanks for thinking about this.\n\nYou can contribute us by filing issues, bugs and PRs. You can also "
},
{
"path": "LICENSE",
"chars": 11343,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 10668,
"preview": "# Permission Flow for Android\n\nKnow about real-time state of a Android app Permissions with Kotlin Flow APIs. _Made with"
},
{
"path": "app/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "app/README.md",
"chars": 121,
"preview": "# Example\n\nhttps://github.com/PatilShreyas/permission-flow-android/assets/19620536/5ea5ac2c-24a6-4d16-87f8-1b5a0b8e708e\n"
},
{
"path": "app/build.gradle",
"chars": 2324,
"preview": "plugins {\n id 'com.android.application'\n id 'org.jetbrains.kotlin.android'\n id 'org.jetbrains.kotlin.plugin.com"
},
{
"path": "app/proguard-rules.pro",
"chars": 750,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "app/src/androidTest/java/dev/shreyaspatil/permissionFlow/ExampleInstrumentedTest.kt",
"chars": 1288,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 1711,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:to"
},
{
"path": "app/src/main/java/dev/shreyaspatil/permissionFlow/example/data/ContactRepository.kt",
"chars": 824,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "app/src/main/java/dev/shreyaspatil/permissionFlow/example/data/impl/AndroidDefaultContactRepository.kt",
"chars": 4078,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "app/src/main/java/dev/shreyaspatil/permissionFlow/example/data/model/Contact.kt",
"chars": 727,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/MainActivity.kt",
"chars": 2058,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/composePermission/ComposePermissionActivity.kt",
"chars": 3096,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/contacts/ContactsActivity.kt",
"chars": 2980,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/contacts/ContactsUiEvents.kt",
"chars": 1022,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/contacts/ContactsViewModel.kt",
"chars": 3028,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/fragment/ExampleFragment.kt",
"chars": 3205,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/fragment/ExampleFragmentActivity.kt",
"chars": 1273,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "app/src/main/java/dev/shreyaspatil/permissionFlow/example/ui/multipermission/MultiPermissionActivity.kt",
"chars": 3348,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "app/src/main/res/drawable/ic_launcher_background.xml",
"chars": 5606,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:wi"
},
{
"path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
"chars": 1702,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:aapt=\"http://schemas.android.com/aapt\"\n "
},
{
"path": "app/src/main/res/layout/activity_contacts.xml",
"chars": 1512,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/activity_fragment_example.xml",
"chars": 430,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/activity_main.xml",
"chars": 1276,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmln"
},
{
"path": "app/src/main/res/layout/activity_multipermission.xml",
"chars": 1977,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/view_fragment_example.xml",
"chars": 1878,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 272,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 272,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 378,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"purple_200\">#FFBB86FC</color>\n <color name=\"purpl"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 76,
"preview": "<resources>\n <string name=\"app_name\">PermissionFlow</string>\n</resources>"
},
{
"path": "app/src/main/res/values/themes.xml",
"chars": 836,
"preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n <!-- Base application theme. -->\n <style name=\"Theme.P"
},
{
"path": "app/src/main/res/values-night/themes.xml",
"chars": 836,
"preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n <!-- Base application theme. -->\n <style name=\"Theme.P"
},
{
"path": "app/src/main/res/xml/backup_rules.xml",
"chars": 478,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n Sample backup rules file; uncomment and customize as necessary.\n See htt"
},
{
"path": "app/src/main/res/xml/data_extraction_rules.xml",
"chars": 551,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n Sample data extraction rules file; uncomment and customize as necessary.\n "
},
{
"path": "app/src/test/java/dev/shreyaspatil/permissionFlow/ExampleUnitTest.kt",
"chars": 960,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "build.gradle",
"chars": 1796,
"preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nbuildscript {\n ex"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 233,
"preview": "#Sat Mar 08 22:38:14 IST 2025\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://"
},
{
"path": "gradle.properties",
"chars": 990,
"preview": "org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8\norg.gradle.parallel=true\norg.gradle.configureondemand=true\nandroid.us"
},
{
"path": "gradlew",
"chars": 5766,
"preview": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0"
},
{
"path": "gradlew.bat",
"chars": 2674,
"preview": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \""
},
{
"path": "permission-flow/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "permission-flow/build.gradle",
"chars": 2131,
"preview": "plugins {\n id 'com.android.library'\n id 'org.jetbrains.kotlin.android'\n id 'org.jetbrains.kotlinx.kover'\n id"
},
{
"path": "permission-flow/consumer-rules.pro",
"chars": 0,
"preview": ""
},
{
"path": "permission-flow/gradle.properties",
"chars": 171,
"preview": "POM_ARTIFACT_ID=permission-flow-android\nPOM_NAME=Permission Flow for Android\nPOM_DESCRIPTION=Know about real-time state "
},
{
"path": "permission-flow/proguard-rules.pro",
"chars": 750,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "permission-flow/src/androidTest/java/dev/shreyaspatil/permissionFlow/ExampleInstrumentedTest.kt",
"chars": 1293,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/main/AndroidManifest.xml",
"chars": 628,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:to"
},
{
"path": "permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/PermissionFlow.kt",
"chars": 8440,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/State.kt",
"chars": 1855,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/contract/RequestPermissionsContract.kt",
"chars": 2146,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/impl/PermissionFlowImpl.kt",
"chars": 2607,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/initializer/PermissionFlowInitializer.kt",
"chars": 1128,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/internal/ApplicationStateMonitor.kt",
"chars": 7080,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/utils/ActivityResultLauncherExt.kt",
"chars": 941,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/utils/PermissionResultLauncher.kt",
"chars": 4556,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/utils/stateFlow/CombinedStateFlow.kt",
"chars": 1979,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/main/java/dev/shreyaspatil/permissionFlow/watchmen/PermissionWatchmen.kt",
"chars": 5469,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/MultiplePermissionStateTest.kt",
"chars": 4018,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/PermissionFlowTest.kt",
"chars": 1958,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/contract/RequestPermissionsContractTest.kt",
"chars": 2632,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/impl/PermissionFlowImplTest.kt",
"chars": 3108,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/initializer/PermissionFlowInitializerTest.kt",
"chars": 2315,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/internal/ApplicationStateMonitorTest.kt",
"chars": 14793,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/utils/ActivityResultLauncherExtTest.kt",
"chars": 1118,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/utils/PermissionResultLauncherTest.kt",
"chars": 4728,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/utils/stateFlow/CombinedStateFlowTest.kt",
"chars": 4923,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow/src/test/java/dev/shreyaspatil/permissionFlow/watchmen/PermissionWatchmenTest.kt",
"chars": 12077,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "permission-flow-compose/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "permission-flow-compose/build.gradle",
"chars": 1428,
"preview": "plugins {\n id 'com.android.library'\n id 'org.jetbrains.kotlin.android'\n id 'org.jetbrains.kotlin.plugin.compose"
},
{
"path": "permission-flow-compose/consumer-rules.pro",
"chars": 0,
"preview": ""
},
{
"path": "permission-flow-compose/gradle.properties",
"chars": 195,
"preview": "POM_ARTIFACT_ID=permission-flow-compose\nPOM_NAME=Permission Flow for Android and Jetpack Compose\nPOM_DESCRIPTION=Know ab"
},
{
"path": "permission-flow-compose/proguard-rules.pro",
"chars": 750,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "permission-flow-compose/src/main/AndroidManifest.xml",
"chars": 62,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest>\n\n</manifest>"
},
{
"path": "permission-flow-compose/src/main/java/dev/shreyaspatil/permissionflow/compose/PermissionFlow.kt",
"chars": 4052,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "settings.gradle",
"chars": 394,
"preview": "pluginManagement {\n repositories {\n gradlePluginPortal()\n google()\n mavenCentral()\n }\n}\ndepen"
},
{
"path": "spotless/copyright.kt",
"chars": 594,
"preview": "/**\n * Copyright 2022 Shreyas Patil\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the PatilShreyas/permission-flow-android GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 85 files (190.6 KB), approximately 45.1k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.