Skip to content

Commit e71edc3

Browse files
committed
one interface to handle different autofill situations;
only support saving from version P;
1 parent 770d37d commit e71edc3

11 files changed

Lines changed: 906 additions & 1245 deletions

File tree

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
33
plugins {
44
id 'com.android.application'
55
id 'kotlin-android'
6+
id 'kotlin-parcelize'
67
id 'com.google.devtools.ksp' version "$ksp_version"
78
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
89
id 'org.jetbrains.kotlin.plugin.compose' version "$kotlin_version"

app/src/main/java/com/hegocre/nextcloudpasswords/services/autofill/AutofillHelper.kt

Lines changed: 12 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -22,46 +22,11 @@ import androidx.autofill.inline.v1.InlineSuggestionUi
2222
import com.hegocre.nextcloudpasswords.R
2323
import com.hegocre.nextcloudpasswords.ui.activities.MainActivity
2424
import android.service.autofill.SaveInfo
25-
import android.os.Parcel
26-
import android.os.Parcelable
2725
import android.os.Bundle
2826
import android.util.Log
2927
import android.view.autofill.AutofillManager
30-
31-
data class SaveData(
32-
val label: String,
33-
val username: String,
34-
val password: String,
35-
val url: String
36-
) : Parcelable {
37-
constructor(parcel: Parcel) : this(
38-
parcel.readString() ?: "",
39-
parcel.readString() ?: "",
40-
parcel.readString() ?: "",
41-
parcel.readString() ?: ""
42-
)
43-
44-
override fun writeToParcel(parcel: Parcel, flags: Int) {
45-
parcel.writeString(label)
46-
parcel.writeString(username)
47-
parcel.writeString(password)
48-
parcel.writeString(url)
49-
}
50-
51-
override fun describeContents(): Int = 0
52-
53-
companion object CREATOR : Parcelable.Creator<SaveData> {
54-
override fun createFromParcel(parcel: Parcel): SaveData {
55-
return SaveData(parcel)
56-
}
57-
58-
override fun newArray(size: Int): Array<SaveData?> {
59-
return arrayOfNulls(size)
60-
}
61-
}
62-
}
63-
64-
data class PasswordAutofillData(val id: String?, val label: String, val username: String?, val password: String?)
28+
import com.hegocre.nextcloudpasswords.utils.AutofillData
29+
import com.hegocre.nextcloudpasswords.utils.PasswordAutofillData
6530

6631
@RequiresApi(Build.VERSION_CODES.O)
6732
object AutofillHelper {
@@ -91,7 +56,7 @@ object AutofillHelper {
9156
}
9257
}
9358

94-
@RequiresApi(Build.VERSION_CODES.O_MR1)
59+
@RequiresApi(Build.VERSION_CODES.P)
9560
fun buildSaveInfo(helper: AssistStructureParser): Pair<SaveInfo, Bundle?>? {
9661
val requiredIds = mutableListOf<AutofillId>()
9762
val optionalIds = mutableListOf<AutofillId>()
@@ -153,7 +118,7 @@ object AutofillHelper {
153118
needsAppLock: Boolean = false
154119
): Dataset {
155120
// build redacted dataset when app lock is needed
156-
return if (needsAppLock && password != null && password.id != null) {
121+
return if (needsAppLock && password?.id != null) {
157122
Dataset.Builder().apply {
158123
helper.usernameAutofillIds.forEach { autofillId ->
159124
addInlineAutofillValue(
@@ -173,7 +138,7 @@ object AutofillHelper {
173138
inlinePresentationSpec
174139
)
175140
}
176-
setAuthentication(buildAppLockIntent(context, password.id, helper))
141+
setAuthentication(buildIntent(context, 1002, AutofillData.FromId(id=password.id, structure=helper.structure)))
177142
}.build()
178143
} else {
179144
Dataset.Builder().apply {
@@ -208,15 +173,15 @@ object AutofillHelper {
208173
needsAppLock: Boolean = false
209174
): Dataset {
210175
// build redacted dataset when app lock is needed
211-
return if (needsAppLock && password != null && password.id != null) {
176+
return if (needsAppLock && password?.id != null) {
212177
Dataset.Builder().apply {
213178
helper.usernameAutofillIds.forEach { autofillId ->
214179
addAutofillValue(context, autofillId, password.label, null)
215180
}
216181
helper.passwordAutofillIds.forEach { autofillId ->
217182
addAutofillValue(context, autofillId, password.label, null)
218183
}
219-
setAuthentication(buildAppLockIntent(context, password.id, helper))
184+
setAuthentication(buildIntent(context, 1002, AutofillData.FromId(id=password.id, structure=helper.structure)))
220185
}.build()
221186
} else {
222187
Dataset.Builder().apply {
@@ -281,9 +246,8 @@ object AutofillHelper {
281246
) {
282247
val autofillLabel = label ?: context.getString(R.string.app_name)
283248

284-
val authIntent = Intent().apply {
249+
val authIntent = Intent(AUTOFILL_INTENT_ID).apply {
285250
setPackage(context.packageName)
286-
identifier = AUTOFILL_INTENT_ID
287251
}
288252

289253
val intentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -340,17 +304,15 @@ object AutofillHelper {
340304
}
341305
}
342306

343-
fun buildAppLockIntent(context: Context, passwordId: String, helper: AssistStructureParser): IntentSender {
344-
val authIntent = Intent(context, MainActivity::class.java).apply {
345-
putExtra(NCPAutofillService.AUTOFILL_REQUEST, true)
346-
putExtra(NCPAutofillService.PASSWORD_ID, passwordId)
347-
putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, helper.structure)
307+
fun buildIntent(context: Context, code: Int, autofillData: AutofillData): IntentSender {
308+
val appIntent = Intent(context, MainActivity::class.java).apply {
309+
putExtra(NCPAutofillService.AUTOFILL_DATA, autofillData)
348310
}
349311

350312
val intentFlags = PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
351313

352314
return PendingIntent.getActivity(
353-
context, 1001, authIntent, intentFlags // TODO: unique code?
315+
context, code, appIntent, intentFlags
354316
).intentSender
355317
}
356318

app/src/main/java/com/hegocre/nextcloudpasswords/services/autofill/NCPAutofillService.kt

Lines changed: 51 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ import android.content.IntentSender
4141
import com.hegocre.nextcloudpasswords.utils.encryptValue
4242
import com.hegocre.nextcloudpasswords.utils.sha1Hash
4343
import com.hegocre.nextcloudpasswords.api.FoldersApi
44+
import com.hegocre.nextcloudpasswords.utils.AutofillData
45+
import com.hegocre.nextcloudpasswords.utils.PasswordAutofillData
46+
import com.hegocre.nextcloudpasswords.utils.SaveData
47+
48+
data class ListDecryptionStateNonNullable<T>(
49+
val decryptedList: List<T> = emptyList(),
50+
val isLoading: Boolean = false
51+
)
4452

4553
@TargetApi(Build.VERSION_CODES.O)
4654
class NCPAutofillService : AutofillService() {
@@ -60,21 +68,24 @@ class NCPAutofillService : AutofillService() {
6068
val searchByUsername by lazy { preferencesManager.getSearchByUsername() }
6169
val strictUrlMatching by lazy { preferencesManager.getUseStrictUrlMatching() }
6270

63-
private lateinit var decryptedPasswordsState: StateFlow<List<Password>>
71+
private lateinit var decryptedPasswordsState: StateFlow<ListDecryptionStateNonNullable<Password>>
72+
73+
private val passwordsDecrypted = MutableStateFlow(false)
6474

6575
override fun onCreate() {
6676
super.onCreate()
6777
decryptedPasswordsState = combine(
6878
passwordController.getPasswords().asFlow(),
6979
apiController.csEv1Keychain.asFlow()
7080
) { passwords, keychain ->
71-
passwords.filter { !it.trashed && !it.hidden }.decryptPasswords(keychain)
81+
ListDecryptionStateNonNullable<Password>(isLoading = true)
82+
ListDecryptionStateNonNullable(passwords.filter { !it.trashed && !it.hidden }.decryptPasswords(keychain), false)
7283
}
7384
.flowOn(Dispatchers.Default)
7485
.stateIn(
7586
scope = serviceScope,
7687
started = SharingStarted.Eagerly,
77-
initialValue = emptyList()
88+
initialValue = ListDecryptionStateNonNullable(isLoading = true)
7889
)
7990
}
8091

@@ -122,24 +133,23 @@ class NCPAutofillService : AutofillService() {
122133
return null
123134
}
124135

136+
// TODO: when to sync with server?
125137
// Check Login Status
126-
try {
127-
userController.getServer()
128-
} catch (_: UserException) {
129-
Log.e(TAG, "User not logged in, cannot autofill")
130-
return null
131-
}
138+
//try {
139+
// userController.getServer()
140+
//} catch (_: UserException) {
141+
// Log.e(TAG, "User not logged in, cannot autofill")
142+
// return null
143+
//}
132144

133-
Log.d(TAG, "User is logged in")
145+
//Log.d(TAG, "User is logged in")
134146

135147
// Try to open Session
136-
if (!apiController.sessionOpen.value && !apiController.openSession(preferencesManager.getMasterPassword())) {
137-
Log.w(TAG, "Session is not open and cannot be opened")
138-
// TODO: stop if we need the decrypted keychain
139-
}
140-
Log.d(TAG, "Session is open")
148+
//if (!apiController.sessionOpen.value && !apiController.openSession(preferencesManager.getMasterPassword())) {
149+
// Log.w(TAG, "Session is not open and cannot be opened")
150+
//}
151+
//Log.d(TAG, "Session is open")
141152

142-
// TODO: when to update?
143153
//if (apiController.sessionOpen.value) {
144154
// passwordController.syncPasswords()
145155
//}
@@ -150,7 +160,9 @@ class NCPAutofillService : AutofillService() {
150160
Log.d(TAG, "Search hint determined: $searchHint")
151161

152162
// wait for passwords to be decrypted, then filter by search hint and sort them
153-
val filteredList = decryptedPasswordsState.value.filter {
163+
decryptedPasswordsState.first { !it.isLoading }
164+
165+
val filteredList = decryptedPasswordsState.value.decryptedList.filter {
154166
it.matches(searchHint, strictUrlMatching.first()) ||
155167
(searchByUsername.first() && it.username.contains(searchHint, ignoreCase = true))
156168
}.let { list ->
@@ -222,10 +234,10 @@ class NCPAutofillService : AutofillService() {
222234
builder.addDataset(
223235
AutofillHelper.buildDataset(
224236
applicationContext,
225-
PasswordAutofillData(label = "Create new password", id = null, username = null, password = null),
237+
PasswordAutofillData(label = "Create new password", id = null, username = null, password = null), // TODO: translation
226238
helper,
227239
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) inlineRequest?.inlinePresentationSpecs?.first() else null,
228-
buildSaveIntent(applicationContext, saveData, true),
240+
AutofillHelper.buildIntent(applicationContext, 1003, AutofillData.SaveAutofill(searchHint, saveData, helper.structure)),
229241
false
230242
)
231243
)
@@ -237,22 +249,22 @@ class NCPAutofillService : AutofillService() {
237249
builder.addDataset(
238250
AutofillHelper.buildDataset(
239251
applicationContext,
240-
null,
252+
PasswordAutofillData(label = ">", id = null, username = null, password = null), // TODO use icon
241253
helper,
242254
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) inlineRequest?.inlinePresentationSpecs?.first() else null,
243-
buildMainAppIntent(applicationContext, searchHint),
255+
AutofillHelper.buildIntent(applicationContext, 1004, AutofillData.ChoosePwd(searchHint, helper.structure)),
244256
false
245257
)
246258
)
247259

248260
Log.d(TAG, "Button to open app added to FillResponse")
249261

250262
// set Save Info, with an optional bundle if delaying the save
251-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
263+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
252264
AutofillHelper.buildSaveInfo(helper)?.let { pair ->
253265
builder.setSaveInfo(pair.first)
254266
pair.second?.let { bundle ->
255-
builder.setClientState(bundle)
267+
builder.setClientState(bundle)
256268
}
257269
}
258270
}
@@ -279,18 +291,22 @@ class NCPAutofillService : AutofillService() {
279291
}
280292

281293
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
282-
val job = serviceScope.launch {
283-
try {
284-
val intent: IntentSender? = withContext(Dispatchers.Default) {
285-
processSaveRequest(request)
294+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
295+
val job = serviceScope.launch {
296+
try {
297+
val intent: IntentSender? = withContext(Dispatchers.Default) {
298+
processSaveRequest(request)
299+
}
300+
if (intent != null) callback.onSuccess(intent)
301+
else callback.onFailure("Unable to complete Save Request")
302+
} catch (e: CancellationException) {
303+
throw e
304+
} catch (e: Exception) {
305+
callback.onFailure("Error handling save request: ${e.message}")
286306
}
287-
if (intent != null) callback.onSuccess(intent)
288-
else callback.onFailure("Unable to complete Save Request")
289-
} catch (e: CancellationException) {
290-
throw e
291-
} catch (e: Exception) {
292-
callback.onFailure("Error handling save request: ${e.message}")
293307
}
308+
} else {
309+
callback.onFailure("Saving not supported on android < 9.0")
294310
}
295311
}
296312

@@ -325,63 +341,12 @@ class NCPAutofillService : AutofillService() {
325341
// Determine Search Hint
326342
val searchHint = helper.webDomain ?: getAppLabel(helper.packageName)
327343

328-
return buildSaveIntent(applicationContext, prepareSaveData(searchHint, username, password, searchHint))
329-
}
330-
331-
private suspend fun prepareSaveData(label: String, username: String, password: String, url: String): SaveData {
332-
val keychain = apiController.csEv1Keychain.asFlow().first()
333-
val serverSettings = apiController.serverSettings.asFlow().first()
334-
335-
return if(keychain != null && serverSettings.encryptionCse != 0) SaveData(
336-
password = password.encryptValue(keychain.current, keychain),
337-
label = label.encryptValue(keychain.current, keychain),
338-
username = username.encryptValue(keychain.current, keychain),
339-
url = url.encryptValue(keychain.current, keychain),
340-
)
341-
else SaveData(
342-
password = password,
343-
label = label,
344-
username = username,
345-
url = url,
346-
)
347-
}
348-
349-
private fun buildMainAppIntent(context: Context, searchHint: String): IntentSender {
350-
val appIntent = Intent(context, MainActivity::class.java).apply {
351-
putExtra(AUTOFILL_REQUEST, true)
352-
putExtra(AUTOFILL_SEARCH_HINT, searchHint)
353-
}
354-
355-
val intentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
356-
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
357-
} else {
358-
PendingIntent.FLAG_CANCEL_CURRENT
359-
}
360-
361-
return PendingIntent.getActivity(
362-
context, 1001, appIntent, intentFlags
363-
).intentSender
364-
}
365-
366-
private fun buildSaveIntent(context: Context, saveData: SaveData, isAutofill: Boolean = false): IntentSender {
367-
val appIntent = Intent(context, MainActivity::class.java).apply {
368-
if (isAutofill) putExtra(AUTOFILL_REQUEST, true)
369-
putExtra(SAVE_DATA, saveData)
370-
}
371-
372-
val intentFlags = PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
373-
374-
return PendingIntent.getActivity(
375-
context, 1001, appIntent, intentFlags
376-
).intentSender
344+
return AutofillHelper.buildIntent(applicationContext, 1005, AutofillData.Save(searchHint, SaveData(searchHint, username, password, searchHint)))
377345
}
378346

379347
companion object {
380348
const val TAG = "NCPAutofillService"
381349
private const val TIMEOUT_MS = 2000L
382-
const val AUTOFILL_REQUEST = "autofill_request"
383-
const val AUTOFILL_SEARCH_HINT = "autofill_query"
384-
const val PASSWORD_ID = "password_id"
385-
const val SAVE_DATA = "save_data"
350+
const val AUTOFILL_DATA = "autofill_data"
386351
}
387352
}

0 commit comments

Comments
 (0)