aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/dev/msfjarvis/aps/util/services/OreoAutofillService.kt
blob: 3b4315253b7be7180d698f7eb8c7bfa5dd57ef4c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/*
 * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
 * SPDX-License-Identifier: GPL-3.0-only
 */
package dev.msfjarvis.aps.util.services

import android.content.Context
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.AutofillService
import android.service.autofill.FillCallback
import android.service.autofill.FillRequest
import android.service.autofill.FillResponse
import android.service.autofill.SaveCallback
import android.service.autofill.SaveRequest
import androidx.annotation.RequiresApi
import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e
import com.github.androidpasswordstore.autofillparser.AutofillScenario
import com.github.androidpasswordstore.autofillparser.Credentials
import com.github.androidpasswordstore.autofillparser.FillableForm
import com.github.androidpasswordstore.autofillparser.FixedSaveCallback
import com.github.androidpasswordstore.autofillparser.FormOrigin
import com.github.androidpasswordstore.autofillparser.cachePublicSuffixList
import com.github.androidpasswordstore.autofillparser.passwordValue
import com.github.androidpasswordstore.autofillparser.recoverNodes
import com.github.androidpasswordstore.autofillparser.usernameValue
import dev.msfjarvis.aps.BuildConfig
import dev.msfjarvis.aps.R
import dev.msfjarvis.aps.ui.autofill.AutofillSaveActivity
import dev.msfjarvis.aps.util.autofill.Api30AutofillResponseBuilder
import dev.msfjarvis.aps.util.autofill.AutofillResponseBuilder
import dev.msfjarvis.aps.util.settings.PreferenceKeys
import dev.msfjarvis.aps.util.extensions.getString
import dev.msfjarvis.aps.util.extensions.hasFlag
import dev.msfjarvis.aps.util.extensions.sharedPrefs

@RequiresApi(Build.VERSION_CODES.O)
class OreoAutofillService : AutofillService() {

    companion object {

        // TODO: Provide a user-configurable denylist
        private val DENYLISTED_PACKAGES = listOf(
            BuildConfig.APPLICATION_ID,
            "android",
            "com.android.settings",
            "com.android.settings.intelligence",
            "com.android.systemui",
            "com.oneplus.applocker",
            "org.sufficientlysecure.keychain",
        )

        private const val DISABLE_AUTOFILL_DURATION_MS = 1000 * 60 * 60 * 24L
    }

    override fun onCreate() {
        super.onCreate()
        cachePublicSuffixList(applicationContext)
    }

    override fun onFillRequest(
        request: FillRequest,
        cancellationSignal: CancellationSignal,
        callback: FillCallback
    ) {
        val structure = request.fillContexts.lastOrNull()?.structure ?: run {
            callback.onSuccess(null)
            return
        }
        if (structure.activityComponent.packageName in DENYLISTED_PACKAGES) {
            if (Build.VERSION.SDK_INT >= 28) {
                callback.onSuccess(FillResponse.Builder().run {
                    disableAutofill(DISABLE_AUTOFILL_DURATION_MS)
                    build()
                })
            } else {
                callback.onSuccess(null)
            }
            return
        }
        val formToFill = FillableForm.parseAssistStructure(
            this, structure,
            isManualRequest = request.flags hasFlag FillRequest.FLAG_MANUAL_REQUEST,
            getCustomSuffixes(),
        ) ?: run {
            d { "Form cannot be filled" }
            callback.onSuccess(null)
            return
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            Api30AutofillResponseBuilder(formToFill).fillCredentials(this, request.inlineSuggestionsRequest, callback)
        } else {
            AutofillResponseBuilder(formToFill).fillCredentials(this, callback)
        }
    }

    override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
        // SaveCallback's behavior and feature set differs based on both target and device SDK, so
        // we replace it with a wrapper that works the same in all situations.
        @Suppress("NAME_SHADOWING") val callback = FixedSaveCallback(this, callback)
        val structure = request.fillContexts.lastOrNull()?.structure ?: run {
            callback.onFailure(getString(R.string.oreo_autofill_save_app_not_supported))
            return
        }
        val clientState = request.clientState ?: run {
            e { "Received save request without client state" }
            callback.onFailure(getString(R.string.oreo_autofill_save_internal_error))
            return
        }
        val scenario = AutofillScenario.fromClientState(clientState)?.recoverNodes(structure)
            ?: run {
                e { "Failed to recover client state or nodes from client state" }
                callback.onFailure(getString(R.string.oreo_autofill_save_internal_error))
                return
            }
        val formOrigin = FormOrigin.fromBundle(clientState) ?: run {
            e { "Failed to recover form origin from client state" }
            callback.onFailure(getString(R.string.oreo_autofill_save_internal_error))
            return
        }

        val username = scenario.usernameValue
        val password = scenario.passwordValue ?: run {
            callback.onFailure(getString(R.string.oreo_autofill_save_passwords_dont_match))
            return
        }
        callback.onSuccess(
            AutofillSaveActivity.makeSaveIntentSender(
                this,
                credentials = Credentials(username, password, null),
                formOrigin = formOrigin
            )
        )
    }
}

fun Context.getDefaultUsername() = sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME)

fun Context.getCustomSuffixes(): Sequence<String> {
    return sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES)
        ?.splitToSequence('\n')
        ?.filter { it.isNotBlank() && it.first() != '.' && it.last() != '.' }
        ?: emptySequence()
}