{"id":15953157,"url":"https://github.com/likethesalad/aaper","last_synced_at":"2025-04-09T16:08:10.068Z","repository":{"id":53293563,"uuid":"289293129","full_name":"LikeTheSalad/aaper","owner":"LikeTheSalad","description":"Ensure Android runtime permissions using annotations only","archived":false,"fork":false,"pushed_at":"2024-01-02T09:46:04.000Z","size":593,"stargazers_count":224,"open_issues_count":0,"forks_count":9,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-04-09T16:07:59.699Z","etag":null,"topics":["android","annotations","gradle-plugin","permission-android"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/LikeTheSalad.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":"FUNDING.yml","license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"ko_fi":"likethesalad"}},"created_at":"2020-08-21T14:45:25.000Z","updated_at":"2025-01-04T03:30:13.000Z","dependencies_parsed_at":"2025-01-10T13:35:11.974Z","dependency_job_id":"f6f271da-9313-4d97-8465-32ba6c3309f7","html_url":"https://github.com/LikeTheSalad/aaper","commit_stats":{"total_commits":361,"total_committers":5,"mean_commits":72.2,"dds":"0.30193905817174516","last_synced_commit":"8f2c88bb4272b0cb276eb006100f8ec17b70bebf"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LikeTheSalad%2Faaper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LikeTheSalad%2Faaper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LikeTheSalad%2Faaper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LikeTheSalad%2Faaper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LikeTheSalad","download_url":"https://codeload.github.com/LikeTheSalad/aaper/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248065286,"owners_count":21041871,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","annotations","gradle-plugin","permission-android"],"created_at":"2024-10-07T13:11:00.326Z","updated_at":"2025-04-09T16:08:10.039Z","avatar_url":"https://github.com/LikeTheSalad.png","language":"Kotlin","funding_links":["https://ko-fi.com/likethesalad"],"categories":[],"sub_categories":[],"readme":"![version](https://img.shields.io/maven-central/v/com.likethesalad.android/aaper)\n\n# Aaper\n\nTable of Contents\n=================  \n\n* [What it is](#what-it-is)\n    * [Default behavior example](#default-behavior-example)\n* [How to use](#how-to-use)\n* [Changing the default behavior](#changing-the-default-behavior)\n    * [Custom Strategy example](#custom-strategy-example)\n    * [Using our custom Strategy](#using-our-custom-strategy)\n    * [Changing the pre-request behavior](#changing-the-pre-request-behavior)\n* [Adding Aaper into your project](#adding-aaper-into-your-project)\n    * [Prerequisites](#prerequisites)\n    * [Aaper Gradle dependency](#aaper-gradle-dependency)\n* [Troubleshooting](#troubleshooting)\n* [Advanced configuration](#advanced-configuration)\n    * [Creating a custom RequestStrategyFactory](#creating-a-custom-requeststrategyfactory)\n    * [Overriding permission's status query and request launch](#overriding-permissions-status-query-and-request-launch)\n* [License](#license)\n\nWhat it is\n---  \n**A**nnotated **A**ndroid **Per**missions takes care of ensuring Android runtime permissions for\nan `EnsurePermissions`-annotated method inside either an Activity or a Fragment. The idea is to do\nso without having to override any Activity and/or Fragment method related to\nruntime permission requests and also without having to duplicate the code that handles the\noverall requests' processes thanks to Aaper's reusable [strategies](#changing-the-default-behavior).\n\n### Default behavior example\n\n```xml\n\u003c!--Your AndroidManifest.xml--\u003e\n\n\u003cmanifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\u003e\n\n    \u003c!--THIS IS VERY IMPORTANT!!--\u003e\n    \u003cuses-permission\n        android:name=\"android.permission.CAMERA\" /\u003e  \u003c!--Declare the permission in your manifest. Otherwise the runtime request won't work.--\u003e\n\u003c/manifest\u003e\n```\n\n```kotlin  \n// Aaper usage  \n\nclass MyActivity/MyFragment {\n\n    override fun onCreate/onViewCreated(...) {\n    takePhotoButton.setOnClickListener {\n        takePhoto()\n    }\n}\n\n    @EnsurePermissions(permissions = [Manifest.permission.CAMERA])\n    fun takePhoto() {\n        Toast.makeText(this, \"Camera permission granted\", Toast.LENGTH_SHORT).show()\n    }\n}  \n```  \n\nJust by adding the `EnsurePermissions` annotation to `takePhoto()`, what will happen (by default)\nwhen we run that code and click on the `takePhotoButton` button is:\n\n- Aaper will check if your app has the CAMERA permission already granted.\n- If already granted, Aaper will proceed to run `takePhoto()` right away and it will all end there.\n- If NOT granted, Aaper will NOT run `takePhoto()` right away, and rather will proceed to run the\n  default permission `RequestStrategy` which is to launch the System's permission dialog asking the\n  user to grant the CAMERA permission.\n- If the user approves it, Aaper will proceed to run `takePhoto()`.\n- If the user denies it, the default behavior is just not running `takePhoto()`.\n\nAaper's default behavior can be easily changed if you wanted to, you can find more details below\nunder `Changing the default behavior`.\n\nHow to use\n---  \nAs we could see above in the default behavior example, there are two things we need to do in order\nto use Aaper into our Activities or Fragments:\n\n- **Step one:** Make sure that the permissions you'll request with Aaper **are defined in\n  your** `AndroidManifest.xml` **file too**. If you attempt to request a permission at runtime that\n  isn't in your manifest, the OS will silently ignore your request.\n- **Step two:** Annotate an Activity or Fragment method with the `@EnsurePermissions` annotation\n  where you provide a list of permissions (that are also defined in your `AndroidManifest.xml`) that\n  such method needs in order to work properly. Alternatively, you can also pass an optional\n  parameter named `strategy`, where you can specify the behavior of handling such permissions'\n  request. More info below under `Changing the default behavior`.\n\nThat's it, if you want to know how to modify Aaper's behavior to suit your needs, take a look\nat `Changing the default behavior`.\n\n\u003e It is very important to bear in mind that the @EnsurePermissions annotation only works on methods\n\u003e inside either an `Activity` or a` Fragment`, more specifically,\n\u003e an `androidx.fragment.app.Fragment`\n\u003e Fragment. Any @EnsurePermissions annotated method that isn't inside of either an Activity or a\n\u003e Fragment will be ignored.\n\nChanging the default behavior\n---  \nAaper's permission requests behavior is fully customizable, you can define what to do before and\nafter a permission request is executed, and even how the request is executed, by creating your\nown `RequestStrategy` class. The way Aaper works is by delegating the request actions to\na `RequestStrategy` instance, you can tell Aaper which strategy to use by:\n\n- Specifying the strategy in the @EnsurePermissions annotation.\n- Setting your own `RequestStrategy` as default.\n\n### Custom Strategy example\n\nWe'll create a custom strategy that will finish the host Activity in the case that at least one of\nthe permissions requested by Aaper is denied.\n\nWe'll start by creating our class that extends from `ActivityRequestStrategy`:\n\n```kotlin  \nclass FinishActivityOnDeniedStrategy : ActivityRequestStrategy() {\n\n    override fun onPermissionsRequestResults(\n        host: Activity,\n        data: PermissionsResult\n    ): Boolean {\n        TODO(\"Not yet implemented\")\n    }\n}  \n```  \n\nThere are three types of `RequestStrategy` base classes that we can choose from when creating our\ncustom `RequestStrategy`, those are:\n\n- `ActivityRequestStrategy` - Only supports EnsurePermissions-annotated methods inside Activities.\n- `FragmentRequestStrategy` - Only supports EnsurePermissions-annotated methods inside Fragments.\n- `AllRequestStrategy` - Supports both Activities and Fragment classes' EnsurePermissions-annotated\n  methods.\n\n  All three have the same structure and same base methods, the main difference from an\n  implementation point of view, would be the type of `host` provided in their base functions, for\n  example in the method `onPermissionsRequestResults` we see that our host is of type `Activity`,\n  because we extend from `ActivityRequestStrategy`, whereas if we extended\n  from `FragmentRequestStrategy`, the host will be a `Fragment`. For `AllRequestStrategy`, the host\n  is `Any` or `Object` and you'd have to check its type manually in order to verify whether the\n  current request is for an Activity or a Fragment.\n\nIn this example, we will annotate a method inside an Activity and nowhere else,\ntherefore `ActivityRequestStrategy` seems to suit better for this case.\n\nWe must provide for every custom `RequestStrategy` a boolean as response\nfor the `onPermissionsRequestResults` method, depending on what\nwe return there, this is what will happen after a permission request is executed:\n\n- If `onPermissionsRequestResults` returns TRUE, it means that the request was successful in our\n  Strategy and therefore the EnsurePermissions-annotated method will get executed.\n- If `onPermissionsRequestResults` returns FALSE, it means that the request failed in our Strategy\n  and therefore the EnsurePermissions-annotated method will NOT get executed.\n\nFor our example, this is what it will end up looking like:\n\n```kotlin  \nclass FinishActivityOnDeniedStrategy : ActivityRequestStrategy() {\n\n    override fun onPermissionsRequestResults(\n        host: Activity,\n        data: PermissionsResult\n    ): Boolean {\n        if (data.denied.isNotEmpty()) {\n            // At least one permission was denied.  \n            host.finish()\n            return false // So that the annotated method doesn't get called.  \n        }\n\n        // No permissions were denied, therefore proceed to call the annotated method.  \n        return true\n    }\n}  \n```  \n\nAs we can see in `onPermissionsRequestResults`, we check the `denied` permissions list we get\nfrom `data`, and verify whether it's not empty, which would mean that there are some denied\npermissions, therefore our Strategy will treat the request process as failed and will return `false`\nso that the annotated method won't get called, and before that, we call `host.finish()`, in order to\nclose our Activity too.\n\nIf the `denied` permissions list is empty, it means that all of the requested permissions were\napproved, therefore our Strategy will treat the request process as successful and will return `true`\nin order to proceed to call the annotated method.\n\n#### Other configurable aspects of a RequestStrategy\n\nYou can customize other things in your custom `RequestStrategy`, such as the `requestCode` of the\npermission's request for example, by overriding the `getRequestCode()` method. You can also change\nthe behavior of the pre-request action, for example if you want to display some information before\nrequesting for some permissions, you can do so as well. More info on this, below\nunder `Changing the pre-request behavior`.\n\nFinally, you can even change things such as how to launch a System's permission dialog request, and\nalso how to change the way your Strategy queries the current granted permissions of your app, by\noverriding the respective `RequestStrategy` getters. More info on this, below\nunder `Advanced configuration`.\n\n### Using our custom Strategy\n\n#### Use it per annotation param only\n\nThis can be achieved by passing our strategy type into the `EnsurePermissions` annotation, like so:\n\n```kotlin  \n@EnsurePermissions(\n    permissions = [(PERMISSION NAMES)],\n    strategy = FinishActivityOnDeniedStrategy::class\n)\nfun methodThatNeedsThePermissions() {\n    //...\n}  \n```  \n\n#### Or, Set it as the default strategy\n\nWe can set our custom RequestStrategy as default for all the annotated methods by doing the\nfollowing:\n\n```kotlin  \n// Application.onCreate  \n// ...  \nAaper.setDefaultStrategy(FinishActivityOnDeniedStrategy::class.java) \n```  \n\nAfter doing so, you won't have to explicitly pass `FinishActivityOnDenied::class` to\nthe `EnsurePermissions` annotation in order to use this custom strategy, as it will be the default\none.\n\n### Changing the pre-request behavior\n\nSometimes we want to do something right before launching our permissions request, such as displaying\nan information message that explains the users why our app needs the permissions that it is about to\nrequest.\n\nIn order to make our custom `RequestStrategy` able to handle those cases, we can override the\nmethod `onBeforeLaunchingRequest`, which is called right before launching the System's permissions\nrequest dialog. Following our previous example, if we override such method, our custom strategy will\nlook like the following:\n\n```kotlin  \nclass FinishActivityOnDeniedStrategy : ActivityRequestStrategy() {\n\n    // Other methods...  \n\n    override fun onBeforeLaunchingRequest(\n        host: Activity,\n        data: PermissionsRequest,\n        request: RequestRunner\n    ): Boolean {\n        return super.onBeforeLaunchingRequest(host, data, request)\n    }\n}  \n```  \n\nThe `onBeforeLaunchingRequest` method returns a `boolean` which by default is `FALSE`.\n\n- When `onBeforeLaunchingRequest` returns FALSE, it allows Aaper to proceed to launch the System's\n  permissions request dialog.\n- When `onBeforeLaunchingRequest` returns TRUE, Aaper won't launch the System's permission request\n  dialog, and rather **it'll have to be run manually** by the `RequestStrategy` at some point, this\n  is achieved by calling the `RequestRunner.run()` method of the `request` parameter passed\n  to `onBeforeLaunchingRequest`.\n\nThe `onBeforeLaunchingRequest` method provides us with three parameters, host, data (contains the\npermissions requested for the annotated method) and the `RequestRunner`.\n\n`RequestRunner` is a runnable object that, when is run, it launches the System's permission\nrequest dialog. This method should only be called if the `onBeforeLaunchingRequest` method\nreturns `TRUE`, which means that the Strategy will do some operation prior to the permission\nrequest. When the pre-request process is done and the `RequestStrategy` wants to proceed launching\nthe System's permission dialog, it then must call `RequestRunner.run()`.\n\n#### Example\n\nIn this example, we use a dialog with a single button, if the user clicks on it, then\nwe launch the permissions request, otherwise we don't.\n\n```kotlin  \n// My custom RequestStrategy  \n\n// ...  \noverride fun onBeforeLaunchingRequest(\n    host: Activity,\n    data: PermissionsRequest,\n    request: RequestRunner\n): Boolean {\n    val infoDialog = AlertDialog.Builder(host).setPositiveButton(\"CONTINUE\") { _, _ -\u003e\n        // When the user has read the information and wants to continue.  \n        request.run() // Execute the runnable to launch the System's permission dialog.  \n    }.setTitle(\"We need these permissions\")\n        .setMessage(\"Pretty please approve the permissions :(\")\n        .create()\n\n    infoDialog.show()\n\n    return true // This is so that Aaper doesn't launch the permissions request as we're going to launch it manually.  \n}  \n```  \n\nAdding Aaper into your project\n---  \n\n### Prerequisites\n\n#### Android Gradle plugin \u003e= 7.4.0\n\nAaper relies on\nthe [transformation API](https://developer.android.com/reference/tools/gradle-api/7.2/com/android/build/api/variant/Instrumentation)\nadded in the Android Gradle plugin version 7.2.0 and\nthe [Scoped Artifacts API](https://developer.android.com/reference/tools/gradle-api/7.4/com/android/build/api/artifact/Artifacts#forScope(com.android.build.api.variant.ScopedArtifacts.Scope))\nadded in 7.4.0. Combined they allow to add and modify bytecode at compile time\nusing [ASM](https://asm.ow2.io/). This allows Aaper to write all the boilerplate code for you,\ntherefore it will be required for your project to use at least version `7.4.0` of the Android Gradle\nplugin or higher.\n\n### Aaper Gradle dependency\n\nIn order to add the [Aaper plugin](https://plugins.gradle.org/plugin/com.likethesalad.aaper) into\nyour project, you just have to add the following line into your app's build.gradle `plugins` block:\n\n```groovy  \nid 'com.likethesalad.aaper' version '3.0.0'\n```\n\n**Full app's build.gradle example:**\n\n```groovy  \n// Your app's build.gradle file  \nplugins {\n    id 'com.android.application'\n    id 'com.likethesalad.aaper' version '3.0.0'\n}\n```  \n\nTroubleshooting\n---  \n\n### The OS permission request dialog doesn't show up\n\nMake sure that the permissions you've added to the `EnsurePermissions` annotation are ALSO added to\nyour `AndroidManifest.xml` file. The Android OS will ignore any permission request for permissions\nnot listed within your app's manifest.\n\nAdvanced configuration\n---  \n\n### Creating a custom RequestStrategyFactory\n\nAaper's behavior is all about its `RequestStrategy` objects, and the way Aaper can access to them\nis through an instance of `RequestStrategyFactory`. By default, the `RequestStrategyFactory` that\nAaper will use is\nthe [DefaultRequestStrategyFactory](https://github.com/LikeTheSalad/aaper/blob/main/aaper/src/main/java/com/likethesalad/android/aaper/strategy/DefaultRequestStrategyFactory.kt),\nif you need to change it, take a look\nat `Using your custom RequestStrategyFactory`.\n\nThe `DefaultRequestStrategyFactory` implementation instantiates strategies that have either a\nconstructor with an `android.content.Context`, or an empty constructor. If the strategy has\na `Context` param, the Application context will be passed.\n\nSometimes it might be needed to pass some parameters to instantiate your strategies that are not\ncovered by the default strategy factory implementation. For these cases, you can create your own\nimplementation of `RequestStrategyFactory`, where you'd be able to provide your\nown `RequestStrategy` instances the way you'd like the most, either by creating them on-demand or\njust by storing them in memory, or both. Implementing from `RequestStrategyFactory` is pretty\nstraightforward as it only requires you to override one method:\n\n```kotlin  \nclass MyRequestStrategyFactory : RequestStrategyFactory {\n\n    override fun \u003cT : RequestStrategy\u003cout Any\u003e\u003e getStrategy(host: Any, type: Class\u003cT\u003e): T {\n        // Return an instance of the strategy of type `type`.\n    }\n}  \n```  \n\n#### Using your custom RequestStrategyFactory\n\nAfter you've created your own `RequestStrategyFactory`, you'll need to pass it to `Aaper` like so:\n\n```kotlin  \npackage my.app\n\nclass MyApp : Application() {\n    override fun onCreate() {\n        super.onCreate()\n        val myRequestStrategyFactory = MyRequestStrategyFactory()\n        Aaper.setRequestStrategyFactory(myRequestStrategyFactory) // You can only call this method once.\n    }\n}  \n```  \n\n**NOTE**: Make sure your application class is set in your `AndroidManifest.xml` file as shown below:\n\n```xml\n\u003c!--Your AndroidManifest.xml--\u003e\n\n\u003cmanifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\u003e\n\n    \u003c!--yada yada...--\u003e\n\n    \u003c!--You need to add your application class (shown above) to the manifest too (if you don't have it already) as shown below--\u003e\n    \u003capplication android:name=\"my.app.MyApplication\"\u003e\n        \u003c!--yada yada...--\u003e\n    \u003c/application\u003e\n\u003c/manifest\u003e\n```\n\nAnd that's it, Aaper will now use your custom `RequestStrategyFactory` in order to get all of the\nStrategies it needs.\n\n### Overriding permission's status query and request launch\n\nThere are two methods in every `RequestStrategy` that provide the tools to both, querying if a\npermission is granted, and also to launch a set of permissions' request. Those methods\nare `getPermissionStatusProvider`, which provides an instance of `PermissionStatusProvider`,\nand `getRequestLauncher`, which provides an instance of `RequestLauncher`. More info on these\nclasses in the javadoc: https://javadoc.io/doc/com.likethesalad.android/aaper-api.\n\nFor the `PermissionStatusProvider` class, the default behavior for both `Activity` and `Fragment` is\nto use `androidx.core.content.ContextCompat.checkSelfPermission`, and for the `RequestLauncher` one,\nthe Activity's implementation makes use of `ActivityCompat.requestPermissions`, whereas for\nFragment's implementation, the `requestPermissions` method is called straight from the host Fragment\nitself.\n\nThe defaults for both Activity and Fragment operations should suffice for all cases, though if for\nwhatever reason you'd like to customize these actions, you can just override the aforementioned\ngetters in your custom `RequestStrategy` and provide your own implementations for these classes.\n\nLicense\n---  \n\n    MIT License  \n  \n    Copyright (c) 2020 LikeTheSalad.  \n  \n    Permission is hereby granted, free of charge, to any person obtaining a copy  \n    of this software and associated documentation files (the \"Software\"), to deal  \n    in the Software without restriction, including without limitation the rights  \n    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell  \n    copies of the Software, and to permit persons to whom the Software is  \n    furnished to do so, subject to the following conditions:  \n  \n    The above copyright notice and this permission notice shall be included in all  \n    copies or substantial portions of the Software.  \n  \n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  \n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  \n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE  \n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER  \n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,  \n    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE  \n    SOFTWARE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flikethesalad%2Faaper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flikethesalad%2Faaper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flikethesalad%2Faaper/lists"}