Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
Karn committed Feb 14, 2021
2 parents 996f295 + 88fbf57 commit 6719dee
Show file tree
Hide file tree
Showing 22 changed files with 561 additions and 32 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ dependencies {
#### USAGE
The most basic case is as follows:

```diff
// Add the following delegate to your activity.
- class MyActivity : AppCompatActivity() {
+ class MyActivity : AppCompatActivity(), Permissions by SentryPermissionHandler {

// or optionally manually delegate to the SentryPermissionHandler by adding the following override
// in your Activity
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
+ SentryPermissionHandler.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ }
}
```

Then anywhere in your activity you can make a request to fetch a permission.

```Kotlin
Sentry
// A reference to your current activity.
Expand Down
6 changes: 4 additions & 2 deletions gradle/configuration.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@


def versions = [
libCode : 2,
libName : '0.0.2',
libCode : 3,
libName : '0.1.0',

kotlin : '1.3.11',
core: '1.2.0-alpha04',
appcompat: '1.0.2',

jacoco : '0.8.2',
Expand All @@ -36,6 +37,7 @@ def build = [
def dependencies = [
kotlin : "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}",
androidx: [
core: "androidx.core:core-ktx:${versions.core}",
appcompat: "androidx.appcompat:appcompat:${versions.appcompat}"
]
]
Expand Down
58 changes: 42 additions & 16 deletions library/src/main/java/io/karn/sentry/Sentry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,22 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.PermissionChecker
import java.lang.ref.WeakReference
import kotlin.random.Random

/**
* Provides a typealias for aesthetic purposes.
*/
typealias Permissions = ActivityCompat.OnRequestPermissionsResultCallback

/**
* A lightweight class which makes requesting permissions a breeze.
*/
class Sentry internal constructor(activity: AppCompatActivity, private val permissionHelper: IPermissionHelper) : ActivityCompat.OnRequestPermissionsResultCallback by activity {
class Sentry internal constructor(activity: AppCompatActivity, private val permissionHelper: IPermissionHelper) {

companion object {
// Tracks the requests that are made and their callbacks
internal val receivers = HashMap<Int, (granted: Boolean) -> Unit>()

/**
* A fluent API to create an instance of the Sentry object.
*
Expand All @@ -49,45 +57,63 @@ class Sentry internal constructor(activity: AppCompatActivity, private val permi
}
}

private val requestCode: Int = this.hashCode()
private lateinit var callback: ((Boolean) -> Unit)
// Stores a reference to the activity
private val activity: WeakReference<AppCompatActivity> = WeakReference(activity)

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (grantResults.isEmpty()) {
return
}

if (requestCode == this.requestCode) {
callback(grantResults[0] == PermissionChecker.PERMISSION_GRANTED)
}
}

/**
* Request a [permission] and return the result of the request through the [callback] receiver.
*
* @param permission One of the many Android permissions. See: [Manifest.permission]
* @param callback A receiver for the result of the permission request.
* @return The request code associated with the request
*/
fun requestPermission(permission: String, callback: (granted: Boolean) -> Unit) {
fun requestPermission(permission: String, callback: (granted: Boolean) -> Unit): Int {
if (permission.isBlank()) {
throw IllegalArgumentException("Invalid permission specified.")
}

this.callback = callback
// Generate a request code for the request
var requestCode: Int
do {
requestCode = Random.nextInt(1, Int.MAX_VALUE)
} while (receivers.containsKey(requestCode))

with(activity.get()) {
this ?: return
this ?: return@with

// We can immediately resolve if we've been granted the permission
if (permissionHelper.hasPermission(this, permission)) {
return@with callback(true)
}

// Track the request
receivers[requestCode] = callback

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return@with this.requestPermissions(arrayOf(permission), requestCode)
}

callback(true)
}

return requestCode
}
}

/**
* A delegated receiver for the onRequestPermissionsResult, this allows the activity's permissions
* results to be intercepted by Sentry and forwarded to the defined receiver.
*/
object SentryPermissionHandler : Permissions {
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (grantResults.isEmpty()) {
return
}

// Ensure that there is a pending request code available and remove it once its been tracked
val callback = Sentry.receivers.remove(requestCode)
?: return // No handler for request code

callback.invoke(grantResults[0] == PermissionChecker.PERMISSION_GRANTED)
}
}
41 changes: 28 additions & 13 deletions library/src/test/java/io/karn/sentry/SentryTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.PermissionChecker.PERMISSION_DENIED
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
import com.nhaarman.mockitokotlin2.anyArray
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.mockito.AdditionalMatchers
import org.mockito.Mockito
import org.mockito.Mockito.*
import java.lang.reflect.Field
Expand Down Expand Up @@ -80,12 +82,21 @@ internal class SentryTest {
val code = requestCode ?: it.getArgument<Int>(1)

// Set the permission result to DENIED to validate the flow.
sentry.onRequestPermissionsResult(code, permissions, intArrayOf(permissionResult))
activity.onRequestPermissionsResult(code, permissions, intArrayOf(permissionResult))
}
}
}

private val activity = mock(AppCompatActivity::class.java)!!
private val activity = mock(AppCompatActivity::class.java)!!.also {
// Provide a manual override of the result
`when`(it.onRequestPermissionsResult(anyInt(), anyArray(), any<IntArray>())).then {
val requestCode = it.getArgument<Int>(0)
val permissions = it.getArgument<Array<String>>(1)
val grantResults = it.getArgument<IntArray>(2)

SentryPermissionHandler.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
private val permissionHelper = mock(IPermissionHelper::class.java)!!
private val callback = mockFrom<(Boolean) -> Unit>()

Expand Down Expand Up @@ -136,10 +147,11 @@ internal class SentryTest {
setupPermissionResult(activity, sentry, PERMISSION_GRANTED, -1)

// Perform action
sentry.requestPermission(ARBITRARY_PERMISSION, callback)
val requestCode = sentry.requestPermission(ARBITRARY_PERMISSION, callback)

// Assert
verify(activity, times(1)).requestPermissions(any(), eq(sentry.hashCode()))
verify(activity, times(1)).requestPermissions(any(), eq(requestCode))
verify(activity, times(1)).onRequestPermissionsResult(eq(-1), anyArray(), any<IntArray>())
verify(callback, never()).invoke(any(Boolean::class.java))

verifyNoMoreInteractions(activity)
Expand All @@ -159,14 +171,15 @@ internal class SentryTest {
val code = it.getArgument<Int>(1)

// Set the permission result to an empty array.
sentry.onRequestPermissionsResult(code, permissions, intArrayOf())
activity.onRequestPermissionsResult(code, permissions, intArrayOf())
}

// Perform action
sentry.requestPermission(ARBITRARY_PERMISSION, callback)
val requestCode = sentry.requestPermission(ARBITRARY_PERMISSION, callback)

// Assert
verify(activity, times(1)).requestPermissions(any(), eq(sentry.hashCode()))
verify(activity, times(1)).requestPermissions(any(), eq(requestCode))
verify(activity, times(1)).onRequestPermissionsResult(eq(requestCode), anyArray(), AdditionalMatchers.aryEq(IntArray(0)))
verify(callback, never()).invoke(any(Boolean::class.java))

verifyNoMoreInteractions(activity)
Expand All @@ -186,10 +199,10 @@ internal class SentryTest {
setupPermissionResult(activity, sentry, PERMISSION_DENIED)

// Perform action
sentry.requestPermission(ARBITRARY_PERMISSION, callback)
val requestCode = sentry.requestPermission(ARBITRARY_PERMISSION, callback)

// Assert
verify(activity, never()).requestPermissions(any(), eq(sentry.hashCode()))
verify(activity, never()).requestPermissions(any(), eq(requestCode))
verify(callback, times(1)).invoke(eq(true))

verifyNoMoreInteractions(activity)
Expand All @@ -206,10 +219,11 @@ internal class SentryTest {
setupPermissionResult(activity, sentry, PERMISSION_GRANTED)

// Perform action
sentry.requestPermission(ARBITRARY_PERMISSION, callback)
val requestCode = sentry.requestPermission(ARBITRARY_PERMISSION, callback)

// Assert
verify(activity, times(1)).requestPermissions(any(), eq(sentry.hashCode()))
verify(activity, times(1)).requestPermissions(any(), eq(requestCode))
verify(activity, times(1)).onRequestPermissionsResult(eq(requestCode), AdditionalMatchers.aryEq(arrayOf(ARBITRARY_PERMISSION)), AdditionalMatchers.aryEq(intArrayOf(PERMISSION_GRANTED)))
verify(callback, times(1)).invoke(eq(true))

verifyNoMoreInteractions(activity)
Expand All @@ -226,10 +240,11 @@ internal class SentryTest {
setupPermissionResult(activity, sentry, PERMISSION_DENIED)

// Perform action
sentry.requestPermission(ARBITRARY_PERMISSION, callback)
val requestCode = sentry.requestPermission(ARBITRARY_PERMISSION, callback)

// Assert
verify(activity, times(1)).requestPermissions(any(), eq(sentry.hashCode()))
verify(activity, times(1)).requestPermissions(any(), eq(requestCode))
verify(activity, times(1)).onRequestPermissionsResult(eq(requestCode), AdditionalMatchers.aryEq(arrayOf(ARBITRARY_PERMISSION)), AdditionalMatchers.aryEq(intArrayOf(PERMISSION_DENIED)))
verify(callback, times(1)).invoke(eq(false))

verifyNoMoreInteractions(activity)
Expand Down
1 change: 1 addition & 0 deletions sample/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
60 changes: 60 additions & 0 deletions sample/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* MIT License
*
* Copyright (c) 2018 Karn Saheb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion config.build.compileSdk

defaultConfig {
targetSdkVersion config.build.targetSdk
minSdkVersion 21

applicationId "io.karn.sentry.sample"
versionCode 1
versionName "1.0"

testInstrumentationRunner config.testDeps.instrumentationRunner
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])

implementation config.deps.kotlin

implementation config.deps.androidx.core
implementation config.deps.androidx.appcompat

implementation project(path: ':library')
}

// Skip testing and linting.
tasks.whenTaskAdded { task ->
if (task.name == "lint" || task.name.contains("Test")) {
task.enabled = false
}
}
21 changes: 21 additions & 0 deletions sample/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 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
49 changes: 49 additions & 0 deletions sample/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ MIT License
~
~ Copyright (c) 2018 Karn Saheb
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.karn.sentry.sample">

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_icon"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/DefaultTheme">
<activity
android:name="presentation.MainActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

<supports-screens android:xlargeScreens="false" />

</manifest>

0 comments on commit 6719dee

Please sign in to comment.