diff options
author | Douglas Sigelbaum <sigelbaum@google.com> | 2017-06-07 16:21:09 -0400 |
---|---|---|
committer | Douglas Sigelbaum <sigelbaum@google.com> | 2017-06-07 17:53:35 -0400 |
commit | d44566a42776d03195b15ba394d0d861306c0057 (patch) | |
tree | 8b24e014ce03b3e50826ef628bf283459d46c8f6 /input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice | |
parent | b7d6772dc5b2a35a1d23cd274f9e3e17b4b55b08 (diff) | |
download | android-d44566a42776d03195b15ba394d0d861306c0057.tar.gz |
Renaming sample autofill service to Multidataset
AutofillService.
Bug: 38182790
Test: manual
Change-Id: I21784ece130355f9bd1efec811bba7c0bfba5b70
Diffstat (limited to 'input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice')
12 files changed, 1047 insertions, 0 deletions
diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AuthActivity.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AuthActivity.kt new file mode 100644 index 00000000..d3e07ce2 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AuthActivity.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.autofillframework.multidatasetservice + +import android.app.Activity +import android.app.PendingIntent +import android.app.assist.AssistStructure +import android.content.Context +import android.content.Intent +import android.content.IntentSender +import android.os.Bundle +import android.service.autofill.Dataset +import android.service.autofill.FillResponse +import android.util.Log +import android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE +import android.view.autofill.AutofillManager.EXTRA_AUTHENTICATION_RESULT +import android.widget.Toast +import com.example.android.autofillframework.CommonUtil.EXTRA_DATASET_NAME +import com.example.android.autofillframework.CommonUtil.EXTRA_FOR_RESPONSE +import com.example.android.autofillframework.CommonUtil.TAG +import com.example.android.autofillframework.R +import com.example.android.autofillframework.multidatasetservice.datasource.SharedPrefsAutofillRepository +import com.example.android.autofillframework.multidatasetservice.settings.MyPreferences +import kotlinx.android.synthetic.main.multidataset_service_auth_activity.cancel +import kotlinx.android.synthetic.main.multidataset_service_auth_activity.login +import kotlinx.android.synthetic.main.multidataset_service_auth_activity.master_password + +/** + * This Activity controls the UI for logging in to the Autofill service. + * It is launched when an Autofill Response or specific Dataset within the Response requires + * authentication to access. It bundles the result in an Intent. + */ +class AuthActivity : Activity() { + + private var mReplyIntent: Intent? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.multidataset_service_auth_activity) + login.setOnClickListener { submitLogin() } + cancel.setOnClickListener { + onFailure() + this@AuthActivity.finish() + } + } + + private fun submitLogin() { + val password = master_password.text + if (password.toString() == MyPreferences.getMasterPassword(this@AuthActivity)) { + onSuccess() + } else { + Toast.makeText(this, "Password incorrect", Toast.LENGTH_SHORT).show() + onFailure() + } + finish() + } + + override fun finish() { + if (mReplyIntent != null) { + setResult(Activity.RESULT_OK, mReplyIntent) + } else { + setResult(Activity.RESULT_CANCELED) + } + super.finish() + } + + private fun onFailure() { + Log.w(TAG, "Failed auth.") + mReplyIntent = null + } + + private fun onSuccess() { + val intent = intent + val forResponse = intent.getBooleanExtra(EXTRA_FOR_RESPONSE, true) + val structure = intent.getParcelableExtra<AssistStructure>(EXTRA_ASSIST_STRUCTURE) + val parser = StructureParser(structure) + parser.parseForFill() + val autofillFields = parser.autofillFields + mReplyIntent = Intent() + val clientFormDataMap = SharedPrefsAutofillRepository + .getClientFormData(this, autofillFields.focusedAutofillHints, autofillFields.allAutofillHints) + if (forResponse) { + AutofillHelper.newResponse(this, false, autofillFields, clientFormDataMap)?.let(this::setResponseIntent) + } else { + val datasetName = intent.getStringExtra(EXTRA_DATASET_NAME) + clientFormDataMap?.let { + it[datasetName]?.let { clientFormData -> + AutofillHelper.newDataset(this, autofillFields, clientFormData, false)?.let(this::setDatasetIntent) + } + } + } + } + + private fun setResponseIntent(fillResponse: FillResponse) { + mReplyIntent?.putExtra(EXTRA_AUTHENTICATION_RESULT, fillResponse) + } + + private fun setDatasetIntent(dataset: Dataset) { + mReplyIntent?.putExtra(EXTRA_AUTHENTICATION_RESULT, dataset) + } + + companion object { + + // Unique autofillId for dataset intents. + private var datasetPendingIntentId = 0 + + internal fun getAuthIntentSenderForResponse(context: Context): IntentSender { + val intent = Intent(context, AuthActivity::class.java) + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) + .intentSender + } + + internal fun getAuthIntentSenderForDataset(context: Context, datasetName: String): IntentSender { + val intent = Intent(context, AuthActivity::class.java) + intent.putExtra(EXTRA_DATASET_NAME, datasetName) + intent.putExtra(EXTRA_FOR_RESPONSE, false) + return PendingIntent.getActivity(context, ++datasetPendingIntentId, intent, + PendingIntent.FLAG_CANCEL_CURRENT).intentSender + } + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AutofillHelper.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AutofillHelper.kt new file mode 100644 index 00000000..4d67d5bc --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AutofillHelper.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.autofillframework.multidatasetservice + +import android.content.Context +import android.service.autofill.Dataset +import android.service.autofill.FillResponse +import android.service.autofill.SaveInfo +import android.util.Log +import android.widget.RemoteViews +import com.example.android.autofillframework.CommonUtil.TAG +import com.example.android.autofillframework.R +import com.example.android.autofillframework.multidatasetservice.model.AutofillFieldsCollection +import com.example.android.autofillframework.multidatasetservice.model.ClientFormData +import java.util.HashMap + +/** + * This is a class containing helper methods for building Autofill Datasets and Responses. + */ +object AutofillHelper { + + /** + * Wraps autofill data in a [Dataset] object which can then be sent back to the + * client View. + */ + fun newDataset(context: Context, autofillFields: AutofillFieldsCollection, + clientFormData: ClientFormData, datasetAuth: Boolean): Dataset? { + clientFormData.datasetName?.let { datasetName -> + val datasetBuilder = Dataset.Builder(newRemoteViews(context.packageName, datasetName)) + val setValueAtLeastOnce = clientFormData.applyToFields(autofillFields, datasetBuilder) + if (datasetAuth) { + val sender = AuthActivity.getAuthIntentSenderForDataset(context, datasetName) + datasetBuilder.setAuthentication(sender) + } + if (setValueAtLeastOnce) { + return datasetBuilder.build() + } + } + return null + } + + fun newRemoteViews(packageName: String, remoteViewsText: String): RemoteViews { + val presentation = RemoteViews(packageName, R.layout.multidataset_service_list_item) + presentation.setTextViewText(R.id.text1, remoteViewsText) + return presentation + } + + /** + * Wraps autofill data in a [FillResponse] object (essentially a series of Datasets) which can + * then be sent back to the client View. + */ + fun newResponse(context: Context, + datasetAuth: Boolean, autofillFields: AutofillFieldsCollection, + clientFormDataMap: HashMap<String, ClientFormData>?): FillResponse? { + val responseBuilder = FillResponse.Builder() + clientFormDataMap?.keys?.let { datasetNames -> + for (datasetName in datasetNames) { + clientFormDataMap[datasetName]?.let { clientFormData -> + val dataset = newDataset(context, autofillFields, clientFormData, datasetAuth) + dataset?.let(responseBuilder::addDataset) + } + } + } + if (autofillFields.saveType != 0) { + val autofillIds = autofillFields.autofillIds + responseBuilder.setSaveInfo(SaveInfo.Builder(autofillFields.saveType, + autofillIds.toTypedArray()).build()) + return responseBuilder.build() + } else { + Log.d(TAG, "These fields are not meant to be saved by autofill.") + return null + } + } +}
\ No newline at end of file diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/MyAutofillService.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/MyAutofillService.kt new file mode 100644 index 00000000..f9169029 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/MyAutofillService.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.autofillframework.multidatasetservice + +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 android.util.Log +import com.example.android.autofillframework.CommonUtil.TAG +import com.example.android.autofillframework.CommonUtil.bundleToString +import com.example.android.autofillframework.R +import com.example.android.autofillframework.multidatasetservice.datasource.SharedPrefsAutofillRepository +import com.example.android.autofillframework.multidatasetservice.settings.MyPreferences + +class MyAutofillService : AutofillService() { + + override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal, + callback: FillCallback) { + val structure = request.getFillContexts().get(request.getFillContexts().size - 1).structure + val data = request.clientState + Log.d(TAG, "onFillRequest(): data=" + bundleToString(data)) + + // Temporary hack for disabling autofill for components in this autofill service. + // i.e. we don't want to autofill components in AuthActivity. + if (structure.activityComponent.toShortString() + .contains("com.example.android.autofillframework.service")) { + callback.onSuccess(null) + return + } + cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill not implemented in this sample.") } + // Parse AutoFill data in Activity + val parser = StructureParser(structure) + parser.parseForFill() + val autofillFields = parser.autofillFields + + val responseBuilder = FillResponse.Builder() + // Check user's settings for authenticating Responses and Datasets. + val responseAuth = MyPreferences.isResponseAuth(this) + if (responseAuth) { + // If the entire Autofill Response is authenticated, AuthActivity is used + // to generate Response. + val sender = AuthActivity.getAuthIntentSenderForResponse(this) + val presentation = AutofillHelper + .newRemoteViews(packageName, getString(R.string.autofill_sign_in_prompt)) + responseBuilder + .setAuthentication(autofillFields.autofillIds.toTypedArray(), sender, presentation) + callback.onSuccess(responseBuilder.build()) + } else { + val datasetAuth = MyPreferences.isDatasetAuth(this) + val clientFormDataMap = SharedPrefsAutofillRepository.getClientFormData(this, + autofillFields.focusedAutofillHints, autofillFields.allAutofillHints) + val response = AutofillHelper.newResponse(this, datasetAuth, autofillFields, clientFormDataMap) + callback.onSuccess(response) + } + } + + override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { + val context = request.fillContexts + val structure = context[context.size - 1].structure + val data = request.clientState + Log.d(TAG, "onSaveRequest(): data=" + bundleToString(data)) + val parser = StructureParser(structure) + parser.parseForSave() + SharedPrefsAutofillRepository.saveClientFormData(this, parser.clientFormData) + } + + override fun onConnected() { + Log.d(TAG, "onConnected") + } + + override fun onDisconnected() { + Log.d(TAG, "onDisconnected") + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/StructureParser.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/StructureParser.kt new file mode 100644 index 00000000..78cab5ee --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/StructureParser.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.autofillframework.multidatasetservice + +import android.app.assist.AssistStructure +import android.app.assist.AssistStructure.ViewNode +import android.util.Log +import com.example.android.autofillframework.CommonUtil.TAG +import com.example.android.autofillframework.multidatasetservice.model.AutofillField +import com.example.android.autofillframework.multidatasetservice.model.AutofillFieldsCollection +import com.example.android.autofillframework.multidatasetservice.model.ClientFormData +import com.example.android.autofillframework.multidatasetservice.model.SavableAutofillData + +/** + * Parser for an AssistStructure object. This is invoked when the Autofill Service receives an + * AssistStructure from the client Activity, representing its View hierarchy. In this sample, it + * parses the hierarchy and collects autofill metadata from {@link ViewNode}s along the way. + */ +internal class StructureParser(private val mStructure: AssistStructure) { + val autofillFields = AutofillFieldsCollection() + var clientFormData: ClientFormData = ClientFormData() + private set + + + fun parseForFill() { + parse(true) + } + + fun parseForSave() { + parse(false) + } + + /** + * Traverse AssistStructure and add ViewNode metadata to a flat list. + */ + private fun parse(forFill: Boolean) { + Log.d(TAG, "Parsing structure for " + mStructure.activityComponent) + val nodes = mStructure.windowNodeCount + clientFormData = ClientFormData() + for (i in 0..nodes - 1) { + val node = mStructure.getWindowNodeAt(i) + val view = node.rootViewNode + parseLocked(forFill, view) + } + } + + private fun parseLocked(forFill: Boolean, viewNode: ViewNode) { + viewNode.autofillHints?.let { autofillHints -> + if (autofillHints.isNotEmpty()) { + if (forFill) { + autofillFields.add(AutofillField(viewNode)) + } else { + clientFormData.setAutofillValuesForHints(viewNode.autofillHints, + SavableAutofillData(viewNode)) + } + } + } + val childrenSize = viewNode.childCount + if (childrenSize > 0) { + for (i in 0..childrenSize - 1) { + parseLocked(forFill, viewNode.getChildAt(i)) + } + } + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/AutofillRepository.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/AutofillRepository.kt new file mode 100644 index 00000000..9e4d6e20 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/AutofillRepository.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.autofillframework.multidatasetservice.datasource + +import android.content.Context +import com.example.android.autofillframework.multidatasetservice.model.ClientFormData +import java.util.HashMap + +interface AutofillRepository { + + /** + * Gets saved ClientFormData that contains some objects that can autofill fields with these + * `autofillHints`. + */ + fun getClientFormData(context: Context, focusedAutofillHints: List<String>, + allAutofillHints: List<String>): HashMap<String, ClientFormData>? + + /** + * Saves LoginCredential under this datasetName. + */ + fun saveClientFormData(context: Context, clientFormData: ClientFormData) + + /** + * Clears all data. + */ + fun clear(context: Context) +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsAutofillRepository.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsAutofillRepository.kt new file mode 100644 index 00000000..8aeac0df --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsAutofillRepository.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.autofillframework.multidatasetservice.datasource + +import android.content.Context +import android.content.SharedPreferences +import android.util.ArraySet +import com.example.android.autofillframework.multidatasetservice.model.ClientFormData +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + + +/** + * Singleton autofill data repository that stores autofill fields to SharedPreferences. + * Disclaimer: you should not store sensitive fields like user data unencrypted. This is done + * here only for simplicity and learning purposes. + */ +object SharedPrefsAutofillRepository : AutofillRepository { + private val SHARED_PREF_KEY = "com.example.android.autofillframework.service" + private val CLIENT_FORM_DATA_KEY = "loginCredentialDatasets" + private val DATASET_NUMBER_KEY = "datasetNumber" + + private fun getPrefs(context: Context): SharedPreferences { + return context.applicationContext.getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE) + } + + override fun getClientFormData(context: Context, focusedAutofillHints: List<String>, + allAutofillHints: List<String>): HashMap<String, ClientFormData>? { + var hasDataForFocusedAutofillHints = false + val clientFormDataMap = HashMap<String, ClientFormData>() + val clientFormDataStringSet = getAllAutofillDataStringSet(context) + for (clientFormDataString in clientFormDataStringSet) { + val type = object : TypeToken<ClientFormData>() {}.type + Gson().fromJson<ClientFormData>(clientFormDataString, type)?.let { clientFormData -> + if (clientFormData.helpsWithHints(focusedAutofillHints)) { + // Saved data has data relevant to at least 1 of the hints associated with the + // View in focus. + hasDataForFocusedAutofillHints = true + clientFormData.datasetName?.let { datasetName -> + if (clientFormData.helpsWithHints(allAutofillHints)) { + // Saved data has data relevant to at least 1 of these hints associated with any + // of the Views in the hierarchy. + clientFormDataMap.put(datasetName, clientFormData) + } + } + } + } + } + if (hasDataForFocusedAutofillHints) { + return clientFormDataMap + } else { + return null + } + } + + override fun saveClientFormData(context: Context, clientFormData: ClientFormData) { + val datasetName = "dataset-" + getDatasetNumber(context) + clientFormData.datasetName = datasetName + val allAutofillData = getAllAutofillDataStringSet(context) + allAutofillData.add(Gson().toJson(clientFormData).toString()) + saveAllAutofillDataStringSet(context, allAutofillData) + incrementDatasetNumber(context) + } + + override fun clear(context: Context) { + getPrefs(context).edit().remove(CLIENT_FORM_DATA_KEY).remove(DATASET_NUMBER_KEY).apply() + } + + private fun getAllAutofillDataStringSet(context: Context): MutableSet<String> { + return getPrefs(context).getStringSet(CLIENT_FORM_DATA_KEY, ArraySet<String>()) + } + + private fun saveAllAutofillDataStringSet(context: Context, allAutofillDataStringSet: Set<String>) { + getPrefs(context).edit().putStringSet(CLIENT_FORM_DATA_KEY, allAutofillDataStringSet).apply() + } + + /** + * For simplicity, datasets will be named in the form "dataset-X" where X means + * this was the Xth dataset saved. + */ + private fun getDatasetNumber(context: Context): Int { + return getPrefs(context).getInt(DATASET_NUMBER_KEY, 0) + } + + /** + * Every time a dataset is saved, this should be called to increment the dataset number. + * (only important for this service's dataset naming scheme). + */ + private fun incrementDatasetNumber(context: Context) { + getPrefs(context).edit().putInt(DATASET_NUMBER_KEY, getDatasetNumber(context) + 1).apply() + } +}
\ No newline at end of file diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/model/AutofillField.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/model/AutofillField.kt new file mode 100644 index 00000000..474454a3 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/model/AutofillField.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.autofillframework.multidatasetservice.model + +import android.app.assist.AssistStructure.ViewNode; +import android.service.autofill.SaveInfo +import android.view.View +import android.view.autofill.AutofillId + +/** + * A stripped down version of a [ViewNode] that contains only autofill-relevant metadata. It also + * contains a `saveType` flag that is calculated based on the [ViewNode]'s autofill hints. + */ +class AutofillField(view: ViewNode) { + var saveType = 0 + private set + + val autofillHints: Array<String> = view.autofillHints + val autofillId: AutofillId = view.autofillId + val autofillType: Int = view.autofillType + val autofillOptions: Array<CharSequence>? = view.autofillOptions + val isFocused: Boolean = view.isFocused + + init { + updateSaveTypeFromHints() + } + + /** + * When the [ViewNode] is a list that the user needs to choose a string from (i.e. a spinner), + * this is called to return the index of a specific item in the list. + */ + fun getAutofillOptionIndex(value: CharSequence): Int? { + return autofillOptions?.indexOf(value) + } + + private fun updateSaveTypeFromHints() { + saveType = 0 + for (hint in autofillHints) { + when (hint) { + View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE, + View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY, + View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, + View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, + View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, + View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE -> { + saveType = saveType or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD + } + View.AUTOFILL_HINT_EMAIL_ADDRESS -> { + saveType = saveType or SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS + } + View.AUTOFILL_HINT_PHONE, View.AUTOFILL_HINT_NAME -> { + saveType = saveType or SaveInfo.SAVE_DATA_TYPE_GENERIC + } + View.AUTOFILL_HINT_PASSWORD -> { + saveType = saveType or SaveInfo.SAVE_DATA_TYPE_PASSWORD + saveType = saveType and SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS.inv() + saveType = saveType and SaveInfo.SAVE_DATA_TYPE_USERNAME.inv() + } + View.AUTOFILL_HINT_POSTAL_ADDRESS, + View.AUTOFILL_HINT_POSTAL_CODE -> { + saveType = saveType or SaveInfo.SAVE_DATA_TYPE_ADDRESS + } + View.AUTOFILL_HINT_USERNAME -> { + saveType = saveType or SaveInfo.SAVE_DATA_TYPE_USERNAME + } + } + } + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/model/AutofillFieldsCollection.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/model/AutofillFieldsCollection.kt new file mode 100644 index 00000000..c62ea327 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/model/AutofillFieldsCollection.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.autofillframework.multidatasetservice.model + +import android.view.autofill.AutofillId +import java.util.ArrayList +import java.util.HashMap + +/** + * Data structure that stores a collection of `AutofillField`s. Contains all of the client's `View` + * hierarchy autofill-relevant metadata. + */ +data class AutofillFieldsCollection(val autofillIds: ArrayList<AutofillId> = ArrayList<AutofillId>(), + val allAutofillHints: ArrayList<String> = ArrayList<String>(), + val focusedAutofillHints: ArrayList<String> = ArrayList<String>()) { + + private val autofillHintsToFieldsMap = HashMap<String, MutableList<AutofillField>>() + var saveType = 0 + private set + + fun add(autofillField: AutofillField) { + saveType = saveType or autofillField.saveType + autofillIds.add(autofillField.autofillId) + val hintsList = autofillField.autofillHints + allAutofillHints.addAll(hintsList) + if (autofillField.isFocused) { + focusedAutofillHints.addAll(hintsList) + } + autofillField.autofillHints.forEach { autofillHint -> + autofillHintsToFieldsMap[autofillHint] = autofillHintsToFieldsMap[autofillHint] ?: ArrayList<AutofillField>() + autofillHintsToFieldsMap[autofillHint]?.add(autofillField) + } + } + + fun getFieldsForHint(hint: String): MutableList<AutofillField>? { + return autofillHintsToFieldsMap[hint] + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/model/ClientFormData.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/model/ClientFormData.kt new file mode 100644 index 00000000..8f434abe --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/model/ClientFormData.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.autofillframework.multidatasetservice.model + +import android.service.autofill.Dataset +import android.util.Log +import android.view.View +import android.view.autofill.AutofillId +import android.view.autofill.AutofillValue +import java.util.HashMap + + +/** + * ClientFormData is the model that represents all of the form data on a client app's page, plus the + * dataset name associated with it. + */ +class ClientFormData constructor(var datasetName: String? = null, + private val hintMap: HashMap<String, SavableAutofillData> = HashMap<String, SavableAutofillData>()) { + + private val TAG = "ClientFormData" + + /** + * Sets values for a list of autofillHints. + */ + fun setAutofillValuesForHints(autofillHints: Array<String>, autofillData: SavableAutofillData) { + autofillHints.forEach { hint -> + hintMap[hint] = autofillData + } + } + + /** + * Populates a [Dataset.Builder] with appropriate values for each [AutofillId] + * in a `AutofillFieldsCollection`. + */ + fun applyToFields(autofillFieldsCollection: AutofillFieldsCollection, + datasetBuilder: Dataset.Builder): Boolean { + var setValueAtLeastOnce = false + for (hint in autofillFieldsCollection.allAutofillHints) { + val autofillFields = autofillFieldsCollection.getFieldsForHint(hint) ?: continue + for (autofillField in autofillFields) { + val autofillId = autofillField.autofillId + val autofillType = autofillField.autofillType + val savedAutofillValue = hintMap[hint] + when (autofillType) { + View.AUTOFILL_TYPE_LIST -> { + savedAutofillValue?.textValue?.let(autofillField::getAutofillOptionIndex)?.let { index -> + datasetBuilder.setValue(autofillId, AutofillValue.forList(index)) + setValueAtLeastOnce = true + } + } + View.AUTOFILL_TYPE_DATE -> { + savedAutofillValue?.dateValue?.let { date -> + datasetBuilder.setValue(autofillId, AutofillValue.forDate(date)) + setValueAtLeastOnce = true + } + } + View.AUTOFILL_TYPE_TEXT -> { + savedAutofillValue?.textValue?.let { text -> + datasetBuilder.setValue(autofillId, AutofillValue.forText(text)) + setValueAtLeastOnce = true + } + } + View.AUTOFILL_TYPE_TOGGLE -> { + savedAutofillValue?.toggleValue?.let { toggle -> + datasetBuilder.setValue(autofillId, AutofillValue.forToggle(toggle)) + setValueAtLeastOnce = true + } + } + else -> Log.w(TAG, "Invalid autofill type - " + autofillType) + } + } + } + return setValueAtLeastOnce + } + + /** + * Returns whether this model contains autofill data that is relevant to any of the + * autofillHints that are passed in. + */ + fun helpsWithHints(autofillHints: List<String>): Boolean { + for (autofillHint in autofillHints) { + hintMap[autofillHint]?.let { savedAutofillValue -> + if (!savedAutofillValue.isNull()) { + return true + } + } + } + return false + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/model/SavableAutofillData.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/model/SavableAutofillData.kt new file mode 100644 index 00000000..3c50d339 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/model/SavableAutofillData.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.autofillframework.multidatasetservice.model + +import android.app.assist.AssistStructure +import android.view.autofill.AutofillValue + +/** + * JSON serializable data class containing the same data as an [AutofillValue]. + */ +class SavableAutofillData(viewNode: AssistStructure.ViewNode) { + var textValue: CharSequence? = null + var dateValue: Long? = null + var toggleValue: Boolean? = null + + init { + viewNode.autofillValue?.let { autofillValue -> + if (autofillValue.isList) { + val index = autofillValue.listValue + viewNode.autofillOptions?.let { autofillOptions -> + if (autofillOptions.size > index) { + textValue = autofillOptions[index] + } + } + } else if (autofillValue.isDate) { + dateValue = autofillValue.dateValue + } else if (autofillValue.isText) { + // Using toString of AutofillValue.getTextValue in order to save it to + // SharedPreferences. + textValue = autofillValue.textValue.toString() + } else { + } + } + } + + fun isNull(): Boolean { + return textValue == null && dateValue == null && toggleValue == null + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/settings/MyPreferences.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/settings/MyPreferences.kt new file mode 100644 index 00000000..1ddf3dab --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/settings/MyPreferences.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.autofillframework.multidatasetservice.settings + +import android.content.Context +import android.content.SharedPreferences +import android.service.autofill.Dataset +import android.service.autofill.FillResponse + +object MyPreferences { + private val TAG = "MyPreferences" + + private val SHARED_PREF_KEY = "com.example.android.autofillframework.service.settings.MyPreferences" + private val RESPONSE_AUTH_KEY = "response_auth" + private val DATASET_AUTH_KEY = "dataset_auth" + private val MASTER_PASSWORD_KEY = "master_password" + + private fun getPrefs(context: Context): SharedPreferences { + return context.applicationContext.getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE) + } + + /** + * Determines whether [FillResponse]s should require authentication. + */ + fun isResponseAuth(context: Context): Boolean { + return getPrefs(context).getBoolean(RESPONSE_AUTH_KEY, false) + } + + fun setResponseAuth(context: Context, responseAuth: Boolean) { + getPrefs(context).edit().putBoolean(RESPONSE_AUTH_KEY, responseAuth).apply() + } + + /** + * Determines whether [Dataset]s should require authentication. + */ + fun isDatasetAuth(context: Context): Boolean { + return getPrefs(context).getBoolean(DATASET_AUTH_KEY, false) + } + + fun setDatasetAuth(context: Context, datasetAuth: Boolean) { + getPrefs(context).edit().putBoolean(DATASET_AUTH_KEY, datasetAuth).apply() + } + + /** + * Gets autofill master password. + */ + fun getMasterPassword(context: Context): String? { + return getPrefs(context).getString(MASTER_PASSWORD_KEY, null) + } + + /** + * Sets autofill master password. + */ + fun setMasterPassword(context: Context, masterPassword: String) { + getPrefs(context).edit().putString(MASTER_PASSWORD_KEY, masterPassword).apply() + } + + /** + * Removes master password. + */ + fun clearCredentials(context: Context) { + getPrefs(context).edit().remove(MASTER_PASSWORD_KEY).apply() + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/settings/SettingsActivity.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/settings/SettingsActivity.kt new file mode 100644 index 00000000..e314ccd7 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/settings/SettingsActivity.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.autofillframework.multidatasetservice.settings + +import android.os.Bundle +import android.support.v7.app.AlertDialog +import android.support.v7.app.AppCompatActivity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.CompoundButton +import android.widget.EditText +import android.widget.ImageView +import android.widget.Switch +import android.widget.TextView +import com.example.android.autofillframework.R +import com.example.android.autofillframework.multidatasetservice.datasource.SharedPrefsAutofillRepository +import kotlinx.android.synthetic.main.multidataset_service_settings_activity.settings_auth_credentials_container +import kotlinx.android.synthetic.main.multidataset_service_settings_activity.settings_auth_credentials_icon +import kotlinx.android.synthetic.main.multidataset_service_settings_activity.settings_auth_credentials_label +import kotlinx.android.synthetic.main.multidataset_service_settings_activity.settings_auth_datasets_container +import kotlinx.android.synthetic.main.multidataset_service_settings_activity.settings_auth_datasets_label +import kotlinx.android.synthetic.main.multidataset_service_settings_activity.settings_auth_datasets_switch +import kotlinx.android.synthetic.main.multidataset_service_settings_activity.settings_auth_responses_container +import kotlinx.android.synthetic.main.multidataset_service_settings_activity.settings_auth_responses_label +import kotlinx.android.synthetic.main.multidataset_service_settings_activity.settings_auth_responses_switch +import kotlinx.android.synthetic.main.multidataset_service_settings_activity.settings_clear_data_container +import kotlinx.android.synthetic.main.multidataset_service_settings_activity.settings_clear_data_icon +import kotlinx.android.synthetic.main.multidataset_service_settings_activity.settings_clear_data_label + +class SettingsActivity : AppCompatActivity() { + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.multidataset_service_settings_activity) + setupSettingsSwitch(settings_auth_responses_container, + settings_auth_responses_label, + settings_auth_responses_switch, + MyPreferences.isResponseAuth(this), + CompoundButton.OnCheckedChangeListener { compoundButton, b -> + MyPreferences.setResponseAuth(this@SettingsActivity, b) + }) + setupSettingsSwitch(settings_auth_datasets_container, + settings_auth_datasets_label, + settings_auth_datasets_switch, + MyPreferences.isDatasetAuth(this), + CompoundButton.OnCheckedChangeListener { compoundButton, b -> + MyPreferences.setDatasetAuth(this@SettingsActivity, b) + }) + setupSettingsButton(settings_clear_data_container, + settings_clear_data_label, + settings_clear_data_icon, + View.OnClickListener { buildClearDataDialog().show() }) + + setupSettingsButton(settings_auth_credentials_container, + settings_auth_credentials_label, + settings_auth_credentials_icon, + View.OnClickListener { + MyPreferences.getMasterPassword(this@SettingsActivity)?.let { + buildCurrentCredentialsDialog().show() + } ?: buildNewCredentialsDialog().show() + }) + } + + private fun buildClearDataDialog(): AlertDialog { + return AlertDialog.Builder(this@SettingsActivity) + .setMessage(R.string.settings_clear_data_confirmation) + .setTitle(R.string.settings_clear_data_confirmation_title) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.ok) { dialog, which -> + SharedPrefsAutofillRepository.clear(this@SettingsActivity) + MyPreferences.clearCredentials(this@SettingsActivity) + dialog.dismiss() + } + .create() + } + + private fun prepareCredentialsDialog(): AlertDialog.Builder { + return AlertDialog.Builder(this@SettingsActivity) + .setTitle(R.string.settings_auth_change_credentials_title) + .setNegativeButton(R.string.cancel, null) + } + + private fun buildCurrentCredentialsDialog(): AlertDialog { + val currentPasswordField = LayoutInflater + .from(this@SettingsActivity) + .inflate(R.layout.multidataset_service_settings_authentication_dialog, null) + .findViewById<EditText>(R.id.master_password_field) + return prepareCredentialsDialog() + .setMessage(R.string.settings_auth_enter_current_password) + .setView(currentPasswordField) + .setPositiveButton(R.string.ok) { dialog, which -> + val password = currentPasswordField.text.toString() + if (MyPreferences.getMasterPassword(this@SettingsActivity) == password) { + buildNewCredentialsDialog().show() + dialog.dismiss() + } + } + .create() + } + + private fun buildNewCredentialsDialog(): AlertDialog { + val newPasswordField = LayoutInflater + .from(this@SettingsActivity) + .inflate(R.layout.multidataset_service_settings_authentication_dialog, null) + .findViewById<EditText>(R.id.master_password_field) + return prepareCredentialsDialog() + .setMessage(R.string.settings_auth_enter_new_password) + .setView(newPasswordField) + .setPositiveButton(R.string.ok) { dialog, which -> + val password = newPasswordField.text.toString() + MyPreferences.setMasterPassword(this@SettingsActivity, password) + dialog.dismiss() + } + .create() + } + + private fun setupSettingsSwitch(container: ViewGroup, switchLabelView: TextView, switchView: Switch, checked: Boolean, + checkedChangeListener: CompoundButton.OnCheckedChangeListener) { + val switchLabel = switchLabelView.text.toString() + switchView.contentDescription = switchLabel + switchView.isChecked = checked + container.setOnClickListener { switchView.performClick() } + switchView.setOnCheckedChangeListener(checkedChangeListener) + } + + private fun setupSettingsButton(container: ViewGroup, buttonLabelView: TextView, imageView: ImageView, + onClickListener: View.OnClickListener) { + val buttonLabel = buttonLabelView.text.toString() + imageView.contentDescription = buttonLabel + container.setOnClickListener(onClickListener) + } +} |