-
-
Save rickmune/eadcac82a1f4e292cdcb7c111f22030d to your computer and use it in GitHub Desktop.
Android: Request permission with rationale via registerForActivityResult example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.some.site.dialpad | |
import android.content.Context | |
import android.os.Bundle | |
import android.view.KeyEvent | |
import android.view.LayoutInflater | |
import android.view.View | |
import android.view.ViewGroup | |
import androidx.activity.result.contract.ActivityResultContracts | |
import androidx.fragment.app.Fragment | |
import androidx.lifecycle.Lifecycle | |
import androidx.lifecycle.flowWithLifecycle | |
import androidx.lifecycle.lifecycleScope | |
import androidx.navigation.navGraphViewModels | |
import com.unify.circuit.log.Log | |
import com.unify.circuit.util.permission.askCallPhonePermissionWithRationale | |
import com.unify.circuit.util.permission.isCallPhonePermissionGranted | |
import com.unify.ngtc.R | |
import com.unify.ngtc.databinding.FragmentDialpadTabBinding | |
import com.unify.ngtc.dialpad.model.Command | |
import com.unify.ngtc.dialpad.model.ErrorType | |
import com.unify.ngtc.ktx.content.showToast | |
import com.unify.ngtc.main.appComponent | |
import kotlinx.coroutines.flow.collect | |
import kotlinx.coroutines.launch | |
import javax.inject.Inject | |
class DialPadFragment : Fragment(), | |
DialPadKey.OnPressedListener, | |
View.OnClickListener, View.OnLongClickListener { | |
private val binding get() = requireNotNull(_binding) | |
private var _binding: FragmentDialpadTabBinding? = null | |
private val dialPadViewModel by navGraphViewModels<DialPadViewModel>(R.id.mainActivityGraph) { dialPadVMFactory } | |
private val adapter = DialPadSearchAdapter() | |
private val callPermissionResult = | |
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> | |
if (granted) { | |
dialPadViewModel.startOutgoingCallIfPossible() | |
} | |
} | |
@Inject | |
lateinit var dialPadVMFactory: DialPadViewModel.Factory | |
override fun onAttach(context: Context) { | |
context.appComponent.inject(this) | |
super.onAttach(context) | |
} | |
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { | |
_binding = FragmentDialpadTabBinding.inflate(inflater) | |
return binding.root | |
} | |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | |
super.onViewCreated(view, savedInstanceState) | |
initObservers() | |
initViews() | |
binding.searchList.adapter = adapter | |
dialPadViewModel.onViewCreated() | |
binding.callButton.setOnClickListener { onCallButton() } | |
} | |
private fun onCallButton() { | |
dialPadViewModel.savePhoneNumber(binding.dialpadDigits.text.toString()) | |
if (isCallPhonePermissionGranted(requireContext())) { | |
dialPadViewModel.startOutgoingCallIfPossible() | |
} else { | |
askCallPhonePermissionWithRationale(this, callPermissionResult) | |
} | |
} | |
private fun initViews() { | |
configureKeypadListeners() | |
with(binding.dialpadDigits) { | |
isCursorVisible = false | |
setOnClickListener(this@DialPadFragment) | |
setOnLongClickListener(this@DialPadFragment) | |
} | |
with(binding.deleteButton) { | |
setOnClickListener(this@DialPadFragment) | |
setOnLongClickListener(this@DialPadFragment) | |
} | |
} | |
override fun onResume() { | |
super.onResume() | |
prepareNumberField() | |
} | |
private fun initObservers() { | |
val dialPadDigits = binding.dialpadDigits | |
lifecycleScope.launch { | |
dialPadViewModel.clickEvent.flowWithLifecycle(lifecycle, Lifecycle.State.CREATED).collect { | |
when (it) { | |
DialPadViewModel.ClickEvent.DeleteEvent -> keyPressed(KeyEvent.KEYCODE_DEL) | |
DialPadViewModel.ClickEvent.DialPadDigitsEvent -> if (dialPadDigits.length() != 0) { | |
dialPadDigits.isCursorVisible = true | |
} | |
is DialPadViewModel.ClickEvent.DialButtonEvent -> keyPressed(it.buttonCode) | |
} | |
} | |
} | |
lifecycleScope.launch { | |
dialPadViewModel.longClickEvent.flowWithLifecycle(lifecycle, Lifecycle.State.CREATED).collect { | |
when (it) { | |
DialPadViewModel.LongClickEvent.DeleteEvent -> dialPadDigits.text?.clear() | |
DialPadViewModel.LongClickEvent.DialPadKeyPlusEvent -> keyPressed(KeyEvent.KEYCODE_PLUS) | |
DialPadViewModel.LongClickEvent.DialPadDigitsEvent -> dialPadDigits.isCursorVisible = true | |
} | |
} | |
} | |
lifecycleScope.launch { | |
dialPadViewModel.command.flowWithLifecycle(lifecycle, Lifecycle.State.CREATED).collect { result -> | |
when (result) { | |
is Command.LoadContacts -> adapter.updateContacts(result.contacts) | |
is Command.Error -> onError(result.errorType) | |
is Command.ShowEmergencyDisclaimer -> onShowEmergencyDisclaimer() | |
is Command.StartOutgoingCall -> dialPadViewModel.openOutgoingCallScreen(requireActivity()) | |
} | |
} | |
} | |
} | |
private fun onShowEmergencyDisclaimer() { | |
// TODO("Not yet implemented") | |
} | |
private fun onError(type: ErrorType) { | |
when (type) { | |
ErrorType.BLANK_NUMBER -> requireContext().showToast(R.string.res_DialNumber) | |
ErrorType.UNABLE_MAKE_CALL -> requireContext().showToast(R.string.unable_make_call) | |
ErrorType.OTHER -> requireContext().showToast(R.string.res_UnexpectedError) | |
} | |
} | |
private fun keyPressed(keyCode: Int) { | |
val dialPadDigits = binding.dialpadDigits | |
dialPadDigits.onKeyDown(keyCode, KeyEvent(KeyEvent.ACTION_DOWN, keyCode)) | |
// If the cursor is at the end of the text we hide it. | |
val length = dialPadDigits.length() | |
if (length == dialPadDigits.selectionStart && length == dialPadDigits.selectionEnd) { | |
dialPadDigits.isCursorVisible = false | |
} | |
} | |
private fun prepareNumberField() { | |
with(binding.dialpadDigits) { | |
setText(dialPadViewModel.getSavedPhoneNumber()) | |
resizeText() | |
clearFocus() | |
setSelection(length(), length()) | |
} | |
} | |
private fun configureKeypadListeners() { | |
for (buttonId in dialPadViewModel.dialIdsToKeyCodes.keys) { | |
view?.findViewById<DialPadKey>(buttonId)?.let { | |
it.setOnClickListener(this) | |
if (it.id == R.id.dialpad_key_0) { | |
it.setOnLongClickListener(this) | |
} | |
} | |
} | |
} | |
override fun onPressed(view: View) { | |
dialPadViewModel.handleClick(view.id) | |
} | |
override fun onClick(view: View) { | |
dialPadViewModel.handleClick(view.id) | |
} | |
override fun onLongClick(view: View): Boolean { | |
return dialPadViewModel.handleLongClick(view.id) | |
} | |
override fun onStop() { | |
super.onStop() | |
dialPadViewModel.savePhoneNumber(binding.dialpadDigits.text.toString()) | |
binding.callButton.requestFocus() | |
} | |
override fun onDestroyView() { | |
Log.i(TAG, "onDestroyView") | |
super.onDestroyView() | |
_binding = null | |
} | |
private companion object { | |
private const val TAG = "DialPadFragment" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.some.site.util.permission | |
import android.Manifest.permission.CALL_PHONE | |
import android.content.Context | |
import android.content.pm.PackageManager | |
import androidx.activity.result.ActivityResultLauncher | |
import androidx.appcompat.app.AlertDialog | |
import androidx.core.app.ActivityCompat | |
import androidx.fragment.app.Fragment | |
import com.unify.circuit.log.Log | |
import com.unify.ngtc.resources.R | |
import com.unify.ngtc.res.R as MockRes | |
private const val TAG = "PermissionUtil" | |
/** | |
* Checks if [CALL_PHONE] permission granted. | |
* | |
* @return `true` if permission have been granted and `false` otherwise | |
*/ | |
fun isCallPhonePermissionGranted(context: Context) = | |
arePermissionsGranted(context, arrayOf(CALL_PHONE)) | |
/** | |
* Asks permission to make a phone call. | |
* | |
* @return `true` if user will be asked and `false` otherwise | |
*/ | |
fun askCallPhonePermissionWithRationale( | |
fragment: Fragment, | |
callPermissionResult: ActivityResultLauncher<String> | |
) = askPermissionWithRationale( | |
fragment, | |
callPermissionResult, | |
CALL_PHONE, | |
MockRes.string.call_permission_rationale | |
) | |
/** | |
* Verifies if permissions are granted and show rational description if needed. | |
*/ | |
fun askPermissionWithRationale( | |
fragment: Fragment, | |
permissionResult: ActivityResultLauncher<String>, | |
permission: String, | |
rationaleMessageId: Int, | |
allowRationaleListener: () -> Unit = { permissionResult.launch(permission) }, | |
noThanksRationaleListener: () -> Unit = { } | |
) { | |
val context = fragment.context ?: return | |
// check permission | |
if (arePermissionsGranted(context, arrayOf(permission))) { | |
return | |
} | |
// check if we should show Permissions Rationale | |
if (shouldShowRequestPermissionsRationale(fragment, arrayOf(permission))) { | |
showRationale(context, rationaleMessageId, allowRationaleListener, noThanksRationaleListener) | |
return | |
} | |
// ask for permission | |
permissionResult.launch(permission) | |
} | |
/** | |
* Checks, if all specified permissions were granted. | |
* | |
* @param context Android context | |
* @param permissions permissions to check | |
* @return `true` if every permission is already granted by the system, and `false` otherwise | |
*/ | |
private fun arePermissionsGranted( | |
context: Context, | |
permissions: Array<String> | |
): Boolean { | |
if (permissions.isEmpty()) { | |
return true | |
} | |
return permissions.none { ActivityCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED } | |
} | |
/** | |
* Checks, if there is a need to show rationale for at least one permission request. | |
* | |
* @param fragment fragment | |
* @param permissions permissions to check | |
* @return `true` if there is at least one such permission, and `false` otherwise | |
*/ | |
private fun shouldShowRequestPermissionsRationale( | |
fragment: Fragment, | |
permissions: Array<String> | |
): Boolean { | |
if (permissions.isEmpty()) { | |
return false | |
} | |
return permissions.any { fragment.shouldShowRequestPermissionRationale(it) } | |
} | |
/** | |
* Shows a rationale to user about some permission. | |
* | |
* @param context context | |
* @param messageId resource ID of the rationale message | |
* @param okListener code to invoke when user will agree with the rationale | |
* @param noThanksListener code to invoke when user will discard the rationale | |
*/ | |
private fun showRationale( | |
context: Context, | |
messageId: Int, | |
okListener: () -> Unit, | |
noThanksListener: () -> Unit | |
) { | |
Log.i(TAG, "Show Rationale dialog: ${context.getString(messageId)}") | |
AlertDialog.Builder(context) | |
.setMessage(messageId) | |
.setPositiveButton(R.string.res_Allow) { _, _ -> | |
run { | |
Log.u(TAG, "Allow") | |
okListener() | |
} | |
} | |
.setNegativeButton(R.string.res_NoThanks) { _, _ -> | |
run { | |
Log.u(TAG, "Cancel") | |
noThanksListener() | |
} | |
} | |
.show() | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment