diff --git a/README.md b/README.md index 98dc428..f8b2fef 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Within each module, you will find detailed TODO comments that guide you through Currently it contains codebases for: - [App Shortcuts](https://developer.android.com/develop/ui/views/launch/shortcuts): Shortcuts can be displayed in a supported launcher. They help users quickly start common or recommended tasks within apps. +- [Autofill](https://developers.google.com/identity/sms-retriever/request): Retrieve otp from SMS automatically using Google SMS Retriever API. - [Broadcast Receiver](https://github.com/AsemLab/Samples/tree/main/broadcast_receiver): Create a custom broadcast receiver. - [Chuncker](https://github.com/ChuckerTeam/chucker): an HTTP inspector for Android & OkHTTP. Apps using Chucker will display a notification showing a summary of ongoing HTTP activity. - [Firebase](https://firebase.google.com/): diff --git a/autofill/build.gradle.kts b/autofill/build.gradle.kts index 977e19c..e72ed7d 100644 --- a/autofill/build.gradle.kts +++ b/autofill/build.gradle.kts @@ -19,7 +19,16 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - + + // TODO 1.1 Must be signed with YOUR keystore + signingConfigs { + create("release") { + storeFile = File(project.parent?.projectDir!!.path + "/samples.jks") + storePassword = "12345678" + keyAlias = "sampleskey" + keyPassword = "12345678" + } + } buildTypes { release { @@ -28,11 +37,13 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + signingConfig = signingConfigs.getByName("release") } debug { isDebuggable = true isMinifyEnabled = false + signingConfig = signingConfigs.getByName("release") } } compileOptions { @@ -42,7 +53,7 @@ android { kotlinOptions { jvmTarget = "1.8" } - buildFeatures{ + buildFeatures { dataBinding = true } } @@ -58,4 +69,7 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + // TODO 1. Add Play Services SMS dependencies + implementation(libs.play.services.auth) + implementation(libs.play.services.auth.api.phone) } \ No newline at end of file diff --git a/autofill/src/main/AndroidManifest.xml b/autofill/src/main/AndroidManifest.xml index 2ab33a7..7b3432f 100644 --- a/autofill/src/main/AndroidManifest.xml +++ b/autofill/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + + + + + + diff --git a/autofill/src/main/java/com/asemlab/samples/autofill/AutoFillApp.kt b/autofill/src/main/java/com/asemlab/samples/autofill/AutoFillApp.kt new file mode 100644 index 0000000..fb24a9c --- /dev/null +++ b/autofill/src/main/java/com/asemlab/samples/autofill/AutoFillApp.kt @@ -0,0 +1,9 @@ +package com.asemlab.samples.autofill + +import android.app.Application +import androidx.lifecycle.MutableLiveData + +class AutoFillApp : Application() { + + val otp = MutableLiveData() +} \ No newline at end of file diff --git a/autofill/src/main/java/com/asemlab/samples/autofill/services/SMSReceiver.kt b/autofill/src/main/java/com/asemlab/samples/autofill/services/SMSReceiver.kt new file mode 100644 index 0000000..1b1d735 --- /dev/null +++ b/autofill/src/main/java/com/asemlab/samples/autofill/services/SMSReceiver.kt @@ -0,0 +1,49 @@ +package com.asemlab.samples.autofill.services + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.asemlab.samples.autofill.AutoFillApp +import com.google.android.gms.auth.api.phone.SmsRetriever +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import java.util.regex.Pattern + + +/** + * BroadcastReceiver to wait for SMS messages. This can be registered either + * in the AndroidManifest or at runtime. Should filter Intents on + * SmsRetriever.SMS_RETRIEVED_ACTION. + */ +class SMSReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + Log.d("SMS Receiver", "OnReceive") + + if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) { + val extras = intent.extras + val status = extras?.get(SmsRetriever.EXTRA_STATUS) as Status + + when (status.statusCode) { + CommonStatusCodes.SUCCESS -> { + // TODO 5. Get SMS message contents and parse the otp + val message = extras.getString(SmsRetriever.EXTRA_SMS_MESSAGE) + val pattern = (Pattern.compile("Your code is: (\\d{4}).*")) + message?.let { + val matcher = pattern.matcher(it) + if (matcher.find()) { + (context.applicationContext as AutoFillApp).otp.postValue(matcher.group(1)) + } + } + + Log.d("SMS Receiver", "message: $message") + } + + CommonStatusCodes.TIMEOUT -> { + Log.e("SMS Receiver", "Timeout: ${status.statusMessage}") + } + } + } + } +} \ No newline at end of file diff --git a/autofill/src/main/java/com/asemlab/samples/autofill/ui/MainActivity.kt b/autofill/src/main/java/com/asemlab/samples/autofill/ui/MainActivity.kt index e17a2e0..4554820 100644 --- a/autofill/src/main/java/com/asemlab/samples/autofill/ui/MainActivity.kt +++ b/autofill/src/main/java/com/asemlab/samples/autofill/ui/MainActivity.kt @@ -1,15 +1,27 @@ package com.asemlab.samples.autofill.ui import android.os.Bundle +import android.util.Log +import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.databinding.DataBindingUtil +import com.asemlab.samples.autofill.AutoFillApp import com.asemlab.samples.autofill.R import com.asemlab.samples.autofill.databinding.ActivityMainBinding +import com.asemlab.samples.autofill.utils.AppSignatureHelper +import com.google.android.gms.auth.api.phone.SmsRetriever - +/*** + * TODO This is an otp retrieval demo (get otp from SMS automatically) + * + * the SMS should be in form: + * """ Your code is: #### + * THE APP_SIGNATURE HASH """ + * + */ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding @@ -25,5 +37,36 @@ class MainActivity : AppCompatActivity() { insets } + // TODO 2. Get hash string for this app to added to otp SMS + val appHash = AppSignatureHelper(this).appSignatures[0] + Log.d("MainActivity", appHash) + + // TODO 3. Start listening for SMS + // Get an instance of SmsRetrieverClient, used to start listening for a matching SMS message. + val client = SmsRetriever.getClient(this) + + // Starts SmsRetriever, which waits for ONE matching SMS message until timeout + // (5 minutes). The matching SMS message will be sent via a Broadcast Intent with + // action SmsRetriever#SMS_RETRIEVED_ACTION. + val task = client.startSmsRetriever() + task.addOnSuccessListener { + // Successfully started retriever, expect broadcast intent + Toast.makeText(this, "Success", Toast.LENGTH_SHORT).show() + } + task.addOnFailureListener { + // Failed to start retriever, inspect Exception for more details + Toast.makeText(this, "Failed", Toast.LENGTH_SHORT).show() + } + + + // TODO 6. Insert the otp into the EditText field + (application as AutoFillApp).otp.observe(this) { + binding.smsOtp.text.apply { + clear() + append(it) + } + + } + } } \ No newline at end of file diff --git a/autofill/src/main/java/com/asemlab/samples/autofill/utils/AppSignatureHelper.java b/autofill/src/main/java/com/asemlab/samples/autofill/utils/AppSignatureHelper.java new file mode 100644 index 0000000..616874f --- /dev/null +++ b/autofill/src/main/java/com/asemlab/samples/autofill/utils/AppSignatureHelper.java @@ -0,0 +1,77 @@ +package com.asemlab.samples.autofill.utils; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.util.Base64; +import android.util.Log; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * This is a helper class to generate your message hash to be included in your SMS message. + * + * Without the correct hash, your app won't receive the message callback. This only needs to be + * generated once per app and stored. Then you can remove this helper class from your code. + */ +public class AppSignatureHelper extends ContextWrapper { + public static final String TAG = AppSignatureHelper.class.getSimpleName(); + + private static final String HASH_TYPE = "SHA-256"; + public static final int NUM_HASHED_BYTES = 9; + public static final int NUM_BASE64_CHAR = 11; + + public AppSignatureHelper(Context context) { + super(context); + } + + + public ArrayList getAppSignatures() { + ArrayList appCodes = new ArrayList<>(); + + try { + // Get all package signatures for the current package + String packageName = getPackageName(); + PackageManager packageManager = getPackageManager(); + Signature[] signatures = packageManager.getPackageInfo(packageName, + PackageManager.GET_SIGNATURES).signatures; + + // For each signature create a compatible hash + for (Signature signature : signatures) { + String hash = hash(packageName, signature.toCharsString()); + if (hash != null) { + appCodes.add(String.format("%s", hash)); + } + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Unable to find package to obtain hash.", e); + } + return appCodes; + } + + private static String hash(String packageName, String signature) { + String appInfo = packageName + " " + signature; + try { + MessageDigest messageDigest = MessageDigest.getInstance(HASH_TYPE); + messageDigest.update(appInfo.getBytes(StandardCharsets.UTF_8)); + byte[] hashSignature = messageDigest.digest(); + + // truncated into NUM_HASHED_BYTES + hashSignature = Arrays.copyOfRange(hashSignature, 0, NUM_HASHED_BYTES); + // encode into Base64 + String base64Hash = Base64.encodeToString(hashSignature, Base64.NO_PADDING | Base64.NO_WRAP); + base64Hash = base64Hash.substring(0, NUM_BASE64_CHAR); + + Log.d(TAG, String.format("pkg: %s -- hash: %s", packageName, base64Hash)); + return base64Hash; + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "hash:NoSuchAlgorithm", e); + } + return null; + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 60eb427..1470ef9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,8 @@ android-application = "8.1.0" kotlin = "2.0.10" google-shortcuts = "1.0.0" okhttpBom = "4.10.0" +playServicesAuth = "21.2.0" +playServicesAuthApiPhone = "18.1.0" robolectric = "4.11.1" stripeAndroid = "20.42.0" timber = "5.0.1" @@ -83,6 +85,8 @@ mpAndroidChart = { module = "com.github.PhilJay:MPAndroidChart", version.ref = " okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" } okhttp = { module = "com.squareup.okhttp3:okhttp" } okhttp-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" } +play-services-auth = { module = "com.google.android.gms:play-services-auth", version.ref = "playServicesAuth" } +play-services-auth-api-phone = { module = "com.google.android.gms:play-services-auth-api-phone", version.ref = "playServicesAuthApiPhone" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } robolectric-annotations = { module = "org.robolectric:annotations", version.ref = "robolectric" } stripe-android = { module = "com.stripe:stripe-android", version.ref = "stripeAndroid" }