diff options
author | Douglas Sigelbaum <sigelbaum@google.com> | 2017-05-11 23:44:42 -0700 |
---|---|---|
committer | Douglas Sigelbaum <sigelbaum@google.com> | 2017-05-16 18:06:48 -0700 |
commit | 5525a586f56baeb2310dbacfe34a96afd3dd3f0f (patch) | |
tree | 6c76b03f8e46b64ff4337a48880f3381b918170f | |
parent | 283fd752a6066e4d91bf668a5eacc38f29d411e7 (diff) | |
download | android-5525a586f56baeb2310dbacfe34a96afd3dd3f0f.tar.gz |
Added hints and save info to Autofill service.
Test: manual
Bug: 38182790
Change-Id: Ifd0b6b970e5357814376149bc76457ec33ec1e1a
18 files changed, 486 insertions, 313 deletions
diff --git a/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml b/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml index e6985b01..a9f3558d 100644 --- a/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml +++ b/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml @@ -60,7 +60,7 @@ Not necessary for a real service. --> <activity - android:name=".service.SettingsActivity" + android:name=".service.settings.SettingsActivity" android:exported="true" android:label="@string/settings_name" android:taskAffinity=".SettingsActivity" > diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/CustomVirtualView.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/CustomVirtualView.java index bec2c25b..6b01039e 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/CustomVirtualView.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/CustomVirtualView.java @@ -241,13 +241,11 @@ public class CustomVirtualView extends View { private final class Line { + // Boundaries of the text field, relative to the CustomView + final Rect bounds = new Rect(); private Item labelItem; private Item fieldTextItem; private String idEntry; - - // Boundaries of the text field, relative to the CustomView - final Rect bounds = new Rect(); - private boolean focused; private Line(String idEntry, String label, String text, boolean sanitized) { diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/LoginActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/LoginActivity.java index 9a1e00cd..cb7192eb 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/LoginActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/LoginActivity.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.view.View; +import android.view.autofill.AutofillManager; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; @@ -44,10 +45,14 @@ public class LoginActivity extends Activity { setContentView(R.layout.login_activity); - mLoginButton = (Button) findViewById(R.id.login); - mClearButton = (Button) findViewById(R.id.clear); - mUsernameEditText = (EditText) findViewById(R.id.usernameField); - mPasswordEditText = (EditText) findViewById(R.id.passwordField); + mLoginButton = findViewById(R.id.login); + mClearButton = findViewById(R.id.clear); + mUsernameEditText = findViewById(R.id.usernameField); + mPasswordEditText = findViewById(R.id.passwordField); + mUsernameEditText.setAutofillHints(View.AUTOFILL_HINT_USERNAME); + mPasswordEditText.setAutofillHints(View.AUTOFILL_HINT_PASSWORD); + mUsernameEditText.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_YES); + mPasswordEditText.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_YES); mLoginButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -77,6 +82,7 @@ public class LoginActivity extends Activity { if (valid) { Intent intent = WelcomeActivity.getStartActivityIntent(LoginActivity.this); startActivity(intent); + finish(); } else { Toast.makeText(this, "Authentication failed.", Toast.LENGTH_SHORT).show(); } diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.java index 2ea3f1aa..356fa678 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.java @@ -29,14 +29,17 @@ import android.text.Editable; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; -import android.view.autofill.AutofillId; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; import com.example.android.autofillframework.R; +import com.example.android.autofillframework.service.datasource.LocalAutofillRepository; +import com.example.android.autofillframework.service.model.AutofillField; +import com.example.android.autofillframework.service.model.LoginCredential; -import java.util.Map; +import java.util.HashMap; +import java.util.List; import static android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE; import static android.view.autofill.AutofillManager.EXTRA_AUTHENTICATION_RESULT; @@ -133,12 +136,11 @@ public class AuthActivity extends Activity { AssistStructure structure = intent.getParcelableExtra(EXTRA_ASSIST_STRUCTURE); StructureParser parser = new StructureParser(structure); parser.parse(); - AutofillId usernameId = parser.getUsernameField().getId(); - AutofillId passwordId = parser.getPasswordField().getId(); - Map<String, LoginCredential> loginCredentialMap = - AutofillData.getInstance().getCredentialsMap(this); - if (usernameId == null || passwordId == null || loginCredentialMap == null || - loginCredentialMap.isEmpty()) { + List<AutofillField> autofillFields = parser.getAutofillFields(); + HashMap<String, LoginCredential> loginCredentialMap = + LocalAutofillRepository.getInstance(this).getLoginCredentials(); + int saveType = parser.getSaveTypes(); + if (loginCredentialMap == null || loginCredentialMap.isEmpty()) { Log.d(TAG, "No Autofill data found for this Activity."); return; } @@ -146,8 +148,8 @@ public class AuthActivity extends Activity { if (forResponse) { // The response protected by auth, so we can send the entire response since we // passed auth. - FillResponse response = AutofillHelper.newCredentialsResponse(this, false, usernameId, - passwordId, loginCredentialMap); + FillResponse response = AutofillHelper + .newCredentialResponse(this, false, autofillFields, saveType, loginCredentialMap); if (response != null) { mReplyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, response); } @@ -157,7 +159,7 @@ public class AuthActivity extends Activity { if (loginCredentialMap.containsKey(datasetName)) { LoginCredential credential = loginCredentialMap.get(datasetName); Dataset dataset = AutofillHelper - .newCredentialDataset(this, credential, usernameId, passwordId); + .newCredentialDataset(this, autofillFields, credential); mReplyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, dataset); } } diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillData.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillData.java deleted file mode 100644 index 9eb41147..00000000 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillData.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.service; - -import android.content.Context; -import android.util.Log; - -import java.util.LinkedHashMap; -import java.util.Map; - -import static com.example.android.autofillframework.CommonUtil.TAG; - -/** - * Singleton holding Autofill data. In this simple Autofill service, it only holds LoginCredentials - * for every client app that uses the service. - */ -final class AutofillData { - - private static AutofillData sAutoFillData; - - // Structure: {<packageName> : {<datasetName> : <loginCredential object>, ...}, ...} - private final Map<String, Map<String, LoginCredential>> mLoginCredentials = - new LinkedHashMap<>(); - - private AutofillData() { - } - - public static AutofillData getInstance() { - if (sAutoFillData == null) { - sAutoFillData = new AutofillData(); - } - return sAutoFillData; - } - - /** - * Get all login credentials associated with the caller's package. - */ - public Map<String, LoginCredential> getCredentialsMap(Context context) { - int numDatasets = MyPreferences.getInstance(context).getNumberDatasets(); - for (int i = 0; i < numDatasets; i++) { - LoginCredential loginCredential = - new LoginCredential("user" + i, "user" + i); - updateCredentials(context.getPackageName(), loginCredential); - } - return mLoginCredentials.get(context.getPackageName()); - } - - /** - * Add a loginCredential mapped to the caller's package (maintaining idempotency). - */ - public void updateCredentials(String packageName, LoginCredential loginCredential) { - if (!mLoginCredentials.containsKey(packageName)) { - mLoginCredentials.put(packageName, new LinkedHashMap<String, LoginCredential>()); - } - mLoginCredentials.get(packageName).put(loginCredential.getDatasetName(), loginCredential); - Log.d(TAG, "Creating credentials for " + packageName + ":" + - loginCredential.getDatasetName()); - } -} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillField.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillField.java deleted file mode 100644 index 450480b4..00000000 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillField.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.service; - -import android.app.assist.AssistStructure; -import android.view.autofill.AutofillId; - -/** - * Class that represents a field that can be autofilled. It will contain a description - * (what type data the field holds), an AutoFillId (an ID unique to the rest of the ViewStructure), - * and a value (what data is currently in the field). - */ -public class AutofillField { - private final String description; - private AutofillId id; - - // For simplicity, we will only support text values. - private String value; - - public AutofillField(String description) { - this.description = description; - } - - void setFrom(AssistStructure.ViewNode view) { - id = view.getAutofillId(); - CharSequence text = view.getText(); - value = text == null ? null : text.toString(); - } - - public String getDescription() { - return description; - } - - public AutofillId getId() { - return id; - } - - public String getValue() { - return value; - } - - @Override - public String toString() { - return "AutofillField: [id=" + id + ", value=" + value + "]"; - } - -} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.java index 8b6297e8..8087843e 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.java @@ -19,14 +19,19 @@ import android.content.Context; import android.content.IntentSender; import android.service.autofill.Dataset; import android.service.autofill.FillResponse; +import android.service.autofill.SaveInfo; import android.util.Log; +import android.view.View; import android.view.autofill.AutofillId; import android.view.autofill.AutofillValue; import android.widget.RemoteViews; import com.example.android.autofillframework.R; +import com.example.android.autofillframework.service.model.AutofillField; +import com.example.android.autofillframework.service.model.LoginCredential; -import java.util.Map; +import java.util.HashMap; +import java.util.List; import java.util.Set; import static com.example.android.autofillframework.CommonUtil.TAG; @@ -35,61 +40,81 @@ import static com.example.android.autofillframework.CommonUtil.TAG; * This is a class containing helper methods for building Autofill Datasets and Responses. */ public final class AutofillHelper { + /** - * Wraps autofill data in a Dataset object which can then be sent back to the client View. + * Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the + * client View. */ - public static Dataset newCredentialDataset(Context context, - LoginCredential loginCredential, AutofillId usernameId, - AutofillId passwordId) { - String datasetName = loginCredential.getDatasetName(); - RemoteViews presentation = new RemoteViews(context.getPackageName(), - R.layout.list_item); - presentation.setTextViewText(R.id.text1, datasetName); - Dataset.Builder datasetBuilder = new Dataset.Builder(presentation); - datasetBuilder.setValue(usernameId, AutofillValue.forText(loginCredential.getUsername())); - datasetBuilder.setValue(passwordId, AutofillValue.forText(loginCredential.getPassword())); + public static Dataset newCredentialDataset(Context context, List<AutofillField> autofillFields, LoginCredential loginCredential) { + Dataset.Builder datasetBuilder = new Dataset.Builder + (newRemoteViews(context.getPackageName(), loginCredential.getDatasetName())); + // Not using enhanced for loop on Collection to prevent extra iterator allocation. + for (int i = 0; i < autofillFields.size(); i++) { + AutofillField field = autofillFields.get(i); + boolean shouldBreak = false; + for (String autofillHint : field.getHints()) { + switch (autofillHint) { + case View.AUTOFILL_HINT_USERNAME: + datasetBuilder.setValue(field.getId(), AutofillValue.forText(loginCredential.getUsername())); + shouldBreak = true; + break; + case View.AUTOFILL_HINT_PASSWORD: + datasetBuilder.setValue(field.getId(), AutofillValue.forText(loginCredential.getPassword())); + shouldBreak = true; + break; + } + if (shouldBreak) { + break; + } + } + } return datasetBuilder.build(); } + public static RemoteViews newRemoteViews(String packageName, String remoteViewsText) { + RemoteViews presentation = new RemoteViews(packageName, R.layout.list_item); + presentation.setTextViewText(R.id.text1, remoteViewsText); + return presentation; + } + /** * Wraps autofill data in a Response object (essentially a series of Datasets) which can then * be sent back to the client View. */ - public static FillResponse newCredentialsResponse(Context context, - boolean datasetAuth, AutofillId usernameId, AutofillId passwordId, - Map<String, LoginCredential> credentialsMap) { + public static FillResponse newCredentialResponse(Context context, boolean datasetAuth, + List<AutofillField> autofillFields, int saveType, + HashMap<String, LoginCredential> loginCredentialMap) { FillResponse.Builder responseBuilder = new FillResponse.Builder(); - if (usernameId == null || passwordId == null || - credentialsMap == null || credentialsMap.isEmpty()) { - // Activity does not have usernameField and passwordField, can't do anything - return null; - } else { - int numReplies = 0; - Set<Map.Entry<String, LoginCredential>> credentialSet = credentialsMap.entrySet(); - for (Map.Entry<String, LoginCredential> credential : credentialSet) { - if (datasetAuth) { - String datasetName = credential.getKey(); - RemoteViews presentation = new RemoteViews(context.getPackageName(), - R.layout.list_item); - presentation.setTextViewText(R.id.text1, datasetName); - Dataset.Builder datasetBuilder = new Dataset.Builder(presentation); - IntentSender sender = - AuthActivity.getAuthIntentSenderForDataset(context, datasetName); - datasetBuilder.setAuthentication(sender); - responseBuilder.addDataset(datasetBuilder.build()); - } else { - Dataset dataset = newCredentialDataset(context, - credential.getValue(), usernameId, passwordId); - responseBuilder.addDataset(dataset); - } - numReplies++; - } - if (numReplies > 0) { - return responseBuilder.build(); + Set<String> datasetNames = loginCredentialMap.keySet(); + for (String datasetName : datasetNames) { + LoginCredential loginCredential = loginCredentialMap.get(datasetName); + if (datasetAuth) { + Dataset.Builder datasetBuilder = + new Dataset.Builder(newRemoteViews(context.getPackageName(), loginCredential.getDatasetName())); + IntentSender sender = + AuthActivity.getAuthIntentSenderForDataset(context, loginCredential.getDatasetName()); + datasetBuilder.setAuthentication(sender); + responseBuilder.addDataset(datasetBuilder.build()); } else { - Log.d(TAG, "No Autofill data found."); - return null; + Dataset dataset = newCredentialDataset(context, autofillFields, loginCredential); + responseBuilder.addDataset(dataset); } } + if (saveType != 0) { + AutofillId[] autofillIds = getAutofillIds(autofillFields); + responseBuilder.setSaveInfo(new SaveInfo.Builder(saveType, autofillIds).build()); + return responseBuilder.build(); + } else { + Log.d(TAG, "No Autofill data found."); + return null; + } + } + + public static AutofillId[] getAutofillIds(List<AutofillField> autofillFields) { + AutofillId[] autofillIds = new AutofillId[autofillFields.size()]; + for (int i = 0; i < autofillFields.size(); i++) { + autofillIds[i] = autofillFields.get(i).getId(); + } + return autofillIds; } } diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/LoginCredential.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/LoginCredential.java deleted file mode 100644 index 4e3efe9a..00000000 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/LoginCredential.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.service; - - -public class LoginCredential implements DatasetModel { - private final String username; - private final String password; - - LoginCredential(String username, String password) { - this.username = username; - this.password = password; - } - - @Override - public String getDatasetName() { - return username; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - LoginCredential that = (LoginCredential) o; - - if (username != null ? !username.equals(that.username) : that.username != null) { - return false; - } - return password != null ? password.equals(that.password) : that.password == null; - } - - @Override - public int hashCode() { - int result = username != null ? username.hashCode() : 0; - result = 31 * result + (password != null ? password.hashCode() : 0); - return result; - } - - public String getUsername() { - return username; - } - - public String getPassword() { - return password; - } -} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.java index a502f8e6..aa0d38e3 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.java @@ -31,10 +31,14 @@ import android.view.autofill.AutofillId; import android.widget.RemoteViews; import com.example.android.autofillframework.R; +import com.example.android.autofillframework.service.datasource.LocalAutofillRepository; +import com.example.android.autofillframework.service.model.AutofillField; +import com.example.android.autofillframework.service.model.LoginCredential; +import com.example.android.autofillframework.service.settings.MyPreferences; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.Set; import static com.example.android.autofillframework.CommonUtil.TAG; import static com.example.android.autofillframework.CommonUtil.bundleToString; @@ -61,6 +65,7 @@ public class MyAutofillService extends AutofillService { 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.getActivityComponent().toShortString() .contains("com.example.android.autofillframework.service")) { callback.onSuccess(null); @@ -75,8 +80,11 @@ public class MyAutofillService extends AutofillService { // Parse AutoFill data in Activity StructureParser parser = new StructureParser(structure); parser.parse(); - AutofillId usernameId = parser.getUsernameField().getId(); - AutofillId passwordId = parser.getPasswordField().getId(); + List<AutofillField> autofillFields = parser.getAutofillFields(); + HashMap<String, LoginCredential> loginCredentialMap = + LocalAutofillRepository.getInstance(this).getLoginCredentials(); + AutofillId[] autofillIds = AutofillHelper.getAutofillIds(autofillFields); + int saveTypes = parser.getSaveTypes(); FillResponse.Builder responseBuilder = new FillResponse.Builder(); // Check user's settings for authenticating Responses and Datasets @@ -85,26 +93,19 @@ public class MyAutofillService extends AutofillService { // If the entire Autofill Response is authenticated, AuthActivity is used // to generate Response IntentSender sender = AuthActivity.getAuthIntentSenderForResponse(this); - RemoteViews presentation = new RemoteViews(getPackageName(), R.layout.list_item); - presentation.setTextViewText(R.id.text1, getString(R.string.autofill_sign_in_prompt)); - responseBuilder.setAuthentication(new AutofillId[]{usernameId, passwordId}, - sender, presentation); + RemoteViews presentation = AutofillHelper + .newRemoteViews(getPackageName(), getString(R.string.autofill_sign_in_prompt)); + responseBuilder.setAuthentication + (autofillIds, sender, presentation); callback.onSuccess(responseBuilder.build()); } else { boolean datasetAuth = MyPreferences.getInstance(this).isDatasetAuth(); - Map<String, LoginCredential> credentialsMap = - AutofillData.getInstance().getCredentialsMap(this); - if (usernameId == null || passwordId == null || - credentialsMap == null || credentialsMap.isEmpty()) { - // Activity does not have usernameField and passwordField fields, or service does not - // have any usernameField and passwordField autofill data. - Log.d(TAG, "No Autofill data found for this Activity"); - callback.onSuccess(null); - return; + FillResponse response = null; + if (parser.isLoginPage()) { + response = AutofillHelper.newCredentialResponse( + this, datasetAuth, autofillFields, saveTypes, loginCredentialMap); } - FillResponse response = AutofillHelper.newCredentialsResponse( - this, datasetAuth, usernameId, passwordId, credentialsMap); callback.onSuccess(response); } } @@ -114,14 +115,10 @@ public class MyAutofillService extends AutofillService { List<FillContext> context = request.getFillContexts(); final AssistStructure structure = context.get(context.size() - 1).getStructure(); final Bundle data = request.getClientState(); - Log.d(TAG, "onSaveFillRequest(): data=" + bundleToString(data)); + Log.d(TAG, "onSaveRequest(): data=" + bundleToString(data)); StructureParser parser = new StructureParser(structure); parser.parse(); - String packageName = structure.getActivityComponent().getPackageName(); - String username = parser.getUsernameField().getValue(); - String password = parser.getPasswordField().getValue(); - LoginCredential loginCredential = new LoginCredential(username, password); - AutofillData.getInstance().updateCredentials(packageName, loginCredential); + List<AutofillField> autofillFields = parser.getAutofillFields(); } @Override diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.java index 11f80faf..ea3f7630 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.java @@ -19,6 +19,14 @@ import android.app.assist.AssistStructure; import android.app.assist.AssistStructure.ViewNode; import android.app.assist.AssistStructure.WindowNode; import android.util.Log; +import android.view.View; + +import com.example.android.autofillframework.service.datasource.AutofillRepository; +import com.example.android.autofillframework.service.model.AutofillField; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; import static com.example.android.autofillframework.CommonUtil.TAG; @@ -30,14 +38,13 @@ import static com.example.android.autofillframework.CommonUtil.TAG; */ final class StructureParser { - // This simple AutoFill service is capable of parsing these fields: - private final AutofillField usernameField = new AutofillField("usernameField"); - private final AutofillField passwordField = new AutofillField("passwordField"); - private final AssistStructure structure; - + private AssistStructure structure; + private int mSaveTypes = 0; + private List<AutofillField> mAutofillFields; StructureParser(AssistStructure structure) { this.structure = structure; + mAutofillFields = new ArrayList<>(); } /** @@ -46,23 +53,30 @@ final class StructureParser { */ void parse() { Log.d(TAG, "Parsing structure for " + structure.getActivityComponent()); - final int nodes = structure.getWindowNodeCount(); + int nodes = structure.getWindowNodeCount(); for (int i = 0; i < nodes; i++) { WindowNode node = structure.getWindowNodeAt(i); ViewNode view = node.getRootViewNode(); parseLocked(view); } + updateSaveTypes(); + } + + private void updateSaveTypes() { + mSaveTypes = 0; + for (int i = 0; i < mAutofillFields.size(); i++) { + AutofillField field = mAutofillFields.get(i); + mSaveTypes |= field.getSaveType(); + } } private void parseLocked(ViewNode view) { - final String resourceId = view.getIdEntry(); - Log.d(TAG, "resourceId == " + resourceId); - if (resourceId != null && resourceId.equals(usernameField.getDescription())) { - usernameField.setFrom(view); - } else if (resourceId != null && resourceId.equals(passwordField.getDescription())) { - passwordField.setFrom(view); + if (view.getAutofillType() != View.AUTOFILL_TYPE_NONE) { + AutofillField field = new AutofillField(); + field.setFrom(view); + mAutofillFields.add(field); } - final int childrenSize = view.getChildCount(); + int childrenSize = view.getChildCount(); if (childrenSize > 0) { for (int i = 0; i < childrenSize; i++) { parseLocked(view.getChildAt(i)); @@ -70,11 +84,19 @@ final class StructureParser { } } - public AutofillField getUsernameField() { - return usernameField; + public List<AutofillField> getAutofillFields() { + return mAutofillFields; + } + + public int getSaveTypes() { + return mSaveTypes; } - public AutofillField getPasswordField() { - return passwordField; + /** + * Is this View structure + */ + public boolean isLoginPage() { + // TODO remove this. + return true; } } diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/datasource/AutofillRepository.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/datasource/AutofillRepository.java new file mode 100644 index 00000000..b10bc8ef --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/datasource/AutofillRepository.java @@ -0,0 +1,33 @@ +/* + * 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.service.datasource; + +import com.example.android.autofillframework.service.model.LoginCredential; + +import java.util.HashMap; + +public interface AutofillRepository { + + /** + * Gets LoginCredential that was originally saved with this {@code datasetName}. + */ + HashMap<String, LoginCredential> getLoginCredentials(); + + /** + * Saves LoginCredential under this datasetName. + */ + void saveLoginCredential(LoginCredential loginCredential); +} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/datasource/LocalAutofillRepository.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/datasource/LocalAutofillRepository.java new file mode 100644 index 00000000..6c00c81f --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/datasource/LocalAutofillRepository.java @@ -0,0 +1,69 @@ +package com.example.android.autofillframework.service.datasource; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.ArraySet; + +import com.example.android.autofillframework.service.model.LoginCredential; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Set; + +/** + * Singleton autofill data repository, that stores autofill fields to SharedPreferences. + * DISCLAIMER, you should not store sensitive fields like user data unencrypted. This is only done + * here for simplicity and learning purposes. + */ +public class LocalAutofillRepository implements AutofillRepository { + private static final String SHARED_PREF_KEY = "com.example.android.autofillframework.service"; + private static final String LOGIN_CREDENTIAL_DATASETS_KEY = "loginCredentialDatasetNames"; + + private static LocalAutofillRepository sInstance; + + private final SharedPreferences mPrefs; + private int datasetNumber = 0; + + private LocalAutofillRepository(Context context) { + mPrefs = context.getSharedPreferences(SHARED_PREF_KEY, + Context.MODE_PRIVATE); + } + + public static LocalAutofillRepository getInstance(Context context) { + if (sInstance == null) { + sInstance = new LocalAutofillRepository(context); + } + return sInstance; + } + + @Override + public HashMap<String, LoginCredential> getLoginCredentials() { + try { + HashMap<String, LoginCredential> loginCredentials = new HashMap<>(); + Set<String> loginCredentialStringSet = + mPrefs.getStringSet(LOGIN_CREDENTIAL_DATASETS_KEY, new ArraySet<String>()); + for (String loginCredentialString : loginCredentialStringSet) { + LoginCredential loginCredential = LoginCredential + .fromJson(new JSONObject(loginCredentialString)); + if (loginCredential != null) { + loginCredentials.put(loginCredential.getDatasetName(), loginCredential); + } + } + return loginCredentials; + } catch (JSONException e) { + return null; + } + } + + @Override + public void saveLoginCredential(LoginCredential loginCredential) { + String datasetName = "dataset-" + datasetNumber++; + loginCredential.setDatasetName(datasetName); + Set<String> loginCredentialStringSet = + mPrefs.getStringSet(LOGIN_CREDENTIAL_DATASETS_KEY, new ArraySet<String>()); + loginCredentialStringSet.add(loginCredential.toJson().toString()); + mPrefs.edit().putStringSet(LOGIN_CREDENTIAL_DATASETS_KEY, loginCredentialStringSet).apply(); + } +}
\ No newline at end of file diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillField.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillField.java new file mode 100644 index 00000000..648eb797 --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillField.java @@ -0,0 +1,118 @@ +/* + * 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.service.model; + +import android.app.assist.AssistStructure; +import android.service.autofill.SaveInfo; +import android.view.View; +import android.view.autofill.AutofillId; + +/** + * Class that represents a field that can be autofilled. It will contain a description + * (what type data the field holds), an AutoFillId (an ID unique to the rest of the ViewStructure), + * and a value (what data is currently in the field). + */ +public class AutofillField { + private int saveType = 0; + private String[] hints; + private AutofillId id; + + // For simplicity, we will only support text values. + // TODO support multiple value types. + private String value; + + public AutofillField() { + } + + public void setFrom(AssistStructure.ViewNode view) { + id = view.getAutofillId(); + setHints(view.getAutofillHints()); + } + + public String[] getHints() { + return hints; + } + + public void setHints(String[] hints) { + this.hints = hints; + updateSaveTypeFromHints(); + } + + public int getSaveType() { + return saveType; + } + + public AutofillId getId() { + return id; + } + + public void setId(AutofillId id) { + this.id = id; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public String toString() { + return "AutofillField: [id=" + id + ", value=" + value + "]"; + } + + private void updateSaveTypeFromHints() { + saveType = 0; + if(hints == null) { + return; + } + for (String hint : hints) { + switch (hint) { + case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE: + case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY: + case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH: + case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR: + case View.AUTOFILL_HINT_CREDIT_CARD_NUMBER: + case View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE: + saveType |= SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD; + break; + case View.AUTOFILL_HINT_EMAIL_ADDRESS: + saveType |= SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS; + break; + case View.AUTOFILL_HINT_NAME: + saveType |= SaveInfo.SAVE_DATA_TYPE_GENERIC; + break; + case View.AUTOFILL_HINT_PASSWORD: + saveType |= SaveInfo.SAVE_DATA_TYPE_PASSWORD; + saveType &= ~SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS; + saveType &= ~SaveInfo.SAVE_DATA_TYPE_USERNAME; + break; + case View.AUTOFILL_HINT_PHONE: + saveType |= SaveInfo.SAVE_DATA_TYPE_GENERIC; + break; + case View.AUTOFILL_HINT_POSTAL_ADDRESS: + case View.AUTOFILL_HINT_POSTAL_CODE: + saveType |= SaveInfo.SAVE_DATA_TYPE_ADDRESS; + break; + case View.AUTOFILL_HINT_USERNAME: + saveType |= SaveInfo.SAVE_DATA_TYPE_USERNAME; + break; + } + } + } +} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/DatasetModel.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/DatasetModel.java index c2c9e662..218e916b 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/DatasetModel.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/DatasetModel.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.example.android.autofillframework.service; +package com.example.android.autofillframework.service.model; /** * Blueprint for Objects that are the underlying data model for Autofill Datasets. In this basic diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/LoginCredential.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/LoginCredential.java new file mode 100644 index 00000000..742c7582 --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/LoginCredential.java @@ -0,0 +1,95 @@ +/* + * 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.service.model; + + +import org.json.JSONException; +import org.json.JSONObject; + +public class LoginCredential implements DatasetModel { + private final String username; + private final String password; + private String datasetName; + + LoginCredential(String username, String password) { + this(username, password, null); + } + + LoginCredential(String username, String password, String datasetName) { + this.username = username; + this.password = password; + } + + public static LoginCredential fromJson(JSONObject jsonObject) { + LoginCredential loginCredential = null; + try { + loginCredential = new LoginCredential(jsonObject.getString("username"), + jsonObject.getString("password")); + } catch (JSONException e) { + return null; + } + return loginCredential; + } + + @Override + public String getDatasetName() { + return username; + } + + public void setDatasetName(String datasetName) { + this.datasetName = datasetName; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public JSONObject toJson() { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("username", username); + jsonObject.put("password", password); + } catch (JSONException e) { + return null; + } + return jsonObject; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + LoginCredential that = (LoginCredential) o; + + if (!username.equals(that.username)) return false; + if (!password.equals(that.password)) return false; + return datasetName != null ? datasetName.equals(that.datasetName) : that.datasetName == null; + + } + + @Override + public int hashCode() { + int result = username.hashCode(); + result = 31 * result + password.hashCode(); + result = 31 * result + (datasetName != null ? datasetName.hashCode() : 0); + return result; + } +} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/MyPreferences.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/settings/MyPreferences.java index 9533da79..92fac4ec 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/MyPreferences.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/settings/MyPreferences.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.example.android.autofillframework.service; +package com.example.android.autofillframework.service.settings; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; -final class MyPreferences { +public class MyPreferences { private static final String TAG = "MyPreferences"; private static final String PREF_NUMBER_DATASET = "number_datasets"; @@ -33,7 +33,7 @@ final class MyPreferences { Context.MODE_PRIVATE); } - static MyPreferences getInstance(Context context) { + public static MyPreferences getInstance(Context context) { if (sInstance == null) { sInstance = new MyPreferences(context); } @@ -43,14 +43,14 @@ final class MyPreferences { /** * Gets the number of {@link Dataset}s that should be added to a {@link FillResponse}. */ - int getNumberDatasets() { + public int getNumberDatasets() { return mPrefs.getInt(PREF_NUMBER_DATASET, 2); } /** * Gets whether {@link FillResponse}s should require authentication. */ - boolean isResponseAuth() { + public boolean isResponseAuth() { return mPrefs.getBoolean(PREF_RESPONSE_AUTH, false); } @@ -58,11 +58,11 @@ final class MyPreferences { /** * Gets whether {@link Dataset}s should require authentication. */ - boolean isDatasetAuth() { + public boolean isDatasetAuth() { return mPrefs.getBoolean(PREF_DATASET_AUTH, false); } - void bulkEdit(int numberDatasets, boolean responseAuth, boolean datasetAuth) { + public void bulkEdit(int numberDatasets, boolean responseAuth, boolean datasetAuth) { Log.v(TAG, "bulk edit:" + numberDatasets + ":" + responseAuth + ":" + datasetAuth); mPrefs.edit() .putInt(PREF_NUMBER_DATASET, numberDatasets) diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/SettingsActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/settings/SettingsActivity.java index 599fed4f..e771d185 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/SettingsActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/settings/SettingsActivity.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.example.android.autofillframework.service; +package com.example.android.autofillframework.service.settings; import android.app.Activity; import android.os.Bundle; @@ -39,11 +39,11 @@ public class SettingsActivity extends Activity { super.onCreate(savedInstanceState); setContentView(R.layout.settings_activity); - mNumberDatasets = (EditText) findViewById(R.id.number_datasets); - mResponseAuth = (CheckBox) findViewById(R.id.response_auth); - mDatasetAuth = (CheckBox) findViewById(R.id.dataset_auth); - mSave = (Button) findViewById(R.id.save); - mCancel = (Button) findViewById(R.id.cancel); + mNumberDatasets = findViewById(R.id.number_datasets); + mResponseAuth = findViewById(R.id.response_auth); + mDatasetAuth = findViewById(R.id.dataset_auth); + mSave = findViewById(R.id.save); + mCancel = findViewById(R.id.cancel); final MyPreferences p = MyPreferences.getInstance(this); diff --git a/input/autofill/AutofillFramework/gradle/wrapper/gradle-wrapper.properties b/input/autofill/AutofillFramework/gradle/wrapper/gradle-wrapper.properties index 1081cc34..eaba3011 100644 --- a/input/autofill/AutofillFramework/gradle/wrapper/gradle-wrapper.properties +++ b/input/autofill/AutofillFramework/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip |