Skip to content

Instantly share code, notes, and snippets.

@rickmune
Forked from ziginsider/DialPadFragment.kt
Created May 15, 2024 19:41
Show Gist options
  • Save rickmune/eadcac82a1f4e292cdcb7c111f22030d to your computer and use it in GitHub Desktop.
Save rickmune/eadcac82a1f4e292cdcb7c111f22030d to your computer and use it in GitHub Desktop.
Android: Request permission with rationale via registerForActivityResult example
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"
}
}
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