main 257d9243bd3b cached
85 files
190.6 KB
45.1k tokens
1 requests
Download .txt
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_.

[![Build](https://github.com/PatilShreyas/permission-flow-android/actions/workflows/build.yml/badge.svg)](https://github.com/PatilShreyas/permission-flow-android/actions/workflows/build.yml)
[![Release](https://github.com/PatilShreyas/permission-flow-android/actions/workflows/release.yml/badge.svg)](https://github.com/PatilShreyas/permission-flow-android/actions/workflows/release.yml)
[![codecov](https://codecov.io/gh/PatilShreyas/permission-flow-android/branch/main/graph/badge.svg?token=6TOHNLQDVW)](https://codecov.io/gh/PatilShreyas/permission-flow-android)
[![Maven Central](https://img.shields.io/maven-central/v/dev.shreyaspatil.permission-flow/permission-flow-android?label=Maven%20Central&logo=android&style=flat-square)](https://search.maven.org/artifact/dev.shreyaspatil.permission-flow/permission-flow-android)
[![GitHub](https://img.shields.io/github/license/PatilShreyas/permission-flow-android?label=License)](LICENSE)

[![dokka](https://img.shields.io/badge/Dokka-Docs-blueviolet.svg?style=flat&logo=kotlin)](https://patilshreyas.github.io/permission-flow-android/docs/)
[![kover](https://img.shields.io/badge/Kover-Coverage-blueviolet.svg?style=flat&logo=kotlin)](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.(
Download .txt
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.

Copied to clipboard!