Skip to content

Commit 6719dee

Browse files
committed
Merge branch 'develop'
2 parents 996f295 + 88fbf57 commit 6719dee

File tree

22 files changed

+561
-32
lines changed

22 files changed

+561
-32
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ dependencies {
3434
#### USAGE
3535
The most basic case is as follows:
3636

37+
```diff
38+
// Add the following delegate to your activity.
39+
- class MyActivity : AppCompatActivity() {
40+
+ class MyActivity : AppCompatActivity(), Permissions by SentryPermissionHandler {
41+
42+
// or optionally manually delegate to the SentryPermissionHandler by adding the following override
43+
// in your Activity
44+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
45+
+ SentryPermissionHandler.onRequestPermissionsResult(requestCode, permissions, grantResults)
46+
+ }
47+
}
48+
```
49+
50+
Then anywhere in your activity you can make a request to fetch a permission.
51+
3752
```Kotlin
3853
Sentry
3954
// A reference to your current activity.

gradle/configuration.gradle

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77

88

99
def versions = [
10-
libCode : 2,
11-
libName : '0.0.2',
10+
libCode : 3,
11+
libName : '0.1.0',
1212

1313
kotlin : '1.3.11',
14+
core: '1.2.0-alpha04',
1415
appcompat: '1.0.2',
1516

1617
jacoco : '0.8.2',
@@ -36,6 +37,7 @@ def build = [
3637
def dependencies = [
3738
kotlin : "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}",
3839
androidx: [
40+
core: "androidx.core:core-ktx:${versions.core}",
3941
appcompat: "androidx.appcompat:appcompat:${versions.appcompat}"
4042
]
4143
]

library/src/main/java/io/karn/sentry/Sentry.kt

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,22 @@ import androidx.appcompat.app.AppCompatActivity
3030
import androidx.core.app.ActivityCompat
3131
import androidx.core.content.PermissionChecker
3232
import java.lang.ref.WeakReference
33+
import kotlin.random.Random
3334

35+
/**
36+
* Provides a typealias for aesthetic purposes.
37+
*/
38+
typealias Permissions = ActivityCompat.OnRequestPermissionsResultCallback
3439

3540
/**
3641
* A lightweight class which makes requesting permissions a breeze.
3742
*/
38-
class Sentry internal constructor(activity: AppCompatActivity, private val permissionHelper: IPermissionHelper) : ActivityCompat.OnRequestPermissionsResultCallback by activity {
43+
class Sentry internal constructor(activity: AppCompatActivity, private val permissionHelper: IPermissionHelper) {
3944

4045
companion object {
46+
// Tracks the requests that are made and their callbacks
47+
internal val receivers = HashMap<Int, (granted: Boolean) -> Unit>()
48+
4149
/**
4250
* A fluent API to create an instance of the Sentry object.
4351
*
@@ -49,45 +57,63 @@ class Sentry internal constructor(activity: AppCompatActivity, private val permi
4957
}
5058
}
5159

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

56-
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
57-
if (grantResults.isEmpty()) {
58-
return
59-
}
60-
61-
if (requestCode == this.requestCode) {
62-
callback(grantResults[0] == PermissionChecker.PERMISSION_GRANTED)
63-
}
64-
}
65-
6663
/**
6764
* Request a [permission] and return the result of the request through the [callback] receiver.
6865
*
6966
* @param permission One of the many Android permissions. See: [Manifest.permission]
7067
* @param callback A receiver for the result of the permission request.
68+
* @return The request code associated with the request
7169
*/
72-
fun requestPermission(permission: String, callback: (granted: Boolean) -> Unit) {
70+
fun requestPermission(permission: String, callback: (granted: Boolean) -> Unit): Int {
7371
if (permission.isBlank()) {
7472
throw IllegalArgumentException("Invalid permission specified.")
7573
}
7674

77-
this.callback = callback
75+
// Generate a request code for the request
76+
var requestCode: Int
77+
do {
78+
requestCode = Random.nextInt(1, Int.MAX_VALUE)
79+
} while (receivers.containsKey(requestCode))
7880

7981
with(activity.get()) {
80-
this ?: return
82+
this ?: return@with
8183

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

89+
// Track the request
90+
receivers[requestCode] = callback
91+
8692
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
8793
return@with this.requestPermissions(arrayOf(permission), requestCode)
8894
}
8995

9096
callback(true)
9197
}
98+
99+
return requestCode
100+
}
101+
}
102+
103+
/**
104+
* A delegated receiver for the onRequestPermissionsResult, this allows the activity's permissions
105+
* results to be intercepted by Sentry and forwarded to the defined receiver.
106+
*/
107+
object SentryPermissionHandler : Permissions {
108+
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
109+
if (grantResults.isEmpty()) {
110+
return
111+
}
112+
113+
// Ensure that there is a pending request code available and remove it once its been tracked
114+
val callback = Sentry.receivers.remove(requestCode)
115+
?: return // No handler for request code
116+
117+
callback.invoke(grantResults[0] == PermissionChecker.PERMISSION_GRANTED)
92118
}
93119
}

library/src/test/java/io/karn/sentry/SentryTest.kt

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ import android.os.Build
2828
import androidx.appcompat.app.AppCompatActivity
2929
import androidx.core.content.PermissionChecker.PERMISSION_DENIED
3030
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
31+
import com.nhaarman.mockitokotlin2.anyArray
3132
import org.junit.Before
3233
import org.junit.Ignore
3334
import org.junit.Test
35+
import org.mockito.AdditionalMatchers
3436
import org.mockito.Mockito
3537
import org.mockito.Mockito.*
3638
import java.lang.reflect.Field
@@ -80,12 +82,21 @@ internal class SentryTest {
8082
val code = requestCode ?: it.getArgument<Int>(1)
8183

8284
// Set the permission result to DENIED to validate the flow.
83-
sentry.onRequestPermissionsResult(code, permissions, intArrayOf(permissionResult))
85+
activity.onRequestPermissionsResult(code, permissions, intArrayOf(permissionResult))
8486
}
8587
}
8688
}
8789

88-
private val activity = mock(AppCompatActivity::class.java)!!
90+
private val activity = mock(AppCompatActivity::class.java)!!.also {
91+
// Provide a manual override of the result
92+
`when`(it.onRequestPermissionsResult(anyInt(), anyArray(), any<IntArray>())).then {
93+
val requestCode = it.getArgument<Int>(0)
94+
val permissions = it.getArgument<Array<String>>(1)
95+
val grantResults = it.getArgument<IntArray>(2)
96+
97+
SentryPermissionHandler.onRequestPermissionsResult(requestCode, permissions, grantResults)
98+
}
99+
}
89100
private val permissionHelper = mock(IPermissionHelper::class.java)!!
90101
private val callback = mockFrom<(Boolean) -> Unit>()
91102

@@ -136,10 +147,11 @@ internal class SentryTest {
136147
setupPermissionResult(activity, sentry, PERMISSION_GRANTED, -1)
137148

138149
// Perform action
139-
sentry.requestPermission(ARBITRARY_PERMISSION, callback)
150+
val requestCode = sentry.requestPermission(ARBITRARY_PERMISSION, callback)
140151

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

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

161173
// Set the permission result to an empty array.
162-
sentry.onRequestPermissionsResult(code, permissions, intArrayOf())
174+
activity.onRequestPermissionsResult(code, permissions, intArrayOf())
163175
}
164176

165177
// Perform action
166-
sentry.requestPermission(ARBITRARY_PERMISSION, callback)
178+
val requestCode = sentry.requestPermission(ARBITRARY_PERMISSION, callback)
167179

168180
// Assert
169-
verify(activity, times(1)).requestPermissions(any(), eq(sentry.hashCode()))
181+
verify(activity, times(1)).requestPermissions(any(), eq(requestCode))
182+
verify(activity, times(1)).onRequestPermissionsResult(eq(requestCode), anyArray(), AdditionalMatchers.aryEq(IntArray(0)))
170183
verify(callback, never()).invoke(any(Boolean::class.java))
171184

172185
verifyNoMoreInteractions(activity)
@@ -186,10 +199,10 @@ internal class SentryTest {
186199
setupPermissionResult(activity, sentry, PERMISSION_DENIED)
187200

188201
// Perform action
189-
sentry.requestPermission(ARBITRARY_PERMISSION, callback)
202+
val requestCode = sentry.requestPermission(ARBITRARY_PERMISSION, callback)
190203

191204
// Assert
192-
verify(activity, never()).requestPermissions(any(), eq(sentry.hashCode()))
205+
verify(activity, never()).requestPermissions(any(), eq(requestCode))
193206
verify(callback, times(1)).invoke(eq(true))
194207

195208
verifyNoMoreInteractions(activity)
@@ -206,10 +219,11 @@ internal class SentryTest {
206219
setupPermissionResult(activity, sentry, PERMISSION_GRANTED)
207220

208221
// Perform action
209-
sentry.requestPermission(ARBITRARY_PERMISSION, callback)
222+
val requestCode = sentry.requestPermission(ARBITRARY_PERMISSION, callback)
210223

211224
// Assert
212-
verify(activity, times(1)).requestPermissions(any(), eq(sentry.hashCode()))
225+
verify(activity, times(1)).requestPermissions(any(), eq(requestCode))
226+
verify(activity, times(1)).onRequestPermissionsResult(eq(requestCode), AdditionalMatchers.aryEq(arrayOf(ARBITRARY_PERMISSION)), AdditionalMatchers.aryEq(intArrayOf(PERMISSION_GRANTED)))
213227
verify(callback, times(1)).invoke(eq(true))
214228

215229
verifyNoMoreInteractions(activity)
@@ -226,10 +240,11 @@ internal class SentryTest {
226240
setupPermissionResult(activity, sentry, PERMISSION_DENIED)
227241

228242
// Perform action
229-
sentry.requestPermission(ARBITRARY_PERMISSION, callback)
243+
val requestCode = sentry.requestPermission(ARBITRARY_PERMISSION, callback)
230244

231245
// Assert
232-
verify(activity, times(1)).requestPermissions(any(), eq(sentry.hashCode()))
246+
verify(activity, times(1)).requestPermissions(any(), eq(requestCode))
247+
verify(activity, times(1)).onRequestPermissionsResult(eq(requestCode), AdditionalMatchers.aryEq(arrayOf(ARBITRARY_PERMISSION)), AdditionalMatchers.aryEq(intArrayOf(PERMISSION_DENIED)))
233248
verify(callback, times(1)).invoke(eq(false))
234249

235250
verifyNoMoreInteractions(activity)

sample/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

sample/build.gradle

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2018 Karn Saheb
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
apply plugin: 'com.android.application'
26+
apply plugin: 'kotlin-android'
27+
apply plugin: 'kotlin-android-extensions'
28+
29+
android {
30+
compileSdkVersion config.build.compileSdk
31+
32+
defaultConfig {
33+
targetSdkVersion config.build.targetSdk
34+
minSdkVersion 21
35+
36+
applicationId "io.karn.sentry.sample"
37+
versionCode 1
38+
versionName "1.0"
39+
40+
testInstrumentationRunner config.testDeps.instrumentationRunner
41+
}
42+
}
43+
44+
dependencies {
45+
implementation fileTree(dir: 'libs', include: ['*.jar'])
46+
47+
implementation config.deps.kotlin
48+
49+
implementation config.deps.androidx.core
50+
implementation config.deps.androidx.appcompat
51+
52+
implementation project(path: ':library')
53+
}
54+
55+
// Skip testing and linting.
56+
tasks.whenTaskAdded { task ->
57+
if (task.name == "lint" || task.name.contains("Test")) {
58+
task.enabled = false
59+
}
60+
}

sample/proguard-rules.pro

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
~ MIT License
3+
~
4+
~ Copyright (c) 2018 Karn Saheb
5+
~
6+
~ Permission is hereby granted, free of charge, to any person obtaining a copy
7+
~ of this software and associated documentation files (the "Software"), to deal
8+
~ in the Software without restriction, including without limitation the rights
9+
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
~ copies of the Software, and to permit persons to whom the Software is
11+
~ furnished to do so, subject to the following conditions:
12+
~
13+
~ The above copyright notice and this permission notice shall be included in all
14+
~ copies or substantial portions of the Software.
15+
~
16+
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
~ SOFTWARE.
23+
-->
24+
25+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
26+
package="io.karn.sentry.sample">
27+
28+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
29+
30+
<application
31+
android:allowBackup="true"
32+
android:icon="@mipmap/ic_icon"
33+
android:label="@string/app_name"
34+
android:supportsRtl="true"
35+
android:theme="@style/DefaultTheme">
36+
<activity
37+
android:name="presentation.MainActivity"
38+
android:screenOrientation="portrait">
39+
<intent-filter>
40+
<action android:name="android.intent.action.MAIN" />
41+
42+
<category android:name="android.intent.category.LAUNCHER" />
43+
</intent-filter>
44+
</activity>
45+
</application>
46+
47+
<supports-screens android:xlargeScreens="false" />
48+
49+
</manifest>

0 commit comments

Comments
 (0)