Fill OTP fields with SMS codes (#900)
* Fill OTP fields with SMS codes * Allow SMS OTP fill also for web origins * Introduce free and nonFree build variants * Fix up workflow * Improve layout and feature detection * Workflow changes * Add Changelog entry * github: update release workflow for nonFree/Free split Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * Switch to lifecycleScope * github: make snapshot deploy free variant Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
parent
c702d4aa9e
commit
ca9c951a53
16 changed files with 330 additions and 25 deletions
2
.github/workflows/deploy_snapshot.yml
vendored
2
.github/workflows/deploy_snapshot.yml
vendored
|
@ -51,7 +51,7 @@ jobs:
|
||||||
run: ./gradlew dependencies
|
run: ./gradlew dependencies
|
||||||
|
|
||||||
- name: Build release app
|
- name: Build release app
|
||||||
run: ./gradlew :app:assembleRelease
|
run: ./gradlew :app:assembleFreeRelease
|
||||||
env:
|
env:
|
||||||
SNAPSHOT: "true"
|
SNAPSHOT: "true"
|
||||||
|
|
||||||
|
|
2
.github/workflows/pull_request.yml
vendored
2
.github/workflows/pull_request.yml
vendored
|
@ -7,7 +7,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
api-level: [23, 29]
|
api-level: [23, 29]
|
||||||
variant: [Debug, Release]
|
variant: [freeDebug, freeRelease, nonFreeRelease]
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Check if relevant files have changed
|
- name: Check if relevant files have changed
|
||||||
|
|
62
.github/workflows/release.yml
vendored
62
.github/workflows/release.yml
vendored
|
@ -50,20 +50,26 @@ jobs:
|
||||||
- name: Download gradle dependencies
|
- name: Download gradle dependencies
|
||||||
run: ./gradlew dependencies
|
run: ./gradlew dependencies
|
||||||
|
|
||||||
- name: Build release APK and bundle
|
- name: Build release binaries
|
||||||
run: ./gradlew :app:assembleRelease :app:bundleRelease
|
run: ./gradlew :app:assembleFreeRelease :app:assembleNonFreeRelease :app:bundleNonFreeRelease
|
||||||
|
|
||||||
- name: Upload release APK
|
- name: Upload non-free release APK
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@master
|
||||||
with:
|
with:
|
||||||
name: APS Release APK
|
name: APS Non-Free Release APK
|
||||||
path: app/build/outputs/apk/release/app-release.apk
|
path: app/build/outputs/apk/nonFree/release/app-release.apk
|
||||||
|
|
||||||
- name: Upload release Bundle
|
- name: Upload non-free release Bundle
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@master
|
||||||
with:
|
with:
|
||||||
name: APS Release Bundle
|
name: APS Non-Free Release Bundle
|
||||||
path: app/build/outputs/bundle/release/app-release.aab
|
path: app/build/outputs/bundle/nonFree/release/app-release.aab
|
||||||
|
|
||||||
|
- name: Upload free release APK
|
||||||
|
uses: actions/upload-artifact@master
|
||||||
|
with:
|
||||||
|
name: APS Free Release APK
|
||||||
|
path: app/build/outputs/apk/free/release/app-release.apk
|
||||||
|
|
||||||
- name: Clean secrets
|
- name: Clean secrets
|
||||||
if: always()
|
if: always()
|
||||||
|
@ -77,17 +83,23 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
- name: Get APK
|
- name: Get Non-Free Release APK
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: APS Release APK
|
name: APS Non-Free Release APK
|
||||||
path: artifacts
|
path: artifacts/nonFree
|
||||||
|
|
||||||
- name: Get Bundle
|
- name: Get Non-Free Bundle
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: APS Release Bundle
|
name: APS Non-Free Release Bundle
|
||||||
path: artifacts
|
path: artifacts/nonFree
|
||||||
|
|
||||||
|
- name: Get Free Release APK
|
||||||
|
uses: actions/download-artifact@v1
|
||||||
|
with:
|
||||||
|
name: APS Free Release APK
|
||||||
|
path: artifacts/free
|
||||||
|
|
||||||
- name: Get Changelog Entry
|
- name: Get Changelog Entry
|
||||||
id: changelog_reader
|
id: changelog_reader
|
||||||
|
@ -112,22 +124,32 @@ jobs:
|
||||||
id: get_version
|
id: get_version
|
||||||
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
|
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
|
||||||
|
|
||||||
- name: Upload Release Apk
|
- name: Upload Non-Free Release Apk
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
asset_path: ./artifacts/app-release.apk
|
asset_path: ./artifacts/nonFree/app-release.apk
|
||||||
asset_name: APS_${{ steps.get_version.outputs.VERSION }}.apk
|
asset_name: APS-nonFree_${{ steps.get_version.outputs.VERSION }}.apk
|
||||||
asset_content_type: application/vnd.android.package-archive
|
asset_content_type: application/vnd.android.package-archive
|
||||||
|
|
||||||
- name: Upload Release Bundle
|
- name: Upload Non-Free Release Bundle
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
asset_path: ./artifacts/app-release.aab
|
asset_path: ./artifacts/nonFree/app-release.aab
|
||||||
asset_name: APS_${{ steps.get_version.outputs.VERSION }}.aab
|
asset_name: APS-nonFree_${{ steps.get_version.outputs.VERSION }}.aab
|
||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
|
- name: Upload Free Release Apk
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./artifacts/free/app-release.apk
|
||||||
|
asset_name: APS-free_${{ steps.get_version.outputs.VERSION }}.apk
|
||||||
|
asset_content_type: application/vnd.android.package-archive
|
||||||
|
|
|
@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
- TOTP support is reintroduced by popular demand. HOTP continues to be unsupported and heavily discouraged.
|
- TOTP support is reintroduced by popular demand. HOTP continues to be unsupported and heavily discouraged.
|
||||||
- Initial support for detecting and filling OTP fields with Autofill
|
- Initial support for detecting and filling OTP fields with Autofill
|
||||||
|
- OTP codes can be automatically filled from SMS (requires Android P+ and Google Play Services)
|
||||||
- Importing TOTP secrets using QR codes
|
- Importing TOTP secrets using QR codes
|
||||||
- Navigate into newly created folders and scroll to newly created passwords
|
- Navigate into newly created folders and scroll to newly created passwords
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,15 @@ android {
|
||||||
buildTypes.release.signingConfig = signingConfigs.release
|
buildTypes.release.signingConfig = signingConfigs.release
|
||||||
buildTypes.debug.signingConfig = signingConfigs.release
|
buildTypes.debug.signingConfig = signingConfigs.release
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions "free"
|
||||||
|
productFlavors {
|
||||||
|
free {
|
||||||
|
versionNameSuffix "-free"
|
||||||
|
}
|
||||||
|
nonFree {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -117,6 +126,8 @@ dependencies {
|
||||||
debugImplementation deps.third_party.whatthestack
|
debugImplementation deps.third_party.whatthestack
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonFreeImplementation deps.non_free.google_play_auth_api_phone
|
||||||
|
|
||||||
// Testing-only dependencies
|
// Testing-only dependencies
|
||||||
androidTestImplementation deps.testing.junit
|
androidTestImplementation deps.testing.junit
|
||||||
androidTestImplementation deps.testing.kotlin_test_junit
|
androidTestImplementation deps.testing.kotlin_test_junit
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
package com.zeapo.pwdstore.autofill.oreo.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.IntentSender
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.zeapo.pwdstore.autofill.oreo.FormOrigin
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
class AutofillSmsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun shouldOfferFillFromSms(context: Context): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeFillOtpFromSmsIntentSender(context: Context): IntentSender {
|
||||||
|
throw NotImplementedError("Filling OTPs from SMS requires non-free dependencies")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -138,6 +138,11 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".autofill.oreo.ui.AutofillSaveActivity"
|
android:name=".autofill.oreo.ui.AutofillSaveActivity"
|
||||||
android:theme="@style/NoBackgroundTheme" />
|
android:theme="@style/NoBackgroundTheme" />
|
||||||
|
<activity
|
||||||
|
android:name=".autofill.oreo.ui.AutofillSmsActivity"
|
||||||
|
android:configChanges="orientation"
|
||||||
|
android:theme="@style/DialogLikeTheme"
|
||||||
|
android:windowSoftInputMode="adjustNothing" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".autofill.oreo.ui.AutofillPublisherChangedActivity"
|
android:name=".autofill.oreo.ui.AutofillPublisherChangedActivity"
|
||||||
android:configChanges="orientation|keyboardHidden"
|
android:configChanges="orientation|keyboardHidden"
|
||||||
|
|
|
@ -87,7 +87,7 @@ val AssistStructure.ViewNode.webOrigin: String?
|
||||||
"$scheme://$domain"
|
"$scheme://$domain"
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Credentials(val username: String?, val password: String, val otp: String?) {
|
data class Credentials(val username: String?, val password: String?, val otp: String?) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromStoreEntry(
|
fun fromStoreEntry(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -141,6 +141,13 @@ fun makeGenerateAndFillRemoteView(context: Context, formOrigin: FormOrigin): Rem
|
||||||
return makeRemoteView(context, title, summary, iconRes)
|
return makeRemoteView(context, title, summary, iconRes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun makeFillOtpFromSmsRemoteView(context: Context, formOrigin: FormOrigin): RemoteViews {
|
||||||
|
val title = formOrigin.getPrettyIdentifier(context, untrusted = true)
|
||||||
|
val summary = context.getString(R.string.oreo_autofill_fill_otp_from_sms)
|
||||||
|
val iconRes = R.drawable.ic_autofill_sms
|
||||||
|
return makeRemoteView(context, title, summary, iconRes)
|
||||||
|
}
|
||||||
|
|
||||||
fun makePlaceholderRemoteView(context: Context): RemoteViews {
|
fun makePlaceholderRemoteView(context: Context): RemoteViews {
|
||||||
return makeRemoteView(context, "PLACEHOLDER", "PLACEHOLDER", R.mipmap.ic_launcher)
|
return makeRemoteView(context, "PLACEHOLDER", "PLACEHOLDER", R.mipmap.ic_launcher)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import androidx.annotation.RequiresApi
|
||||||
import com.github.ajalt.timberkt.e
|
import com.github.ajalt.timberkt.e
|
||||||
|
|
||||||
enum class AutofillAction {
|
enum class AutofillAction {
|
||||||
Match, Search, Generate
|
Match, Search, Generate, FillOtpFromSms
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -112,8 +112,13 @@ sealed class AutofillScenario<out T : Any> {
|
||||||
AutofillAction.Match -> passwordFieldsToFillOnMatch + listOfNotNull(otp)
|
AutofillAction.Match -> passwordFieldsToFillOnMatch + listOfNotNull(otp)
|
||||||
AutofillAction.Search -> passwordFieldsToFillOnSearch + listOfNotNull(otp)
|
AutofillAction.Search -> passwordFieldsToFillOnSearch + listOfNotNull(otp)
|
||||||
AutofillAction.Generate -> passwordFieldsToFillOnGenerate
|
AutofillAction.Generate -> passwordFieldsToFillOnGenerate
|
||||||
|
AutofillAction.FillOtpFromSms -> listOfNotNull(otp)
|
||||||
}
|
}
|
||||||
return when {
|
return when {
|
||||||
|
action == AutofillAction.FillOtpFromSms -> {
|
||||||
|
// When filling from an SMS, we cannot get any data other than the OTP itself.
|
||||||
|
credentialFieldsToFill
|
||||||
|
}
|
||||||
credentialFieldsToFill.isNotEmpty() -> {
|
credentialFieldsToFill.isNotEmpty() -> {
|
||||||
// If the current action would fill into any password field, we also fill into the
|
// If the current action would fill into any password field, we also fill into the
|
||||||
// username field if possible.
|
// username field if possible.
|
||||||
|
|
|
@ -25,6 +25,7 @@ import com.zeapo.pwdstore.autofill.oreo.ui.AutofillDecryptActivity
|
||||||
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillFilterView
|
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillFilterView
|
||||||
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillPublisherChangedActivity
|
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillPublisherChangedActivity
|
||||||
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSaveActivity
|
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSaveActivity
|
||||||
|
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSmsActivity
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -285,6 +286,14 @@ class FillableForm private constructor(
|
||||||
return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Generate)
|
return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Generate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun makeFillOtpFromSmsDataset(context: Context): Dataset? {
|
||||||
|
if (scenario.fieldsToFillOn(AutofillAction.FillOtpFromSms).isEmpty()) return null
|
||||||
|
if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null
|
||||||
|
val remoteView = makeFillOtpFromSmsRemoteView(context, formOrigin)
|
||||||
|
val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context)
|
||||||
|
return makePlaceholderDataset(remoteView, intentSender, AutofillAction.FillOtpFromSms)
|
||||||
|
}
|
||||||
|
|
||||||
private fun makePublisherChangedDataset(
|
private fun makePublisherChangedDataset(
|
||||||
context: Context,
|
context: Context,
|
||||||
publisherChangedException: AutofillPublisherChangedException
|
publisherChangedException: AutofillPublisherChangedException
|
||||||
|
@ -341,6 +350,10 @@ class FillableForm private constructor(
|
||||||
hasDataset = true
|
hasDataset = true
|
||||||
addDataset(it)
|
addDataset(it)
|
||||||
}
|
}
|
||||||
|
makeFillOtpFromSmsDataset(context)?.let {
|
||||||
|
hasDataset = true
|
||||||
|
addDataset(it)
|
||||||
|
}
|
||||||
if (!hasDataset) return null
|
if (!hasDataset) return null
|
||||||
makeSaveInfo()?.let { setSaveInfo(it) }
|
makeSaveInfo()?.let { setSaveInfo(it) }
|
||||||
setClientState(clientState)
|
setClientState(clientState)
|
||||||
|
|
10
app/src/main/res/drawable/ic_autofill_sms.xml
Normal file
10
app/src/main/res/drawable/ic_autofill_sms.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM9,11L7,11L7,9h2v2zM13,11h-2L11,9h2v2zM17,11h-2L15,9h2v2z" />
|
||||||
|
</vector>
|
61
app/src/main/res/layout/activity_oreo_autofill_sms.xml
Normal file
61
app/src/main/res/layout/activity_oreo_autofill_sms.xml
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||||
|
~ SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="10dp"
|
||||||
|
tools:context="com.zeapo.pwdstore.autofill.oreo.ui.AutofillFilterView">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/cover"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/app_name"
|
||||||
|
android:src="@drawable/ic_launcher_foreground"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/text"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:layout_margin="10dp"
|
||||||
|
app:layout_constraintVertical_bias="0.0" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/oreo_autofill_waiting_for_sms"
|
||||||
|
android:layout_margin="10dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/cover" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
style="@style/Widget.MaterialComponents.ProgressIndicator.Circular.Indeterminate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="10dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/cancelButton"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/cancelButton"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_margin="10dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/dialog_cancel"
|
||||||
|
android:textColor="?attr/colorSecondary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/progress" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -253,6 +253,8 @@
|
||||||
<string name="oreo_autofill_save_app_not_supported">This app is currently not supported</string>
|
<string name="oreo_autofill_save_app_not_supported">This app is currently not supported</string>
|
||||||
<string name="oreo_autofill_save_passwords_dont_match">Passwords don\'t match</string>
|
<string name="oreo_autofill_save_passwords_dont_match">Passwords don\'t match</string>
|
||||||
<string name="oreo_autofill_generate_password">Generate password…</string>
|
<string name="oreo_autofill_generate_password">Generate password…</string>
|
||||||
|
<string name="oreo_autofill_fill_otp_from_sms">Extract code from SMS…</string>
|
||||||
|
<string name="oreo_autofill_waiting_for_sms">Waiting for SMS…</string>
|
||||||
<string name="oreo_autofill_max_matches_reached">Maximum number of matches (%1$d) reached; clear matches before adding new ones.</string>
|
<string name="oreo_autofill_max_matches_reached">Maximum number of matches (%1$d) reached; clear matches before adding new ones.</string>
|
||||||
<string name="oreo_autofill_warning_publisher_header">This app\'s publisher has changed since you first associated a Password Store entry with it:</string>
|
<string name="oreo_autofill_warning_publisher_header">This app\'s publisher has changed since you first associated a Password Store entry with it:</string>
|
||||||
<string name="oreo_autofill_warning_publisher_footer"><b>The currently installed app may be trying to steal your credentials by pretending to be a trusted app.</b>\n\nTry to uninstall and reinstall the app from a trusted source, such as the Play Store, Amazon Appstore, F-Droid, or your phone manufacturer\'s store.</string>
|
<string name="oreo_autofill_warning_publisher_footer"><b>The currently installed app may be trying to steal your credentials by pretending to be a trusted app.</b>\n\nTry to uninstall and reinstall the app from a trusted source, such as the Play Store, Amazon Appstore, F-Droid, or your phone manufacturer\'s store.</string>
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
package com.zeapo.pwdstore.autofill.oreo.ui
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.IntentSender
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.autofill.AutofillManager
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.github.ajalt.timberkt.e
|
||||||
|
import com.github.ajalt.timberkt.w
|
||||||
|
import com.google.android.gms.auth.api.phone.SmsCodeRetriever
|
||||||
|
import com.google.android.gms.auth.api.phone.SmsRetriever
|
||||||
|
import com.google.android.gms.common.ConnectionResult
|
||||||
|
import com.google.android.gms.common.GoogleApiAvailability
|
||||||
|
import com.google.android.gms.common.api.ResolvableApiException
|
||||||
|
import com.google.android.gms.tasks.Tasks
|
||||||
|
import com.zeapo.pwdstore.autofill.oreo.AutofillAction
|
||||||
|
import com.zeapo.pwdstore.autofill.oreo.Credentials
|
||||||
|
import com.zeapo.pwdstore.autofill.oreo.FillableForm
|
||||||
|
import com.zeapo.pwdstore.databinding.ActivityOreoAutofillSmsBinding
|
||||||
|
import com.zeapo.pwdstore.utils.viewBinding
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
class AutofillSmsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private var fillOtpFromSmsRequestCode = 1
|
||||||
|
|
||||||
|
fun shouldOfferFillFromSms(context: Context): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
|
||||||
|
return false
|
||||||
|
val googleApiAvailabilityInstance = GoogleApiAvailability.getInstance()
|
||||||
|
val googleApiStatus = googleApiAvailabilityInstance.isGooglePlayServicesAvailable(context)
|
||||||
|
if (googleApiStatus != ConnectionResult.SUCCESS) {
|
||||||
|
w { "Google Play Services unavailable or not updated: ${googleApiAvailabilityInstance.getErrorString(googleApiStatus)}" }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// https://developer.android.com/guide/topics/text/autofill-services#sms-autofill
|
||||||
|
if (googleApiAvailabilityInstance.getApkVersion(context) < 190056000) {
|
||||||
|
w { "Google Play Service 19.0.56 or higher required for SMS OTP Autofill" }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeFillOtpFromSmsIntentSender(context: Context): IntentSender {
|
||||||
|
val intent = Intent(context, AutofillSmsActivity::class.java)
|
||||||
|
return PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
fillOtpFromSmsRequestCode++,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
).intentSender
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val binding by viewBinding(ActivityOreoAutofillSmsBinding::inflate)
|
||||||
|
|
||||||
|
private lateinit var clientState: Bundle
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(binding.root)
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
binding.cancelButton.setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
clientState = intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) ?: run {
|
||||||
|
e { "AutofillSmsActivity started without EXTRA_CLIENT_STATE" }
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registerReceiver(smsCodeRetrievedReceiver, IntentFilter(SmsCodeRetriever.SMS_CODE_RETRIEVED_ACTION), SmsRetriever.SEND_PERMISSION, null)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
waitForSms()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry starting the SMS code retriever after a permission request.
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
if (resultCode != Activity.RESULT_OK)
|
||||||
|
return
|
||||||
|
lifecycleScope.launch {
|
||||||
|
waitForSms()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun waitForSms() {
|
||||||
|
val smsClient = SmsCodeRetriever.getAutofillClient(this@AutofillSmsActivity)
|
||||||
|
try {
|
||||||
|
Tasks.await(smsClient.startSmsCodeRetriever())
|
||||||
|
} catch (e: ResolvableApiException) {
|
||||||
|
e.startResolutionForResult(this, 1)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e(e)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val smsCodeRetrievedReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val smsCode = intent.getStringExtra(SmsCodeRetriever.EXTRA_SMS_CODE)
|
||||||
|
val fillInDataset =
|
||||||
|
FillableForm.makeFillInDataset(
|
||||||
|
this@AutofillSmsActivity,
|
||||||
|
Credentials(null, null, smsCode),
|
||||||
|
clientState,
|
||||||
|
AutofillAction.FillOtpFromSms
|
||||||
|
)
|
||||||
|
setResult(RESULT_OK, Intent().apply {
|
||||||
|
putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset)
|
||||||
|
})
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -64,6 +64,10 @@ ext.deps = [
|
||||||
whatthestack: 'com.github.haroldadmin:WhatTheStack:0.0.3',
|
whatthestack: 'com.github.haroldadmin:WhatTheStack:0.0.3',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
non_free: [
|
||||||
|
google_play_auth_api_phone: 'com.google.android.gms:play-services-auth-api-phone:17.4.0',
|
||||||
|
],
|
||||||
|
|
||||||
testing: [
|
testing: [
|
||||||
junit: 'junit:junit:4.13',
|
junit: 'junit:junit:4.13',
|
||||||
kotlin_test_junit: 'org.jetbrains.kotlin:kotlin-test-junit:1.3.72',
|
kotlin_test_junit: 'org.jetbrains.kotlin:kotlin-test-junit:1.3.72',
|
||||||
|
|
|
@ -5,7 +5,7 @@ mkdir -p "$SSHDIR"
|
||||||
echo "$ACTIONS_DEPLOY_KEY" > "$SSHDIR/key"
|
echo "$ACTIONS_DEPLOY_KEY" > "$SSHDIR/key"
|
||||||
chmod 600 "$SSHDIR/key"
|
chmod 600 "$SSHDIR/key"
|
||||||
export SERVER_DEPLOY_STRING="$SSH_USERNAME@$SERVER_ADDRESS:$SERVER_DESTINATION"
|
export SERVER_DEPLOY_STRING="$SSH_USERNAME@$SERVER_ADDRESS:$SERVER_DESTINATION"
|
||||||
cd "$GITHUB_WORKSPACE/app/build/outputs/apk/release"
|
cd "$GITHUB_WORKSPACE/app/build/outputs/apk/free/release"
|
||||||
rm output.json
|
rm output.json
|
||||||
rsync -ahvcr --omit-dir-times --progress --delete --no-o --no-g -e "ssh -i $SSHDIR/key -o StrictHostKeyChecking=no -p $SSH_PORT" . "$SERVER_DEPLOY_STRING" || exit 1
|
rsync -ahvcr --omit-dir-times --progress --delete --no-o --no-g -e "ssh -i $SSHDIR/key -o StrictHostKeyChecking=no -p $SSH_PORT" . "$SERVER_DEPLOY_STRING" || exit 1
|
||||||
exit 0
|
exit 0
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue