Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Phone number sharing api #49

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions app/src/main/java/com/togitech/togii/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.togitech.ccp.autofill.PhoneNumberRetrievalResultSender
import com.togitech.ccp.component.TogiCountryCodePicker
import com.togitech.togii.ui.theme.TogiiTheme

class MainActivity : ComponentActivity() {

private val phoneNumberIntentSender = PhoneNumberRetrievalResultSender(this)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Expand All @@ -51,7 +55,9 @@ class MainActivity : ComponentActivity() {
) { top ->
top.calculateTopPadding()
Surface(modifier = Modifier.fillMaxSize()) {
CountryCodePick()
CountryCodePick(
phoneNumberIntentSender = phoneNumberIntentSender
)
}
}
}
Expand All @@ -60,7 +66,9 @@ class MainActivity : ComponentActivity() {
}

@Composable
fun CountryCodePick() {
fun CountryCodePick(
phoneNumberIntentSender: PhoneNumberRetrievalResultSender? = null
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
Expand All @@ -82,6 +90,7 @@ fun CountryCodePick() {
fullPhoneNumber = code + phone
isNumberValid = isValid
},
phoneNumberIntentSender = phoneNumberIntentSender,
label = { Text("Phone Number") },
)
Spacer(modifier = Modifier.height(10.dp))
Expand Down
2 changes: 2 additions & 0 deletions ccp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ android {
dependencies {
api(libs.kotlinx.immutable)
api(libs.libphonenumber)
implementation(libs.play.services.auth)
debugImplementation(libs.compose.tooling)
implementation(libs.androidx.core)
implementation(libs.androidx.lifecycle.compose)
Expand All @@ -56,6 +57,7 @@ dependencies {
implementation(libs.compose.activity)
implementation(libs.compose.material)
implementation(libs.compose.tooling.preview)
implementation(libs.androidx.appcompat)

detektPlugins("ru.kode:detekt-rules-compose:1.3.0")
detektPlugins("io.nlopez.compose.rules:detekt:0.3.0")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.togitech.ccp.autofill

import android.app.PendingIntent
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.withResumed
import com.google.android.gms.auth.api.identity.GetPhoneNumberHintIntentRequest
import com.google.android.gms.auth.api.identity.Identity
import com.google.android.gms.tasks.Task
import kotlinx.coroutines.coroutineScope

class PhoneNumberRetrievalResultSender(private val rootActivity: ComponentActivity) {
@Suppress("AvoidVarsExceptWithDelegate")
private var callback: ((String) -> Unit)? = null

private val phoneNumberHintIntentResultLauncher: ActivityResultLauncher<IntentSenderRequest> =
rootActivity.registerForActivityResult(
ActivityResultContracts.StartIntentSenderForResult(),
) {
onActivityComplete(it)
}

@Suppress("NoCallbacksInFunctions")
suspend fun triggerPhoneNumberRetrieval(phoneNumberCallback: (String) -> Unit): Task<PendingIntent> =
coroutineScope {
// A previous contract may still be pending resolution (via the onActivityComplete method).
// Wait for the Activity lifecycle to reach the RESUMED state, which guarantees that any
// previous Activity results will have been received and their callback cleared. Blocking
// here will lead to either (a) the Activity eventually reaching the RESUMED state, or
// (b) the Activity terminating, destroying it's lifecycle-linked scope and cancelling this
// Job.
rootActivity.lifecycle.withResumed { // NOTE: runs in Dispatchers.MAIN context
check(callback == null) { "Received an activity start request while another is pending" }
callback = phoneNumberCallback

val request = GetPhoneNumberHintIntentRequest.builder().build()

Identity.getSignInClient(rootActivity)
.getPhoneNumberHintIntent(request)
.addOnSuccessListener {
phoneNumberHintIntentResultLauncher.launch(
IntentSenderRequest.Builder(it.intentSender).build(),
)
}
.addOnFailureListener {
Log.e(LOG_TAG, it.message.toString())
}
}
}

@Suppress("TooGenericExceptionCaught")
private fun onActivityComplete(activityResult: ActivityResult) {
try {
val phoneNumber = Identity.getSignInClient(rootActivity)
.getPhoneNumberFromIntent(activityResult.data)
callback?.let { it(phoneNumber) }
} catch (e: Exception) {
Log.e(LOG_TAG, e.message.toString())
}
callback = null
}

companion object {
private const val LOG_TAG = "PHONE_NUMBER_RETRIEVAL_RESULT_SENDER"
}
}
15 changes: 15 additions & 0 deletions ccp/src/main/java/com/togitech/ccp/component/Autofill.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.togitech.ccp.component

import android.util.Log
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AutofillNode
Expand All @@ -12,15 +14,19 @@ import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalAutofill
import androidx.compose.ui.platform.LocalAutofillTree
import com.togitech.ccp.autofill.PhoneNumberRetrievalResultSender
import kotlinx.coroutines.launch

@ExperimentalComposeUiApi
internal fun Modifier.autofill(
autofillTypes: List<AutofillType>,
onFill: (String) -> Unit,
focusRequester: FocusRequester,
phoneNumberIntentSender: PhoneNumberRetrievalResultSender? = null,
): Modifier = this then composed {
val autofill = LocalAutofill.current
val autofillNode = AutofillNode(onFill = onFill, autofillTypes = autofillTypes)
val coroutineScope = rememberCoroutineScope()
LocalAutofillTree.current += autofillNode

LaunchedEffect(Unit) {
Expand All @@ -34,6 +40,15 @@ internal fun Modifier.autofill(
.onFocusChanged { focusState ->
autofill?.run {
if (focusState.isFocused) {
coroutineScope.launch {
try {
phoneNumberIntentSender?.triggerPhoneNumberRetrieval {
onFill(it)
}
} catch (exception: IllegalStateException) {
Log.e("AutoFill", "Unable to autofill phone number", exception)
}
}
requestAutofillForNode(autofillNode)
} else {
cancelAutofillForNode(autofillNode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
Expand All @@ -40,10 +41,11 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.togitech.ccp.R
import com.togitech.ccp.autofill.PhoneNumberRetrievalResultSender
import com.togitech.ccp.data.CountryData
import com.togitech.ccp.data.Iso31661alpha2
import com.togitech.ccp.data.PhoneCode
import com.togitech.ccp.data.utils.ValidatePhoneNumber
import com.togitech.ccp.data.utils.PhoneParsingUtils
import com.togitech.ccp.data.utils.getCountryFromPhoneCode
import com.togitech.ccp.data.utils.getUserIsoCode
import com.togitech.ccp.data.utils.numberHint
Expand Down Expand Up @@ -81,6 +83,7 @@ private const val TAG = "TogiCountryCodePicker"
* Defaults to MaterialTheme.typography.body1
* @param [keyboardOptions] An optional [KeyboardOptions] to customize keyboard options.
* @param [keyboardActions] An optional [KeyboardActions] to customize keyboard actions.
* @param [phoneNumberIntentSender] An optional [PhoneNumberRetrievalResultSender] to trigger autofill hint
*/
@OptIn(ExperimentalComposeUiApi::class)
@Suppress("LongMethod")
Expand All @@ -106,6 +109,7 @@ fun TogiCountryCodePicker(
),
keyboardOptions: KeyboardOptions? = null,
keyboardActions: KeyboardActions? = null,
phoneNumberIntentSender: PhoneNumberRetrievalResultSender? = null,
) {
val context = LocalContext.current
val focusRequester = remember { FocusRequester() }
Expand Down Expand Up @@ -141,11 +145,11 @@ fun TogiCountryCodePicker(
val phoneNumberTransformation = remember(country) {
PhoneNumberTransformation(country.countryIso, context)
}
val validatePhoneNumber = remember(context) { ValidatePhoneNumber(context) }
val phoneParsingUtils = remember(context) { PhoneParsingUtils(context) }

var isNumberValid: Boolean by rememberSaveable(country, phoneNumber) {
mutableStateOf(
validatePhoneNumber(
phoneParsingUtils.isValidPhoneNumber(
fullPhoneNumber = country.countryPhoneCode + phoneNumber.text,
),
)
Expand All @@ -161,7 +165,7 @@ fun TogiCountryCodePicker(
text = preFilteredPhoneNumber,
selection = TextRange(preFilteredPhoneNumber.length),
)
isNumberValid = validatePhoneNumber(
isNumberValid = phoneParsingUtils.isValidPhoneNumber(
fullPhoneNumber = country.countryPhoneCode + phoneNumber.text,
)
onValueChange(country.countryPhoneCode to phoneNumber.text, isNumberValid)
Expand All @@ -170,18 +174,24 @@ fun TogiCountryCodePicker(
.fillMaxWidth()
.focusable()
.autofill(
autofillTypes = listOf(AutofillType.PhoneNumberNational),
autofillTypes = listOf(AutofillType.PhoneNumber),
phoneNumberIntentSender = phoneNumberIntentSender,
onFill = { filledPhoneNumber ->
val preFilteredPhoneNumber =
phoneNumberTransformation.preFilter(filledPhoneNumber)
isNumberValid =
phoneParsingUtils.isValidPhoneNumber(fullPhoneNumber = preFilteredPhoneNumber)
val countryCode = phoneParsingUtils.getCountryCode(preFilteredPhoneNumber)
country = CountryData.isoMap.getOrDefault(countryCode, country)

val nationalPhoneNumber =
phoneParsingUtils.getNationalNumber(preFilteredPhoneNumber)

phoneNumber = TextFieldValue(
text = preFilteredPhoneNumber,
selection = TextRange(preFilteredPhoneNumber.length),
)
isNumberValid = validatePhoneNumber(
fullPhoneNumber = country.countryPhoneCode + phoneNumber.text,
text = nationalPhoneNumber,
selection = TextRange(nationalPhoneNumber.length),
)
onValueChange(country.countryPhoneCode to phoneNumber.text, isNumberValid)
onValueChange(country.countryPhoneCode to nationalPhoneNumber, isNumberValid)
keyboardController?.hide()
coroutineScope.launch {
focusRequester.safeFreeFocus()
Expand All @@ -199,20 +209,22 @@ fun TogiCountryCodePicker(
}
},
leadingIcon = {
TogiCodeDialog(
selectedCountry = country,
includeOnly = includeOnly,
onCountryChange = { countryData ->
country = countryData
isNumberValid = validatePhoneNumber(
fullPhoneNumber = country.countryPhoneCode + phoneNumber.text,
)
onValueChange(country.countryPhoneCode to phoneNumber.text, isNumberValid)
},
showCountryCode = showCountryCode,
showFlag = showCountryFlag,
textStyle = textStyle,
)
key(country) {
TogiCodeDialog(
selectedCountry = country,
includeOnly = includeOnly,
onCountryChange = { countryData ->
country = countryData
isNumberValid = phoneParsingUtils.isValidPhoneNumber(
fullPhoneNumber = country.countryPhoneCode + phoneNumber.text,
)
onValueChange(country.countryPhoneCode to phoneNumber.text, isNumberValid)
},
showCountryCode = showCountryCode,
showFlag = showCountryFlag,
textStyle = textStyle,
)
}
},
trailingIcon = {
if (clearIcon != null) {
Expand Down
36 changes: 36 additions & 0 deletions ccp/src/main/java/com/togitech/ccp/data/utils/PhoneParsingUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.togitech.ccp.data.utils

import android.content.Context
import io.michaelrocks.libphonenumber.android.NumberParseException
import io.michaelrocks.libphonenumber.android.PhoneNumberUtil

private const val MIN_PHONE_LENGTH = 6

internal class PhoneParsingUtils(private val context: Context) {
private val phoneUtil: PhoneNumberUtil by lazy { PhoneNumberUtil.createInstance(context) }

fun isValidPhoneNumber(fullPhoneNumber: String): Boolean =
if (fullPhoneNumber.length > MIN_PHONE_LENGTH) {
try {
phoneUtil.isValidNumber(phoneUtil.parse(fullPhoneNumber, null))
} catch (ex: NumberParseException) {
false
}
} else {
false
}

fun getCountryCode(fullPhoneNumber: String): String {
return try {
val parsedNumber = phoneUtil.parse(fullPhoneNumber, null)
phoneUtil.getRegionCodeForCountryCode(parsedNumber.countryCode)
} catch (ex: NumberParseException) {
""
}
}

fun getNationalNumber(fullPhoneNumber: String): String {
val parsedNumber = phoneUtil.parse(fullPhoneNumber, null)
return phoneUtil.getNationalSignificantNumber(parsedNumber)
}
}

This file was deleted.

4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[versions]
appcompat = "1.6.1"
kotlin = "1.9.10"
dokka = "1.9.0"

Expand Down Expand Up @@ -32,9 +33,11 @@ libphonenumber = "8.13.17"
junit = "4.13.2"
robolectric = "4.10.3"
paparazzi = "1.3.2-SNAPSHOT"
playServicesAuth = "20.7.0"

[libraries]
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-lifecycle-compose = {module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle"}
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
Expand All @@ -60,6 +63,7 @@ kotlinx-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immuta
junit = { module = "junit:junit", version.ref = "junit" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
roboelectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "playServicesAuth" }

[plugins]
gradleVersions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" }
Expand Down