Skip to main content
Version: v20

Create Kotlin input control

OBJECTIVES

Create a Kotlin input control.

In this tutorial, we want to create a Kotlin input control that can get a phone number from a contact.

Manifest file

First, we have to create a manifest.json file to register the input control in the app. The file contents:

Resources/Mobile/inputControls/input-control-phoneContact/manifest.json
{
"name": "phoneContact",
"type" : "text",
"target" : ["ios", "android"],
"capabilities": {
"android": [ "android.permission.READ_CONTACTS" ]
},
}

PhoneContact.kt

The PhoneContact.kt file contains the logic to fetch the number of a contact on click. The file contents:

Resources/Mobile/inputControls/input-control-phoneContact/android/inputControlPhoneContact.kt
package ___PACKAGE___

import android.net.Uri
import android.provider.ContactsContract
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import com.qmobile.qmobiledatasync.utils.BaseKotlinInputControl
import com.qmobile.qmobiledatasync.utils.KotlinInputControl
import com.qmobile.qmobileui.activity.mainactivity.ActivityResultController
import com.qmobile.qmobileui.activity.mainactivity.MainActivity
import com.qmobile.qmobileui.ui.SnackbarHelper
import com.qmobile.qmobileui.utils.PermissionChecker

@KotlinInputControl
class PhoneContact(private val view: View) : BaseKotlinInputControl {

override val autocomplete: Boolean = false

override fun getIconName(): String {
return "call.xml"
}

private lateinit var outputCallback: (outputText: String) -> Unit

private val contactPhoneNumberCallback: (contactUri: Uri?) -> Unit = { contactUri ->
contactUri?.let {
(view.context as MainActivity?)?.apply {
contentResolver.query(contactUri, null, null, null, null)?.let { cursor ->
if (cursor.moveToFirst()) {
val contactIdIndex = cursor.getColumnIndex(ContactsContract.Contacts._ID)
val hasPhoneIndex = cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER)
val contactId = cursor.getString(contactIdIndex)
val hasNumber = cursor.getString(hasPhoneIndex)
if (Integer.valueOf(hasNumber) == 1) {
contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " + contactId,
null,
null
)?.let { numbersCursor ->
while (numbersCursor.moveToNext()) {
val phoneNumberIndex =
numbersCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
val phoneNumber = numbersCursor.getString(phoneNumberIndex)
outputCallback(phoneNumber)
break
}
}
} else {
SnackbarHelper.show(this, "No phone number found in contact")
outputCallback("")
}
}
}
}
}
}

override fun process(inputValue: Any?, outputCallback: (output: Any) -> Unit) {
(view.context as PermissionChecker?)?.askPermission(
permission = android.Manifest.permission.READ_CONTACTS,
rationale = "Permission required to read contacts"
) { isGranted ->
if (isGranted) {
this.outputCallback = outputCallback
(view.context as ActivityResultController?)?.launch(
type = ActivityResultContracts.PickContact(),
input = null,
callback = contactPhoneNumberCallback
)
}
}
}
}

Note the following points:

  • The PhoneContact.kt file must be located in the inputControl folder at the following path: input-control-phoneContact/android/inputControlPhoneContact.kt
  • Our Kotlin input control must inherit from BaseKotlinInputControl interface.
  • On the first line, keep this ___PACKAGE___ line. It will be used and replaced at project generation.
  • The Kotlin input control main class is identified by the annotation @KotlinInputControl Once your app is generated, you will find your Kotlin input control in Your App / app / src / main / java / com / your_company / your_app / inputcontrol / YourCustomInputControl.kt

Auto fill

The autocomplete variable sets if the Kotlin input control requires a click to call its process() method or if it should process as soon as created.

Icon

If your Kotlin input control has an icon, override the method getIconName() as follows :


override fun getIconName(): String {
return "call.xml"
}

Resource files

You are free to add any resource file to your Kotlin input control. Everything you put in a res folder under android folder will be copied to the generated app.

For example, your Kotlin .kt input control can make use of a resource in res/drawable/your_resource.xml , therefore your will need to have a your resource.xml file in YourInputControl/android/res/drawable/your_resource.xml.

Permissions

You may need to add specific permissions to your app.

Add the following capabilities block in your manifest.json file:

{
...
"capabilities" : {
"android" : [ "android.permission.READ_CONTACTS" ]
}
}

There are registered permissions which you can simply define with the following:

{
...
"capabilities" : {
"contacts": true,
"location": true
}
}

contacts setups android.permission.READ_CONTACTS permission. location setups android.permission.ACCESS_COARSE_LOCATION and android.permission.ACCESS_FINE_LOCATION permissions.

In order to ask user permission in your custom Kotlin input control, check out this code sample where we want the READ_CONTACTS permission:

(context as PermissionChecker?)?.askPermission(
permission = android.Manifest.permission.READ_CONTACTS,
rationale = "Permission required to read contacts"
) { isGranted ->
if (isGranted) {
...
}
}

We offer access to PermissionChecker that will delegate to the Activity the permission request. The rationale string will be displayed in case of the user denies the permission request.

ActivityResultController

We offer access to ActivityResultController that will delegate any ActivityResultContracts to MainActivity. For example, to get a contact from your phone contacts, use ActivityResultContracts.PickContact().

(view.context as ActivityResultController?)?.launch(
type = ActivityResultContracts.PickContact(),
input = null,
callback = contactPhoneNumberCallback
)

ActivityResultContacts.PickContact() is of ActivityResultContract<Void?, Uri?> type. It means it takes no input, and returns the chosen contact Uri. ActivityResultContracts.TakePicture() is of ActivityResultContract<Uri, Boolean> type. It means it takes an Uri to store the image and returns if success or not.

The available contracts can be found here: https://developer.android.com/reference/androidx/activity/result/contract/ActivityResultContracts.

We support the following types:

  • ActivityResultContracts.StartActivityForResult of type ActivityResultContract<Intent, ActivityResult>
  • ActivityResultContracts.StartIntentSenderForResult of type ActivityResultContract<IntentSenderRequest, ActivityResult>
  • ActivityResultContracts.RequestMultiplePermissions of type ActivityResultContract<Array<String>, Map<String, Boolean>>
  • ActivityResultContracts.RequestPermission of type ActivityResultContract<String, Boolean>
  • ActivityResultContracts.TakePicturePreview of type ActivityResultContract<Void?, Bitmap?>
  • ActivityResultContracts.TakePicture of type ActivityResultContract<Uri, Boolean>
  • ActivityResultContracts.CaptureVideo of type ActivityResultContract<Uri, Boolean>
  • ActivityResultContracts.PickContact of type ActivityResultContract<Void?, Uri?>
  • ActivityResultContracts.GetContent of type ActivityResultContract<String, Uri?>
  • ActivityResultContracts.GetMultipleContents of type ActivityResultContract<String, List<Uri>>
  • ActivityResultContracts.OpenDocument of type ActivityResultContract<Array<String>, Uri?>
  • ActivityResultContracts.OpenMultipleDocuments of type ActivityResultContract<Array<String>, List<Uri>>
  • ActivityResultContracts.OpenDocumentTree of type ActivityResultContract<Uri?, Uri?>