) {
66 | // don't care what the permissions param is, always request SYSTEM_ALERT_WINDOW permission.
67 | pb.requestSystemAlertWindowPermissionNow(this)
68 | }
69 | }
--------------------------------------------------------------------------------
/permissionx/src/main/java/com/permissionx/guolindev/PermissionX.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) guolin, PermissionX Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.permissionx.guolindev;
18 |
19 | import android.content.Context;
20 | import android.content.pm.PackageManager;
21 |
22 | import androidx.annotation.NonNull;
23 | import androidx.core.app.NotificationManagerCompat;
24 | import androidx.core.content.ContextCompat;
25 | import androidx.fragment.app.Fragment;
26 | import androidx.fragment.app.FragmentActivity;
27 |
28 | /**
29 | * An open source Android library that makes handling runtime permissions extremely easy.
30 | *
31 | * The following snippet shows the simple usage:
32 | *
33 | * PermissionX.init(activity)
34 | * .permissions(Manifest.permission.READ_CONTACTS, Manifest.permission.CAMERA)
35 | * .request { allGranted, grantedList, deniedList ->
36 | * // handling the logic
37 | * }
38 | *
39 | *
40 | * @author guolin
41 | * @since 2019/11/2
42 | */
43 | public class PermissionX {
44 |
45 | /**
46 | * Init PermissionX to make everything prepare to work.
47 | *
48 | * @param activity An instance of FragmentActivity
49 | * @return PermissionCollection instance.
50 | */
51 | public static PermissionMediator init(@NonNull FragmentActivity activity) {
52 | return new PermissionMediator(activity);
53 | }
54 |
55 | /**
56 | * Init PermissionX to make everything prepare to work.
57 | *
58 | * @param fragment An instance of Fragment
59 | * @return PermissionCollection instance.
60 | */
61 | public static PermissionMediator init(@NonNull Fragment fragment) {
62 | return new PermissionMediator(fragment);
63 | }
64 |
65 | /**
66 | * A helper function to check a permission is granted or not.
67 | *
68 | * @param context Any context, will not be retained.
69 | * @param permission Specific permission name to check. e.g. [android.Manifest.permission.CAMERA].
70 | * @return True if this permission is granted, False otherwise.
71 | */
72 | public static boolean isGranted(@NonNull Context context, @NonNull String permission) {
73 | return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
74 | }
75 |
76 | /**
77 | * A helper function to check are notifications are enabled for current app.
78 | * @param context
79 | * Any context, will not be retained.
80 | * @return Note that if Android version is lower than N, the return value will always be true.
81 | */
82 | public static boolean areNotificationsEnabled(@NonNull Context context) {
83 | return NotificationManagerCompat.from(context).areNotificationsEnabled();
84 | }
85 |
86 | public static final class permission {
87 | /**
88 | * Define the const to compat with system lower than T.
89 | */
90 | public static final String POST_NOTIFICATIONS = "android.permission.POST_NOTIFICATIONS";
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/permissionx/src/main/java/com/permissionx/guolindev/request/RequestBodySensorsBackgroundPermission.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) guolin, PermissionX Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.permissionx.guolindev.request
17 |
18 | import android.Manifest
19 | import android.os.Build
20 | import com.permissionx.guolindev.PermissionX
21 |
22 | /**
23 | * Implementation for request ACCESS_BACKGROUND_LOCATION permission.
24 | * @author guolin
25 | * @since 2022/8/26
26 | */
27 | internal class RequestBodySensorsBackgroundPermission internal constructor(permissionBuilder: PermissionBuilder)
28 | : BaseTask(permissionBuilder) {
29 |
30 | override fun request() {
31 | if (pb.shouldRequestBodySensorsBackgroundPermission()) {
32 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
33 | // If app runs under Android T, there's no BODY_SENSORS_BACKGROUND permissions.
34 | // We remove it from request list, but will append it to the request callback as denied permission.
35 | pb.specialPermissions.remove(BODY_SENSORS_BACKGROUND)
36 | pb.permissionsWontRequest.add(BODY_SENSORS_BACKGROUND)
37 | finish()
38 | return
39 | }
40 | if (PermissionX.isGranted(pb.activity, BODY_SENSORS_BACKGROUND)) {
41 | // BODY_SENSORS_BACKGROUND has already granted, we can finish this task now.
42 | finish()
43 | return
44 | }
45 | val bodySensorGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
46 | PermissionX.isGranted(pb.activity, Manifest.permission.BODY_SENSORS)
47 | } else {
48 | false
49 | }
50 | if (bodySensorGranted) {
51 | if (pb.explainReasonCallback != null || pb.explainReasonCallbackWithBeforeParam != null) {
52 | val requestList = mutableListOf(BODY_SENSORS_BACKGROUND)
53 | if (pb.explainReasonCallbackWithBeforeParam != null) {
54 | // callback ExplainReasonCallbackWithBeforeParam prior to ExplainReasonCallback
55 | pb.explainReasonCallbackWithBeforeParam!!.onExplainReason(explainScope, requestList, true)
56 | } else {
57 | pb.explainReasonCallback!!.onExplainReason(explainScope, requestList)
58 | }
59 | } else {
60 | // No implementation of explainReasonCallback, so we have to request BODY_SENSORS_BACKGROUND without explanation.
61 | requestAgain(emptyList())
62 | }
63 | return
64 | }
65 | }
66 | // Shouldn't request BODY_SENSORS_BACKGROUND at this time, so we call finish() to finish this task.
67 | finish()
68 | }
69 |
70 | override fun requestAgain(permissions: List) {
71 | // Don't care what the permissions param is, always request BODY_SENSORS_BACKGROUND.
72 | pb.requestBodySensorsBackgroundPermissionNow(this)
73 | }
74 |
75 | companion object {
76 | /**
77 | * Define the const to compat with system lower than T.
78 | */
79 | const val BODY_SENSORS_BACKGROUND = "android.permission.BODY_SENSORS_BACKGROUND"
80 | }
81 | }
--------------------------------------------------------------------------------
/permissionx/src/main/java/com/permissionx/guolindev/request/RequestBackgroundLocationPermission.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) guolin, PermissionX Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.permissionx.guolindev.request
17 |
18 | import android.Manifest
19 | import android.os.Build
20 | import com.permissionx.guolindev.PermissionX
21 |
22 | /**
23 | * Implementation for request ACCESS_BACKGROUND_LOCATION permission.
24 | * @author guolin
25 | * @since 2020/6/10
26 | */
27 | internal class RequestBackgroundLocationPermission internal constructor(permissionBuilder: PermissionBuilder)
28 | : BaseTask(permissionBuilder) {
29 |
30 | override fun request() {
31 | if (pb.shouldRequestBackgroundLocationPermission()) {
32 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
33 | // If app runs under Android Q, there's no ACCESS_BACKGROUND_LOCATION permissions.
34 | // We remove it from request list, but will append it to the request callback as denied permission.
35 | pb.specialPermissions.remove(ACCESS_BACKGROUND_LOCATION)
36 | pb.permissionsWontRequest.add(ACCESS_BACKGROUND_LOCATION)
37 | finish()
38 | return
39 | }
40 | if (PermissionX.isGranted(pb.activity, ACCESS_BACKGROUND_LOCATION)) {
41 | // ACCESS_BACKGROUND_LOCATION has already granted, we can finish this task now.
42 | finish()
43 | return
44 | }
45 | val accessFindLocationGranted = PermissionX.isGranted(pb.activity, Manifest.permission.ACCESS_FINE_LOCATION)
46 | val accessCoarseLocationGranted = PermissionX.isGranted(pb.activity, Manifest.permission.ACCESS_COARSE_LOCATION)
47 | if (accessFindLocationGranted || accessCoarseLocationGranted) {
48 | if (pb.explainReasonCallback != null || pb.explainReasonCallbackWithBeforeParam != null) {
49 | val requestList = mutableListOf(ACCESS_BACKGROUND_LOCATION)
50 | if (pb.explainReasonCallbackWithBeforeParam != null) {
51 | // callback ExplainReasonCallbackWithBeforeParam prior to ExplainReasonCallback
52 | pb.explainReasonCallbackWithBeforeParam!!.onExplainReason(explainScope, requestList, true)
53 | } else {
54 | pb.explainReasonCallback!!.onExplainReason(explainScope, requestList)
55 | }
56 | } else {
57 | // No implementation of explainReasonCallback, so we have to request ACCESS_BACKGROUND_LOCATION without explanation.
58 | requestAgain(emptyList())
59 | }
60 | return
61 | }
62 | }
63 | // Shouldn't request ACCESS_BACKGROUND_LOCATION at this time, so we call finish() to finish this task.
64 | finish()
65 | }
66 |
67 | override fun requestAgain(permissions: List) {
68 | // Don't care what the permissions param is, always request ACCESS_BACKGROUND_LOCATION.
69 | pb.requestAccessBackgroundLocationPermissionNow(this)
70 | }
71 |
72 | companion object {
73 | /**
74 | * Define the const to compat with system lower than Q.
75 | */
76 | const val ACCESS_BACKGROUND_LOCATION = "android.permission.ACCESS_BACKGROUND_LOCATION"
77 | }
78 | }
--------------------------------------------------------------------------------
/permissionx/src/main/java/com/permissionx/guolindev/PermissionMediator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) guolin, PermissionX Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.permissionx.guolindev
18 |
19 | import android.os.Build
20 | import androidx.fragment.app.Fragment
21 | import androidx.fragment.app.FragmentActivity
22 | import com.permissionx.guolindev.dialog.allSpecialPermissions
23 | import com.permissionx.guolindev.request.PermissionBuilder
24 | import com.permissionx.guolindev.request.RequestBackgroundLocationPermission
25 | import kotlin.collections.LinkedHashSet
26 |
27 | /**
28 | * An internal class to provide specific scope for passing permissions param.
29 | *
30 | * @author guolin
31 | * @since 2019/11/2
32 | */
33 | class PermissionMediator {
34 |
35 | private var activity: FragmentActivity? = null
36 | private var fragment: Fragment? = null
37 |
38 | constructor(activity: FragmentActivity) {
39 | this.activity = activity
40 | }
41 |
42 | constructor(fragment: Fragment) {
43 | this.fragment = fragment
44 | }
45 |
46 | /**
47 | * All permissions that you want to request.
48 | *
49 | * @param permissions A vararg param to pass permissions.
50 | * @return PermissionBuilder itself.
51 | */
52 | fun permissions(permissions: List): PermissionBuilder {
53 | val normalPermissionSet = LinkedHashSet()
54 | val specialPermissionSet = LinkedHashSet()
55 | val osVersion = Build.VERSION.SDK_INT
56 | val targetSdkVersion = if (activity != null) {
57 | activity!!.applicationInfo.targetSdkVersion
58 | } else {
59 | fragment!!.requireContext().applicationInfo.targetSdkVersion
60 | }
61 | for (permission in permissions) {
62 | if (permission in allSpecialPermissions) {
63 | specialPermissionSet.add(permission)
64 | } else {
65 | normalPermissionSet.add(permission)
66 | }
67 | }
68 | if (RequestBackgroundLocationPermission.ACCESS_BACKGROUND_LOCATION in specialPermissionSet) {
69 | if (osVersion == Build.VERSION_CODES.Q ||
70 | (osVersion == Build.VERSION_CODES.R && targetSdkVersion < Build.VERSION_CODES.R)) {
71 | // If we request ACCESS_BACKGROUND_LOCATION on Q or on R but targetSdkVersion below R,
72 | // We don't need to request specially, just request as normal permission.
73 | specialPermissionSet.remove(RequestBackgroundLocationPermission.ACCESS_BACKGROUND_LOCATION)
74 | normalPermissionSet.add(RequestBackgroundLocationPermission.ACCESS_BACKGROUND_LOCATION)
75 | }
76 | }
77 | if (PermissionX.permission.POST_NOTIFICATIONS in specialPermissionSet) {
78 | if (osVersion >= Build.VERSION_CODES.TIRAMISU && targetSdkVersion >= Build.VERSION_CODES.TIRAMISU) {
79 | // If we request POST_NOTIFICATIONS on TIRAMISU or above and targetSdkVersion >= TIRAMISU,
80 | // We don't need to request specially, just request as normal permission.
81 | specialPermissionSet.remove(PermissionX.permission.POST_NOTIFICATIONS)
82 | normalPermissionSet.add(PermissionX.permission.POST_NOTIFICATIONS)
83 | }
84 | }
85 | return PermissionBuilder(activity, fragment, normalPermissionSet, specialPermissionSet)
86 | }
87 |
88 | /**
89 | * All permissions that you want to request.
90 | *
91 | * @param permissions A vararg param to pass permissions.
92 | * @return PermissionBuilder itself.
93 | */
94 | fun permissions(vararg permissions: String): PermissionBuilder {
95 | return permissions(listOf(*permissions))
96 | }
97 |
98 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/permissionx/app/CustomDialog.kt:
--------------------------------------------------------------------------------
1 | package com.permissionx.app
2 |
3 | import android.Manifest
4 | import android.annotation.TargetApi
5 | import android.content.Context
6 | import android.os.Bundle
7 | import android.view.View
8 | import com.permissionx.app.databinding.CustomDialogLayoutBinding
9 | import com.permissionx.app.databinding.PermissionsItemBinding
10 | import com.permissionx.guolindev.dialog.RationaleDialog
11 |
12 | @TargetApi(30)
13 | class CustomDialog(context: Context, private val message: String, private val permissions: List) : RationaleDialog(context, R.style.CustomDialog) {
14 |
15 | private val permissionMap = mapOf(Manifest.permission.READ_CALENDAR to Manifest.permission_group.CALENDAR,
16 | Manifest.permission.WRITE_CALENDAR to Manifest.permission_group.CALENDAR,
17 | Manifest.permission.READ_CALL_LOG to Manifest.permission_group.CALL_LOG,
18 | Manifest.permission.WRITE_CALL_LOG to Manifest.permission_group.CALL_LOG,
19 | "android.permission.PROCESS_OUTGOING_CALLS" to Manifest.permission_group.CALL_LOG,
20 | Manifest.permission.CAMERA to Manifest.permission_group.CAMERA,
21 | Manifest.permission.READ_CONTACTS to Manifest.permission_group.CONTACTS,
22 | Manifest.permission.WRITE_CONTACTS to Manifest.permission_group.CONTACTS,
23 | Manifest.permission.GET_ACCOUNTS to Manifest.permission_group.CONTACTS,
24 | Manifest.permission.ACCESS_FINE_LOCATION to Manifest.permission_group.LOCATION,
25 | Manifest.permission.ACCESS_COARSE_LOCATION to Manifest.permission_group.LOCATION,
26 | Manifest.permission.ACCESS_BACKGROUND_LOCATION to Manifest.permission_group.LOCATION,
27 | Manifest.permission.RECORD_AUDIO to Manifest.permission_group.MICROPHONE,
28 | Manifest.permission.READ_PHONE_STATE to Manifest.permission_group.PHONE,
29 | Manifest.permission.READ_PHONE_NUMBERS to Manifest.permission_group.PHONE,
30 | Manifest.permission.CALL_PHONE to Manifest.permission_group.PHONE,
31 | Manifest.permission.ANSWER_PHONE_CALLS to Manifest.permission_group.PHONE,
32 | Manifest.permission.ADD_VOICEMAIL to Manifest.permission_group.PHONE,
33 | Manifest.permission.USE_SIP to Manifest.permission_group.PHONE,
34 | Manifest.permission.ACCEPT_HANDOVER to Manifest.permission_group.PHONE,
35 | Manifest.permission.BODY_SENSORS to Manifest.permission_group.SENSORS,
36 | Manifest.permission.ACTIVITY_RECOGNITION to Manifest.permission_group.ACTIVITY_RECOGNITION,
37 | Manifest.permission.SEND_SMS to Manifest.permission_group.SMS,
38 | Manifest.permission.RECEIVE_SMS to Manifest.permission_group.SMS,
39 | Manifest.permission.READ_SMS to Manifest.permission_group.SMS,
40 | Manifest.permission.RECEIVE_WAP_PUSH to Manifest.permission_group.SMS,
41 | Manifest.permission.RECEIVE_MMS to Manifest.permission_group.SMS,
42 | Manifest.permission.READ_EXTERNAL_STORAGE to Manifest.permission_group.STORAGE,
43 | Manifest.permission.WRITE_EXTERNAL_STORAGE to Manifest.permission_group.STORAGE,
44 | Manifest.permission.ACCESS_MEDIA_LOCATION to Manifest.permission_group.STORAGE
45 | )
46 |
47 | private val groupSet = HashSet()
48 |
49 | private lateinit var binding: CustomDialogLayoutBinding
50 |
51 | override fun onCreate(savedInstanceState: Bundle?) {
52 | super.onCreate(savedInstanceState)
53 | binding = CustomDialogLayoutBinding.inflate(layoutInflater)
54 | setContentView(binding.root)
55 | binding.messageText.text = message
56 | buildPermissionsLayout()
57 | window?.let {
58 | val param = it.attributes
59 | val width = (context.resources.displayMetrics.widthPixels * 0.8).toInt()
60 | val height = param.height
61 | it.setLayout(width, height)
62 | }
63 | }
64 |
65 | override fun getNegativeButton(): View {
66 | return binding.negativeBtn
67 | }
68 |
69 | override fun getPositiveButton(): View {
70 | return binding.positiveBtn
71 | }
72 |
73 | override fun getPermissionsToRequest(): List {
74 | return permissions
75 | }
76 |
77 | private fun buildPermissionsLayout() {
78 | for (permission in permissions) {
79 | val permissionGroup = permissionMap[permission]
80 | if (permissionGroup != null && !groupSet.contains(permissionGroup)) {
81 | val itemBinding = PermissionsItemBinding.inflate(layoutInflater, binding.permissionsLayout, false)
82 | itemBinding.root.text = context.packageManager.getPermissionGroupInfo(permissionGroup, 0).loadLabel(context.packageManager)
83 | binding.permissionsLayout.addView(itemBinding.root)
84 | groupSet.add(permissionGroup)
85 | }
86 | }
87 | }
88 |
89 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
45 |
46 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
67 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/app/src/main/java/com/permissionx/app/CustomDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package com.permissionx.app
2 |
3 | import android.Manifest
4 | import android.annotation.TargetApi
5 | import android.graphics.Color
6 | import android.graphics.drawable.ColorDrawable
7 | import android.os.Bundle
8 | import android.view.LayoutInflater
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import android.widget.TextView
12 | import com.permissionx.app.databinding.CustomDialogLayoutBinding
13 | import com.permissionx.app.databinding.PermissionsItemBinding
14 | import com.permissionx.guolindev.dialog.RationaleDialogFragment
15 |
16 | @TargetApi(30)
17 | class CustomDialogFragment() : RationaleDialogFragment() {
18 |
19 | var mMessage: String = ""
20 |
21 | var mPermissions: List = emptyList()
22 |
23 | constructor(message: String, permissions: List): this() {
24 | mMessage = message
25 | mPermissions = permissions
26 | }
27 |
28 | private val permissionMap = mapOf(Manifest.permission.READ_CALENDAR to Manifest.permission_group.CALENDAR,
29 | Manifest.permission.WRITE_CALENDAR to Manifest.permission_group.CALENDAR,
30 | Manifest.permission.READ_CALL_LOG to Manifest.permission_group.CALL_LOG,
31 | Manifest.permission.WRITE_CALL_LOG to Manifest.permission_group.CALL_LOG,
32 | "android.permission.PROCESS_OUTGOING_CALLS" to Manifest.permission_group.CALL_LOG,
33 | Manifest.permission.CAMERA to Manifest.permission_group.CAMERA,
34 | Manifest.permission.READ_CONTACTS to Manifest.permission_group.CONTACTS,
35 | Manifest.permission.WRITE_CONTACTS to Manifest.permission_group.CONTACTS,
36 | Manifest.permission.GET_ACCOUNTS to Manifest.permission_group.CONTACTS,
37 | Manifest.permission.ACCESS_FINE_LOCATION to Manifest.permission_group.LOCATION,
38 | Manifest.permission.ACCESS_COARSE_LOCATION to Manifest.permission_group.LOCATION,
39 | Manifest.permission.ACCESS_BACKGROUND_LOCATION to Manifest.permission_group.LOCATION,
40 | Manifest.permission.RECORD_AUDIO to Manifest.permission_group.MICROPHONE,
41 | Manifest.permission.READ_PHONE_STATE to Manifest.permission_group.PHONE,
42 | Manifest.permission.READ_PHONE_NUMBERS to Manifest.permission_group.PHONE,
43 | Manifest.permission.CALL_PHONE to Manifest.permission_group.PHONE,
44 | Manifest.permission.ANSWER_PHONE_CALLS to Manifest.permission_group.PHONE,
45 | Manifest.permission.ADD_VOICEMAIL to Manifest.permission_group.PHONE,
46 | Manifest.permission.USE_SIP to Manifest.permission_group.PHONE,
47 | Manifest.permission.ACCEPT_HANDOVER to Manifest.permission_group.PHONE,
48 | Manifest.permission.BODY_SENSORS to Manifest.permission_group.SENSORS,
49 | Manifest.permission.ACTIVITY_RECOGNITION to Manifest.permission_group.ACTIVITY_RECOGNITION,
50 | Manifest.permission.SEND_SMS to Manifest.permission_group.SMS,
51 | Manifest.permission.RECEIVE_SMS to Manifest.permission_group.SMS,
52 | Manifest.permission.READ_SMS to Manifest.permission_group.SMS,
53 | Manifest.permission.RECEIVE_WAP_PUSH to Manifest.permission_group.SMS,
54 | Manifest.permission.RECEIVE_MMS to Manifest.permission_group.SMS,
55 | Manifest.permission.READ_EXTERNAL_STORAGE to Manifest.permission_group.STORAGE,
56 | Manifest.permission.WRITE_EXTERNAL_STORAGE to Manifest.permission_group.STORAGE,
57 | Manifest.permission.ACCESS_MEDIA_LOCATION to Manifest.permission_group.STORAGE
58 | )
59 |
60 | private val groupSet = HashSet()
61 |
62 | private var _binding: CustomDialogLayoutBinding? = null
63 |
64 | private val binding get() = _binding!!
65 |
66 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
67 | dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
68 | _binding = CustomDialogLayoutBinding.inflate(inflater, container, false)
69 | return binding.root
70 | }
71 |
72 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
73 | super.onViewCreated(view, savedInstanceState)
74 | binding.messageText.text = mMessage
75 | buildPermissionsLayout()
76 | }
77 |
78 | override fun onDestroyView() {
79 | super.onDestroyView()
80 | _binding = null
81 | }
82 |
83 | override fun getNegativeButton(): View {
84 | return binding.negativeBtn
85 | }
86 |
87 | override fun getPositiveButton(): View {
88 | return binding.positiveBtn
89 | }
90 |
91 | override fun getPermissionsToRequest(): List {
92 | return mPermissions
93 | }
94 |
95 | private fun buildPermissionsLayout() {
96 | for (permission in mPermissions) {
97 | val permissionGroup = permissionMap[permission]
98 | if (permissionGroup != null && !groupSet.contains(permissionGroup)) {
99 | val itemBinding = PermissionsItemBinding.inflate(layoutInflater, binding.permissionsLayout, false)
100 | itemBinding.root.text = context?.let {
101 | it.packageManager.getPermissionGroupInfo(permissionGroup, 0).loadLabel(it.packageManager)
102 | }
103 | binding.permissionsLayout.addView(itemBinding.root)
104 | groupSet.add(permissionGroup)
105 | }
106 | }
107 | }
108 |
109 | }
--------------------------------------------------------------------------------
/permissionx/src/main/java/com/permissionx/guolindev/dialog/PermissionMap.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) guolin, PermissionX Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.permissionx.guolindev.dialog
18 |
19 | import android.Manifest
20 | import android.annotation.TargetApi
21 | import android.os.Build
22 | import com.permissionx.guolindev.PermissionX
23 | import com.permissionx.guolindev.request.RequestBackgroundLocationPermission
24 | import com.permissionx.guolindev.request.RequestBodySensorsBackgroundPermission
25 | import com.permissionx.guolindev.request.RequestInstallPackagesPermission
26 | import com.permissionx.guolindev.request.RequestManageExternalStoragePermission
27 |
28 | /**
29 | * Maintains all the special permissions that we need to handle by special case.
30 | */
31 | val allSpecialPermissions = setOf(
32 | RequestBackgroundLocationPermission.ACCESS_BACKGROUND_LOCATION,
33 | Manifest.permission.SYSTEM_ALERT_WINDOW,
34 | Manifest.permission.WRITE_SETTINGS,
35 | RequestManageExternalStoragePermission.MANAGE_EXTERNAL_STORAGE,
36 | RequestInstallPackagesPermission.REQUEST_INSTALL_PACKAGES,
37 | PermissionX.permission.POST_NOTIFICATIONS,
38 | RequestBodySensorsBackgroundPermission.BODY_SENSORS_BACKGROUND,
39 | )
40 |
41 | /**
42 | * Based on this link https://developer.android.com/about/versions/10/privacy/changes#permission-groups-removed
43 | * Since Android Q, we can not get the permission group name by permission name anymore.
44 | * So we need to keep a track of relationship between permissions and permission groups on every
45 | * Android release since Android Q.
46 | */
47 | @TargetApi(Build.VERSION_CODES.Q)
48 | val permissionMapOnQ = mapOf(
49 | Manifest.permission.READ_CALENDAR to Manifest.permission_group.CALENDAR,
50 | Manifest.permission.WRITE_CALENDAR to Manifest.permission_group.CALENDAR,
51 | Manifest.permission.READ_CALL_LOG to Manifest.permission_group.CALL_LOG,
52 | Manifest.permission.WRITE_CALL_LOG to Manifest.permission_group.CALL_LOG,
53 | "android.permission.PROCESS_OUTGOING_CALLS" to Manifest.permission_group.CALL_LOG,
54 | Manifest.permission.CAMERA to Manifest.permission_group.CAMERA,
55 | Manifest.permission.READ_CONTACTS to Manifest.permission_group.CONTACTS,
56 | Manifest.permission.WRITE_CONTACTS to Manifest.permission_group.CONTACTS,
57 | Manifest.permission.GET_ACCOUNTS to Manifest.permission_group.CONTACTS,
58 | Manifest.permission.ACCESS_FINE_LOCATION to Manifest.permission_group.LOCATION,
59 | Manifest.permission.ACCESS_COARSE_LOCATION to Manifest.permission_group.LOCATION,
60 | Manifest.permission.ACCESS_BACKGROUND_LOCATION to Manifest.permission_group.LOCATION,
61 | Manifest.permission.RECORD_AUDIO to Manifest.permission_group.MICROPHONE,
62 | Manifest.permission.READ_PHONE_STATE to Manifest.permission_group.PHONE,
63 | Manifest.permission.READ_PHONE_NUMBERS to Manifest.permission_group.PHONE,
64 | Manifest.permission.CALL_PHONE to Manifest.permission_group.PHONE,
65 | Manifest.permission.ANSWER_PHONE_CALLS to Manifest.permission_group.PHONE,
66 | Manifest.permission.ADD_VOICEMAIL to Manifest.permission_group.PHONE,
67 | Manifest.permission.USE_SIP to Manifest.permission_group.PHONE,
68 | Manifest.permission.ACCEPT_HANDOVER to Manifest.permission_group.PHONE,
69 | Manifest.permission.BODY_SENSORS to Manifest.permission_group.SENSORS,
70 | Manifest.permission.ACTIVITY_RECOGNITION to Manifest.permission_group.ACTIVITY_RECOGNITION,
71 | Manifest.permission.SEND_SMS to Manifest.permission_group.SMS,
72 | Manifest.permission.RECEIVE_SMS to Manifest.permission_group.SMS,
73 | Manifest.permission.READ_SMS to Manifest.permission_group.SMS,
74 | Manifest.permission.RECEIVE_WAP_PUSH to Manifest.permission_group.SMS,
75 | Manifest.permission.RECEIVE_MMS to Manifest.permission_group.SMS,
76 | Manifest.permission.READ_EXTERNAL_STORAGE to Manifest.permission_group.STORAGE,
77 | Manifest.permission.WRITE_EXTERNAL_STORAGE to Manifest.permission_group.STORAGE,
78 | Manifest.permission.ACCESS_MEDIA_LOCATION to Manifest.permission_group.STORAGE,
79 | )
80 |
81 | /**
82 | * Thankfully Android R has no permission added or removed than Android Q.
83 | */
84 | @TargetApi(Build.VERSION_CODES.R)
85 | val permissionMapOnR = mapOf(
86 | Manifest.permission.MANAGE_EXTERNAL_STORAGE to Manifest.permission_group.STORAGE,
87 | ).toMutableMap().apply {
88 | putAll(permissionMapOnQ)
89 | }.toMap()
90 |
91 | /**
92 | * Android S added 3 bluetooth related runtime permissions.
93 | */
94 | @TargetApi(Build.VERSION_CODES.S)
95 | val permissionMapOnS = mapOf(
96 | Manifest.permission.BLUETOOTH_SCAN to Manifest.permission_group.NEARBY_DEVICES,
97 | Manifest.permission.BLUETOOTH_ADVERTISE to Manifest.permission_group.NEARBY_DEVICES,
98 | Manifest.permission.BLUETOOTH_CONNECT to Manifest.permission_group.NEARBY_DEVICES,
99 | ).toMutableMap().apply {
100 | putAll(permissionMapOnR)
101 | }.toMap()
102 |
103 | /**
104 | * Android S added 3 bluetooth related runtime permissions.
105 | */
106 | @TargetApi(Build.VERSION_CODES.TIRAMISU)
107 | val permissionMapOnT = mapOf(
108 | Manifest.permission.READ_MEDIA_IMAGES to Manifest.permission_group.READ_MEDIA_VISUAL,
109 | Manifest.permission.READ_MEDIA_VIDEO to Manifest.permission_group.READ_MEDIA_VISUAL,
110 | Manifest.permission.READ_MEDIA_AUDIO to Manifest.permission_group.READ_MEDIA_AURAL,
111 | Manifest.permission.POST_NOTIFICATIONS to Manifest.permission_group.NOTIFICATIONS,
112 | Manifest.permission.NEARBY_WIFI_DEVICES to Manifest.permission_group.NEARBY_DEVICES,
113 | Manifest.permission.BODY_SENSORS_BACKGROUND to Manifest.permission_group.SENSORS,
114 | ).toMutableMap().apply {
115 | putAll(permissionMapOnS)
116 | }.toMap()
--------------------------------------------------------------------------------
/permissionx/src/main/java/com/permissionx/guolindev/request/BaseTask.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) guolin, PermissionX Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.permissionx.guolindev.request
17 |
18 | import android.Manifest
19 | import android.os.Build
20 | import android.os.Environment
21 | import android.provider.Settings
22 | import com.permissionx.guolindev.PermissionX
23 | import java.util.*
24 |
25 | /**
26 | * Define a BaseTask to implement the duplicate logic codes. No need to implement them in every task.
27 | *
28 | * @author guolin
29 | * @since 2020/6/10
30 | */
31 | internal abstract class BaseTask(@JvmField var pb: PermissionBuilder) : ChainTask {
32 | /**
33 | * Point to the next task. When this task finish will run next task. If there's no next task, the request process end.
34 | */
35 | @JvmField
36 | var next: ChainTask? = null
37 |
38 | /**
39 | * Provide specific scopes for explainReasonCallback for specific functions to call.
40 | */
41 | private var explainReasonScope = ExplainScope(pb, this)
42 |
43 | /**
44 | * Provide specific scopes for forwardToSettingsCallback for specific functions to call.
45 | */
46 | private var forwardToSettingsScope = ForwardScope(pb, this)
47 |
48 | override fun getExplainScope() = explainReasonScope
49 |
50 | override fun getForwardScope() = forwardToSettingsScope
51 |
52 | override fun finish() {
53 | // If there's next task, then run it.
54 | next?.request() ?: run {
55 | // If there's no next task, finish the request process and notify the result
56 | val deniedList: MutableList = ArrayList()
57 | deniedList.addAll(pb.deniedPermissions)
58 | deniedList.addAll(pb.permanentDeniedPermissions)
59 | deniedList.addAll(pb.permissionsWontRequest)
60 | if (pb.shouldRequestBackgroundLocationPermission()) {
61 | if (PermissionX.isGranted(pb.activity, RequestBackgroundLocationPermission.ACCESS_BACKGROUND_LOCATION)) {
62 | pb.grantedPermissions.add(RequestBackgroundLocationPermission.ACCESS_BACKGROUND_LOCATION)
63 | } else {
64 | deniedList.add(RequestBackgroundLocationPermission.ACCESS_BACKGROUND_LOCATION)
65 | }
66 | }
67 | if (pb.shouldRequestSystemAlertWindowPermission()
68 | && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && pb.targetSdkVersion >= Build.VERSION_CODES.M) {
69 | if (Settings.canDrawOverlays(pb.activity)) {
70 | pb.grantedPermissions.add(Manifest.permission.SYSTEM_ALERT_WINDOW)
71 | } else {
72 | deniedList.add(Manifest.permission.SYSTEM_ALERT_WINDOW)
73 | }
74 | }
75 | if (pb.shouldRequestWriteSettingsPermission()
76 | && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && pb.targetSdkVersion >= Build.VERSION_CODES.M) {
77 | if (Settings.System.canWrite(pb.activity)) {
78 | pb.grantedPermissions.add(Manifest.permission.WRITE_SETTINGS)
79 | } else {
80 | deniedList.add(Manifest.permission.WRITE_SETTINGS)
81 | }
82 | }
83 | if (pb.shouldRequestManageExternalStoragePermission()) {
84 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
85 | Environment.isExternalStorageManager()) {
86 | pb.grantedPermissions.add(RequestManageExternalStoragePermission.MANAGE_EXTERNAL_STORAGE)
87 | } else {
88 | deniedList.add(RequestManageExternalStoragePermission.MANAGE_EXTERNAL_STORAGE)
89 | }
90 | }
91 | if (pb.shouldRequestInstallPackagesPermission()) {
92 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && pb.targetSdkVersion >= Build.VERSION_CODES.O) {
93 | if (pb.activity.packageManager.canRequestPackageInstalls()) {
94 | pb.grantedPermissions.add(RequestInstallPackagesPermission.REQUEST_INSTALL_PACKAGES)
95 | } else {
96 | deniedList.add(RequestInstallPackagesPermission.REQUEST_INSTALL_PACKAGES)
97 | }
98 | } else {
99 | deniedList.add(RequestInstallPackagesPermission.REQUEST_INSTALL_PACKAGES)
100 | }
101 | }
102 | if (pb.shouldRequestNotificationPermission()) {
103 | if (PermissionX.areNotificationsEnabled(pb.activity)) {
104 | pb.grantedPermissions.add(PermissionX.permission.POST_NOTIFICATIONS)
105 | } else {
106 | deniedList.add(PermissionX.permission.POST_NOTIFICATIONS)
107 | }
108 | }
109 | if (pb.shouldRequestBodySensorsBackgroundPermission()) {
110 | if (PermissionX.isGranted(pb.activity, RequestBodySensorsBackgroundPermission.BODY_SENSORS_BACKGROUND)) {
111 | pb.grantedPermissions.add(RequestBodySensorsBackgroundPermission.BODY_SENSORS_BACKGROUND)
112 | } else {
113 | deniedList.add(RequestBodySensorsBackgroundPermission.BODY_SENSORS_BACKGROUND)
114 | }
115 | }
116 | if (pb.requestCallback != null) {
117 | pb.requestCallback!!.onResult(deniedList.isEmpty(), ArrayList(pb.grantedPermissions), deniedList)
118 | }
119 |
120 | pb.endRequest()
121 | }
122 | }
123 |
124 | init {
125 | explainReasonScope = ExplainScope(pb, this)
126 | forwardToSettingsScope = ForwardScope(pb, this)
127 | }
128 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PermissionX
2 |
3 | [中文文档](https://blog.csdn.net/sinyu890807/category_10108528.html)
4 |
5 | PermissionX is an extension Android library that makes Android runtime permission request extremely easy. You can use it for basic permission request occasions or handle more complex conditions, like showing rationale dialog or go to app settings for allowance manually.
6 |
7 | ## Quick Setup
8 |
9 | Edit your build.gradle file and add below dependency.
10 |
11 | ```groovy
12 | repositories {
13 | google()
14 | mavenCentral()
15 | }
16 |
17 | dependencies {
18 | implementation 'com.guolindev.permissionx:permissionx:1.8.1'
19 | }
20 | ```
21 |
22 | That's all. Now you are ready to go.
23 |
24 | ## Basic Usage
25 |
26 | Use PermissionX to request Android runtime permissions is extremely simple.
27 |
28 | For example. If you want to request READ_CONTACTS, CAMERA and CALL_PHONE permissions, declared them in the AndroidManifest.xml first.
29 |
30 | ```xml
31 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | ```
40 |
41 | Then you can use below codes to request.
42 |
43 | ```kotlin
44 | PermissionX.init(activity)
45 | .permissions(Manifest.permission.READ_CONTACTS, Manifest.permission.CAMERA, Manifest.permission.CALL_PHONE)
46 | .request { allGranted, grantedList, deniedList ->
47 | if (allGranted) {
48 | Toast.makeText(this, "All permissions are granted", Toast.LENGTH_LONG).show()
49 | } else {
50 | Toast.makeText(this, "These permissions are denied: $deniedList", Toast.LENGTH_LONG).show()
51 | }
52 | }
53 | ```
54 |
55 | Pass any instance of FragmentActivity or Fragment into **init** method, and specify the permissions that you want to request in the **permissions** method, then call **request** method for actual request.
56 |
57 | The request result will be callback in the request lambda. **allGranted** means if all permissions that you requested are granted by user, maybe true or false. **grantedList** holds all granted permissions and **deniedList** holds all denied permissions.
58 |
59 |
60 |
61 | Now you can write your own logic in the request lambda to handle the specific cases of your app.
62 |
63 | ## More Usage
64 |
65 | As you know, Android provide **shouldShowRequestPermissionRationale** method to indicate us if we should show a rationale dialog to explain to user why we need this permission. Otherwise user may deny the permissions we requested and checked **never ask again** option.
66 |
67 | To simplify this process, PermissionX provide **onExplainRequestReason** method. Chain this method before **request** method, If user deny one of the permissions, **onExplainRequestReason** method will get callback first. Then you can call **showRequestReasonDialog** method to explain to user why these permissions are necessary like below.
68 |
69 | ```kotlin
70 | PermissionX.init(activity)
71 | .permissions(Manifest.permission.READ_CONTACTS, Manifest.permission.CAMERA, Manifest.permission.CALL_PHONE)
72 | .onExplainRequestReason { scope, deniedList ->
73 | scope.showRequestReasonDialog(deniedList, "Core fundamental are based on these permissions", "OK", "Cancel")
74 | }
75 | .request { allGranted, grantedList, deniedList ->
76 | if (allGranted) {
77 | Toast.makeText(this, "All permissions are granted", Toast.LENGTH_LONG).show()
78 | } else {
79 | Toast.makeText(this, "These permissions are denied: $deniedList", Toast.LENGTH_LONG).show()
80 | }
81 | }
82 | ```
83 |
84 | **showRequestReasonDialog** method will prompt a rationale dialog with the information that second parameter provide. If user click positive button which shows text as third parameter provide, PermissionX will request again with the permissions that first parameter provide.
85 |
86 | The fourth parameter as text for negative button is optional. If the denied permissions are necessary, you can ignore the fourth parameter and the dialog will be uncancelable. Which means user must allow these permissions for further usage.
87 |
88 |
89 |
90 | Of course, user still may deny some permissions and checked **never ask again** option. In this case, each time we request these permissions again will be denied automatically. The only thing we could do is prompt to users they need to allow these permissions manually in app settings for continuation usage. But PermissionX did better.
91 |
92 | PermissionX provide **onForwardToSettings** method for handling this occasion. Chain this method before **request** method, If some permissions are "denied and never ask again" by user, **onForwardToSettings** method will get callback. Then you can call **showForwardToSettingsDialog** method like below.
93 |
94 | ```kotlin
95 | PermissionX.init(activity)
96 | .permissions(Manifest.permission.READ_CONTACTS, Manifest.permission.CAMERA, Manifest.permission.CALL_PHONE)
97 | .onExplainRequestReason { scope, deniedList ->
98 | scope.showRequestReasonDialog(deniedList, "Core fundamental are based on these permissions", "OK", "Cancel")
99 | }
100 | .onForwardToSettings { scope, deniedList ->
101 | scope.showForwardToSettingsDialog(deniedList, "You need to allow necessary permissions in Settings manually", "OK", "Cancel")
102 | }
103 | .request { allGranted, grantedList, deniedList ->
104 | if (allGranted) {
105 | Toast.makeText(this, "All permissions are granted", Toast.LENGTH_LONG).show()
106 | } else {
107 | Toast.makeText(this, "These permissions are denied: $deniedList", Toast.LENGTH_LONG).show()
108 | }
109 | }
110 | ```
111 |
112 | The parameters in **onForwardToSettings** method are similar with **showRequestReasonDialog** method. When user click positive button, PermissionX will forward to the settings page of your app and user can turn on the necessary permissions very quickly. When user switch back to app, PermissionX will request the necessary permissions again automatically.
113 |
114 |
115 |
116 | ## Explain Before Request
117 |
118 | It is always a good manner to show the rationale dialog and explain to users why you need these permissions before you actually request them.
119 |
120 | To do that with PermissionX is quite simple. Just use **explainReasonBeforeRequest** method like below.
121 |
122 | ```kotlin
123 | PermissionX.init(activity)
124 | .permissions(Manifest.permission.READ_CONTACTS, Manifest.permission.CAMERA, Manifest.permission.CALL_PHONE)
125 | .explainReasonBeforeRequest()
126 | ...
127 | ```
128 |
129 | Now everything works like charm.
130 |
131 |
132 |
133 | ## Dark Theme
134 |
135 | The rationale dialog provided by PermissionsX support Android dark theme automatically. If you change your device into dark theme, everything just works great.
136 |
137 |
138 |
139 | ## License
140 |
141 | ```
142 | Copyright (C) guolin, PermissionX Open Source Project
143 |
144 | Licensed under the Apache License, Version 2.0 (the "License");
145 | you may not use this file except in compliance with the License.
146 | You may obtain a copy of the License at
147 |
148 | http://www.apache.org/licenses/LICENSE-2.0
149 |
150 | Unless required by applicable law or agreed to in writing, software
151 | distributed under the License is distributed on an "AS IS" BASIS,
152 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
153 | See the License for the specific language governing permissions and
154 | limitations under the License.
155 | ```
156 |
--------------------------------------------------------------------------------
/permissionx/src/main/java/com/permissionx/guolindev/dialog/DefaultDialog.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) guolin, PermissionX Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.permissionx.guolindev.dialog
18 |
19 | import android.Manifest
20 | import android.content.Context
21 | import android.content.pm.PackageManager
22 | import android.content.res.Configuration
23 | import android.graphics.PorterDuff
24 | import android.os.Build
25 | import android.os.Bundle
26 | import android.view.Gravity
27 | import android.view.View
28 | import com.permissionx.guolindev.R
29 | import com.permissionx.guolindev.databinding.PermissionxDefaultDialogLayoutBinding
30 | import com.permissionx.guolindev.databinding.PermissionxPermissionItemBinding
31 |
32 | /**
33 | * Default rationale dialog to show if developers did not implement their own custom rationale dialog.
34 | *
35 | * @author guolin
36 | * @since 2020/8/27
37 | */
38 | class DefaultDialog(context: Context,
39 | private val permissions: List,
40 | private val message: String,
41 | private val positiveText: String,
42 | private val negativeText: String?,
43 | private val lightColor: Int,
44 | private val darkColor: Int
45 | ) : RationaleDialog(context, R.style.PermissionXDefaultDialog) {
46 |
47 | private lateinit var binding: PermissionxDefaultDialogLayoutBinding
48 |
49 | override fun onCreate(savedInstanceState: Bundle?) {
50 | super.onCreate(savedInstanceState)
51 | binding = PermissionxDefaultDialogLayoutBinding.inflate(layoutInflater)
52 | setContentView(binding.root)
53 | setupText()
54 | buildPermissionsLayout()
55 | setupWindow()
56 | }
57 |
58 | /**
59 | * Provide the positive button instance to continue requesting.
60 | * @return Positive button instance to continue requesting.
61 | */
62 | override fun getPositiveButton(): View {
63 | return binding.positiveBtn
64 | }
65 |
66 | /**
67 | * Provide the negative button instance to abort requesting.
68 | * This is alternative. If negativeText is null we just return null, means all these permissions are necessary.
69 | * @return Negative button instance to abort requesting. Or null if all these permissions are necessary.
70 | */
71 | override fun getNegativeButton(): View? {
72 | return negativeText?.let {
73 | return binding.negativeBtn
74 | }
75 | }
76 |
77 | /**
78 | * Provide the permissions to request again.
79 | * @return Permissions to request again.
80 | */
81 | override fun getPermissionsToRequest(): List {
82 | return permissions
83 | }
84 |
85 | /**
86 | * Check if the permission layout if empty.
87 | * It is possible if all the permissions passed in are invalid permission such as a string named
88 | * "hello world". We won't add these into permission layout.
89 | */
90 | internal fun isPermissionLayoutEmpty(): Boolean {
91 | return binding.permissionsLayout.childCount == 0
92 | }
93 |
94 | /**
95 | * Setup text and text color on the dialog.
96 | */
97 | private fun setupText() {
98 | binding.messageText.text = message
99 | binding.positiveBtn.text = positiveText
100 | if (negativeText != null) {
101 | binding.negativeLayout.visibility = View.VISIBLE
102 | binding.negativeBtn.text = negativeText
103 | } else {
104 | binding.negativeLayout.visibility = View.GONE
105 | }
106 | if (isDarkTheme()) {
107 | if (darkColor != -1) {
108 | binding.positiveBtn.setTextColor(darkColor)
109 | binding.negativeBtn.setTextColor(darkColor)
110 | }
111 | } else {
112 | if (lightColor != -1) {
113 | binding.positiveBtn.setTextColor(lightColor)
114 | binding.negativeBtn.setTextColor(lightColor)
115 | }
116 | }
117 | }
118 |
119 | /**
120 | * Add every permission that need to explain the request reason to the dialog.
121 | * But we only need to add the permission group. So if there're two permissions belong to one group, only one item will be added to the dialog.
122 | */
123 | private fun buildPermissionsLayout() {
124 | val tempSet = HashSet()
125 | val currentVersion = Build.VERSION.SDK_INT
126 | for (permission in permissions) {
127 | val permissionGroup = when {
128 | currentVersion < Build.VERSION_CODES.Q -> {
129 | try {
130 | val permissionInfo = context.packageManager.getPermissionInfo(permission, 0)
131 | permissionInfo.group
132 | } catch (e: PackageManager.NameNotFoundException) {
133 | e.printStackTrace()
134 | null
135 | }
136 | }
137 | currentVersion == Build.VERSION_CODES.Q -> permissionMapOnQ[permission]
138 | currentVersion == Build.VERSION_CODES.R -> permissionMapOnR[permission]
139 | currentVersion == Build.VERSION_CODES.S -> permissionMapOnS[permission]
140 | currentVersion == Build.VERSION_CODES.TIRAMISU -> permissionMapOnT[permission]
141 | else -> {
142 | // If currentVersion is newer than the latest version we support, we just use
143 | // the latest version for temp. Will upgrade in the next release.
144 | permissionMapOnT[permission]
145 | }
146 | }
147 | if ((permission in allSpecialPermissions && !tempSet.contains(permission))
148 | || (permissionGroup != null && !tempSet.contains(permissionGroup))) {
149 | val itemBinding = PermissionxPermissionItemBinding.inflate(layoutInflater, binding.permissionsLayout, false)
150 | when {
151 | permission == Manifest.permission.ACCESS_BACKGROUND_LOCATION -> {
152 | itemBinding.permissionText.text = context.getString(R.string.permissionx_access_background_location)
153 | itemBinding.permissionIcon.setImageResource(context.packageManager.getPermissionGroupInfo(permissionGroup!!, 0).icon)
154 | }
155 | permission == Manifest.permission.SYSTEM_ALERT_WINDOW -> {
156 | itemBinding.permissionText.text = context.getString(R.string.permissionx_system_alert_window)
157 | itemBinding.permissionIcon.setImageResource(R.drawable.permissionx_ic_alert)
158 | }
159 | permission == Manifest.permission.WRITE_SETTINGS -> {
160 | itemBinding.permissionText.text = context.getString(R.string.permissionx_write_settings)
161 | itemBinding.permissionIcon.setImageResource(R.drawable.permissionx_ic_setting)
162 | }
163 | permission == Manifest.permission.MANAGE_EXTERNAL_STORAGE -> {
164 | itemBinding.permissionText.text = context.getString(R.string.permissionx_manage_external_storage)
165 | itemBinding.permissionIcon.setImageResource(context.packageManager.getPermissionGroupInfo(permissionGroup!!, 0).icon)
166 | }
167 | permission == Manifest.permission.REQUEST_INSTALL_PACKAGES -> {
168 | itemBinding.permissionText.text = context.getString(R.string.permissionx_request_install_packages)
169 | itemBinding.permissionIcon.setImageResource(R.drawable.permissionx_ic_install)
170 | }
171 | permission == Manifest.permission.POST_NOTIFICATIONS
172 | && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU -> {
173 | // When OS version is lower than Android 13, there isn't a notification icon or labelRes for us to get.
174 | // So we need to handle it as special permission's way.
175 | itemBinding.permissionText.text = context.getString(R.string.permissionx_post_notification)
176 | itemBinding.permissionIcon.setImageResource(R.drawable.permissionx_ic_notification)
177 | }
178 | permission == Manifest.permission.BODY_SENSORS_BACKGROUND -> {
179 | itemBinding.permissionText.text = context.getString(R.string.permissionx_body_sensor_background)
180 | itemBinding.permissionIcon.setImageResource(context.packageManager.getPermissionGroupInfo(permissionGroup!!, 0).icon)
181 | }
182 | else -> {
183 | itemBinding.permissionText.text = context.getString(context.packageManager.getPermissionGroupInfo(permissionGroup!!, 0).labelRes)
184 | itemBinding.permissionIcon.setImageResource(context.packageManager.getPermissionGroupInfo(permissionGroup, 0).icon)
185 | }
186 | }
187 | if (isDarkTheme()) {
188 | if (darkColor != -1) {
189 | itemBinding.permissionIcon.setColorFilter(darkColor, PorterDuff.Mode.SRC_ATOP)
190 | }
191 | } else {
192 | if (lightColor != -1) {
193 | itemBinding.permissionIcon.setColorFilter(lightColor, PorterDuff.Mode.SRC_ATOP)
194 | }
195 | }
196 | binding.permissionsLayout.addView(itemBinding.root)
197 | tempSet.add(permissionGroup ?: permission)
198 | }
199 | }
200 | }
201 |
202 | /**
203 | * Setup dialog window to show. Control the different window size in portrait and landscape mode.
204 | */
205 | private fun setupWindow() {
206 | val width = context.resources.displayMetrics.widthPixels
207 | val height = context.resources.displayMetrics.heightPixels
208 | if (width < height) {
209 | // now we are in portrait
210 | window?.let {
211 | val param = it.attributes
212 | it.setGravity(Gravity.CENTER)
213 | param.width = (width * 0.86).toInt()
214 | it.attributes = param
215 | }
216 | } else {
217 | // now we are in landscape
218 | window?.let {
219 | val param = it.attributes
220 | it.setGravity(Gravity.CENTER)
221 | param.width = (width * 0.6).toInt()
222 | it.attributes = param
223 | }
224 | }
225 | }
226 |
227 | /**
228 | * Currently we are in dark theme or not.
229 | */
230 | private fun isDarkTheme(): Boolean {
231 | val flag = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
232 | return flag == Configuration.UI_MODE_NIGHT_YES
233 | }
234 |
235 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/permissionx/src/main/java/com/permissionx/guolindev/request/PermissionBuilder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) guolin, PermissionX Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.permissionx.guolindev.request
17 |
18 | import android.Manifest
19 | import android.annotation.SuppressLint
20 | import android.app.Dialog
21 | import android.content.pm.ActivityInfo
22 | import android.content.res.Configuration
23 | import android.os.Build
24 | import android.view.View
25 | import androidx.fragment.app.Fragment
26 | import androidx.fragment.app.FragmentActivity
27 | import androidx.fragment.app.FragmentManager
28 | import com.permissionx.guolindev.PermissionX
29 | import com.permissionx.guolindev.callback.ExplainReasonCallback
30 | import com.permissionx.guolindev.callback.ExplainReasonCallbackWithBeforeParam
31 | import com.permissionx.guolindev.callback.ForwardToSettingsCallback
32 | import com.permissionx.guolindev.callback.RequestCallback
33 | import com.permissionx.guolindev.dialog.DefaultDialog
34 | import com.permissionx.guolindev.dialog.RationaleDialog
35 | import com.permissionx.guolindev.dialog.RationaleDialogFragment
36 | import java.util.*
37 |
38 | /**
39 | * More APIs for developers to control PermissionX functions.
40 | *
41 | * @author guolin
42 | * @since 2019/11/17
43 | */
44 | class PermissionBuilder(
45 | fragmentActivity: FragmentActivity?,
46 | fragment: Fragment?,
47 | normalPermissions: MutableSet,
48 | specialPermissions: MutableSet
49 | ) {
50 |
51 | /**
52 | * Instance of activity for everything.
53 | */
54 | lateinit var activity: FragmentActivity
55 |
56 | /**
57 | * Instance of fragment for everything as an alternative choice for activity.
58 | */
59 | private var fragment: Fragment? = null
60 |
61 | /**
62 | * The custom tint color to set on the DefaultDialog in light theme.
63 | */
64 | private var lightColor = -1
65 |
66 | /**
67 | * The custom tint color to set on the DefaultDialog in dark theme.
68 | */
69 | private var darkColor = -1
70 |
71 | /**
72 | * The origin request orientation of the current Activity. We need to restore it when
73 | * permission request finished.
74 | */
75 | private var originRequestOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
76 |
77 | /**
78 | * Get the FragmentManager if it's in Activity, or the ChildFragmentManager if it's in Fragment.
79 | * @return The FragmentManager to operate Fragment.
80 | */
81 | private val fragmentManager: FragmentManager
82 | get() {
83 | return fragment?.childFragmentManager ?: activity.supportFragmentManager
84 | }
85 |
86 | /**
87 | * Get the invisible fragment in activity for request permissions.
88 | * If there is no invisible fragment, add one into activity.
89 | * Don't worry. This is very lightweight.
90 | */
91 | private val invisibleFragment: InvisibleFragment
92 | get() {
93 | val existedFragment = fragmentManager.findFragmentByTag(FRAGMENT_TAG)
94 | return if (existedFragment != null) {
95 | existedFragment as InvisibleFragment
96 | } else {
97 | val invisibleFragment = InvisibleFragment()
98 | fragmentManager.beginTransaction()
99 | .add(invisibleFragment, FRAGMENT_TAG)
100 | .commitNowAllowingStateLoss()
101 | invisibleFragment
102 | }
103 | }
104 |
105 | /**
106 | * Instance of the current dialog that shows to user.
107 | * We need to dismiss this dialog when InvisibleFragment destroyed.
108 | */
109 | @JvmField
110 | var currentDialog: Dialog? = null
111 |
112 | /**
113 | * Normal runtime permissions that app want to request.
114 | */
115 | @JvmField
116 | var normalPermissions: MutableSet
117 |
118 | /**
119 | * Special permissions that we need to handle by special case.
120 | * Such as SYSTEM_ALERT_WINDOW, WRITE_SETTINGS and MANAGE_EXTERNAL_STORAGE.
121 | */
122 | @JvmField
123 | var specialPermissions: MutableSet
124 |
125 | /**
126 | * Indicates should PermissionX explain request reason before request.
127 | */
128 | @JvmField
129 | var explainReasonBeforeRequest = false
130 |
131 | /**
132 | * Indicates [ExplainScope.showRequestReasonDialog] or [ForwardScope.showForwardToSettingsDialog]
133 | * is called in [.onExplainRequestReason] or [.onForwardToSettings] callback.
134 | * If not called, requestCallback will be called by PermissionX automatically.
135 | */
136 | @JvmField
137 | var showDialogCalled = false
138 |
139 | /**
140 | * Some permissions shouldn't request will be stored here. And notify back to user when request finished.
141 | */
142 | @JvmField
143 | var permissionsWontRequest: MutableSet = LinkedHashSet()
144 |
145 | /**
146 | * Holds permissions that have already granted in the requested permissions.
147 | */
148 | @JvmField
149 | var grantedPermissions: MutableSet = LinkedHashSet()
150 |
151 | /**
152 | * Holds permissions that have been denied in the requested permissions.
153 | */
154 | @JvmField
155 | var deniedPermissions: MutableSet = LinkedHashSet()
156 |
157 | /**
158 | * Holds permissions that have been permanently denied in the requested permissions.
159 | * (Deny and never ask again)
160 | */
161 | @JvmField
162 | var permanentDeniedPermissions: MutableSet = LinkedHashSet()
163 |
164 | /**
165 | * When we request multiple permissions. Some are denied, some are permanently denied.
166 | * Denied permissions will be callback first.
167 | * And the permanently denied permissions will store in this tempPermanentDeniedPermissions.
168 | * They will be callback once no more denied permissions exist.
169 | */
170 | @JvmField
171 | var tempPermanentDeniedPermissions: MutableSet = LinkedHashSet()
172 |
173 | @JvmField
174 | var tempReadMediaPermissions: MutableSet = LinkedHashSet()
175 |
176 | /**
177 | * Holds permissions which should forward to Settings to allow them.
178 | * Not all permanently denied permissions should forward to Settings.
179 | * Only the ones developer think they are necessary should.
180 | */
181 | @JvmField
182 | var forwardPermissions: MutableSet = LinkedHashSet()
183 |
184 | /**
185 | * The callback for [.request] method. Can not be null.
186 | */
187 | @JvmField
188 | var requestCallback: RequestCallback? = null
189 |
190 | /**
191 | * The callback for [.onExplainRequestReason] method. Maybe null.
192 | */
193 | @JvmField
194 | var explainReasonCallback: ExplainReasonCallback? = null
195 |
196 | /**
197 | * The callback for [.onExplainRequestReason] method, but with beforeRequest param. Maybe null.
198 | */
199 | @JvmField
200 | var explainReasonCallbackWithBeforeParam: ExplainReasonCallbackWithBeforeParam? = null
201 |
202 | /**
203 | * The callback for [.onForwardToSettings] method. Maybe null.
204 | */
205 | @JvmField
206 | var forwardToSettingsCallback: ForwardToSettingsCallback? = null
207 |
208 | /**
209 | * Get the targetSdkVersion of current app.
210 | *
211 | * @return The targetSdkVersion of current app.
212 | */
213 | val targetSdkVersion: Int
214 | get() = activity.applicationInfo.targetSdkVersion
215 |
216 | /**
217 | * Called when permissions need to explain request reason.
218 | * Typically every time user denies your request would call this method.
219 | * If you chained [.explainReasonBeforeRequest], this method might run before permission request.
220 | *
221 | * @param callback Callback with permissions denied by user.
222 | * @return PermissionBuilder itself.
223 | */
224 | fun onExplainRequestReason(callback: ExplainReasonCallback?): PermissionBuilder {
225 | explainReasonCallback = callback
226 | return this
227 | }
228 |
229 | /**
230 | * Called when permissions need to explain request reason.
231 | * Typically every time user denies your request would call this method.
232 | * If you chained [.explainReasonBeforeRequest], this method might run before permission request.
233 | * beforeRequest param would tell you this method is currently before or after permission request.
234 | *
235 | * @param callback Callback with permissions denied by user.
236 | * @return PermissionBuilder itself.
237 | */
238 | fun onExplainRequestReason(callback: ExplainReasonCallbackWithBeforeParam?): PermissionBuilder {
239 | explainReasonCallbackWithBeforeParam = callback
240 | return this
241 | }
242 |
243 | /**
244 | * Called when permissions need to forward to Settings for allowing.
245 | * Typically user denies your request and checked never ask again would call this method.
246 | * Remember [.onExplainRequestReason] is always prior to this method.
247 | * If [.onExplainRequestReason] is called, this method will not be called in the same request time.
248 | *
249 | * @param callback Callback with permissions denied and checked never ask again by user.
250 | * @return PermissionBuilder itself.
251 | */
252 | fun onForwardToSettings(callback: ForwardToSettingsCallback?): PermissionBuilder {
253 | forwardToSettingsCallback = callback
254 | return this
255 | }
256 |
257 | /**
258 | * If you need to show request permission rationale, chain this method in your request syntax.
259 | * [.onExplainRequestReason] will be called before permission request.
260 | *
261 | * @return PermissionBuilder itself.
262 | */
263 | fun explainReasonBeforeRequest(): PermissionBuilder {
264 | explainReasonBeforeRequest = true
265 | return this
266 | }
267 |
268 | /**
269 | * Set the tint color to the default rationale dialog.
270 | * @param lightColor
271 | * Used in light theme. A color value in the form 0xAARRGGBB. Do not pass a resource ID.
272 | * To get a color value from a resource ID, call getColor.
273 | * @param darkColor
274 | * Used in dark theme. A color value in the form 0xAARRGGBB. Do not pass a resource ID.
275 | * To get a color value from a resource ID, call getColor.
276 | * @return PermissionBuilder itself.
277 | */
278 | fun setDialogTintColor(lightColor: Int, darkColor: Int): PermissionBuilder {
279 | this.lightColor = lightColor
280 | this.darkColor = darkColor
281 | return this
282 | }
283 |
284 | /**
285 | * Request permissions at once, and handle request result in the callback.
286 | *
287 | * @param callback Callback with 3 params. allGranted, grantedList, deniedList.
288 | */
289 | fun request(callback: RequestCallback?) {
290 | requestCallback = callback
291 | startRequest()
292 | }
293 |
294 | /**
295 | * This method is internal, and should not be called by developer.
296 | *
297 | *
298 | * Show a dialog to user and explain why these permissions are necessary.
299 | *
300 | * @param chainTask Instance of current task.
301 | * @param showReasonOrGoSettings Indicates should show explain reason or forward to Settings.
302 | * @param permissions Permissions to request again.
303 | * @param message Message that explain to user why these permissions are necessary.
304 | * @param positiveText Positive text on the positive button to request again.
305 | * @param negativeText Negative text on the negative button. Maybe null if this dialog should not be canceled.
306 | */
307 | fun showHandlePermissionDialog(
308 | chainTask: ChainTask,
309 | showReasonOrGoSettings: Boolean,
310 | permissions: List,
311 | message: String,
312 | positiveText: String,
313 | negativeText: String?
314 | ) {
315 | val defaultDialog = DefaultDialog(
316 | activity,
317 | permissions,
318 | message,
319 | positiveText,
320 | negativeText,
321 | lightColor,
322 | darkColor
323 | )
324 | showHandlePermissionDialog(chainTask, showReasonOrGoSettings, defaultDialog)
325 | }
326 |
327 | /**
328 | * This method is internal, and should not be called by developer.
329 | *
330 | *
331 | * Show a dialog to user and explain why these permissions are necessary.
332 | *
333 | * @param chainTask Instance of current task.
334 | * @param showReasonOrGoSettings Indicates should show explain reason or forward to Settings.
335 | * @param dialog Dialog to explain to user why these permissions are necessary.
336 | */
337 | fun showHandlePermissionDialog(
338 | chainTask: ChainTask,
339 | showReasonOrGoSettings: Boolean,
340 | dialog: RationaleDialog
341 | ) {
342 | showDialogCalled = true
343 | val permissions = dialog.permissionsToRequest
344 | if (permissions.isEmpty()) {
345 | chainTask.finish()
346 | return
347 | }
348 | currentDialog = dialog
349 | dialog.show()
350 | if (dialog is DefaultDialog && dialog.isPermissionLayoutEmpty()) {
351 | // No valid permission to show on the dialog.
352 | // We call dismiss instead.
353 | dialog.dismiss()
354 | chainTask.finish()
355 | }
356 | val positiveButton = dialog.positiveButton
357 | val negativeButton = dialog.negativeButton
358 | dialog.setCancelable(false)
359 | dialog.setCanceledOnTouchOutside(false)
360 | positiveButton.isClickable = true
361 | positiveButton.setOnClickListener {
362 | dialog.dismiss()
363 | if (showReasonOrGoSettings) {
364 | chainTask.requestAgain(permissions)
365 | } else {
366 | forwardToSettings(permissions)
367 | }
368 | }
369 | if (negativeButton != null) {
370 | negativeButton.isClickable = true
371 | negativeButton.setOnClickListener {
372 | dialog.dismiss()
373 | chainTask.finish()
374 | }
375 | }
376 | currentDialog?.setOnDismissListener {
377 | currentDialog = null
378 | }
379 | }
380 |
381 | /**
382 | * This method is internal, and should not be called by developer.
383 | *
384 | *
385 | * Show a DialogFragment to user and explain why these permissions are necessary.
386 | *
387 | * @param chainTask Instance of current task.
388 | * @param showReasonOrGoSettings Indicates should show explain reason or forward to Settings.
389 | * @param dialogFragment DialogFragment to explain to user why these permissions are necessary.
390 | */
391 | fun showHandlePermissionDialog(
392 | chainTask: ChainTask,
393 | showReasonOrGoSettings: Boolean,
394 | dialogFragment: RationaleDialogFragment
395 | ) {
396 | showDialogCalled = true
397 | val permissions = dialogFragment.permissionsToRequest
398 | if (permissions.isEmpty()) {
399 | chainTask.finish()
400 | return
401 | }
402 | dialogFragment.showNow(fragmentManager, "PermissionXRationaleDialogFragment")
403 | val positiveButton = dialogFragment.positiveButton
404 | val negativeButton = dialogFragment.negativeButton
405 | dialogFragment.isCancelable = false
406 | positiveButton.isClickable = true
407 | positiveButton.setOnClickListener {
408 | dialogFragment.dismiss()
409 | if (showReasonOrGoSettings) {
410 | chainTask.requestAgain(permissions)
411 | } else {
412 | forwardToSettings(permissions)
413 | }
414 | }
415 | if (negativeButton != null) {
416 | negativeButton.isClickable = true
417 | negativeButton.setOnClickListener(View.OnClickListener {
418 | dialogFragment.dismiss()
419 | chainTask.finish()
420 | })
421 | }
422 | }
423 |
424 | /**
425 | * Request permissions at once in the fragment.
426 | *
427 | * @param permissions Permissions that you want to request.
428 | * @param chainTask Instance of current task.
429 | */
430 | fun requestNow(permissions: Set, chainTask: ChainTask) {
431 | invisibleFragment.requestNow(this, permissions, chainTask)
432 | }
433 |
434 | /**
435 | * Request ACCESS_BACKGROUND_LOCATION permission at once in the fragment.
436 | *
437 | * @param chainTask Instance of current task.
438 | */
439 | fun requestAccessBackgroundLocationPermissionNow(chainTask: ChainTask) {
440 | invisibleFragment.requestAccessBackgroundLocationPermissionNow(this, chainTask)
441 | }
442 |
443 | /**
444 | * Request SYSTEM_ALERT_WINDOW permission at once in the fragment.
445 | *
446 | * @param chainTask Instance of current task.
447 | */
448 | fun requestSystemAlertWindowPermissionNow(chainTask: ChainTask) {
449 | invisibleFragment.requestSystemAlertWindowPermissionNow(this, chainTask)
450 | }
451 |
452 | /**
453 | * Request WRITE_SETTINGS permission at once in the fragment.
454 | *
455 | * @param chainTask Instance of current task.
456 | */
457 | fun requestWriteSettingsPermissionNow(chainTask: ChainTask) {
458 | invisibleFragment.requestWriteSettingsPermissionNow(this, chainTask)
459 | }
460 |
461 | /**
462 | * Request MANAGE_EXTERNAL_STORAGE permission at once in the fragment.
463 | *
464 | * @param chainTask Instance of current task.
465 | */
466 | fun requestManageExternalStoragePermissionNow(chainTask: ChainTask) {
467 | invisibleFragment.requestManageExternalStoragePermissionNow(this, chainTask)
468 | }
469 |
470 | /**
471 | * Request REQUEST_INSTALL_PACKAGES permission at once in the fragment.
472 | *
473 | * @param chainTask Instance of current task.
474 | */
475 | fun requestInstallPackagePermissionNow(chainTask: ChainTask) {
476 | invisibleFragment.requestInstallPackagesPermissionNow(this, chainTask)
477 | }
478 |
479 | /**
480 | * Request notification permission at once in the fragment.
481 | *
482 | * @param chainTask Instance of current task.
483 | */
484 | fun requestNotificationPermissionNow(chainTask: ChainTask) {
485 | invisibleFragment.requestNotificationPermissionNow(this, chainTask)
486 | }
487 |
488 | /**
489 | * Request BODY_SENSORS_BACKGROUND permission at once in the fragment.
490 | *
491 | * @param chainTask Instance of current task.
492 | */
493 | fun requestBodySensorsBackgroundPermissionNow(chainTask: ChainTask) {
494 | invisibleFragment.requestBodySensorsBackgroundPermissionNow(this, chainTask)
495 | }
496 |
497 | /**
498 | * Should we request ACCESS_BACKGROUND_LOCATION permission or not.
499 | *
500 | * @return True if specialPermissions contains ACCESS_BACKGROUND_LOCATION permission, false otherwise.
501 | */
502 | fun shouldRequestBackgroundLocationPermission(): Boolean {
503 | return specialPermissions.contains(RequestBackgroundLocationPermission.ACCESS_BACKGROUND_LOCATION)
504 | }
505 |
506 | /**
507 | * Should we request SYSTEM_ALERT_WINDOW permission or not.
508 | *
509 | * @return True if specialPermissions contains SYSTEM_ALERT_WINDOW permission, false otherwise.
510 | */
511 | fun shouldRequestSystemAlertWindowPermission(): Boolean {
512 | return specialPermissions.contains(Manifest.permission.SYSTEM_ALERT_WINDOW)
513 | }
514 |
515 | /**
516 | * Should we request WRITE_SETTINGS permission or not.
517 | *
518 | * @return True if specialPermissions contains WRITE_SETTINGS permission, false otherwise.
519 | */
520 | fun shouldRequestWriteSettingsPermission(): Boolean {
521 | return specialPermissions.contains(Manifest.permission.WRITE_SETTINGS)
522 | }
523 |
524 | /**
525 | * Should we request MANAGE_EXTERNAL_STORAGE permission or not.
526 | *
527 | * @return True if specialPermissions contains MANAGE_EXTERNAL_STORAGE permission, false otherwise.
528 | */
529 | fun shouldRequestManageExternalStoragePermission(): Boolean {
530 | return specialPermissions.contains(RequestManageExternalStoragePermission.MANAGE_EXTERNAL_STORAGE)
531 | }
532 |
533 | /**
534 | * Should we request REQUEST_INSTALL_PACKAGES permission or not.
535 | *
536 | * @return True if specialPermissions contains REQUEST_INSTALL_PACKAGES permission, false otherwise.
537 | */
538 | fun shouldRequestInstallPackagesPermission(): Boolean {
539 | return specialPermissions.contains(RequestInstallPackagesPermission.REQUEST_INSTALL_PACKAGES)
540 | }
541 |
542 | /**
543 | * Should we request the specific special permission or not.
544 | *
545 | * @return True if specialPermissions contains POST_NOTIFICATIONS permission, false otherwise.
546 | */
547 | fun shouldRequestNotificationPermission(): Boolean {
548 | return specialPermissions.contains(PermissionX.permission.POST_NOTIFICATIONS)
549 | }
550 |
551 | /**
552 | * Should we request the specific special permission or not.
553 | *
554 | * @return True if specialPermissions contains BODY_SENSORS_BACKGROUND permission, false otherwise.
555 | */
556 | fun shouldRequestBodySensorsBackgroundPermission(): Boolean {
557 | return specialPermissions.contains(RequestBodySensorsBackgroundPermission.BODY_SENSORS_BACKGROUND)
558 | }
559 |
560 | private fun startRequest() {
561 | // Lock the orientation when requesting permissions, or callback maybe missed due to
562 | // activity destroyed.
563 | lockOrientation()
564 |
565 | // Build the request chain. RequestNormalPermissions runs first, then RequestBackgroundLocationPermission runs.
566 | val requestChain = RequestChain()
567 | requestChain.addTaskToChain(RequestNormalPermissions(this))
568 | requestChain.addTaskToChain(RequestBackgroundLocationPermission(this))
569 | requestChain.addTaskToChain(RequestSystemAlertWindowPermission(this))
570 | requestChain.addTaskToChain(RequestWriteSettingsPermission(this))
571 | requestChain.addTaskToChain(RequestManageExternalStoragePermission(this))
572 | requestChain.addTaskToChain(RequestInstallPackagesPermission(this))
573 | requestChain.addTaskToChain(RequestNotificationPermission(this))
574 | requestChain.addTaskToChain(RequestBodySensorsBackgroundPermission(this))
575 | requestChain.runTask()
576 | }
577 |
578 | /**
579 | * Remove the InvisibleFragment from current FragmentManager.
580 | */
581 | private fun removeInvisibleFragment() {
582 | val existedFragment = fragmentManager.findFragmentByTag(FRAGMENT_TAG)
583 | if (existedFragment != null) {
584 | fragmentManager.beginTransaction().remove(existedFragment).commitNowAllowingStateLoss()
585 | }
586 | }
587 |
588 | /**
589 | * Restore the screen orientation. Activity just behave as before locked.
590 | * Android O has bug that only full screen activity can request orientation,
591 | * so we need to exclude Android O.
592 | */
593 | private fun restoreOrientation() {
594 | if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O) {
595 | activity.requestedOrientation = originRequestOrientation
596 | }
597 | }
598 |
599 | /**
600 | * Lock the screen orientation. Activity couldn't rotate with sensor.
601 | * Android O has bug that only full screen activity can request orientation,
602 | * so we need to exclude Android O.
603 | */
604 | @SuppressLint("SourceLockedOrientationActivity")
605 | private fun lockOrientation() {
606 | if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O) {
607 | originRequestOrientation = activity.requestedOrientation
608 | val orientation = activity.resources.configuration.orientation
609 | if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
610 | activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
611 | } else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
612 | activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
613 | }
614 | }
615 | }
616 |
617 | /**
618 | * Go to your app's Settings page to let user turn on the necessary permissions.
619 | *
620 | * @param permissions Permissions which are necessary.
621 | */
622 | private fun forwardToSettings(permissions: List) {
623 | forwardPermissions.clear()
624 | forwardPermissions.addAll(permissions)
625 | invisibleFragment.forwardToSettings()
626 | }
627 |
628 | internal fun endRequest() {
629 | // Remove the InvisibleFragment from current Activity after request finished.
630 | removeInvisibleFragment()
631 | // Restore the orientation after request finished since it's locked before.
632 | restoreOrientation()
633 | }
634 |
635 | companion object {
636 | /**
637 | * TAG of InvisibleFragment to find and create.
638 | */
639 | private const val FRAGMENT_TAG = "InvisibleFragment"
640 | }
641 |
642 | init {
643 | if (fragmentActivity != null) {
644 | activity = fragmentActivity
645 | }
646 | // activity and fragment must not be null at same time
647 | if (fragmentActivity == null && fragment != null) {
648 | activity = fragment.requireActivity()
649 | }
650 | this.fragment = fragment
651 | this.normalPermissions = normalPermissions
652 | this.specialPermissions = specialPermissions
653 | }
654 | }
--------------------------------------------------------------------------------