Update samples
AutofillFramework
EmojiCompat
Test: Built locally
Bug: 117136200
Change-Id: If56c64f6d6a0a727fd9d1dd4df6ba8661df06dba
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/BaseMainFragment.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/BaseMainFragment.java
new file mode 100644
index 0000000..5be875c
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/BaseMainFragment.java
@@ -0,0 +1,24 @@
+/*
+ * 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.autofill.app;
+
+import android.support.annotation.StringRes;
+import android.support.v4.app.Fragment;
+
+public abstract class BaseMainFragment extends Fragment {
+ public abstract @StringRes int getPageTitleResId();
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/Util.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/Util.java
new file mode 100644
index 0000000..288a908
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/Util.java
@@ -0,0 +1,154 @@
+/*
+ * 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.autofill.app;
+
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.app.assist.AssistStructure.WindowNode;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewStructure.HtmlInfo;
+import android.view.autofill.AutofillValue;
+
+import java.util.Arrays;
+import java.util.Set;
+
+public final class Util {
+
+ public static final String TAG = "AutofillSample";
+ public static final boolean DEBUG = true;
+ public static final boolean VERBOSE = false;
+ public static final String EXTRA_DATASET_NAME = "dataset_name";
+ public static final String EXTRA_FOR_RESPONSE = "for_response";
+
+ private static void bundleToString(StringBuilder builder, Bundle data) {
+ final Set<String> keySet = data.keySet();
+ builder.append("[Bundle with ").append(keySet.size()).append(" keys:");
+ for (String key : keySet) {
+ builder.append(' ').append(key).append('=');
+ Object value = data.get(key);
+ if ((value instanceof Bundle)) {
+ bundleToString(builder, (Bundle) value);
+ } else {
+ builder.append((value instanceof Object[])
+ ? Arrays.toString((Object[]) value) : value);
+ }
+ }
+ builder.append(']');
+ }
+
+ public static String bundleToString(Bundle data) {
+ if (data == null) {
+ return "N/A";
+ }
+ final StringBuilder builder = new StringBuilder();
+ bundleToString(builder, data);
+ return builder.toString();
+ }
+
+ public static String getAutofillTypeAsString(int type) {
+ switch (type) {
+ case View.AUTOFILL_TYPE_TEXT:
+ return "TYPE_TEXT";
+ case View.AUTOFILL_TYPE_LIST:
+ return "TYPE_LIST";
+ case View.AUTOFILL_TYPE_NONE:
+ return "TYPE_NONE";
+ case View.AUTOFILL_TYPE_TOGGLE:
+ return "TYPE_TOGGLE";
+ case View.AUTOFILL_TYPE_DATE:
+ return "TYPE_DATE";
+ }
+ return "UNKNOWN_TYPE";
+ }
+
+ private static String getAutofillValueAndTypeAsString(AutofillValue value) {
+ if (value == null) return "null";
+
+ StringBuilder builder = new StringBuilder(value.toString()).append('(');
+ if (value.isText()) {
+ builder.append("isText");
+ } else if (value.isDate()) {
+ builder.append("isDate");
+ } else if (value.isToggle()) {
+ builder.append("isToggle");
+ } else if (value.isList()) {
+ builder.append("isList");
+ }
+ return builder.append(')').toString();
+ }
+
+ public static void dumpStructure(AssistStructure structure) {
+ int nodeCount = structure.getWindowNodeCount();
+ Log.v(TAG, "dumpStructure(): component=" + structure.getActivityComponent()
+ + " numberNodes=" + nodeCount);
+ for (int i = 0; i < nodeCount; i++) {
+ Log.v(TAG, "node #" + i);
+ WindowNode node = structure.getWindowNodeAt(i);
+ dumpNode(" ", node.getRootViewNode());
+ }
+ }
+
+ private static void dumpNode(String prefix, ViewNode node) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(prefix)
+ .append("autoFillId: ").append(node.getAutofillId())
+ .append("\tidEntry: ").append(node.getIdEntry())
+ .append("\tid: ").append(node.getId())
+ .append("\tclassName: ").append(node.getClassName())
+ .append('\n');
+
+ builder.append(prefix)
+ .append("focused: ").append(node.isFocused())
+ .append("\tvisibility").append(node.getVisibility())
+ .append("\tchecked: ").append(node.isChecked())
+ .append("\twebDomain: ").append(node.getWebDomain())
+ .append("\thint: ").append(node.getHint())
+ .append('\n');
+
+ HtmlInfo htmlInfo = node.getHtmlInfo();
+
+ if (htmlInfo != null) {
+ builder.append(prefix)
+ .append("HTML TAG: ").append(htmlInfo.getTag())
+ .append(" attrs: ").append(htmlInfo.getAttributes())
+ .append('\n');
+ }
+
+ String[] afHints = node.getAutofillHints();
+ CharSequence[] options = node.getAutofillOptions();
+ builder.append(prefix).append("afType: ").append(getAutofillTypeAsString(node.getAutofillType()))
+ .append("\tafValue:")
+ .append(getAutofillValueAndTypeAsString(node.getAutofillValue()))
+ .append("\tafOptions:").append(options == null ? "N/A" : Arrays.toString(options))
+ .append("\tafHints: ").append(afHints == null ? "N/A" : Arrays.toString(afHints))
+ .append("\tinputType:").append(node.getInputType())
+ .append('\n');
+
+ int numberChildren = node.getChildCount();
+ builder.append(prefix).append("# children: ").append(numberChildren)
+ .append("\ttext: ").append(node.getText())
+ .append('\n');
+
+ Log.v(TAG, builder.toString());
+ final String prefix2 = prefix + " ";
+ for (int i = 0; i < numberChildren; i++) {
+ Log.v(TAG, prefix + "child #" + i);
+ dumpNode(prefix2, node.getChildAt(i));
+ }
+ }
+}
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/BadViewStructureCreationSignInActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/BadViewStructureCreationSignInActivity.java
new file mode 100644
index 0000000..ddc5798
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/BadViewStructureCreationSignInActivity.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2018 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.autofill.app.antipatterns;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.WelcomeActivity;
+
+/**
+ * This activity behaves the same as
+ * {@link com.example.android.autofill.app.commoncases.StandardSignInActivity}, except that it
+ * creates its view structure on {@link #onStart()}.
+ *
+ * <p>This is a bad pattern anyways—the view structure should be created on
+ * {@code onCreate()}—but it's aggravatted on autofill because when an autofill service
+ * requires authentication, the Android System launches a new activity to handle authentication
+ * using this activity's task. When the authentication acivity finishes, this activity is
+ * resumed, hence if {@link #onStart()} (or {@code onResume()}) re-generates the view structure,
+ * it invalidates the response sent by the autofill service, which triggers a new autofill request
+ * when a field is focused again.
+ */
+public class BadViewStructureCreationSignInActivity extends AppCompatActivity {
+
+ private EditText mUsernameEditText;
+ private EditText mPasswordEditText;
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ setContentView(R.layout.login_activity);
+ mUsernameEditText = findViewById(R.id.usernameField);
+ mPasswordEditText = findViewById(R.id.passwordField);
+ findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ login();
+ }
+ });
+ findViewById(R.id.clear).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AutofillManager afm = getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.cancel();
+ }
+ resetFields();
+ }
+ });
+ }
+
+ private void resetFields() {
+ mUsernameEditText.setText("");
+ mPasswordEditText.setText("");
+ }
+
+ /**
+ * Emulates a login action.
+ */
+ private void login() {
+ String username = mUsernameEditText.getText().toString();
+ String password = mPasswordEditText.getText().toString();
+ boolean valid = isValidCredentials(username, password);
+ if (valid) {
+ Intent intent = WelcomeActivity
+ .getStartActivityIntent(BadViewStructureCreationSignInActivity.this);
+ startActivity(intent);
+ finish();
+ } else {
+ Toast.makeText(this, "Authentication failed.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * Dummy implementation for demo purposes. A real service should use secure mechanisms to
+ * authenticate users.
+ */
+ public boolean isValidCredentials(String username, String password) {
+ return username != null && password != null && username.equalsIgnoreCase(password);
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/CommonCasesFragment.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/CommonCasesFragment.java
new file mode 100644
index 0000000..ebdb3fc
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/CommonCasesFragment.java
@@ -0,0 +1,39 @@
+/*
+* 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.autofill.app.commoncases;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.example.android.autofill.app.BaseMainFragment;
+import com.example.android.autofill.app.R;
+
+public class CommonCasesFragment extends BaseMainFragment {
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_common_cases, container, false);
+ }
+
+ @Override
+ public int getPageTitleResId() {
+ return R.string.common_cases_page_title;
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/CreditCardCompoundViewActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/CreditCardCompoundViewActivity.java
new file mode 100644
index 0000000..b8df27d
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/CreditCardCompoundViewActivity.java
@@ -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.autofill.app.commoncases;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.widget.EditText;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.WelcomeActivity;
+import com.example.android.autofill.app.view.autofillable.CreditCardExpirationDateCompoundView;
+
+public class CreditCardCompoundViewActivity extends AppCompatActivity {
+
+ private CreditCardExpirationDateCompoundView mCcExpDateView;
+ private EditText mCcExpNumber;
+ private EditText mCcSecurityCode;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.credit_card_compound_view_activity);
+ mCcExpDateView = findViewById(R.id.creditCardExpirationView);
+ mCcExpNumber = findViewById(R.id.creditCardNumberField);
+ mCcSecurityCode = findViewById(R.id.creditCardSecurityCode);
+ findViewById(R.id.submitButton).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ submit();
+ }
+ });
+ findViewById(R.id.clearButton).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AutofillManager afm = getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.cancel();
+ }
+ resetFields();
+ }
+ });
+ }
+
+ private void resetFields() {
+ mCcExpDateView.reset();
+ mCcExpNumber.setText("");
+ mCcSecurityCode.setText("");
+ }
+
+ /**
+ * Launches new Activity and finishes, triggering an autofill save request if the user entered
+ * any new data.
+ */
+ private void submit() {
+ Intent intent = WelcomeActivity.getStartActivityIntent(this);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/CreditCardDatePickerActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/CreditCardDatePickerActivity.java
new file mode 100644
index 0000000..4a15886
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/CreditCardDatePickerActivity.java
@@ -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.autofill.app.commoncases;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.widget.EditText;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.WelcomeActivity;
+import com.example.android.autofill.app.view.autofillable.CreditCardExpirationDatePickerView;
+
+import static com.example.android.autofill.app.Util.TAG;
+
+public class CreditCardDatePickerActivity extends AppCompatActivity {
+
+ private CreditCardExpirationDatePickerView mCcExpDateView;
+ private EditText mCcExpNumber;
+ private EditText mCcSecurityCode;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.credit_card_date_picker_activity);
+ mCcExpDateView = findViewById(R.id.creditCardExpirationView);
+ mCcExpNumber = findViewById(R.id.creditCardNumberField);
+ mCcSecurityCode = findViewById(R.id.creditCardSecurityCode);
+ findViewById(R.id.submitButton).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ submit();
+ }
+ });
+ findViewById(R.id.clearButton).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AutofillManager afm = getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.cancel();
+ }
+ resetFields();
+ }
+ });
+
+ mCcExpDateView.reset();
+ }
+
+ private void resetFields() {
+ mCcExpDateView.reset();
+ mCcExpNumber.setText("");
+ mCcSecurityCode.setText("");
+ }
+
+ public void showDatePickerDialog(View v) {
+ if (v != mCcExpDateView) {
+ Log.w(TAG, "showDatePickerDialog() called on invalid view: " + v);
+ return;
+ }
+ mCcExpDateView.showDatePickerDialog(getSupportFragmentManager());
+ }
+
+
+ /**
+ * Launches new Activity and finishes, triggering an autofill save request if the user entered
+ * any new data.
+ */
+ private void submit() {
+ Intent intent = WelcomeActivity.getStartActivityIntent(this);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/CreditCardSpinnersActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/CreditCardSpinnersActivity.java
new file mode 100644
index 0000000..bab5379
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/CreditCardSpinnersActivity.java
@@ -0,0 +1,123 @@
+/*
+ * 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.autofill.app.commoncases;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.Spinner;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.WelcomeActivity;
+
+import java.util.Calendar;
+
+public class CreditCardSpinnersActivity extends AppCompatActivity {
+
+ private static final int CC_EXP_YEARS_COUNT = 5;
+
+ private final String[] years = new String[CC_EXP_YEARS_COUNT];
+
+ private Spinner mCcExpirationDaySpinner;
+ private Spinner mCcExpirationMonthSpinner;
+ private Spinner mCcExpirationYearSpinner;
+ private EditText mCcCardNumber;
+ private EditText mCcSecurityCode;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.credit_card_spinners_activity);
+ mCcExpirationDaySpinner = findViewById(R.id.expirationDay);
+ mCcExpirationMonthSpinner = findViewById(R.id.expirationMonth);
+ mCcExpirationYearSpinner = findViewById(R.id.expirationYear);
+ mCcCardNumber = findViewById(R.id.creditCardNumberField);
+ mCcSecurityCode = findViewById(R.id.creditCardSecurityCode);
+
+ // Create an ArrayAdapter using the string array and a default spinner layout
+ ArrayAdapter<CharSequence> dayAdapter = ArrayAdapter.createFromResource
+ (this, R.array.day_array, android.R.layout.simple_spinner_item);
+ // Specify the layout to use when the list of choices appears
+ dayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ // Apply the adapter to the spinner
+ mCcExpirationDaySpinner.setAdapter(dayAdapter);
+
+ /*
+ R.array.month_array could be an array of Strings like "Jan", "Feb", "March", etc., and
+ the AutofillService would know how to autofill it. However, for the sake of keeping the
+ AutofillService simple, we will stick to a list of numbers (1, 2, ... 12) to represent
+ months; it makes it much easier to generate fake autofill data in the service that can still
+ autofill this spinner.
+ */
+ ArrayAdapter<CharSequence> monthAdapter = ArrayAdapter.createFromResource(
+ this, R.array.month_array, android.R.layout.simple_spinner_item);
+ // Adapter created from resource has getAutofillOptions() implemented by default.
+ monthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mCcExpirationMonthSpinner.setAdapter(monthAdapter);
+
+ int year = Calendar.getInstance().get(Calendar.YEAR);
+ for (int i = 0; i < years.length; i++) {
+ years[i] = Integer.toString(year + i);
+ }
+ // Since the years Spinner uses a custom adapter, it needs to implement getAutofillOptions.
+ mCcExpirationYearSpinner.setAdapter(
+ new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, years) {
+ @Override
+ public CharSequence[] getAutofillOptions() {
+ return years;
+ }
+ });
+ findViewById(R.id.submit).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ submit();
+ }
+ });
+ findViewById(R.id.clear).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AutofillManager afm = getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.cancel();
+ }
+ resetFields();
+ }
+ });
+ }
+
+ private void resetFields() {
+ mCcExpirationDaySpinner.setSelection(0);
+ mCcExpirationMonthSpinner.setSelection(0);
+ mCcExpirationYearSpinner.setSelection(0);
+ mCcCardNumber.setText("");
+ mCcSecurityCode.setText("");
+ }
+
+ /**
+ * Launches new Activity and finishes, triggering an autofill save request if the user entered
+ * any new data.
+ */
+ private void submit() {
+ Intent intent = WelcomeActivity.getStartActivityIntent(CreditCardSpinnersActivity.this);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/EmailComposeActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/EmailComposeActivity.java
new file mode 100644
index 0000000..94a0019
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/EmailComposeActivity.java
@@ -0,0 +1,42 @@
+/*
+ * 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.autofill.app.commoncases;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.WelcomeActivity;
+
+public class EmailComposeActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.email_compose_activity);
+ findViewById(R.id.sendButton).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ startActivity(WelcomeActivity.getStartActivityIntent(EmailComposeActivity.this));
+ finish();
+ }
+ });
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/RecyclerViewActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/RecyclerViewActivity.java
new file mode 100644
index 0000000..c2ce43c
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/RecyclerViewActivity.java
@@ -0,0 +1,358 @@
+/*
+ * 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.autofill.app.commoncases;
+
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.support.annotation.StringRes;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.recyclerview.extensions.ListAdapter;
+import android.support.v7.util.DiffUtil;
+import android.support.v7.widget.DividerItemDecoration;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.WelcomeActivity;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This is mostly a normal Activity containing a RecyclerView. The only difference is, the rows in
+ * the RecyclerView have autofillable fields. Therefore, when we bind data to a recycled view, we
+ * need to also set the {@link AutofillId} on the view.
+ */
+@RequiresApi(28)
+public class RecyclerViewActivity extends AppCompatActivity {
+ private AutofillManager mAfm;
+ private List<FieldMetadata> mFields;
+ private FieldAdapter mFieldAdapter;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.recycler_view_activity);
+ RecyclerView recyclerView = findViewById(R.id.recyclerView);
+ mAfm = getSystemService(AutofillManager.class);
+
+ // Init RecyclerView
+ LinearLayoutManager layoutManager = new LinearLayoutManager(getApplicationContext());
+ DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(
+ recyclerView.getContext(), layoutManager.getOrientation());
+ recyclerView.addItemDecoration(dividerItemDecoration);
+ mFields = initList();
+ mFieldAdapter = new FieldAdapter(mFields);
+ recyclerView.setAdapter(mFieldAdapter);
+ recyclerView.setLayoutManager(layoutManager);
+
+ // Init submit and clear buttons.
+ findViewById(R.id.submitButton).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ submit();
+ }
+ });
+ findViewById(R.id.clearButton).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AutofillManager afm = getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.cancel();
+ }
+ resetFields();
+ }
+ });
+ }
+
+ private void resetFields() {
+ for (FieldMetadata fieldMetadata : mFields) {
+ fieldMetadata.setEnteredText("");
+ }
+ mFieldAdapter.notifyDataSetChanged();
+ }
+
+ private void submit() {
+ Intent intent = WelcomeActivity.getStartActivityIntent(RecyclerViewActivity.this);
+ startActivity(intent);
+ finish();
+ }
+
+ public List<FieldMetadata> initList() {
+ return Arrays.asList(
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ View.AUTOFILL_HINT_NAME,
+ R.string.recycler_view_label_name,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_CLASS_TEXT
+ ),
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ "bday-month",
+ R.string.recycler_view_label_birthday_month,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_CLASS_NUMBER
+ ),
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ View.AUTOFILL_HINT_EMAIL_ADDRESS,
+ R.string.recycler_view_label_email,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ ),
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ View.AUTOFILL_HINT_PHONE,
+ R.string.recycler_view_label_phone,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_CLASS_PHONE
+ ),
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ "tel_extension",
+ R.string.recycler_view_label_tel_extension,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_CLASS_PHONE
+ ),
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ View.AUTOFILL_HINT_CREDIT_CARD_NUMBER,
+ R.string.recycler_view_label_cc_number,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_CLASS_NUMBER
+ ),
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE,
+ R.string.recycler_view_label_cc_sc,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_CLASS_NUMBER
+ ),
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH,
+ R.string.recycler_view_label_cc_exp_month,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_CLASS_NUMBER
+ ),
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR,
+ R.string.recycler_view_label_cc_exp_year,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_CLASS_NUMBER
+ ),
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ "address-line1",
+ R.string.recycler_view_label_address_line_1,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS
+ ),
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ "address-line2",
+ R.string.recycler_view_label_address_line_2,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS
+ ),
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ "address-line3",
+ R.string.recycler_view_label_address_line_3,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS
+ ),
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ View.AUTOFILL_HINT_POSTAL_CODE,
+ R.string.recycler_view_label_postal_code,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_CLASS_NUMBER
+ ),
+ new FieldMetadata(
+ mAfm.getNextAutofillId(),
+ "bday-year",
+ R.string.recycler_view_label_birthday_year,
+ R.drawable.ic_person_black_24dp,
+ InputType.TYPE_CLASS_NUMBER
+ )
+ );
+ }
+
+ static class FieldAdapter extends ListAdapter<FieldMetadata, FieldViewHolder> {
+ private List<FieldMetadata> mFields;
+ public FieldAdapter(List<FieldMetadata> fields) {
+ super(new FieldDiff());
+ mFields = fields;
+ submitList(mFields);
+ }
+
+ @NonNull
+ @Override
+ public FieldViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new FieldViewHolder(LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.user_data_field, parent, false),
+ new FieldWatcher(mFields));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull FieldViewHolder holder, int position) {
+ holder.bind(getItem(position));
+ }
+ }
+
+ static class FieldViewHolder extends RecyclerView.ViewHolder {
+ ImageView mIcon;
+ TextView mLabel;
+ EditText mField;
+ FieldWatcher mWatcher;
+
+ public FieldViewHolder(@NonNull View itemView, FieldWatcher textWatcher) {
+ super(itemView);
+ mIcon = itemView.findViewById(R.id.icon);
+ mLabel = itemView.findViewById(R.id.label);
+ mField = itemView.findViewById(R.id.field);
+ mWatcher = textWatcher;
+ mField.addTextChangedListener(mWatcher);
+ }
+
+ void bind(FieldMetadata fieldMetadata) {
+ mWatcher.updatePosition(getAdapterPosition());
+ Drawable drawable = mIcon.getResources().getDrawable(fieldMetadata.getIconRes());
+ mIcon.setImageDrawable(drawable);
+ mLabel.setText(fieldMetadata.getLabelRes());
+ mField.setAutofillHints(fieldMetadata.getAutofillHint());
+ mField.setInputType(fieldMetadata.getInputType());
+ mField.setText(fieldMetadata.getEnteredText());
+
+ // IMPORTANT: setAutofillId of recycled View.
+ mField.setAutofillId(fieldMetadata.getAutofillId());
+ }
+ }
+
+ /**
+ * TextWatcher implementation to ensure EditTexts get recycled properly.
+ */
+ static class FieldWatcher implements TextWatcher {
+ private int mPosition;
+ private List<FieldMetadata> mFields;
+
+ public FieldWatcher(List<FieldMetadata> fields) {
+ mFields = fields;
+ }
+
+ public void updatePosition(int position) {
+ this.mPosition = position;
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ // NO-OP
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ mFields.get(mPosition).setEnteredText(charSequence);
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ // NO-OP
+ }
+ }
+
+ /**
+ * Model class that holds all of the data needed for a row in the {@link RecyclerView}.
+ */
+ static class FieldMetadata {
+ AutofillId mAutofillId;
+ String mAutofillHint;
+ @StringRes int mLabelRes;
+ @DrawableRes int mIconRes;
+ int mInputType;
+ CharSequence mEnteredText = "";
+
+ FieldMetadata(AutofillId autofillId, String autofillHint, @StringRes int labelRes,
+ @DrawableRes int iconRes, int inputType) {
+ mAutofillId = autofillId;
+ mAutofillHint = autofillHint;
+ mLabelRes = labelRes;
+ mIconRes = iconRes;
+ mInputType = inputType;
+ }
+
+ public AutofillId getAutofillId() {
+ return mAutofillId;
+ }
+
+ public String getAutofillHint() {
+ return mAutofillHint;
+ }
+
+ public int getLabelRes() {
+ return mLabelRes;
+ }
+
+ public int getIconRes() {
+ return mIconRes;
+ }
+
+ public int getInputType() {
+ return mInputType;
+ }
+
+ public void setEnteredText(CharSequence enteredText) {
+ mEnteredText = enteredText;
+ }
+
+ public CharSequence getEnteredText() {
+ return mEnteredText;
+ }
+ }
+
+ static class FieldDiff extends DiffUtil.ItemCallback<FieldMetadata> {
+ @Override
+ public boolean areItemsTheSame(@NonNull FieldMetadata oldItem,
+ @NonNull FieldMetadata newItem) {
+ return oldItem == newItem;
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull FieldMetadata oldItem,
+ @NonNull FieldMetadata newItem) {
+ return oldItem.equals(newItem);
+ }
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/StandardAutoCompleteSignInActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/StandardAutoCompleteSignInActivity.java
new file mode 100644
index 0000000..089c6b2
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/StandardAutoCompleteSignInActivity.java
@@ -0,0 +1,142 @@
+/*
+ * 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.autofill.app.commoncases;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.WelcomeActivity;
+
+import static com.example.android.autofill.app.Util.TAG;
+
+public class StandardAutoCompleteSignInActivity extends AppCompatActivity {
+ private AutoCompleteTextView mUsernameAutoCompleteField;
+ private TextView mPasswordField;
+ private TextView mLoginButton;
+ private TextView mClearButton;
+ private boolean mAutofillReceived = false;
+ private AutofillManager.AutofillCallback mAutofillCallback;
+ private AutofillManager mAutofillManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.login_with_autocomplete_activity);
+
+ mLoginButton = findViewById(R.id.login);
+ mClearButton = findViewById(R.id.clear);
+ mUsernameAutoCompleteField = findViewById(R.id.usernameField);
+ mPasswordField = findViewById(R.id.passwordField);
+ mLoginButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ login();
+ }
+ });
+ mClearButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AutofillManager afm = getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.cancel();
+ }
+ resetFields();
+ }
+ });
+ mAutofillCallback = new MyAutofillCallback();
+ mAutofillManager = getSystemService(AutofillManager.class);
+ ArrayAdapter<CharSequence> mockAutocompleteAdapter = ArrayAdapter.createFromResource
+ (this, R.array.mock_autocomplete_sign_in_suggestions,
+ android.R.layout.simple_dropdown_item_1line);
+ mUsernameAutoCompleteField.setAdapter(mockAutocompleteAdapter);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mAutofillManager.registerCallback(mAutofillCallback);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mAutofillManager.unregisterCallback(mAutofillCallback);
+ }
+
+ private void resetFields() {
+ mUsernameAutoCompleteField.setText("");
+ mPasswordField.setText("");
+ }
+
+ /**
+ * Emulates a login action.
+ */
+ private void login() {
+ String username = mUsernameAutoCompleteField.getText().toString();
+ String password = mPasswordField.getText().toString();
+ boolean valid = isValidCredentials(username, password);
+ if (valid) {
+ Intent intent = WelcomeActivity.getStartActivityIntent(StandardAutoCompleteSignInActivity.this);
+ startActivity(intent);
+ finish();
+ } else {
+ Toast.makeText(this, "Authentication failed.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * Dummy implementation for demo purposes. A real service should use secure mechanisms to
+ * authenticate users.
+ */
+ public boolean isValidCredentials(String username, String password) {
+ return username != null && password != null && username.equals(password);
+ }
+
+ private class MyAutofillCallback extends AutofillManager.AutofillCallback {
+ @Override
+ public void onAutofillEvent(@NonNull View view, int event) {
+ if (view instanceof AutoCompleteTextView) {
+ switch (event) {
+ case AutofillManager.AutofillCallback.EVENT_INPUT_UNAVAILABLE:
+ // no break on purpose
+ case AutofillManager.AutofillCallback.EVENT_INPUT_HIDDEN:
+ if (!mAutofillReceived) {
+ ((AutoCompleteTextView) view).showDropDown();
+ }
+ break;
+ case AutofillManager.AutofillCallback.EVENT_INPUT_SHOWN:
+ mAutofillReceived = true;
+ ((AutoCompleteTextView) view).setAdapter(null);
+ break;
+ default:
+ Log.d(TAG, "Unexpected callback: " + event);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/StandardSignInActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/StandardSignInActivity.java
new file mode 100644
index 0000000..c333bce
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/StandardSignInActivity.java
@@ -0,0 +1,88 @@
+/*
+ * 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.autofill.app.commoncases;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.WelcomeActivity;
+
+public class StandardSignInActivity extends AppCompatActivity {
+
+ private EditText mUsernameEditText;
+ private EditText mPasswordEditText;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.login_activity);
+ mUsernameEditText = findViewById(R.id.usernameField);
+ mPasswordEditText = findViewById(R.id.passwordField);
+ findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ login();
+ }
+ });
+ findViewById(R.id.clear).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AutofillManager afm = getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.cancel();
+ }
+ resetFields();
+ }
+ });
+ }
+
+ private void resetFields() {
+ mUsernameEditText.setText("");
+ mPasswordEditText.setText("");
+ }
+
+ /**
+ * Emulates a login action.
+ */
+ private void login() {
+ String username = mUsernameEditText.getText().toString();
+ String password = mPasswordEditText.getText().toString();
+ boolean valid = isValidCredentials(username, password);
+ if (valid) {
+ Intent intent = WelcomeActivity.getStartActivityIntent(StandardSignInActivity.this);
+ startActivity(intent);
+ finish();
+ } else {
+ Toast.makeText(this, "Authentication failed.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * Dummy implementation for demo purposes. A real service should use secure mechanisms to
+ * authenticate users.
+ */
+ public boolean isValidCredentials(String username, String password) {
+ return username != null && password != null && username.equalsIgnoreCase(password);
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/VirtualSignInActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/VirtualSignInActivity.java
new file mode 100644
index 0000000..baafbf7
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/VirtualSignInActivity.java
@@ -0,0 +1,102 @@
+/*
+ * 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.autofill.app.commoncases;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.widget.Toast;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.WelcomeActivity;
+import com.example.android.autofill.app.view.autofillable.CustomVirtualView;
+
+/**
+ * Activity that uses a virtual views for Username/Password text fields.
+ */
+public class VirtualSignInActivity extends AppCompatActivity {
+
+ private CustomVirtualView mCustomVirtualView;
+ private AutofillManager mAutofillManager;
+ private CustomVirtualView.Line mUsernameLine;
+ private CustomVirtualView.Line mPasswordLine;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.virtual_login_activity);
+
+ mCustomVirtualView = (CustomVirtualView) findViewById(R.id.custom_view);
+
+ CustomVirtualView.Partition credentialsPartition =
+ mCustomVirtualView.addPartition(getString(R.string.partition_credentials));
+ mUsernameLine = credentialsPartition.addLine("username", View.AUTOFILL_TYPE_TEXT,
+ getString(R.string.username_label),
+ " ", false, View.AUTOFILL_HINT_USERNAME);
+ mPasswordLine = credentialsPartition.addLine("password", View.AUTOFILL_TYPE_TEXT,
+ getString(R.string.password_label),
+ " ", true, View.AUTOFILL_HINT_PASSWORD);
+
+ findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ login();
+ }
+ });
+ findViewById(R.id.clear).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ resetFields();
+ mAutofillManager.cancel();
+ }
+ });
+ mAutofillManager = getSystemService(AutofillManager.class);
+ }
+
+ private void resetFields() {
+ mUsernameLine.reset();
+ mPasswordLine.reset();
+ mCustomVirtualView.postInvalidate();
+ }
+
+ /**
+ * Emulates a login action.
+ */
+ private void login() {
+ String username = mUsernameLine.getText().toString();
+ String password = mPasswordLine.getText().toString();
+ boolean valid = isValidCredentials(username, password);
+ if (valid) {
+ Intent intent = WelcomeActivity.getStartActivityIntent(VirtualSignInActivity.this);
+ startActivity(intent);
+ finish();
+ } else {
+ Toast.makeText(this, "Authentication failed.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * Dummy implementation for demo purposes. A real service should use secure mechanisms to
+ * authenticate users.
+ */
+ public boolean isValidCredentials(String username, String password) {
+ return username != null && password != null && username.equals(password);
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/WebViewSignInActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/WebViewSignInActivity.java
new file mode 100644
index 0000000..9bd09f7
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/WebViewSignInActivity.java
@@ -0,0 +1,57 @@
+/*
+* 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.autofill.app.commoncases;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import com.example.android.autofill.app.R;
+
+import static com.example.android.autofill.app.Util.DEBUG;
+import static com.example.android.autofill.app.Util.TAG;
+
+public class WebViewSignInActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.login_webview_activity);
+
+ WebView webView = findViewById(R.id.webview);
+ WebSettings webSettings = webView.getSettings();
+ webView.setWebViewClient(new WebViewClient());
+ webSettings.setJavaScriptEnabled(true);
+
+ String url = getIntent().getStringExtra("url");
+ if (url == null) {
+ url = "file:///android_res/raw/sample_form.html";
+ }
+ if (DEBUG) Log.d(TAG, "Clearing WebView data");
+ webView.clearHistory();
+ webView.clearFormData();
+ webView.clearCache(true);
+ Log.i(TAG, "Loading URL " + url);
+ webView.loadUrl(url);
+ }
+}
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/AbstractMultipleStepsActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/AbstractMultipleStepsActivity.java
new file mode 100644
index 0000000..2baf335
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/AbstractMultipleStepsActivity.java
@@ -0,0 +1,174 @@
+/*
+ * 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.autofill.app.edgecases;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.example.android.autofill.app.R;
+
+import java.util.Map;
+
+import static com.example.android.autofill.app.Util.DEBUG;
+import static com.example.android.autofill.app.Util.TAG;
+
+/**
+ * Activity that emulates a multiple-steps wizard activity, where each step shows just one
+ * label and input.
+ * <p>
+ * <p>Its's useful to verify how an autofill service handles account creation that takes multiple
+ * steps.
+ */
+
+/*
+ * TODO list
+ * - use ConstraintLayout
+ * - use Fragments instead of replacing views directly
+ * - use custom view and/or layout xml for mSteps
+ */
+abstract class AbstractMultipleStepsActivity extends AppCompatActivity {
+
+ private TextView mStatus;
+ private ViewGroup mContainer;
+
+ private Button mPrev;
+ private Button mNext;
+ private Button mFinish;
+
+ private int mCurrentStep;
+ private boolean mFinished;
+
+ private LinearLayout[] mSteps;
+
+ /**
+ * Gets the mapping from resource id to autofill hints.
+ */
+ protected abstract Map<Integer, String> getStepsMap();
+
+ @Override
+ protected final void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.multiple_steps_activity);
+
+ mStatus = findViewById(R.id.status);
+ mContainer = findViewById(R.id.container);
+ mPrev = findViewById(R.id.prev);
+ mNext = findViewById(R.id.next);
+ mFinish = findViewById(R.id.finish);
+
+ View.OnClickListener onClickListener = (v) -> {
+ if (v == mPrev) {
+ showStep(mCurrentStep - 1);
+ } else if (v == mNext) {
+ showStep(mCurrentStep + 1);
+ } else {
+ finishIt();
+ }
+ };
+ mPrev.setOnClickListener(onClickListener);
+ mNext.setOnClickListener(onClickListener);
+ mFinish.setOnClickListener(onClickListener);
+
+ Map<Integer, String> stepsMap = getStepsMap();
+ if (DEBUG) debug("onCreate(): steps=%s", stepsMap);
+ initializeSteps(stepsMap);
+
+ showStep(0);
+ }
+
+ private void showStep(int i) {
+ if (mFinished || i < 0 || i >= mSteps.length) {
+ warn("Invalid step: %d (finished=%s, range=[%d,%d])",
+ mFinished, i, 0, mSteps.length - 1);
+ return;
+ }
+ View step = mSteps[i];
+ mStatus.setText(getString(R.string.message_showing_step, i));
+ if (DEBUG) debug("Showing step %d", i);
+ if (mContainer.getChildCount() > 0) {
+ mContainer.removeViewAt(0);
+ }
+ mContainer.addView(step);
+ mCurrentStep = i;
+
+ mPrev.setEnabled(mCurrentStep != 0);
+ mNext.setEnabled(mCurrentStep != mSteps.length - 1);
+ }
+
+ private void updateButtons() {
+ mPrev.setEnabled(!mFinished && mCurrentStep != 0);
+ mNext.setEnabled(!mFinished && mCurrentStep != mSteps.length - 1);
+ mFinish.setEnabled(!mFinished);
+ }
+
+ private void finishIt() {
+ StringBuilder message = new StringBuilder(getString(R.string.message_finished))
+ .append("\n\n");
+ for (int i = 0; i < mSteps.length; i++) {
+ TextView label = (TextView) mSteps[i].getChildAt(0);
+ EditText input = (EditText) mSteps[i].getChildAt(1);
+ message.append(getString(R.string.message_step_description, label.getText(), input.getText()))
+ .append('\n');
+ }
+ mStatus.setText(message.toString());
+ mContainer.removeAllViews();
+ mFinished = true;
+ updateButtons();
+ }
+
+ private void initializeSteps(Map<Integer, String> stepsMap) {
+ mSteps = new LinearLayout[stepsMap.size()];
+ int i = 0;
+ for (Map.Entry<Integer, String> entry : stepsMap.entrySet()) {
+ int labelId = entry.getKey();
+ String autofillHints = entry.getValue();
+ if (DEBUG) debug("step %d: %s->%s", i, getString(labelId), autofillHints);
+ mSteps[i++] = newStep(labelId, autofillHints);
+ }
+ }
+
+ private LinearLayout newStep(int labelId, String autofillHints) {
+ LinearLayout layout = new LinearLayout(this);
+ layout.setOrientation(LinearLayout.HORIZONTAL);
+
+ TextView label = new TextView(this);
+ label.setText(labelId);
+ layout.addView(label);
+
+ EditText input = new EditText(this);
+ input.setAutofillHints(autofillHints);
+ input.setWidth(500); // TODO: proper size
+ layout.addView(input);
+
+ return layout;
+ }
+
+ protected void debug(String fmt, Object... args) {
+ Log.d(TAG, getLocalClassName() + "." + String.format(fmt, args));
+ }
+
+ protected void warn(String fmt, Object... args) {
+ Log.w(TAG, getLocalClassName() + "." + String.format(fmt, args));
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/CreditCardActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/CreditCardActivity.java
new file mode 100644
index 0000000..3e68a01
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/CreditCardActivity.java
@@ -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.autofill.app.edgecases;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.widget.EditText;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.WelcomeActivity;
+
+public class CreditCardActivity extends AppCompatActivity {
+
+ private EditText mCcExpDayView;
+ private EditText mCcExpMonthView;
+ private EditText mCcExpYearView;
+ private EditText mCcNumber;
+ private EditText mCcSecurityCode;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.credit_card_activity);
+ mCcExpDayView = findViewById(R.id.expirationDay);
+ mCcExpMonthView = findViewById(R.id.expirationMonth);
+ mCcExpYearView = findViewById(R.id.expirationYear);
+ mCcNumber = findViewById(R.id.creditCardNumberField);
+ mCcSecurityCode = findViewById(R.id.creditCardSecurityCode);
+ findViewById(R.id.submitButton).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ submit();
+ }
+ });
+ findViewById(R.id.clearButton).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AutofillManager afm = getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.cancel();
+ }
+ resetFields();
+ }
+ });
+ }
+
+ private void resetFields() {
+ mCcExpDayView.setText("");
+ mCcExpMonthView.setText("");
+ mCcExpYearView.setText("");
+ mCcNumber.setText("");
+ mCcSecurityCode.setText("");
+ }
+
+ /**
+ * Launches new Activity and finishes, triggering an autofill save request if the user entered
+ * any new data.
+ */
+ private void submit() {
+ Intent intent = WelcomeActivity.getStartActivityIntent(this);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/CreditCardAntiPatternActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/CreditCardAntiPatternActivity.java
new file mode 100644
index 0000000..edffcc0
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/CreditCardAntiPatternActivity.java
@@ -0,0 +1,76 @@
+/*
+ * 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.autofill.app.edgecases;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.widget.EditText;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.WelcomeActivity;
+
+public class CreditCardAntiPatternActivity extends AppCompatActivity {
+
+ private EditText mCcExpDateView;
+ private EditText mCcExpNumber;
+ private EditText mCcSecurityCode;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.credit_card_anti_pattern_activity);
+ mCcExpDateView = findViewById(R.id.creditCardExpirationView);
+ mCcExpNumber = findViewById(R.id.creditCardNumberField);
+ mCcSecurityCode = findViewById(R.id.creditCardSecurityCode);
+ findViewById(R.id.submitButton).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ submit();
+ }
+ });
+ findViewById(R.id.clearButton).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AutofillManager afm = getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.cancel();
+ }
+ resetFields();
+ }
+ });
+ }
+
+ private void resetFields() {
+ mCcExpDateView.setText("");
+ mCcExpNumber.setText("");
+ mCcSecurityCode.setText("");
+ }
+
+ /**
+ * Launches new Activity and finishes, triggering an autofill save request if the user entered
+ * any new data.
+ */
+ private void submit() {
+ Intent intent = WelcomeActivity.getStartActivityIntent(this);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/EdgeCasesFragment.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/EdgeCasesFragment.java
new file mode 100644
index 0000000..917be10
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/EdgeCasesFragment.java
@@ -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.autofill.app.edgecases;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.example.android.autofill.app.BaseMainFragment;
+import com.example.android.autofill.app.R;
+
+public class EdgeCasesFragment extends BaseMainFragment {
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_edge_cases, container, false);
+ }
+
+ @Override
+ public int getPageTitleResId() {
+ return R.string.edge_cases_page_title;
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/MultiplePartitionsActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/MultiplePartitionsActivity.java
new file mode 100644
index 0000000..9e09b45
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/MultiplePartitionsActivity.java
@@ -0,0 +1,114 @@
+/*
+ * 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.autofill.app.edgecases;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.widget.Toast;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.Util;
+import com.example.android.autofill.app.view.autofillable.CustomVirtualView;
+import com.example.android.autofill.app.view.autofillable.ScrollableCustomVirtualView;
+
+/**
+ * Activity used to demonstrated safe partitioning of data.
+ * <p>
+ * <p>It has multiple partitions, but only accepts autofill on each partition at time.
+ */
+/*
+ * TODO list
+ *
+ * - Fix top margin.
+ * - Use a combo box to select if credit card expiration date is expressed as date or text.
+ * - Use a dedicated TextView (instead of Toast) for error messages.
+ * - Use wrap_context to CustomView container.
+ * - Use different background color (or borders) for each partition.
+ * - Add more partitions (like address) - should match same partitions from service.
+ * - Add more hints (like w3c ones) - should match same hints from service.
+ */
+public class MultiplePartitionsActivity extends AppCompatActivity {
+
+ private ScrollableCustomVirtualView mCustomVirtualView;
+ private AutofillManager mAutofillManager;
+ private CustomVirtualView.Partition mCredentialsPartition;
+ private CustomVirtualView.Partition mCcPartition;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.multiple_partitions_activity);
+ mCustomVirtualView = findViewById(R.id.custom_view);
+ mCredentialsPartition =
+ mCustomVirtualView.addPartition(getString(R.string.partition_credentials));
+ mCredentialsPartition.addLine("username", View.AUTOFILL_TYPE_TEXT,
+ getString(R.string.username_label),
+ " ", false, View.AUTOFILL_HINT_USERNAME);
+ mCredentialsPartition.addLine("password", View.AUTOFILL_TYPE_TEXT,
+ getString(R.string.password_label),
+ " ", true, View.AUTOFILL_HINT_PASSWORD);
+ int ccExpirationType = View.AUTOFILL_TYPE_DATE;
+ // TODO: add a checkbox to switch between text / date instead
+ Intent intent = getIntent();
+ if (intent != null) {
+ int newType = intent.getIntExtra("dateType", -1);
+ if (newType != -1) {
+ ccExpirationType = newType;
+ String typeMessage = getString(R.string.message_credit_card_expiration_type,
+ Util.getAutofillTypeAsString(ccExpirationType));
+ // TODO: display type in a header or proper status widget
+ Toast.makeText(getApplicationContext(), typeMessage, Toast.LENGTH_LONG).show();
+ }
+ }
+ mCcPartition = mCustomVirtualView.addPartition(getString(R.string.partition_credit_card));
+ mCcPartition.addLine("ccNumber", View.AUTOFILL_TYPE_TEXT,
+ getString(R.string.credit_card_number_label),
+ " ", true, View.AUTOFILL_HINT_CREDIT_CARD_NUMBER);
+ mCcPartition.addLine("ccDay", View.AUTOFILL_TYPE_TEXT,
+ getString(R.string.credit_card_expiration_day_label),
+ " ", true, View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY);
+ mCcPartition.addLine("ccMonth", ccExpirationType,
+ getString(R.string.credit_card_expiration_month_label),
+ " ", true, View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH);
+ mCcPartition.addLine("ccYear", View.AUTOFILL_TYPE_TEXT,
+ getString(R.string.credit_card_expiration_year_label),
+ " ", true, View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR);
+ mCcPartition.addLine("ccDate", ccExpirationType,
+ getString(R.string.credit_card_expiration_date_label),
+ " ", true, View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE);
+ mCcPartition.addLine("ccSecurityCode", View.AUTOFILL_TYPE_TEXT,
+ getString(R.string.credit_card_security_code_label),
+ " ", true, View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE);
+ mAutofillManager = getSystemService(AutofillManager.class);
+ findViewById(R.id.clear).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ resetFields();
+ mCustomVirtualView.resetPositions();
+ mAutofillManager.cancel();
+ }
+ });
+ }
+
+ private void resetFields() {
+ mCredentialsPartition.reset();
+ mCcPartition.reset();
+ mCustomVirtualView.postInvalidate();
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/MultipleStepsCreditCardActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/MultipleStepsCreditCardActivity.java
new file mode 100644
index 0000000..804b77f
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/MultipleStepsCreditCardActivity.java
@@ -0,0 +1,41 @@
+/*
+ * 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.autofill.app.edgecases;
+
+import android.view.View;
+
+import com.example.android.autofill.app.R;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class MultipleStepsCreditCardActivity extends AbstractMultipleStepsActivity {
+
+ @Override
+ protected Map<Integer, String> getStepsMap() {
+ LinkedHashMap<Integer, String> steps = new LinkedHashMap<>(4);
+ steps.put(R.string.credit_card_number_label,
+ View.AUTOFILL_HINT_CREDIT_CARD_NUMBER);
+ steps.put(R.string.credit_card_expiration_month_label,
+ View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH);
+ steps.put(R.string.credit_card_expiration_year_label,
+ View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR);
+ steps.put(R.string.credit_card_security_code_abbrev_label,
+ View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE);
+ return ImmutableMap.copyOf(steps);
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/MultipleStepsSignInActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/MultipleStepsSignInActivity.java
new file mode 100644
index 0000000..ec45a9c
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/MultipleStepsSignInActivity.java
@@ -0,0 +1,35 @@
+/*
+ * 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.autofill.app.edgecases;
+
+import android.view.View;
+
+import com.example.android.autofill.app.R;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class MultipleStepsSignInActivity extends AbstractMultipleStepsActivity {
+
+ @Override
+ protected Map<Integer, String> getStepsMap() {
+ LinkedHashMap<Integer, String> steps = new LinkedHashMap<>(2);
+ steps.put(R.string.username_label, View.AUTOFILL_HINT_USERNAME);
+ steps.put(R.string.password_label, View.AUTOFILL_HINT_PASSWORD);
+ return ImmutableMap.copyOf(steps);
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/VirtualCompatModeSignInActivity.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/VirtualCompatModeSignInActivity.java
new file mode 100644
index 0000000..d55db82
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/VirtualCompatModeSignInActivity.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2018 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.autofill.app.edgecases;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.widget.Toast;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.WelcomeActivity;
+import com.example.android.autofill.app.commoncases.VirtualSignInActivity;
+import com.example.android.autofill.app.view.autofillable.CustomVirtualView;
+import com.example.android.autofill.app.view.autofillable.CustomVirtualViewCompatMode;
+
+/**
+ * Activity that uses a virtual views for Username/Password text fields but doesn't explicitly
+ * implement the Autofill APIs but Accessibility's.
+ *
+ * <p><b>Note:</b> this class is useful to test an Autofill service that supports Compatibility
+ * Mode; real applications with a virtual structure should explicitly support Autofill by
+ * implementing its APIs as {@link VirtualSignInActivity} does.
+
+ * <p>Useful to test an Autofill service that supports Compatibility Mode.
+ *
+ * <p><b>Note: </b>you must whitelist this app's package for compatibility mode. For exmaple, in
+ * a UNIX-like OS such as Linux, you can run:
+ *
+ * <pre>
+ * adb shell settings put global autofill_compat_mode_allowed_packages \
+ * `echo -n com.example.android.autofill.app[custom_virtual_login_header]:; \
+ * adb shell settings get global autofill_compat_mode_allowed_packages`
+ * </pre>
+ */
+public class VirtualCompatModeSignInActivity extends AppCompatActivity {
+
+ private CustomVirtualViewCompatMode mCustomVirtualView;
+ private AutofillManager mAutofillManager;
+ private CustomVirtualView.Line mUsernameLine;
+ private CustomVirtualView.Line mPasswordLine;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.virtual_compat_mode_login_activity);
+
+ mCustomVirtualView = findViewById(R.id.custom_view);
+
+ CustomVirtualView.Partition credentialsPartition =
+ mCustomVirtualView.addPartition(getString(R.string.partition_credentials));
+ mUsernameLine = credentialsPartition.addLine("username", View.AUTOFILL_TYPE_TEXT,
+ getString(R.string.username_label),
+ " ", false, View.AUTOFILL_HINT_USERNAME);
+ mPasswordLine = credentialsPartition.addLine("password", View.AUTOFILL_TYPE_TEXT,
+ getString(R.string.password_label),
+ " ", true, View.AUTOFILL_HINT_PASSWORD);
+
+ findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ login();
+ }
+ });
+ findViewById(R.id.clear).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ resetFields();
+ mAutofillManager.cancel();
+ }
+ });
+ mAutofillManager = getSystemService(AutofillManager.class);
+ }
+
+ private void resetFields() {
+ mUsernameLine.reset();
+ mPasswordLine.reset();
+ mCustomVirtualView.postInvalidate();
+ }
+
+ /**
+ * Emulates a login action.
+ */
+ private void login() {
+ String username = mUsernameLine.getText().toString();
+ String password = mPasswordLine.getText().toString();
+ boolean valid = isValidCredentials(username, password);
+ if (valid) {
+ Intent intent = WelcomeActivity
+ .getStartActivityIntent(VirtualCompatModeSignInActivity.this);
+ startActivity(intent);
+ finish();
+ } else {
+ Toast.makeText(this, "Authentication failed.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * Dummy implementation for demo purposes. A real service should use secure mechanisms to
+ * authenticate users.
+ */
+ public boolean isValidCredentials(String username, String password) {
+ return username != null && password != null && username.equals(password);
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/AbstractCustomVirtualView.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/AbstractCustomVirtualView.java
new file mode 100644
index 0000000..cb5df83
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/AbstractCustomVirtualView.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2018 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.autofill.app.view.autofillable;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Rect;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.autofill.AutofillValue;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.Util;
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Base class for a custom view that manages its own virtual structure, i.e., this is a leaf
+ * {@link View} in the activity's structure, and it draws its own child UI elements.
+ *
+ * <p>This class only draws the views and provides hooks to integrate them with Android APIs such
+ * as Autofill and Accessibility—its up to the subclass to implement these integration points.
+ */
+abstract class AbstractCustomVirtualView extends View {
+
+ protected static final boolean DEBUG = true;
+ protected static final boolean VERBOSE = false;
+
+ /**
+ * When set, it notifies AutofillManager of focus change as the view scrolls, so the
+ * autofill UI is continually drawn.
+ * <p>
+ * <p>This is janky and incompatible with the way the autofill UI works on native views, but
+ * it's a cool experiment!
+ */
+ private static final boolean DRAW_AUTOFILL_UI_AFTER_SCROLL = false;
+
+ private static final String TAG = "AbstractCustomVirtualView";
+ private static final int DEFAULT_TEXT_HEIGHT_DP = 34;
+ private static final int VERTICAL_GAP = 10;
+ private static final int UNFOCUSED_COLOR = Color.BLACK;
+ private static final int FOCUSED_COLOR = Color.RED;
+ private static int sNextId;
+ protected final ArrayList<Line> mVirtualViewGroups = new ArrayList<>();
+ protected final SparseArray<Item> mVirtualViews = new SparseArray<>();
+ private final ArrayMap<String, Partition> mPartitionsByName = new ArrayMap<>();
+ protected Line mFocusedLine;
+ protected int mTopMargin;
+ protected int mLeftMargin;
+ private Paint mTextPaint;
+ private int mTextHeight;
+ private int mLineLength;
+
+ protected AbstractCustomVirtualView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mTextPaint = new Paint();
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomVirtualView,
+ defStyleAttr, defStyleRes);
+ int defaultHeight =
+ (int) (DEFAULT_TEXT_HEIGHT_DP * getResources().getDisplayMetrics().density);
+ mTextHeight = typedArray.getDimensionPixelSize(
+ R.styleable.CustomVirtualView_internalTextSize, defaultHeight);
+ typedArray.recycle();
+ resetCoordinates();
+ }
+
+ protected Item getItem(int id) {
+ final Item item = mVirtualViews.get(id);
+ Preconditions.checkArgument(item != null, "No item for id %s: %s", id, mVirtualViews);
+ return item;
+ }
+
+ protected void resetCoordinates() {
+ mTextPaint.setStyle(Style.FILL);
+ mTextPaint.setTextSize(mTextHeight);
+ mTopMargin = getPaddingTop();
+ mLeftMargin = getPaddingStart();
+ mLineLength = mTextHeight + VERTICAL_GAP;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (VERBOSE) {
+ Log.v(TAG, "onDraw(): " + mVirtualViewGroups.size() + " lines; canvas:" + canvas);
+ }
+ float x;
+ float y = mTopMargin + mLineLength;
+ for (int i = 0; i < mVirtualViewGroups.size(); i++) {
+ Line line = mVirtualViewGroups.get(i);
+ x = mLeftMargin;
+ if (VERBOSE) Log.v(TAG, "Drawing '" + line + "' at " + x + "x" + y);
+ mTextPaint.setColor(line.mFieldTextItem.focused ? FOCUSED_COLOR : UNFOCUSED_COLOR);
+ String readOnlyText = line.mLabelItem.text + ": [";
+ String writeText = line.mFieldTextItem.text + "]";
+ // Paints the label first...
+ canvas.drawText(readOnlyText, x, y, mTextPaint);
+ // ...then paints the edit text and sets the proper boundary
+ float deltaX = mTextPaint.measureText(readOnlyText);
+ x += deltaX;
+ line.mBounds.set((int) x, (int) (y - mLineLength),
+ (int) (x + mTextPaint.measureText(writeText)), (int) y);
+ if (VERBOSE) Log.v(TAG, "setBounds(" + x + ", " + y + "): " + line.mBounds);
+ canvas.drawText(writeText, x, y, mTextPaint);
+ y += mLineLength;
+
+ if (DRAW_AUTOFILL_UI_AFTER_SCROLL) {
+ line.notifyFocusChanged();
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ int y = (int) event.getY();
+ onMotion(y);
+ return super.onTouchEvent(event);
+ }
+
+ /**
+ * Handles a motion event.
+ *
+ * @param y y coordinate.
+ */
+ protected void onMotion(int y) {
+ if (DEBUG) {
+ Log.d(TAG, "onMotion(): y=" + y + ", range=" + mLineLength + ", top=" + mTopMargin);
+ }
+ int lowerY = mTopMargin;
+ int upperY = -1;
+ for (int i = 0; i < mVirtualViewGroups.size(); i++) {
+ Line line = mVirtualViewGroups.get(i);
+ upperY = lowerY + mLineLength;
+ if (DEBUG) Log.d(TAG, "Line " + i + " ranges from " + lowerY + " to " + upperY);
+ if (lowerY <= y && y <= upperY) {
+ if (mFocusedLine != null) {
+ Log.d(TAG, "Removing focus from " + mFocusedLine);
+ mFocusedLine.changeFocus(false);
+ }
+ Log.d(TAG, "Changing focus to " + line);
+ mFocusedLine = line;
+ mFocusedLine.changeFocus(true);
+ invalidate();
+ break;
+ }
+ lowerY += mLineLength;
+ }
+ }
+
+ /**
+ * Creates a new partition with the given name.
+ *
+ * @throws IllegalArgumentException if such partition already exists.
+ */
+ public Partition addPartition(String name) {
+ Preconditions.checkNotNull(name, "Name cannot be null.");
+ Preconditions.checkArgument(!mPartitionsByName.containsKey(name),
+ "Partition with such name already exists.");
+ Partition partition = new Partition(name);
+ mPartitionsByName.put(name, partition);
+ return partition;
+ }
+
+
+ protected abstract void notifyFocusGained(int virtualId, Rect bounds);
+
+ protected abstract void notifyFocusLost(int virtualId);
+
+ protected void onLineAdded(int id, Partition partition) {
+ if (VERBOSE) Log.v(TAG, "onLineAdded: id=" + id + ", partition=" + partition);
+ }
+
+ protected void showError(String message) {
+ showMessage(true, message);
+ }
+
+ protected void showMessage(String message) {
+ showMessage(false, message);
+ }
+
+ private void showMessage(boolean warning, String message) {
+ if (warning) {
+ Log.w(TAG, message);
+ } else {
+ Log.i(TAG, message);
+ }
+ Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
+ }
+
+ protected static final class Item {
+ public final int id;
+ public final String idEntry;
+ public final Line line;
+ public final boolean editable;
+ public final boolean sanitized;
+ public final String[] hints;
+ public final int type;
+ public CharSequence text;
+ public boolean focused = false;
+ public long date;
+ private TextWatcher mListener;
+
+ Item(Line line, int id, String idEntry, String[] hints, int type, CharSequence text,
+ boolean editable, boolean sanitized) {
+ this.line = line;
+ this.id = id;
+ this.idEntry = idEntry;
+ this.text = text;
+ this.editable = editable;
+ this.sanitized = sanitized;
+ this.hints = hints;
+ this.type = type;
+ }
+
+ @Override
+ public String toString() {
+ return id + "/" + idEntry + ": "
+ + (type == AUTOFILL_TYPE_DATE ? date : text) // TODO: use DateFormat for date
+ + " (" + Util.getAutofillTypeAsString(type) + ")"
+ + (editable ? " (editable)" : " (read-only)"
+ + (sanitized ? " (sanitized)" : " (sensitive"))
+ + (hints == null ? " (no hints)" : " ( " + Arrays.toString(hints) + ")");
+ }
+
+ protected String getClassName() {
+ return editable ? EditText.class.getName() : TextView.class.getName();
+ }
+
+ protected AutofillValue getAutofillValue() {
+ switch (type) {
+ case AUTOFILL_TYPE_TEXT:
+ return (TextUtils.getTrimmedLength(text) > 0)
+ ? AutofillValue.forText(text)
+ : null;
+ case AUTOFILL_TYPE_DATE:
+ return AutofillValue.forDate(date);
+ default:
+ return null;
+ }
+ }
+
+ protected AccessibilityNodeInfo provideAccessibilityNodeInfo(View parent, Context context) {
+ final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
+ node.setSource(parent, id);
+ node.setPackageName(context.getPackageName());
+ node.setClassName(getClassName());
+ node.setEditable(editable);
+ node.setViewIdResourceName(idEntry);
+ node.setVisibleToUser(true);
+ final Rect absBounds = line.getAbsCoordinates();
+ if (absBounds != null) {
+ node.setBoundsInScreen(absBounds);
+ }
+ if (TextUtils.getTrimmedLength(text) > 0) {
+ // TODO: Must checked trimmed length because input fields use 8 empty spaces to
+ // set width
+ node.setText(text);
+ }
+ return node;
+ }
+
+ protected void setText(CharSequence value) {
+ if (!editable) {
+ Log.w(TAG, "Item for id " + id + " is not editable: " + this);
+ return;
+ }
+ text = value;
+ if (mListener != null) {
+ Log.d(TAG, "Notify listener: " + text);
+ mListener.onTextChanged(text, 0, 0, 0);
+ }
+ }
+
+ }
+
+ /**
+ * A partition represents a logical group of items, such as credit card info.
+ */
+ public final class Partition {
+ protected final String mName;
+ protected final SparseArray<Line> mLines = new SparseArray<>();
+
+ private Partition(String name) {
+ mName = name;
+ }
+
+ /**
+ * Adds a new line (containining a label and an input field) to the view.
+ *
+ * @param idEntryPrefix id prefix used to identify the line - label node will be suffixed
+ * with {@code Label} and editable node with {@code Field}.
+ * @param autofillType {@link View#getAutofillType() autofill type} of the field.
+ * @param label text used in the label.
+ * @param text initial text used in the input field.
+ * @param sensitive whether the input is considered sensitive.
+ * @param autofillHints list of autofill hints.
+ * @return the new line.
+ */
+ public Line addLine(String idEntryPrefix, int autofillType, String label, String text,
+ boolean sensitive, String... autofillHints) {
+ Preconditions.checkArgument(autofillType == AUTOFILL_TYPE_TEXT
+ || autofillType == AUTOFILL_TYPE_DATE, "Unsupported type: " + autofillType);
+ Line line = new Line(idEntryPrefix, autofillType, label, autofillHints, text,
+ !sensitive);
+ mVirtualViewGroups.add(line);
+ int id = line.mFieldTextItem.id;
+ mLines.put(id, line);
+ mVirtualViews.put(line.mLabelItem.id, line.mLabelItem);
+ mVirtualViews.put(id, line.mFieldTextItem);
+ onLineAdded(id, this);
+
+ return line;
+ }
+
+ /**
+ * Resets the value of all items in the partition.
+ */
+ public void reset() {
+ for (int i = 0; i < mLines.size(); i++) {
+ mLines.valueAt(i).reset();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return mName;
+ }
+ }
+
+ /**
+ * A line in the virtual view contains a label and an input field.
+ */
+ public final class Line {
+
+ protected final Item mFieldTextItem;
+ // Boundaries of the text field, relative to the CustomView
+ protected final Rect mBounds = new Rect();
+ protected final Item mLabelItem;
+ protected final int mAutofillType;
+
+ private Line(String idEntryPrefix, int autofillType, String label, String[] hints,
+ String text, boolean sanitized) {
+ this.mAutofillType = autofillType;
+ this.mLabelItem = new Item(this, ++sNextId, idEntryPrefix + "Label", null,
+ AUTOFILL_TYPE_NONE, label, false, true);
+ this.mFieldTextItem = new Item(this, ++sNextId, idEntryPrefix + "Field", hints,
+ autofillType, text, true, sanitized);
+ }
+
+ private void changeFocus(boolean focused) {
+ mFieldTextItem.focused = focused;
+ notifyFocusChanged();
+ }
+
+ void notifyFocusChanged() {
+ if (mFieldTextItem.focused) {
+ Rect absBounds = getAbsCoordinates();
+ if (DEBUG) {
+ Log.d(TAG, "focus gained on " + mFieldTextItem.id + "; absBounds=" + absBounds);
+ }
+ notifyFocusGained(mFieldTextItem.id, absBounds);
+ } else {
+ if (DEBUG) Log.d(TAG, "focus lost on " + mFieldTextItem.id);
+ notifyFocusLost(mFieldTextItem.id);
+ }
+ }
+
+ private Rect getAbsCoordinates() {
+ // Must offset the boundaries so they're relative to the CustomView.
+ int[] offset = new int[2];
+ getLocationOnScreen(offset);
+ Rect absBounds = new Rect(mBounds.left + offset[0],
+ mBounds.top + offset[1],
+ mBounds.right + offset[0], mBounds.bottom + offset[1]);
+ if (VERBOSE) {
+ Log.v(TAG, "getAbsCoordinates() for " + mFieldTextItem.id + ": bounds=" + mBounds
+ + " offset: " + Arrays.toString(offset) + " absBounds: " + absBounds);
+ }
+ return absBounds;
+ }
+
+ /**
+ * Gets the value of the input field text.
+ */
+ public CharSequence getText() {
+ return mFieldTextItem.text;
+ }
+
+ /**
+ * Resets the value of the input field text.
+ */
+ public void reset() {
+ mFieldTextItem.text = " ";
+ }
+
+ @Override
+ public String toString() {
+ return "Label: " + mLabelItem + " Text: " + mFieldTextItem
+ + " Focused: " + mFieldTextItem.focused + " Type: " + mAutofillType;
+ }
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CreditCardExpirationDateCompoundView.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CreditCardExpirationDateCompoundView.java
new file mode 100644
index 0000000..1cee75a
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CreditCardExpirationDateCompoundView.java
@@ -0,0 +1,135 @@
+/*
+ * 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.autofill.app.view.autofillable;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.FrameLayout;
+import android.widget.Spinner;
+
+import com.example.android.autofill.app.R;
+
+import java.util.Calendar;
+
+import static com.example.android.autofill.app.Util.TAG;
+
+/**
+ * A custom view that represents a {@link View#AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE} using
+ * 2 {@link Spinner spinners} to represent the credit card expiration month and year.
+ */
+public class CreditCardExpirationDateCompoundView extends FrameLayout {
+
+ private static final int CC_EXP_YEARS_COUNT = 5;
+
+ private final String[] mYears = new String[CC_EXP_YEARS_COUNT];
+
+ private Spinner mCcExpMonthSpinner;
+ private Spinner mCcExpYearSpinner;
+
+ public CreditCardExpirationDateCompoundView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public CreditCardExpirationDateCompoundView(@NonNull Context context,
+ @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CreditCardExpirationDateCompoundView(@NonNull Context context,
+ @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CreditCardExpirationDateCompoundView(@NonNull final Context context,
+ @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ View rootView = LayoutInflater.from(context).inflate(R.layout.cc_exp_date, this);
+ mCcExpMonthSpinner = rootView.findViewById(R.id.ccExpMonth);
+ mCcExpYearSpinner = rootView.findViewById(R.id.ccExpYear);
+ setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS);
+ ArrayAdapter<CharSequence> monthAdapter = ArrayAdapter.createFromResource
+ (context, R.array.month_array, android.R.layout.simple_spinner_item);
+ monthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mCcExpMonthSpinner.setAdapter(monthAdapter);
+ int year = Calendar.getInstance().get(Calendar.YEAR);
+ for (int i = 0; i < mYears.length; i++) {
+ mYears[i] = Integer.toString(year + i);
+ }
+ mCcExpYearSpinner.setAdapter(new ArrayAdapter<>(context,
+ android.R.layout.simple_spinner_item, mYears));
+ AdapterView.OnItemSelectedListener onItemSelectedListener =
+ new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ context.getSystemService(AutofillManager.class)
+ .notifyValueChanged(CreditCardExpirationDateCompoundView.this);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ };
+ mCcExpMonthSpinner.setOnItemSelectedListener(onItemSelectedListener);
+ mCcExpYearSpinner.setOnItemSelectedListener(onItemSelectedListener);
+ }
+
+ @Override
+ public AutofillValue getAutofillValue() {
+ Calendar calendar = Calendar.getInstance();
+ // Set hours, minutes, seconds, and millis to 0 to ensure getAutofillValue() == the value
+ // set by autofill(). Without this line, the view will not turn yellow when updated.
+ calendar.clear();
+ int year = Integer.parseInt(mCcExpYearSpinner.getSelectedItem().toString());
+ int month = mCcExpMonthSpinner.getSelectedItemPosition();
+ calendar.set(Calendar.YEAR, year);
+ calendar.set(Calendar.MONTH, month);
+ long unixTime = calendar.getTimeInMillis();
+ return AutofillValue.forDate(unixTime);
+ }
+
+ @Override
+ public void autofill(AutofillValue value) {
+ if (!value.isDate()) {
+ Log.w(TAG, "Ignoring autofill() because service sent a non-date value:" + value);
+ return;
+ }
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(value.getDateValue());
+ int month = calendar.get(Calendar.MONTH);
+ int year = calendar.get(Calendar.YEAR);
+ mCcExpMonthSpinner.setSelection(month);
+ mCcExpYearSpinner.setSelection(year - Integer.parseInt(mYears[0]));
+ }
+
+ @Override
+ public int getAutofillType() {
+ return AUTOFILL_TYPE_DATE;
+ }
+
+ public void reset() {
+ mCcExpMonthSpinner.setSelection(0);
+ mCcExpYearSpinner.setSelection(0);
+ }
+}
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CreditCardExpirationDatePickerView.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CreditCardExpirationDatePickerView.java
new file mode 100644
index 0000000..870825a
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CreditCardExpirationDatePickerView.java
@@ -0,0 +1,163 @@
+/*
+ * 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.autofill.app.view.autofillable;
+
+import android.app.DatePickerDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v7.widget.AppCompatEditText;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.autofill.AutofillValue;
+import android.widget.DatePicker;
+
+import com.example.android.autofill.app.R;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import static com.example.android.autofill.app.Util.DEBUG;
+import static com.example.android.autofill.app.Util.TAG;
+
+/**
+ * A custom view that represents a {@link View#AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE} using
+ * a non-editable {@link EditText} that triggers a {@link DatePickerDialog} to represent the
+ * credit card expiration month and year.
+ */
+public class CreditCardExpirationDatePickerView extends AppCompatEditText {
+
+ private static final int CC_EXP_YEARS_COUNT = 5;
+
+ /**
+ * Calendar instance used for month / year calculations. Should be reset before each use.
+ */
+ private final Calendar mTempCalendar;
+
+ private int mMonth;
+ private int mYear;
+
+ public CreditCardExpirationDatePickerView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public CreditCardExpirationDatePickerView(@NonNull Context context,
+ @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CreditCardExpirationDatePickerView(@NonNull Context context,
+ @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ // Use the current date as the initial date in the picker.
+ mTempCalendar = Calendar.getInstance();
+ mYear = mTempCalendar.get(Calendar.YEAR);
+ mMonth = mTempCalendar.get(Calendar.MONTH);
+ }
+
+ /**
+ * Gets a temporary calendar set with the View's year and month.
+ */
+ private Calendar getCalendar() {
+ mTempCalendar.clear();
+ mTempCalendar.set(Calendar.YEAR, mYear);
+ mTempCalendar.set(Calendar.MONTH, mMonth);
+ mTempCalendar.set(Calendar.DATE, 1);
+ return mTempCalendar;
+ }
+
+ @Override
+ public AutofillValue getAutofillValue() {
+ Calendar c = getCalendar();
+ AutofillValue value = AutofillValue.forDate(c.getTimeInMillis());
+ if (DEBUG) Log.d(TAG, "getAutofillValue(): " + value);
+ return value;
+ }
+
+ @Override
+ public void autofill(AutofillValue value) {
+ if (value == null || !value.isDate()) {
+ Log.w(TAG, "autofill(): invalid value " + value);
+ return;
+ }
+ long time = value.getDateValue();
+ mTempCalendar.setTimeInMillis(time);
+ int year = mTempCalendar.get(Calendar.YEAR);
+ int month = mTempCalendar.get(Calendar.MONTH);
+ if (DEBUG) Log.d(TAG, "autofill(" + value + "): " + month + "/" + year);
+ setDate(year, month);
+ }
+
+ private void setDate(int year, int month) {
+ mYear = year;
+ mMonth = month;
+ Date selectedDate = new Date(getCalendar().getTimeInMillis());
+ String dateString = DateFormat.getDateFormat(getContext()).format(selectedDate);
+ setText(dateString);
+ }
+
+ @Override
+ public int getAutofillType() {
+ return AUTOFILL_TYPE_DATE;
+ }
+
+ public void reset() {
+ mTempCalendar.setTimeInMillis(System.currentTimeMillis());
+ setDate(mTempCalendar.get(Calendar.YEAR), mTempCalendar.get(Calendar.MONTH));
+ }
+
+ public void showDatePickerDialog(FragmentManager fragmentManager) {
+ DatePickerFragment newFragment = new DatePickerFragment();
+ newFragment.mParent = this;
+ newFragment.show(fragmentManager, "datePicker");
+ }
+
+ public static class DatePickerFragment extends DialogFragment
+ implements DatePickerDialog.OnDateSetListener {
+
+ private CreditCardExpirationDatePickerView mParent;
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ DatePickerDialog dialog = new DatePickerDialog(getActivity(),
+ R.style.CustomDatePickerDialogTheme, this, mParent.mYear, mParent.mMonth, 1);
+
+ DatePicker datePicker = dialog.getDatePicker();
+
+ // Limit range.
+ Calendar c = mParent.getCalendar();
+ datePicker.setMinDate(c.getTimeInMillis());
+ c.set(Calendar.YEAR, mParent.mYear + CC_EXP_YEARS_COUNT - 1);
+ datePicker.setMaxDate(c.getTimeInMillis());
+
+ // Remove day.
+ datePicker.findViewById(getResources().getIdentifier("day", "id", "android"))
+ .setVisibility(View.GONE);
+ return dialog;
+ }
+
+ @Override
+ public void onDateSet(DatePicker view, int year, int month, int day) {
+ mParent.setDate(year, month);
+ }
+ }
+}
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualView.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualView.java
new file mode 100644
index 0000000..67c8661
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualView.java
@@ -0,0 +1,189 @@
+/*
+ * 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.autofill.app.view.autofillable;
+
+import static com.example.android.autofill.app.Util.bundleToString;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.ArraySet;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewStructure;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.Util;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+/**
+ * A custom View with a virtual structure that implements the Autofill APIs.
+ */
+public class CustomVirtualView extends AbstractCustomVirtualView {
+
+ private static final String TAG = "CustomView";
+
+ protected final AutofillManager mAutofillManager;
+ private final SparseArray<Partition> mPartitionsByAutofillId = new SparseArray<>();
+
+ public CustomVirtualView(Context context) {
+ this(context, null);
+ }
+
+ public CustomVirtualView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CustomVirtualView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CustomVirtualView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mAutofillManager = context.getSystemService(AutofillManager.class);
+ }
+
+ @Override
+ public void autofill(SparseArray<AutofillValue> values) {
+ Context context = getContext();
+
+ // User has just selected a Dataset from the list of autofill suggestions.
+ // The Dataset is comprised of a list of AutofillValues, with each AutofillValue meant
+ // to fill a specific autofillable view. Now we have to update the UI based on the
+ // AutofillValues in the list, but first we make sure all autofilled values belong to the
+ // same partition
+ if (DEBUG) {
+ Log.d(TAG, "autofill(): " + values);
+ }
+
+ // First get the name of all partitions in the values
+ ArraySet<String> partitions = new ArraySet<>();
+ for (int i = 0; i < values.size(); i++) {
+ int id = values.keyAt(i);
+ Partition partition = mPartitionsByAutofillId.get(id);
+ if (partition == null) {
+ showError(context.getString(R.string.message_autofill_no_partitions, id,
+ mPartitionsByAutofillId));
+ return;
+ }
+ partitions.add(partition.mName);
+ }
+
+ // Then make sure they follow the Highlander rule (There can be only one)
+ if (partitions.size() != 1) {
+ showError(context.getString(R.string.message_autofill_blocked, partitions));
+ return;
+ }
+
+ // Finally, autofill it.
+ DateFormat df = android.text.format.DateFormat.getDateFormat(context);
+ for (int i = 0; i < values.size(); i++) {
+ int id = values.keyAt(i);
+ AutofillValue value = values.valueAt(i);
+ Item item = mVirtualViews.get(id);
+
+ if (item == null) {
+ Log.w(TAG, "No item for id " + id);
+ continue;
+ }
+
+ if (!item.editable) {
+ showError(context.getString(R.string.message_autofill_readonly, item.text));
+ continue;
+ }
+
+ // Check if the type was properly set by the autofill service
+ if (DEBUG) {
+ Log.d(TAG, "Validating " + i
+ + ": expectedType=" + Util.getAutofillTypeAsString(item.type)
+ + "(" + item.type + "), value=" + value);
+ }
+ boolean valid = false;
+ if (value.isText() && item.type == AUTOFILL_TYPE_TEXT) {
+ item.text = value.getTextValue();
+ valid = true;
+ } else if (value.isDate() && item.type == AUTOFILL_TYPE_DATE) {
+ item.text = df.format(new Date(value.getDateValue()));
+ valid = true;
+ } else {
+ Log.w(TAG, "Unsupported type: " + value);
+ }
+ if (!valid) {
+ item.text = context.getString(R.string.message_autofill_invalid);
+ }
+ }
+ postInvalidate();
+ showMessage(context.getString(R.string.message_autofill_ok, partitions.valueAt(0)));
+ }
+
+ @Override
+ public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
+ // Build a ViewStructure that will get passed to the AutofillService by the framework
+ // when it is time to find autofill suggestions.
+ structure.setClassName(getClass().getName());
+ int childrenSize = mVirtualViews.size();
+ if (DEBUG) {
+ Log.d(TAG, "onProvideAutofillVirtualStructure(): flags = " + flags + ", items = "
+ + childrenSize + ", extras: " + bundleToString(structure.getExtras()));
+ }
+ int index = structure.addChildCount(childrenSize);
+ // Traverse through the view hierarchy, including virtual child views. For each view, we
+ // need to set the relevant autofill metadata and add it to the ViewStructure.
+ for (int i = 0; i < childrenSize; i++) {
+ Item item = mVirtualViews.valueAt(i);
+ if (DEBUG) {
+ Log.d(TAG, "Adding new child at index " + index + ": " + item);
+ }
+ ViewStructure child = structure.newChild(index);
+ child.setAutofillId(structure.getAutofillId(), item.id);
+ child.setAutofillHints(item.hints);
+ child.setAutofillType(item.type);
+ child.setAutofillValue(item.getAutofillValue());
+ child.setDataIsSensitive(!item.sanitized);
+ child.setFocused(item.focused);
+ child.setVisibility(View.VISIBLE);
+ child.setDimens(item.line.mBounds.left, item.line.mBounds.top, 0, 0,
+ item.line.mBounds.width(), item.line.mBounds.height());
+ child.setId(item.id, getContext().getPackageName(), null, item.idEntry);
+ child.setClassName(item.getClassName());
+ child.setDimens(item.line.mBounds.left, item.line.mBounds.top, 0, 0,
+ item.line.mBounds.width(), item.line.mBounds.height());
+ index++;
+ }
+ }
+
+ @Override
+ protected void notifyFocusGained(int virtualId, Rect bounds) {
+ mAutofillManager.notifyViewEntered(this, virtualId, bounds);
+ }
+
+ @Override
+ protected void notifyFocusLost(int virtualId) {
+ mAutofillManager.notifyViewExited(this, virtualId);
+ }
+
+ @Override
+ protected void onLineAdded(int id, Partition partition) {
+ mPartitionsByAutofillId.put(id, partition);
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualViewCompatMode.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualViewCompatMode.java
new file mode 100644
index 0000000..7dfb6af
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualViewCompatMode.java
@@ -0,0 +1,139 @@
+/*
+ * 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.autofill.app.view.autofillable;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+
+/**
+ * A custom View with a virtual structure that implements the Accessibility APIs.
+ *
+ * <p><b>Note:</b> this class is useful to test an Autofill service that supports Compatibility
+ * Mode; real applications with a virtual structure should explicitly support Autofill by
+ * implementing its APIs as {@link CustomVirtualView} does.
+ */
+public class CustomVirtualViewCompatMode extends AbstractCustomVirtualView {
+
+ private static final String TAG = "CustomVirtualViewCompatMode";
+
+ private final AccessibilityDelegate mAccessibilityDelegate;
+ private final AccessibilityNodeProvider mAccessibilityNodeProvider;
+
+ public CustomVirtualViewCompatMode(Context context) {
+ this(context, null);
+ }
+
+ public CustomVirtualViewCompatMode(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CustomVirtualViewCompatMode(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CustomVirtualViewCompatMode(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
+ @Override
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+ if (DEBUG) {
+ Log.d(TAG, "createAccessibilityNodeInfo(): id=" + virtualViewId);
+ }
+ switch (virtualViewId) {
+ case AccessibilityNodeProvider.HOST_VIEW_ID:
+ return onProvideAutofillCompatModeAccessibilityNodeInfo();
+ default:
+ final Item item = getItem(virtualViewId);
+ return item.provideAccessibilityNodeInfo(CustomVirtualViewCompatMode.this,
+ getContext());
+ }
+ }
+
+ @Override
+ public boolean performAction(int virtualViewId, int action, Bundle arguments) {
+ if (action == AccessibilityNodeInfo.ACTION_SET_TEXT) {
+ final CharSequence text = arguments.getCharSequence(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
+ final Item item = getItem(virtualViewId);
+ item.setText(text);
+ invalidate();
+ return true;
+ }
+
+ return false;
+ }
+ };
+ mAccessibilityDelegate = new AccessibilityDelegate() {
+ @Override
+ public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
+ return mAccessibilityNodeProvider;
+ }
+ };
+ setAccessibilityDelegate(mAccessibilityDelegate);
+ }
+
+ @Override
+ protected void notifyFocusGained(int virtualId, Rect bounds) {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED, virtualId);
+ }
+
+ @Override
+ protected void notifyFocusLost(int virtualId) {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED, virtualId);
+ }
+
+ private void sendAccessibilityEvent(int eventType, int virtualId) {
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(eventType);
+ event.setSource(this, virtualId);
+ event.setEnabled(true);
+ event.setPackageName(getContext().getPackageName());
+ if (VERBOSE) {
+ Log.v(TAG, "sendAccessibilityEvent(" + eventType + ", " + virtualId + "): " + event);
+ }
+ getContext().getSystemService(AccessibilityManager.class).sendAccessibilityEvent(event);
+ }
+
+ private AccessibilityNodeInfo onProvideAutofillCompatModeAccessibilityNodeInfo() {
+ final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
+
+ final String packageName = getContext().getPackageName();
+ node.setPackageName(packageName);
+ node.setClassName(getClass().getName());
+
+ final int childrenSize = mVirtualViews.size();
+ for (int i = 0; i < childrenSize; i++) {
+ final Item item = mVirtualViews.valueAt(i);
+ if (DEBUG) {
+ Log.d(TAG, "Adding new A11Y child with id " + item.id + ": " + item);
+ }
+ node.addChild(this, item.id);
+ }
+ return node;
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/ScrollableCustomVirtualView.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/ScrollableCustomVirtualView.java
new file mode 100644
index 0000000..cdc9d98
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/ScrollableCustomVirtualView.java
@@ -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.autofill.app.view.autofillable;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+
+/**
+ * A version of {@link CustomVirtualView} that uses gesture to provide scrolling.
+ */
+public class ScrollableCustomVirtualView extends CustomVirtualView
+ implements GestureDetector.OnGestureListener {
+
+ private static final String TAG = "ScrollableCustomView";
+
+ private GestureDetector mGestureDetector;
+
+ public ScrollableCustomVirtualView(Context context) {
+ this(context, null);
+ }
+
+ public ScrollableCustomVirtualView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+
+ }
+
+ public ScrollableCustomVirtualView(Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ScrollableCustomVirtualView(Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mGestureDetector = new GestureDetector(context, this);
+ }
+
+ /**
+ * Resets the UI to the intial state.
+ */
+ public void resetPositions() {
+ super.resetCoordinates();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return mGestureDetector.onTouchEvent(event);
+ }
+
+ /*
+ * Methods below implement GestureDetector.OnGestureListener
+ */
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ if (VERBOSE) Log.v(TAG, "onScroll(): " + distanceX + " - " + distanceY);
+ if (mFocusedLine != null) {
+ mAutofillManager.notifyViewExited(this, mFocusedLine.mFieldTextItem.id);
+ }
+ mTopMargin -= distanceY;
+ mLeftMargin -= distanceX;
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent event) {
+ onMotion((int) event.getY());
+ return true;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/widget/InfoButton.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/widget/InfoButton.java
new file mode 100644
index 0000000..92a221f
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/widget/InfoButton.java
@@ -0,0 +1,54 @@
+/*
+ * 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.autofill.app.view.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.AppCompatImageButton;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.example.android.autofill.app.R;
+
+public class InfoButton extends AppCompatImageButton {
+ public InfoButton(Context context) {
+ this(context, null);
+ }
+
+ public InfoButton(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public InfoButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.InfoButton,
+ defStyleAttr, 0);
+ String infoText = typedArray.getString(R.styleable.InfoButton_dialogText);
+ typedArray.recycle();
+ setInfoText(infoText);
+ }
+
+ public void setInfoText(final String infoText) {
+ setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ new AlertDialog.Builder(InfoButton.this.getContext())
+ .setMessage(infoText).create().show();
+ }
+ });
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/widget/NavigationItem.java b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/widget/NavigationItem.java
new file mode 100644
index 0000000..1647277
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/widget/NavigationItem.java
@@ -0,0 +1,98 @@
+/*
+ * 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.autofill.app.view.widget;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.annotation.ColorRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.os.BuildCompat;
+import android.support.v7.widget.CardView;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.example.android.autofill.app.R;
+
+import static com.example.android.autofill.app.Util.TAG;
+
+public class NavigationItem extends FrameLayout {
+ public NavigationItem(Context context) {
+ this(context, null);
+ }
+
+ public NavigationItem(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public NavigationItem(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public NavigationItem(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.NavigationItem,
+ defStyleAttr, defStyleRes);
+ int activityMinSdk = typedArray.getInteger(R.styleable.NavigationItem_minSdk, 26);
+
+ // TODO: Remove BuildCompat.isAtLeastP() check when API 28 is finalized.
+ int deviceMinSdk = BuildCompat.isAtLeastP() ? 28 : Build.VERSION.SDK_INT;
+ if (deviceMinSdk < activityMinSdk) {
+ // If device's SDK is lower than the minSdk specified by the NavigationItem, hide it.
+ setVisibility(View.GONE);
+ return;
+ }
+ String labelText = typedArray.getString(R.styleable.NavigationItem_labelText);
+ String infoText = typedArray.getString(R.styleable.NavigationItem_infoText);
+ Drawable logoDrawable = typedArray.getDrawable(R.styleable.NavigationItem_itemLogo);
+ @ColorRes int colorRes = typedArray.getResourceId(R.styleable.NavigationItem_imageColor, 0);
+ String launchingActivityName = typedArray.getString(R.styleable.NavigationItem_destinationActivityName);
+ int imageColor = ContextCompat.getColor(getContext(), colorRes);
+ typedArray.recycle();
+ View rootView = LayoutInflater.from(context).inflate(R.layout.navigation_item, this);
+ TextView buttonLabel = rootView.findViewById(R.id.buttonLabel);
+ buttonLabel.setText(labelText);
+ if (logoDrawable != null) {
+ Drawable mutatedLogoDrawable = logoDrawable.mutate();
+ mutatedLogoDrawable.setColorFilter(imageColor, PorterDuff.Mode.SRC_IN);
+ buttonLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(mutatedLogoDrawable, null,
+ null, null);
+ }
+ InfoButton infoButton = rootView.findViewById(R.id.infoButton);
+ infoButton.setInfoText(infoText);
+ infoButton.setColorFilter(imageColor);
+ CardView outerView = rootView.findViewById(R.id.cardView);
+ outerView.setOnClickListener((view) -> {
+ if (launchingActivityName != null) {
+ Intent intent = new Intent();
+ intent.setClassName(getContext().getPackageName(), launchingActivityName);
+ context.startActivity(intent);
+ } else {
+ Log.w(TAG, "Launching Activity name not set.");
+ }
+ });
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/res/drawable/ic_disabled_black_24dp.xml b/prebuilts/gradle/AutofillFramework/Application/src/main/res/drawable/ic_disabled_black_24dp.xml
new file mode 100644
index 0000000..60e4cbd
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/res/drawable/ic_disabled_black_24dp.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10 10,-4.5 10,-10S17.5,2 12,2zM4,12c0,-4.4 3.6,-8 8,-8 1.8,0 3.5,0.6 4.9,1.7L5.7,16.9C4.6,15.5 4,13.8 4,12zM12,20c-1.8,0 -3.5,-0.6 -4.9,-1.7L18.3,7.1C19.4,8.5 20,10.2 20,12c0,4.4 -3.6,8 -8,8z"/>
+</vector>
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/res/drawable/ic_format_list_bulleted_black_24dp.xml b/prebuilts/gradle/AutofillFramework/Application/src/main/res/drawable/ic_format_list_bulleted_black_24dp.xml
new file mode 100644
index 0000000..b53c0be
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/res/drawable/ic_format_list_bulleted_black_24dp.xml
@@ -0,0 +1,24 @@
+<!--
+ * 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M4,10.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM4,4.5c-0.83,0 -1.5,0.67 -1.5,1.5S3.17,7.5 4,7.5 5.5,6.83 5.5,6 4.83,4.5 4,4.5zM4,16.5c-0.83,0 -1.5,0.68 -1.5,1.5s0.68,1.5 1.5,1.5 1.5,-0.68 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM7,19h14v-2L7,17v2zM7,13h14v-2L7,11v2zM7,5v2h14L21,5L7,5z"/>
+</vector>
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/fragment_common_cases.xml b/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/fragment_common_cases.xml
new file mode 100644
index 0000000..5b2223c
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/fragment_common_cases.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * 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.
+-->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingEnd="@dimen/activity_horizontal_margin"
+ android:paddingStart="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin">
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/standardViewSignInButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_red_dark"
+ app:infoText="@string/edittext_login_info"
+ app:itemLogo="@drawable/ic_edittexts_logo_24dp"
+ app:labelText="@string/navigation_button_edittext_login_label"
+ app:destinationActivityName="com.example.android.autofill.app.commoncases.StandardSignInActivity" />
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/virtualViewSignInButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_green_dark"
+ app:infoText="@string/custom_virtual_login_info"
+ app:itemLogo="@drawable/ic_custom_virtual_logo_24dp"
+ app:labelText="@string/navigation_button_custom_virtual_view_login_label"
+ app:destinationActivityName="com.example.android.autofill.app.commoncases.VirtualSignInActivity" />
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/creditCardSpinnersButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_orange_dark"
+ app:infoText="@string/spinners_credit_card_info"
+ app:itemLogo="@drawable/ic_spinners_logo_24dp"
+ app:labelText="@string/navigation_button_spinners_credit_card_label"
+ app:destinationActivityName="com.example.android.autofill.app.commoncases.CreditCardSpinnersActivity" />
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/webviewSignInButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_purple"
+ app:infoText="@string/webview_login_info"
+ app:itemLogo="@drawable/ic_web_black_24dp"
+ app:labelText="@string/navigation_button_web_view_login_label"
+ app:destinationActivityName="com.example.android.autofill.app.commoncases.WebViewSignInActivity" />
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/creditCardCompoundViewButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_blue_light"
+ app:infoText="@string/compound_view_credit_card_info"
+ app:itemLogo="@drawable/ic_view_module_black_24dp"
+ app:labelText="@string/navigation_button_compound_view_credit_card_label"
+ app:destinationActivityName="com.example.android.autofill.app.commoncases.CreditCardCompoundViewActivity" />
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/emailComposeButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_red_dark"
+ app:infoText="@string/email_compose_info"
+ app:itemLogo="@drawable/ic_email_black_24dp"
+ app:labelText="@string/navigation_button_email_compose_label"
+ app:destinationActivityName="com.example.android.autofill.app.commoncases.EmailComposeActivity"/>
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/creditCardDatePickerButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_green_light"
+ app:infoText="@string/date_picker_credit_card_info"
+ app:itemLogo="@drawable/ic_view_module_black_24dp"
+ app:labelText="@string/navigation_button_date_picker_credit_card_label"
+ app:destinationActivityName="com.example.android.autofill.app.commoncases.CreditCardDatePickerActivity" />
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/recyclerViewButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_orange_light"
+ app:infoText="@string/recycler_view_info"
+ app:itemLogo="@drawable/ic_format_list_bulleted_black_24dp"
+ app:labelText="@string/navigation_button_recycler_view_label"
+ app:destinationActivityName="com.example.android.autofill.app.commoncases.RecyclerViewActivity"
+ app:minSdk="28"/>
+ </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/fragment_edge_cases.xml b/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/fragment_edge_cases.xml
new file mode 100644
index 0000000..88a353d
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/fragment_edge_cases.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * 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.
+-->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingEnd="@dimen/activity_horizontal_margin"
+ android:paddingStart="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin">
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/standardLoginWithAutoCompleteButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_blue_dark"
+ app:infoText="@string/autocomplete_login_info"
+ app:itemLogo="@drawable/ic_autocomplete_logo_24dp"
+ app:labelText="@string/navigation_button_autocomplete_login_label"
+ app:destinationActivityName="com.example.android.autofill.app.commoncases.StandardAutoCompleteSignInActivity"/>
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/virtualViewSignInButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_green_dark"
+ app:infoText="@string/custom_virtual_compat_mode_login_info"
+ app:itemLogo="@drawable/ic_custom_virtual_logo_24dp"
+ app:labelText="@string/navigation_button_custom_virtual_view_compat_mode_login_label"
+ app:destinationActivityName="com.example.android.autofill.app.edgecases.VirtualCompatModeSignInActivity" />
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/multiplePartitionsButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_green_dark"
+ app:infoText="@string/multiple_partitions"
+ app:itemLogo="@drawable/ic_custom_virtual_logo_24dp"
+ app:labelText="@string/navigation_button_multiple_partitions_label"
+ app:destinationActivityName="com.example.android.autofill.app.edgecases.MultiplePartitionsActivity"/>
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/creditCardButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_orange_dark"
+ app:infoText="@string/credit_card_info"
+ app:itemLogo="@drawable/ic_edittexts_logo_24dp"
+ app:labelText="@string/navigation_button_credit_card_label"
+ app:destinationActivityName="com.example.android.autofill.app.edgecases.CreditCardActivity"/>
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/creditCardAntiPatternButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_red_dark"
+ app:infoText="@string/anti_pattern_credit_card_info"
+ app:itemLogo="@drawable/ic_disabled_black_24dp"
+ app:labelText="@string/navigation_button_anti_pattern_credit_card_label"
+ app:destinationActivityName="com.example.android.autofill.app.edgecases.CreditCardAntiPatternActivity"/>
+
+ <!--TODO finish multistep impl and show this item-->
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/multistepSignInButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_green_light"
+ app:infoText="@string/multi_step_signin_info"
+ app:itemLogo="@drawable/ic_person_black_24dp"
+ app:labelText="@string/navigation_button_multistep_signin_label"
+ app:destinationActivityName="com.example.android.autofill.app.edgecases.MultipleStepsSignInActivity"/>
+
+ <!--TODO finish multistep impl and show this item-->
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/multistepCreditCardButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_purple"
+ app:infoText="@string/multi_step_cc_info"
+ app:itemLogo="@drawable/ic_spinners_logo_24dp"
+ app:labelText="@string/navigation_button_multistep_cc_label"
+ app:destinationActivityName="com.example.android.autofill.app.edgecases.MultipleStepsCreditCardActivity"/>
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/badViewStructureAntiPatternButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_red_dark"
+ app:infoText="@string/anti_pattern_bad_view_structure_info"
+ app:itemLogo="@drawable/ic_disabled_black_24dp"
+ app:labelText="@string/navigation_button_anti_pattern_bad_view_structure_label"
+ app:destinationActivityName="com.example.android.autofill.app.antipatterns.BadViewStructureCreationSignInActivity"/>
+
+ </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/multiple_steps_activity.xml b/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/multiple_steps_activity.xml
new file mode 100644
index 0000000..fe7fd09
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/multiple_steps_activity.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/status"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <LinearLayout
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="200dp"
+ android:orientation="horizontal"></LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/prev"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/previous_label" />
+
+ <Button
+ android:id="@+id/next"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/next_label" />
+
+ <Button
+ android:id="@+id/finish"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/finish_label" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/recycler_view_activity.xml b/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/recycler_view_activity.xml
new file mode 100644
index 0000000..50090ef
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/recycler_view_activity.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:weightSum="1">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/recyclerView"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="0.8"
+ tools:listItem="@layout/user_data_field" />
+
+ <android.support.constraint.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="0.2">
+
+ <TextView
+ android:id="@+id/clearButton"
+ style="@style/Widget.AppCompat.Button.Borderless"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/spacing_normal"
+ android:layout_marginStart="@dimen/spacing_normal"
+ android:layout_marginTop="@dimen/spacing_normal"
+ android:text="@string/clear_label"
+ android:textColor="@android:color/holo_blue_dark"
+ app:layout_constraintEnd_toStartOf="@+id/submitButton"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="parent" />
+
+ <TextView
+ android:id="@+id/submitButton"
+ style="@style/Widget.AppCompat.Button.Borderless"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/spacing_normal"
+ android:layout_marginStart="@dimen/spacing_normal"
+ android:text="@string/submit_label"
+ android:textColor="@android:color/holo_blue_dark"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/clearButton"
+ app:layout_constraintTop_toTopOf="@+id/clearButton" />
+ </android.support.constraint.ConstraintLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/user_data_field.xml b/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/user_data_field.xml
new file mode 100644
index 0000000..26f0b2e
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/user_data_field.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * 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.
+-->
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/activity_vertical_margin"
+ android:layout_marginBottom="@dimen/activity_vertical_margin"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/spacing_normal"
+ android:layout_marginStart="@dimen/spacing_normal"
+ android:layout_marginEnd="@dimen/spacing_normal"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="@+id/field"
+ app:layout_constraintBottom_toBottomOf="@+id/field"
+ tools:background="@drawable/ic_person_black_24dp" />
+
+ <TextView
+ android:id="@+id/label"
+ style="@style/TextAppearance.AppCompat.Body1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/spacing_normal"
+ android:layout_marginEnd="@dimen/spacing_normal"
+ app:layout_constraintBottom_toBottomOf="@+id/field"
+ app:layout_constraintTop_toTopOf="@+id/field"
+ app:layout_constraintStart_toEndOf="@+id/icon"
+ tools:text="@string/recycler_view_label_name" />
+
+ <EditText
+ android:id="@+id/field"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/spacing_normal"
+ android:layout_marginEnd="@dimen/spacing_normal"
+ android:ems="10"
+ app:layout_constraintStart_toEndOf="@+id/label"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Doug" />
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/virtual_compat_mode_login_activity.xml b/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/virtual_compat_mode_login_activity.xml
new file mode 100644
index 0000000..aeeac4c
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/res/layout/virtual_compat_mode_login_activity.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * Copyright (C) 2018 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.
+-->
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin">
+
+ <TextView
+ android:id="@+id/custom_virtual_compat_mode_login_header"
+ style="@style/TextAppearance.AppCompat.Large"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:gravity="center"
+ android:text="@string/navigation_button_custom_virtual_view_compat_mode_login_label"
+ app:layout_constraintEnd_toStartOf="@+id/imageButton"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintHorizontal_chainStyle="spread"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.example.android.autofill.app.view.widget.InfoButton
+ android:id="@+id/imageButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/ic_info_black_24dp"
+ app:dialogText="@string/custom_virtual_login_info"
+ app:layout_constraintBottom_toBottomOf="@+id/custom_virtual_compat_mode_login_header"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/custom_virtual_login_header"
+ app:layout_constraintTop_toTopOf="@+id/custom_virtual_login_header" />
+
+ <com.example.android.autofill.app.view.autofillable.CustomVirtualViewCompatMode
+ android:id="@+id/custom_view"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/custom_view_height"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:paddingEnd="@dimen/spacing_large"
+ android:paddingStart="@dimen/spacing_large"
+ android:paddingTop="@dimen/spacing_large"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/custom_virtual_compat_mode_login_header" />
+
+ <TextView
+ android:id="@+id/clear"
+ style="@style/Widget.AppCompat.Button.Borderless"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/spacing_normal"
+ android:layout_marginTop="@dimen/spacing_normal"
+ android:text="@string/clear_label"
+ android:textColor="@android:color/holo_blue_dark"
+ app:layout_constraintEnd_toStartOf="@+id/login"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/custom_view" />
+
+ <TextView
+ android:id="@+id/login"
+ style="@style/Widget.AppCompat.Button.Borderless"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/spacing_normal"
+ android:layout_marginStart="@dimen/spacing_normal"
+ android:text="@string/login_label"
+ android:textColor="@android:color/holo_blue_dark"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/clear"
+ app:layout_constraintTop_toTopOf="@+id/clear" />
+</android.support.constraint.ConstraintLayout>
diff --git a/prebuilts/gradle/AutofillFramework/Application/src/main/res/values/styles.xml b/prebuilts/gradle/AutofillFramework/Application/src/main/res/values/styles.xml
index e7549ac..653fc84 100644
--- a/prebuilts/gradle/AutofillFramework/Application/src/main/res/values/styles.xml
+++ b/prebuilts/gradle/AutofillFramework/Application/src/main/res/values/styles.xml
@@ -30,4 +30,4 @@
<style name="MyDatePickerStyle" parent="@android:style/Widget.Material.DatePicker">
<item name="android:datePickerMode">spinner</item>
</style>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/androidTest/java/com/example/android/autofill/service/data/source/local/AutofillDaoTest.java b/prebuilts/gradle/AutofillFramework/afservice/src/androidTest/java/com/example/android/autofill/service/data/source/local/AutofillDaoTest.java
new file mode 100644
index 0000000..c76ba43
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/androidTest/java/com/example/android/autofill/service/data/source/local/AutofillDaoTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 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.autofill.service.data.source.local;
+
+import android.arch.persistence.room.Room;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.View;
+
+import com.example.android.autofill.service.data.source.local.db.AutofillDatabase;
+import com.example.android.autofill.service.model.AutofillDataset;
+import com.example.android.autofill.service.model.DatasetWithFilledAutofillFields;
+import com.example.android.autofill.service.model.FilledAutofillField;
+import com.google.common.collect.ImmutableList;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.UUID;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.hasSize;
+
+@RunWith(AndroidJUnit4.class)
+public class AutofillDaoTest {
+ private final AutofillDataset mDataset =
+ new AutofillDataset(UUID.randomUUID().toString(),
+ "dataset-1", InstrumentationRegistry.getContext().getPackageName());
+ private final FilledAutofillField mUsernameField =
+ new FilledAutofillField(mDataset.getId(), View.AUTOFILL_HINT_USERNAME, "login");
+ private final FilledAutofillField mPasswordField =
+ new FilledAutofillField(mDataset.getId(), View.AUTOFILL_HINT_PASSWORD, "password");
+
+ private AutofillDatabase mDatabase;
+
+ @Before
+ public void setup() {
+ // using an in-memory database because the information stored here disappears when the
+ // process is killed
+ mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
+ AutofillDatabase.class).build();
+
+ }
+
+ @After
+ public void closeDb() {
+ mDatabase.close();
+ }
+
+ @Test
+ public void insertFilledAutofillFieldAndGet() {
+ DatasetWithFilledAutofillFields datasetWithFilledAutofillFields =
+ new DatasetWithFilledAutofillFields();
+ datasetWithFilledAutofillFields.autofillDataset = mDataset;
+ datasetWithFilledAutofillFields.filledAutofillFields =
+ Arrays.asList(mUsernameField, mPasswordField);
+ datasetWithFilledAutofillFields.filledAutofillFields
+ .sort(Comparator.comparing(FilledAutofillField::getFieldTypeName));
+
+ // When inserting a page's autofill fields.
+ mDatabase.autofillDao().insertAutofillDataset(mDataset);
+ mDatabase.autofillDao().insertFilledAutofillFields(
+ datasetWithFilledAutofillFields.filledAutofillFields);
+
+ // Represents all hints of all fields on page.
+ List<String> allHints = ImmutableList.of(View.AUTOFILL_HINT_USERNAME,
+ View.AUTOFILL_HINT_PASSWORD);
+ List<DatasetWithFilledAutofillFields> loadedDatasets = mDatabase.autofillDao()
+ .getDatasets(allHints);
+ loadedDatasets.get(0).filledAutofillFields.sort(
+ Comparator.comparing(FilledAutofillField::getFieldTypeName));
+ assertThat(loadedDatasets, contains(datasetWithFilledAutofillFields));
+ assertThat(loadedDatasets, hasSize(1));
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/androidTest/java/com/example/android/autofill/service/data/source/local/LocalDataSourceTest.java b/prebuilts/gradle/AutofillFramework/afservice/src/androidTest/java/com/example/android/autofill/service/data/source/local/LocalDataSourceTest.java
new file mode 100644
index 0000000..2f445c5
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/androidTest/java/com/example/android/autofill/service/data/source/local/LocalDataSourceTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.autofill.service.data.source.local;
+
+import android.arch.persistence.room.Room;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.autofill.service.data.source.local.dao.AutofillDao;
+import com.example.android.autofill.service.data.source.local.db.AutofillDatabase;
+import com.example.android.autofill.service.util.SingleExecutors;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class LocalDataSourceTest {
+
+ private LocalAutofillDataSource mLocalDataSource;
+ private AutofillDatabase mDatabase;
+
+ @Before
+ public void setup() {
+ // using an in-memory database for testing, since it doesn't survive killing the process
+ mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
+ AutofillDatabase.class)
+ .build();
+ AutofillDao tasksDao = mDatabase.autofillDao();
+ SharedPreferences sharedPreferences = InstrumentationRegistry.getContext()
+ .getSharedPreferences(LocalAutofillDataSource.SHARED_PREF_KEY, Context.MODE_PRIVATE);
+ // Make sure that we're not keeping a reference to the wrong instance.
+ LocalAutofillDataSource.clearInstance();
+ mLocalDataSource = LocalAutofillDataSource.getInstance(sharedPreferences,
+ tasksDao, new SingleExecutors());
+ }
+
+ @After
+ public void cleanUp() {
+ try {
+ mDatabase.close();
+ } finally {
+ LocalAutofillDataSource.clearInstance();
+ }
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/androidTest/java/com/example/android/autofill/service/util/SingleExecutors.java b/prebuilts/gradle/AutofillFramework/afservice/src/androidTest/java/com/example/android/autofill/service/util/SingleExecutors.java
new file mode 100644
index 0000000..baac2b1
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/androidTest/java/com/example/android/autofill/service/util/SingleExecutors.java
@@ -0,0 +1,30 @@
+/*
+ * 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.autofill.service.util;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Allow instant execution of tasks.
+ */
+public final class SingleExecutors extends AppExecutors {
+ private static Executor sInstance = Runnable::run;
+
+ public SingleExecutors() {
+ super(sInstance, sInstance, sInstance);
+ }
+}
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/AndroidManifest.xml b/prebuilts/gradle/AutofillFramework/afservice/src/main/AndroidManifest.xml
index 9bfff73..f3ffe2d 100644
--- a/prebuilts/gradle/AutofillFramework/afservice/src/main/AndroidManifest.xml
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/AndroidManifest.xml
@@ -51,6 +51,12 @@
android:name=".simple.MultiStepsService"
android:label="Multiple-steps Service"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
+ android:name=".simple.HeuristicsService"
+ android:label="Heuristics Autofill Service"
+ android:permission="android.permission.BIND_AUTOFILL_SERVICE">
+ <meta-data
+ android:name="android.autofill"
+ android:resource="@xml/heuristics_service"/>
<intent-filter>
<action android:name="android.service.autofill.AutofillService" />
</intent-filter>
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/ClientParser.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/ClientParser.java
new file mode 100644
index 0000000..048c001
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/ClientParser.java
@@ -0,0 +1,72 @@
+/*
+ * 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.autofill.service;
+
+import android.app.assist.AssistStructure;
+import android.support.annotation.NonNull;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+import static android.app.assist.AssistStructure.ViewNode;
+
+/**
+ * Wrapper for {@link AssistStructure} to make it easy to parse.
+ */
+public final class ClientParser {
+ private final List<AssistStructure> mStructures;
+
+ public ClientParser(@NonNull List<AssistStructure> structures) {
+ Preconditions.checkNotNull(structures);
+ mStructures = structures;
+ }
+
+ public ClientParser(@NonNull AssistStructure structure) {
+ this(ImmutableList.of(structure));
+ }
+
+ /**
+ * Traverses through the {@link AssistStructure} and does something at each {@link ViewNode}.
+ *
+ * @param processor contains action to be performed on each {@link ViewNode}.
+ */
+ public void parse(NodeProcessor processor) {
+ for (AssistStructure structure : mStructures) {
+ int nodes = structure.getWindowNodeCount();
+ for (int i = 0; i < nodes; i++) {
+ AssistStructure.ViewNode viewNode = structure.getWindowNodeAt(i).getRootViewNode();
+ traverseRoot(viewNode, processor);
+ }
+ }
+ }
+
+ private void traverseRoot(AssistStructure.ViewNode viewNode, NodeProcessor processor) {
+ processor.processNode(viewNode);
+ int childrenSize = viewNode.getChildCount();
+ if (childrenSize > 0) {
+ for (int i = 0; i < childrenSize; i++) {
+ traverseRoot(viewNode.getChildAt(i), processor);
+ }
+ }
+ }
+
+ public interface NodeProcessor {
+ void processNode(ViewNode node);
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/ManualActivity.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/ManualActivity.java
new file mode 100644
index 0000000..a9acaa5
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/ManualActivity.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2018 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.autofill.service;
+
+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.content.SharedPreferences;
+import android.os.Bundle;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillResponse;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.DividerItemDecoration;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.example.android.autofill.service.data.ClientViewMetadata;
+import com.example.android.autofill.service.data.ClientViewMetadataBuilder;
+import com.example.android.autofill.service.data.DataCallback;
+import com.example.android.autofill.service.data.adapter.DatasetAdapter;
+import com.example.android.autofill.service.data.adapter.ResponseAdapter;
+import com.example.android.autofill.service.data.source.DefaultFieldTypesSource;
+import com.example.android.autofill.service.data.source.local.DefaultFieldTypesLocalJsonSource;
+import com.example.android.autofill.service.data.source.local.LocalAutofillDataSource;
+import com.example.android.autofill.service.data.source.local.dao.AutofillDao;
+import com.example.android.autofill.service.data.source.local.db.AutofillDatabase;
+import com.example.android.autofill.service.model.AutofillDataset;
+import com.example.android.autofill.service.model.DatasetWithFilledAutofillFields;
+import com.example.android.autofill.service.model.FieldType;
+import com.example.android.autofill.service.model.FieldTypeWithHeuristics;
+import com.example.android.autofill.service.model.FilledAutofillField;
+import com.example.android.autofill.service.settings.MyPreferences;
+import com.example.android.autofill.service.util.AppExecutors;
+import com.google.common.collect.ImmutableList;
+import com.google.gson.GsonBuilder;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+
+import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+import static android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE;
+import static android.view.autofill.AutofillManager.EXTRA_AUTHENTICATION_RESULT;
+import static com.example.android.autofill.service.util.Util.logd;
+
+/**
+ * When the user long-presses on an autofillable field and selects "Autofill", this activity is
+ * launched to allow the user to select the dataset.
+ */
+public class ManualActivity extends AppCompatActivity {
+
+ private static final int RC_SELECT_FIELD = 1;
+
+ // Unique id for dataset intents.
+ private static int sDatasetPendingIntentId = 0;
+
+ private LocalAutofillDataSource mLocalAutofillDataSource;
+ private DatasetAdapter mDatasetAdapter;
+ private ResponseAdapter mResponseAdapter;
+ private ClientViewMetadata mClientViewMetadata;
+ private String mPackageName;
+ private Intent mReplyIntent;
+ private MyPreferences mPreferences;
+ private List<DatasetWithFilledAutofillFields> mAllDatasets;
+ private RecyclerView mRecyclerView;
+
+ public static IntentSender getManualIntentSenderForResponse(Context context) {
+ final Intent intent = new Intent(context, ManualActivity.class);
+ return PendingIntent.getActivity(context, 0, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT).getIntentSender();
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.multidataset_service_manual_activity);
+ SharedPreferences sharedPreferences =
+ getSharedPreferences(LocalAutofillDataSource.SHARED_PREF_KEY, Context.MODE_PRIVATE);
+ DefaultFieldTypesSource defaultFieldTypesSource =
+ DefaultFieldTypesLocalJsonSource.getInstance(getResources(),
+ new GsonBuilder().create());
+ AutofillDao autofillDao = AutofillDatabase.getInstance(this,
+ defaultFieldTypesSource, new AppExecutors()).autofillDao();
+ mLocalAutofillDataSource = LocalAutofillDataSource.getInstance(sharedPreferences,
+ autofillDao, new AppExecutors());
+ mPackageName = getPackageName();
+ mPreferences = MyPreferences.getInstance(this);
+ mRecyclerView = findViewById(R.id.suggestionsList);
+ mRecyclerView.addItemDecoration(new DividerItemDecoration(this, VERTICAL));
+ mLocalAutofillDataSource.getAllAutofillDatasets(
+ new DataCallback<List<DatasetWithFilledAutofillFields>>() {
+ @Override
+ public void onLoaded(List<DatasetWithFilledAutofillFields> datasets) {
+ mAllDatasets = datasets;
+ buildAdapter();
+ }
+
+ @Override
+ public void onDataNotAvailable(String msg, Object... params) {
+
+ }
+ });
+ }
+
+ private void buildAdapter() {
+ List<String> datasetIds = new ArrayList<>();
+ List<String> datasetNames = new ArrayList<>();
+ List<List<String>> allFieldTypes = new ArrayList<>();
+ for (DatasetWithFilledAutofillFields dataset : mAllDatasets) {
+ String datasetName = dataset.autofillDataset.getDatasetName();
+ String datasetId = dataset.autofillDataset.getId();
+ List<String> fieldTypes = new ArrayList<>();
+ for (FilledAutofillField filledAutofillField : dataset.filledAutofillFields) {
+ fieldTypes.add(filledAutofillField.getFieldTypeName());
+ }
+ datasetIds.add(datasetId);
+ datasetNames.add(datasetName);
+ allFieldTypes.add(fieldTypes);
+ }
+ AutofillDatasetsAdapter adapter = new AutofillDatasetsAdapter(datasetIds, datasetNames,
+ allFieldTypes, this);
+ mRecyclerView.setAdapter(adapter);
+ }
+
+ @Override
+ public void finish() {
+ if (mReplyIntent != null) {
+ setResult(RESULT_OK, mReplyIntent);
+ } else {
+ setResult(RESULT_CANCELED);
+ }
+ super.finish();
+ }
+
+ private void onFieldSelected(FilledAutofillField field, FieldType fieldType) {
+ DatasetWithFilledAutofillFields datasetWithFilledAutofillFields = new DatasetWithFilledAutofillFields();
+ String newDatasetId = UUID.randomUUID().toString();
+ FilledAutofillField copyOfField = new FilledAutofillField(newDatasetId,
+ field.getFieldTypeName(), field.getTextValue(), field.getDateValue(),
+ field.getToggleValue());
+ String datasetName = "dataset-manual";
+ AutofillDataset autofillDataset = new AutofillDataset(newDatasetId, datasetName, mPackageName);
+ datasetWithFilledAutofillFields.filledAutofillFields = ImmutableList.of(copyOfField);
+ datasetWithFilledAutofillFields.autofillDataset = autofillDataset;
+ Intent intent = getIntent();
+ AssistStructure structure = intent.getParcelableExtra(EXTRA_ASSIST_STRUCTURE);
+ ClientParser clientParser = new ClientParser(structure);
+ mReplyIntent = new Intent();
+ mLocalAutofillDataSource.getFieldTypeByAutofillHints(
+ new DataCallback<HashMap<String, FieldTypeWithHeuristics>>() {
+ @Override
+ public void onLoaded(HashMap<String, FieldTypeWithHeuristics> fieldTypesByAutofillHint) {
+ ClientViewMetadataBuilder builder = new ClientViewMetadataBuilder(clientParser,
+ fieldTypesByAutofillHint);
+ mClientViewMetadata = builder.buildClientViewMetadata();
+ mDatasetAdapter = new DatasetAdapter(clientParser);
+ mResponseAdapter = new ResponseAdapter(ManualActivity.this,
+ mClientViewMetadata, mPackageName, mDatasetAdapter);
+ FillResponse fillResponse = mResponseAdapter.buildResponseForFocusedNode(
+ datasetName, field, fieldType);
+ setResponseIntent(fillResponse);
+ finish();
+ }
+
+ @Override
+ public void onDataNotAvailable(String msg, Object... params) {
+ }
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode != RC_SELECT_FIELD || resultCode != RESULT_OK) {
+ logd("Ignoring requestCode == %d | resultCode == %d", requestCode,
+ resultCode);
+ return;
+ }
+ String datasetId = data.getStringExtra(ManualFieldPickerActivity.EXTRA_SELECTED_FIELD_DATASET_ID);
+ String fieldTypeName = data.getStringExtra(ManualFieldPickerActivity.EXTRA_SELECTED_FIELD_TYPE_NAME);
+ mLocalAutofillDataSource.getFilledAutofillField(datasetId, fieldTypeName, new DataCallback<FilledAutofillField>() {
+ @Override
+ public void onLoaded(FilledAutofillField field) {
+ mLocalAutofillDataSource.getFieldType(field.getFieldTypeName(), new DataCallback<FieldType>() {
+ @Override
+ public void onLoaded(FieldType fieldType) {
+ onFieldSelected(field, fieldType);
+ }
+
+ @Override
+ public void onDataNotAvailable(String msg, Object... params) {
+
+ }
+ });
+ }
+
+ @Override
+ public void onDataNotAvailable(String msg, Object... params) {
+
+ }
+ });
+ }
+
+
+ private void updateHeuristics() {
+// TODO: update heuristics in data source; something like:
+// mLocalAutofillDataSource.getAutofillDataset(mClientViewMetadata.getAllHints(),
+// datasetName, new DataCallback<DatasetWithFilledAutofillFields>() {
+// @Override
+// public void onLoaded(DatasetWithFilledAutofillFields dataset) {
+// String datasetName = dataset.autofillDataset.getDatasetName();
+// RemoteViews remoteViews = RemoteViewsHelper.viewsWithNoAuth(
+// mPackageName, datasetName);
+// setDatasetIntent(mDatasetAdapter.buildDataset(fieldTypesByAutofillHint,
+// dataset, remoteViews));
+// finish();
+// }
+//
+// @Override
+// public void onDataNotAvailable(String msg, Object... params) {
+// logw(msg, params);
+// finish();
+// }
+// });
+ }
+
+ private void setResponseIntent(FillResponse fillResponse) {
+ mReplyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, fillResponse);
+ }
+
+ private void setDatasetIntent(Dataset dataset) {
+ mReplyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, dataset);
+ }
+
+ /**
+ * Adapter for the {@link RecyclerView} that holds a list of datasets.
+ */
+ private static class AutofillDatasetsAdapter extends RecyclerView.Adapter<DatasetViewHolder> {
+
+ private final List<String> mDatasetIds;
+ private final List<String> mDatasetNames;
+ private final List<List<String>> mFieldTypes;
+ private final Activity mActivity;
+
+ AutofillDatasetsAdapter(List<String> datasetIds, List<String> datasetNames,
+ List<List<String>> fieldTypes, Activity activity) {
+ mDatasetIds = datasetIds;
+ mDatasetNames = datasetNames;
+ mFieldTypes = fieldTypes;
+ mActivity = activity;
+ }
+
+ @Override
+ public DatasetViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return DatasetViewHolder.newInstance(parent, mActivity);
+ }
+
+ @Override
+ public void onBindViewHolder(final DatasetViewHolder holder, final int position) {
+ holder.bind(mDatasetIds.get(position), mDatasetNames.get(position),
+ mFieldTypes.get(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return mDatasetNames.size();
+ }
+ }
+
+ /**
+ * Contains views needed in each row of the list of datasets.
+ */
+ private static class DatasetViewHolder extends RecyclerView.ViewHolder {
+ private final View mRootView;
+ private final TextView mDatasetNameText;
+ private final TextView mFieldTypesText;
+ private final Activity mActivity;
+
+ public DatasetViewHolder(View itemView, Activity activity) {
+ super(itemView);
+ mRootView = itemView;
+ mDatasetNameText = itemView.findViewById(R.id.datasetName);
+ mFieldTypesText = itemView.findViewById(R.id.fieldTypes);
+ mActivity = activity;
+ }
+
+ public static DatasetViewHolder newInstance(ViewGroup parent, Activity activity) {
+ return new DatasetViewHolder(LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.dataset_suggestion, parent, false), activity);
+ }
+
+ public void bind(String datasetId, String datasetName, List<String> fieldTypes) {
+ mDatasetNameText.setText(datasetName);
+ String firstFieldType = null;
+ String secondFieldType = null;
+ int numOfFieldTypes = 0;
+ if (fieldTypes != null) {
+ numOfFieldTypes = fieldTypes.size();
+ if (numOfFieldTypes > 0) {
+ firstFieldType = fieldTypes.get(0);
+ }
+ if (numOfFieldTypes > 1) {
+ secondFieldType = fieldTypes.get(1);
+ }
+ }
+ String fieldTypesString;
+ if (numOfFieldTypes == 1) {
+ fieldTypesString = "Contains data for " + firstFieldType + ".";
+ } else if (numOfFieldTypes == 2) {
+ fieldTypesString = "Contains data for " + firstFieldType + " and " + secondFieldType + ".";
+ } else if (numOfFieldTypes > 2) {
+ fieldTypesString = "Contains data for " + firstFieldType + ", " + secondFieldType + ", and more.";
+ } else {
+ fieldTypesString = "Ignore: Contains no data.";
+ }
+ mFieldTypesText.setText(fieldTypesString);
+ mRootView.setOnClickListener((view) -> {
+ Intent intent = ManualFieldPickerActivity.getIntent(mActivity, datasetId);
+ mActivity.startActivityForResult(intent, RC_SELECT_FIELD);
+ });
+ }
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/ManualFieldPickerActivity.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/ManualFieldPickerActivity.java
new file mode 100644
index 0000000..608ac95
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/ManualFieldPickerActivity.java
@@ -0,0 +1,160 @@
+/*
+ * 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.autofill.service;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.DividerItemDecoration;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.example.android.autofill.service.data.DataCallback;
+import com.example.android.autofill.service.data.source.DefaultFieldTypesSource;
+import com.example.android.autofill.service.data.source.local.DefaultFieldTypesLocalJsonSource;
+import com.example.android.autofill.service.data.source.local.LocalAutofillDataSource;
+import com.example.android.autofill.service.data.source.local.dao.AutofillDao;
+import com.example.android.autofill.service.data.source.local.db.AutofillDatabase;
+import com.example.android.autofill.service.model.DatasetWithFilledAutofillFields;
+import com.example.android.autofill.service.model.FilledAutofillField;
+import com.example.android.autofill.service.util.AppExecutors;
+import com.google.gson.GsonBuilder;
+
+import java.util.List;
+
+import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+
+public class ManualFieldPickerActivity extends AppCompatActivity {
+ private static final String EXTRA_DATASET_ID = "extra_dataset_id";
+ public static final String EXTRA_SELECTED_FIELD_DATASET_ID = "selected_field_dataset_id";
+ public static final String EXTRA_SELECTED_FIELD_TYPE_NAME = "selected_field_type_name";
+
+ private LocalAutofillDataSource mLocalAutofillDataSource;
+
+ private RecyclerView mRecyclerView;
+ private TextView mListTitle;
+ private DatasetWithFilledAutofillFields mDataset;
+
+ public static Intent getIntent(Context originContext, String datasetId) {
+ Intent intent = new Intent(originContext, ManualFieldPickerActivity.class);
+ intent.putExtra(EXTRA_DATASET_ID, datasetId);
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_field_picker);
+ SharedPreferences sharedPreferences = getSharedPreferences(
+ LocalAutofillDataSource.SHARED_PREF_KEY, Context.MODE_PRIVATE);
+ DefaultFieldTypesSource defaultFieldTypesSource =
+ DefaultFieldTypesLocalJsonSource.getInstance(getResources(),
+ new GsonBuilder().create());
+ AutofillDao autofillDao = AutofillDatabase.getInstance(this,
+ defaultFieldTypesSource, new AppExecutors()).autofillDao();
+ String datasetId = getIntent().getStringExtra(EXTRA_DATASET_ID);
+ mRecyclerView = findViewById(R.id.fieldsList);
+ mRecyclerView.addItemDecoration(new DividerItemDecoration(this, VERTICAL));
+ mListTitle = findViewById(R.id.listTitle);
+ mLocalAutofillDataSource = LocalAutofillDataSource.getInstance(sharedPreferences,
+ autofillDao, new AppExecutors());
+ mLocalAutofillDataSource.getAutofillDatasetWithId(datasetId,
+ new DataCallback<DatasetWithFilledAutofillFields>() {
+ @Override
+ public void onLoaded(DatasetWithFilledAutofillFields dataset) {
+ mDataset = dataset;
+ if (mDataset != null) {
+ onLoadedDataset();
+ }
+ }
+
+ @Override
+ public void onDataNotAvailable(String msg, Object... params) {
+
+ }
+ });
+ }
+
+ public void onSelectedDataset(FilledAutofillField field) {
+ Intent data = new Intent()
+ .putExtra(EXTRA_SELECTED_FIELD_DATASET_ID, field.getDatasetId())
+ .putExtra(EXTRA_SELECTED_FIELD_TYPE_NAME, field.getFieldTypeName());
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ public void onLoadedDataset() {
+ FieldsAdapter fieldsAdapter = new FieldsAdapter(this, mDataset.filledAutofillFields);
+ mRecyclerView.setAdapter(fieldsAdapter);
+ mListTitle.setText(getString(R.string.manual_data_picker_title,
+ mDataset.autofillDataset.getDatasetName()));
+ }
+
+ private static class FieldsAdapter extends RecyclerView.Adapter<FieldViewHolder> {
+ private final Activity mActivity;
+ private final List<FilledAutofillField> mFields;
+
+ public FieldsAdapter(Activity activity, List<FilledAutofillField> fields) {
+ mActivity = activity;
+ mFields = fields;
+ }
+
+ @Override
+ public FieldViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new FieldViewHolder(LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.dataset_field, parent, false), mActivity);
+ }
+
+ @Override
+ public void onBindViewHolder(FieldViewHolder holder, int position) {
+ FilledAutofillField field = mFields.get(position);
+ holder.bind(field);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mFields.size();
+ }
+ }
+
+ private static class FieldViewHolder extends RecyclerView.ViewHolder {
+ private final View mRootView;
+ private final TextView mFieldTypeText;
+ private final Activity mActivity;
+
+ public FieldViewHolder(View itemView, Activity activity) {
+ super(itemView);
+ mRootView = itemView;
+ mFieldTypeText = itemView.findViewById(R.id.fieldType);
+ mActivity = activity;
+ }
+
+ public void bind(FilledAutofillField field) {
+ mFieldTypeText.setText(field.getFieldTypeName());
+ mRootView.setOnClickListener((view) -> {
+ ((ManualFieldPickerActivity) mActivity).onSelectedDataset(field);
+ });
+ }
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/RemoteViewsHelper.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/RemoteViewsHelper.java
new file mode 100644
index 0000000..b925b95
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/RemoteViewsHelper.java
@@ -0,0 +1,44 @@
+/*
+ * 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.autofill.service;
+
+import android.support.annotation.DrawableRes;
+import android.widget.RemoteViews;
+
+/**
+ * This is a class containing helper methods for building Autofill Datasets and Responses.
+ */
+public final class RemoteViewsHelper {
+ private RemoteViewsHelper() {
+ }
+
+ public static RemoteViews viewsWithAuth(String packageName, String text) {
+ return simpleRemoteViews(packageName, text, R.drawable.ic_lock_black_24dp);
+ }
+
+ public static RemoteViews viewsWithNoAuth(String packageName, String text) {
+ return simpleRemoteViews(packageName, text, R.drawable.ic_person_black_24dp);
+ }
+
+ private static RemoteViews simpleRemoteViews(String packageName, String remoteViewsText,
+ @DrawableRes int drawableId) {
+ RemoteViews presentation = new RemoteViews(packageName,
+ R.layout.multidataset_service_list_item);
+ presentation.setTextViewText(R.id.text, remoteViewsText);
+ presentation.setImageViewResource(R.id.icon, drawableId);
+ return presentation;
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/AutofillDataBuilder.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/AutofillDataBuilder.java
new file mode 100644
index 0000000..0110bdd
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/AutofillDataBuilder.java
@@ -0,0 +1,25 @@
+/*
+ * 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.autofill.service.data;
+
+import com.example.android.autofill.service.model.DatasetWithFilledAutofillFields;
+
+import java.util.List;
+
+public interface AutofillDataBuilder {
+ List<DatasetWithFilledAutofillFields> buildDatasetsByPartition(int datasetNumber);
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/ClientAutofillDataBuilder.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/ClientAutofillDataBuilder.java
new file mode 100644
index 0000000..2d26833
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/ClientAutofillDataBuilder.java
@@ -0,0 +1,160 @@
+/*
+ * 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.autofill.service.data;
+
+import android.app.assist.AssistStructure;
+import android.support.annotation.NonNull;
+import android.view.View;
+import android.view.autofill.AutofillValue;
+
+import com.example.android.autofill.service.AutofillHints;
+import com.example.android.autofill.service.ClientParser;
+import com.example.android.autofill.service.model.AutofillDataset;
+import com.example.android.autofill.service.model.DatasetWithFilledAutofillFields;
+import com.example.android.autofill.service.model.FieldType;
+import com.example.android.autofill.service.model.FieldTypeWithHeuristics;
+import com.example.android.autofill.service.model.FilledAutofillField;
+import com.google.common.collect.ImmutableList;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import static com.example.android.autofill.service.util.Util.loge;
+
+public class ClientAutofillDataBuilder implements AutofillDataBuilder {
+ private final ClientParser mClientParser;
+ private final HashMap<String, FieldTypeWithHeuristics> mFieldTypesByAutofillHint;
+ private final String mPackageName;
+
+ public ClientAutofillDataBuilder(HashMap<String, FieldTypeWithHeuristics> fieldTypesByAutofillHint,
+ String packageName, ClientParser clientParser) {
+ mClientParser = clientParser;
+ mFieldTypesByAutofillHint = fieldTypesByAutofillHint;
+ mPackageName = packageName;
+ }
+
+ @Override
+ public List<DatasetWithFilledAutofillFields> buildDatasetsByPartition(int datasetNumber) {
+ ImmutableList.Builder<DatasetWithFilledAutofillFields> listBuilder =
+ new ImmutableList.Builder<>();
+ for (int partition : AutofillHints.PARTITIONS) {
+ AutofillDataset autofillDataset = new AutofillDataset(UUID.randomUUID().toString(),
+ "dataset-" + datasetNumber + "." + partition, mPackageName);
+ DatasetWithFilledAutofillFields dataset =
+ buildDatasetForPartition(autofillDataset, partition);
+ if (dataset != null && dataset.filledAutofillFields != null) {
+ listBuilder.add(dataset);
+ }
+ }
+ return listBuilder.build();
+ }
+
+ /**
+ * Parses a client view structure and build a dataset (in the form of a
+ * {@link DatasetWithFilledAutofillFields}) from the view metadata found.
+ */
+ private DatasetWithFilledAutofillFields buildDatasetForPartition(AutofillDataset dataset,
+ int partition) {
+ DatasetWithFilledAutofillFields datasetWithFilledAutofillFields =
+ new DatasetWithFilledAutofillFields();
+ datasetWithFilledAutofillFields.autofillDataset = dataset;
+ mClientParser.parse((node) ->
+ parseAutofillFields(node, datasetWithFilledAutofillFields, partition)
+ );
+ return datasetWithFilledAutofillFields;
+
+ }
+
+ private void parseAutofillFields(AssistStructure.ViewNode viewNode,
+ DatasetWithFilledAutofillFields datasetWithFilledAutofillFields, int partition) {
+ String[] hints = viewNode.getAutofillHints();
+ if (hints == null || hints.length == 0) {
+ return;
+ }
+ AutofillValue autofillValue = viewNode.getAutofillValue();
+ String textValue = null;
+ Long dateValue = null;
+ Boolean toggleValue = null;
+ CharSequence[] autofillOptions = null;
+ Integer listIndex = null;
+ if (autofillValue != null) {
+ if (autofillValue.isText()) {
+ // Using toString of AutofillValue.getTextValue in order to save it to
+ // SharedPreferences.
+ textValue = autofillValue.getTextValue().toString();
+ } else if (autofillValue.isDate()) {
+ dateValue = autofillValue.getDateValue();
+ } else if (autofillValue.isList()) {
+ autofillOptions = viewNode.getAutofillOptions();
+ listIndex = autofillValue.getListValue();
+ } else if (autofillValue.isToggle()) {
+ toggleValue = autofillValue.getToggleValue();
+ }
+ }
+ appendViewMetadata(datasetWithFilledAutofillFields,
+ hints, partition, textValue, dateValue, toggleValue,
+ autofillOptions, listIndex);
+ }
+
+ private void appendViewMetadata(@NonNull DatasetWithFilledAutofillFields
+ datasetWithFilledAutofillFields, @NonNull String[] hints, int partition,
+ @Nullable String textValue, @Nullable Long dateValue, @Nullable Boolean toggleValue,
+ @Nullable CharSequence[] autofillOptions, @Nullable Integer listIndex) {
+ for (int i = 0; i < hints.length; i++) {
+ String hint = hints[i];
+ // Then check if the "actual" hint is supported.
+ FieldTypeWithHeuristics fieldTypeWithHeuristics = mFieldTypesByAutofillHint.get(hint);
+ if (fieldTypeWithHeuristics != null) {
+ FieldType fieldType = fieldTypeWithHeuristics.fieldType;
+ if (!AutofillHints.matchesPartition(fieldType.getPartition(), partition)) {
+ continue;
+ }
+ // Only add the field if the hint is supported by the type.
+ if (textValue != null) {
+ if (!fieldType.getAutofillTypes().ints.contains(View.AUTOFILL_TYPE_TEXT)) {
+ loge("Text is invalid type for hint '%s'", hint);
+ }
+ }
+ if (autofillOptions != null && listIndex != null &&
+ autofillOptions.length > listIndex) {
+ if (!fieldType.getAutofillTypes().ints.contains(View.AUTOFILL_TYPE_LIST)) {
+ loge("List is invalid type for hint '%s'", hint);
+ }
+ textValue = autofillOptions[listIndex].toString();
+ }
+ if (dateValue != null) {
+ if (!fieldType.getAutofillTypes().ints.contains(View.AUTOFILL_TYPE_DATE)) {
+ loge("Date is invalid type for hint '%s'", hint);
+ }
+ }
+ if (toggleValue != null) {
+ if (!fieldType.getAutofillTypes().ints.contains(View.AUTOFILL_TYPE_TOGGLE)) {
+ loge("Toggle is invalid type for hint '%s'", hint);
+ }
+ }
+ String datasetId = datasetWithFilledAutofillFields.autofillDataset.getId();
+ datasetWithFilledAutofillFields.add(new FilledAutofillField(datasetId,
+ fieldType.getTypeName(), textValue, dateValue, toggleValue));
+ } else {
+ loge("Invalid hint: %s", hint);
+ }
+ }
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/ClientViewMetadata.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/ClientViewMetadata.java
new file mode 100644
index 0000000..85b789e
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/ClientViewMetadata.java
@@ -0,0 +1,75 @@
+/*
+ * 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.autofill.service.data;
+
+import android.service.autofill.SaveInfo;
+import android.view.autofill.AutofillId;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * In this simple implementation, the only view data we collect from the client are autofill hints
+ * of the views in the view hierarchy, the corresponding autofill IDs, and the {@link SaveInfo}
+ * based on the hints.
+ */
+public class ClientViewMetadata {
+ private final List<String> mAllHints;
+ private final int mSaveType;
+ private final AutofillId[] mAutofillIds;
+ private final String mWebDomain;
+ private final AutofillId[] mFocusedIds;
+
+ public ClientViewMetadata(List<String> allHints, int saveType, AutofillId[] autofillIds,
+ AutofillId[] focusedIds, String webDomain) {
+ mAllHints = allHints;
+ mSaveType = saveType;
+ mAutofillIds = autofillIds;
+ mWebDomain = webDomain;
+ mFocusedIds = focusedIds;
+ }
+
+ public List<String> getAllHints() {
+ return mAllHints;
+ }
+
+ public AutofillId[] getAutofillIds() {
+ return mAutofillIds;
+ }
+
+ public AutofillId[] getFocusedIds() {
+ return mFocusedIds;
+ }
+
+ public int getSaveType() {
+ return mSaveType;
+ }
+
+ public String getWebDomain() {
+ return mWebDomain;
+ }
+
+ @Override public String toString() {
+ return "ClientViewMetadata{" +
+ "mAllHints=" + mAllHints +
+ ", mSaveType=" + mSaveType +
+ ", mAutofillIds=" + Arrays.toString(mAutofillIds) +
+ ", mWebDomain='" + mWebDomain + '\'' +
+ ", mFocusedIds=" + Arrays.toString(mFocusedIds) +
+ '}';
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/ClientViewMetadataBuilder.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/ClientViewMetadataBuilder.java
new file mode 100644
index 0000000..40a67ef
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/ClientViewMetadataBuilder.java
@@ -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.autofill.service.data;
+
+
+import android.app.assist.AssistStructure;
+import android.util.MutableInt;
+import android.view.autofill.AutofillId;
+
+import com.example.android.autofill.service.ClientParser;
+import com.example.android.autofill.service.model.FieldType;
+import com.example.android.autofill.service.model.FieldTypeWithHeuristics;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import static com.example.android.autofill.service.util.Util.logd;
+
+public class ClientViewMetadataBuilder {
+ private ClientParser mClientParser;
+ private HashMap<String, FieldTypeWithHeuristics> mFieldTypesByAutofillHint;
+
+ public ClientViewMetadataBuilder(ClientParser parser,
+ HashMap<String, FieldTypeWithHeuristics> fieldTypesByAutofillHint) {
+ mClientParser = parser;
+ mFieldTypesByAutofillHint = fieldTypesByAutofillHint;
+ }
+
+ public ClientViewMetadata buildClientViewMetadata() {
+ List<String> allHints = new ArrayList<>();
+ MutableInt saveType = new MutableInt(0);
+ List<AutofillId> autofillIds = new ArrayList<>();
+ StringBuilder webDomainBuilder = new StringBuilder();
+ List<AutofillId> focusedAutofillIds = new ArrayList<>();
+ mClientParser.parse((node) -> parseNode(node, allHints, saveType, autofillIds, focusedAutofillIds));
+ mClientParser.parse((node) -> parseWebDomain(node, webDomainBuilder));
+ String webDomain = webDomainBuilder.toString();
+ AutofillId[] autofillIdsArray = autofillIds.toArray(new AutofillId[autofillIds.size()]);
+ AutofillId[] focusedIds = focusedAutofillIds.toArray(new AutofillId[focusedAutofillIds.size()]);
+ return new ClientViewMetadata(allHints, saveType.value, autofillIdsArray, focusedIds, webDomain);
+ }
+
+ private void parseWebDomain(AssistStructure.ViewNode viewNode, StringBuilder validWebDomain) {
+ String webDomain = viewNode.getWebDomain();
+ if (webDomain != null) {
+ logd("child web domain: %s", webDomain);
+ if (validWebDomain.length() > 0) {
+ if (!webDomain.equals(validWebDomain.toString())) {
+ throw new SecurityException("Found multiple web domains: valid= "
+ + validWebDomain + ", child=" + webDomain);
+ }
+ } else {
+ validWebDomain.append(webDomain);
+ }
+ }
+ }
+
+ private void parseNode(AssistStructure.ViewNode root, List<String> allHints,
+ MutableInt autofillSaveType, List<AutofillId> autofillIds,
+ List<AutofillId> focusedAutofillIds) {
+ String[] hints = root.getAutofillHints();
+ if (hints != null) {
+ for (String hint : hints) {
+ FieldTypeWithHeuristics fieldTypeWithHints = mFieldTypesByAutofillHint.get(hint);
+ if (fieldTypeWithHints != null && fieldTypeWithHints.fieldType != null) {
+ allHints.add(hint);
+ autofillSaveType.value |= fieldTypeWithHints.fieldType.getSaveInfo();
+ autofillIds.add(root.getAutofillId());
+ }
+ }
+ }
+ if (root.isFocused()) {
+ focusedAutofillIds.add(root.getAutofillId());
+ }
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/DataCallback.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/DataCallback.java
new file mode 100644
index 0000000..e765602
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/DataCallback.java
@@ -0,0 +1,23 @@
+/*
+ * 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.autofill.service.data;
+
+public interface DataCallback<T> {
+ void onLoaded(T object);
+
+ void onDataNotAvailable(String msg, Object... params);
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/FakeAutofillDataBuilder.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/FakeAutofillDataBuilder.java
new file mode 100644
index 0000000..67ee880
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/FakeAutofillDataBuilder.java
@@ -0,0 +1,75 @@
+/*
+ * 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.autofill.service.data;
+
+import com.example.android.autofill.service.AutofillHints;
+import com.example.android.autofill.service.model.AutofillDataset;
+import com.example.android.autofill.service.model.DatasetWithFilledAutofillFields;
+import com.example.android.autofill.service.model.FieldTypeWithHeuristics;
+import com.example.android.autofill.service.model.FilledAutofillField;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+import java.util.UUID;
+
+public class FakeAutofillDataBuilder implements AutofillDataBuilder {
+ private final List<FieldTypeWithHeuristics> mFieldTypesWithHints;
+ private final String mPackageName;
+ private final int mSeed;
+
+ public FakeAutofillDataBuilder(List<FieldTypeWithHeuristics> fieldTypesWithHints,
+ String packageName, int seed) {
+ mFieldTypesWithHints = fieldTypesWithHints;
+ mSeed = seed;
+ mPackageName = packageName;
+ }
+
+ @Override
+ public List<DatasetWithFilledAutofillFields> buildDatasetsByPartition(int datasetNumber) {
+ ImmutableList.Builder<DatasetWithFilledAutofillFields> listBuilder =
+ new ImmutableList.Builder<>();
+ for (int partition : AutofillHints.PARTITIONS) {
+ AutofillDataset autofillDataset = new AutofillDataset(UUID.randomUUID().toString(),
+ "dataset-" + datasetNumber + "." + partition, mPackageName);
+ DatasetWithFilledAutofillFields datasetWithFilledAutofillFields =
+ buildCollectionForPartition(autofillDataset, partition);
+ if (datasetWithFilledAutofillFields != null &&
+ datasetWithFilledAutofillFields.filledAutofillFields != null &&
+ !datasetWithFilledAutofillFields.filledAutofillFields.isEmpty()) {
+ listBuilder.add(datasetWithFilledAutofillFields);
+ }
+ }
+ return listBuilder.build();
+ }
+
+ private DatasetWithFilledAutofillFields buildCollectionForPartition(
+ AutofillDataset dataset, int partition) {
+ DatasetWithFilledAutofillFields datasetWithFilledAutofillFields =
+ new DatasetWithFilledAutofillFields();
+ datasetWithFilledAutofillFields.autofillDataset = dataset;
+ for (FieldTypeWithHeuristics fieldTypeWithHeuristics : mFieldTypesWithHints) {
+ if (AutofillHints.matchesPartition(
+ fieldTypeWithHeuristics.getFieldType().getPartition(), partition)) {
+ FilledAutofillField fakeField =
+ AutofillHints.generateFakeField(fieldTypeWithHeuristics, mPackageName,
+ mSeed, dataset.getId());
+ datasetWithFilledAutofillFields.add(fakeField);
+ }
+ }
+ return datasetWithFilledAutofillFields;
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/adapter/DatasetAdapter.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/adapter/DatasetAdapter.java
new file mode 100644
index 0000000..6f7e603
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/adapter/DatasetAdapter.java
@@ -0,0 +1,189 @@
+/*
+ * 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.autofill.service.data.adapter;
+
+import android.app.assist.AssistStructure;
+import android.content.IntentSender;
+import android.service.autofill.Dataset;
+import android.util.MutableBoolean;
+import android.view.View;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+
+import com.example.android.autofill.service.AutofillHints;
+import com.example.android.autofill.service.ClientParser;
+import com.example.android.autofill.service.model.DatasetWithFilledAutofillFields;
+import com.example.android.autofill.service.model.FieldType;
+import com.example.android.autofill.service.model.FieldTypeWithHeuristics;
+import com.example.android.autofill.service.model.FilledAutofillField;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+import static com.example.android.autofill.service.util.Util.indexOf;
+import static com.example.android.autofill.service.util.Util.logv;
+import static com.example.android.autofill.service.util.Util.logw;
+import static java.util.stream.Collectors.toMap;
+
+public class DatasetAdapter {
+ private final ClientParser mClientParser;
+
+ public DatasetAdapter(ClientParser clientParser) {
+ mClientParser = clientParser;
+ }
+
+ /**
+ * Wraps autofill data in a {@link Dataset} object which can then be sent back to the client.
+ */
+ public Dataset buildDataset(HashMap<String, FieldTypeWithHeuristics> fieldTypesByAutofillHint,
+ DatasetWithFilledAutofillFields datasetWithFilledAutofillFields,
+ RemoteViews remoteViews) {
+ return buildDataset(fieldTypesByAutofillHint, datasetWithFilledAutofillFields, remoteViews,
+ null);
+ }
+
+ public Dataset buildDatasetForFocusedNode(FilledAutofillField filledAutofillField,
+ FieldType fieldType, RemoteViews remoteViews) {
+ Dataset.Builder datasetBuilder = new Dataset.Builder(remoteViews);
+ boolean setAtLeastOneValue = bindDatasetToFocusedNode(filledAutofillField,
+ fieldType, datasetBuilder);
+ if (!setAtLeastOneValue) {
+ return null;
+ }
+ return datasetBuilder.build();
+ }
+
+ /**
+ * Wraps autofill data in a {@link Dataset} object with an IntentSender, which can then be
+ * sent back to the client.
+ */
+ public Dataset buildDataset(HashMap<String, FieldTypeWithHeuristics> fieldTypesByAutofillHint,
+ DatasetWithFilledAutofillFields datasetWithFilledAutofillFields,
+ RemoteViews remoteViews, IntentSender intentSender) {
+ Dataset.Builder datasetBuilder = new Dataset.Builder(remoteViews);
+ if (intentSender != null) {
+ datasetBuilder.setAuthentication(intentSender);
+ }
+ boolean setAtLeastOneValue = bindDataset(fieldTypesByAutofillHint,
+ datasetWithFilledAutofillFields, datasetBuilder);
+ if (!setAtLeastOneValue) {
+ return null;
+ }
+ return datasetBuilder.build();
+ }
+
+ /**
+ * Build an autofill {@link Dataset} using saved data and the client's AssistStructure.
+ */
+ private boolean bindDataset(HashMap<String, FieldTypeWithHeuristics> fieldTypesByAutofillHint,
+ DatasetWithFilledAutofillFields datasetWithFilledAutofillFields,
+ Dataset.Builder datasetBuilder) {
+ MutableBoolean setValueAtLeastOnce = new MutableBoolean(false);
+ Map<String, FilledAutofillField> filledAutofillFieldsByTypeName =
+ datasetWithFilledAutofillFields.filledAutofillFields.stream()
+ .collect(toMap(FilledAutofillField::getFieldTypeName, Function.identity()));
+ mClientParser.parse((node) ->
+ parseAutofillFields(node, fieldTypesByAutofillHint, filledAutofillFieldsByTypeName,
+ datasetBuilder, setValueAtLeastOnce)
+ );
+ return setValueAtLeastOnce.value;
+ }
+
+ private boolean bindDatasetToFocusedNode(FilledAutofillField field,
+ FieldType fieldType, Dataset.Builder builder) {
+ MutableBoolean setValueAtLeastOnce = new MutableBoolean(false);
+ mClientParser.parse((node) -> {
+ if (node.isFocused() && node.getAutofillId() != null) {
+ bindValueToNode(node, field, builder, setValueAtLeastOnce);
+ }
+ });
+ return setValueAtLeastOnce.value;
+ }
+
+ private void parseAutofillFields(AssistStructure.ViewNode viewNode,
+ HashMap<String, FieldTypeWithHeuristics> fieldTypesByAutofillHint,
+ Map<String, FilledAutofillField> filledAutofillFieldsByTypeName,
+ Dataset.Builder builder, MutableBoolean setValueAtLeastOnce) {
+ String[] rawHints = viewNode.getAutofillHints();
+ if (rawHints == null || rawHints.length == 0) {
+ logv("No af hints at ViewNode - %s", viewNode.getIdEntry());
+ return;
+ }
+ String fieldTypeName = AutofillHints.getFieldTypeNameFromAutofillHints(
+ fieldTypesByAutofillHint, Arrays.asList(rawHints));
+ if (fieldTypeName == null) {
+ return;
+ }
+ FilledAutofillField field = filledAutofillFieldsByTypeName.get(fieldTypeName);
+ if (field == null) {
+ return;
+ }
+ bindValueToNode(viewNode, field, builder, setValueAtLeastOnce);
+ }
+
+ void bindValueToNode(AssistStructure.ViewNode viewNode,
+ FilledAutofillField field, Dataset.Builder builder,
+ MutableBoolean setValueAtLeastOnce) {
+ AutofillId autofillId = viewNode.getAutofillId();
+ if (autofillId == null) {
+ logw("Autofill ID null for %s", viewNode.toString());
+ return;
+ }
+ int autofillType = viewNode.getAutofillType();
+ switch (autofillType) {
+ case View.AUTOFILL_TYPE_LIST:
+ CharSequence[] options = viewNode.getAutofillOptions();
+ int listValue = -1;
+ if (options != null) {
+ listValue = indexOf(viewNode.getAutofillOptions(), field.getTextValue());
+ }
+ if (listValue != -1) {
+ builder.setValue(autofillId, AutofillValue.forList(listValue));
+ setValueAtLeastOnce.value = true;
+ }
+ break;
+ case View.AUTOFILL_TYPE_DATE:
+ Long dateValue = field.getDateValue();
+ if (dateValue != null) {
+ builder.setValue(autofillId, AutofillValue.forDate(dateValue));
+ setValueAtLeastOnce.value = true;
+ }
+ break;
+ case View.AUTOFILL_TYPE_TEXT:
+ String textValue = field.getTextValue();
+ if (textValue != null) {
+ builder.setValue(autofillId, AutofillValue.forText(textValue));
+ setValueAtLeastOnce.value = true;
+ }
+ break;
+ case View.AUTOFILL_TYPE_TOGGLE:
+ Boolean toggleValue = field.getToggleValue();
+ if (toggleValue != null) {
+ builder.setValue(autofillId, AutofillValue.forToggle(toggleValue));
+ setValueAtLeastOnce.value = true;
+ }
+ break;
+ case View.AUTOFILL_TYPE_NONE:
+ default:
+ logw("Invalid autofill type - %d", autofillType);
+ break;
+ }
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/adapter/ResponseAdapter.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/adapter/ResponseAdapter.java
new file mode 100644
index 0000000..0300fed
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/adapter/ResponseAdapter.java
@@ -0,0 +1,136 @@
+/*
+ * 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.autofill.service.data.adapter;
+
+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.view.autofill.AutofillId;
+import android.widget.RemoteViews;
+
+import com.example.android.autofill.service.AuthActivity;
+import com.example.android.autofill.service.RemoteViewsHelper;
+import com.example.android.autofill.service.data.ClientViewMetadata;
+import com.example.android.autofill.service.model.DatasetWithFilledAutofillFields;
+import com.example.android.autofill.service.model.FieldType;
+import com.example.android.autofill.service.model.FieldTypeWithHeuristics;
+import com.example.android.autofill.service.model.FilledAutofillField;
+
+import java.util.HashMap;
+import java.util.List;
+
+public class ResponseAdapter {
+ private final Context mContext;
+ private final DatasetAdapter mDatasetAdapter;
+ private final String mPackageName;
+ private final ClientViewMetadata mClientViewMetadata;
+
+ public ResponseAdapter(Context context, ClientViewMetadata clientViewMetadata,
+ String packageName, DatasetAdapter datasetAdapter) {
+ mContext = context;
+ mClientViewMetadata = clientViewMetadata;
+ mDatasetAdapter = datasetAdapter;
+ mPackageName = packageName;
+ }
+
+ public FillResponse buildResponseForFocusedNode(String datasetName, FilledAutofillField field,
+ FieldType fieldType) {
+ FillResponse.Builder responseBuilder = new FillResponse.Builder();
+ RemoteViews remoteViews = RemoteViewsHelper.viewsWithNoAuth(
+ mPackageName, datasetName);
+ Dataset dataset = mDatasetAdapter.buildDatasetForFocusedNode(field, fieldType, remoteViews);
+ if (dataset != null) {
+ responseBuilder.addDataset(dataset);
+ return responseBuilder.build();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Wraps autofill data in a Response object (essentially a series of Datasets) which can then
+ * be sent back to the client View.
+ */
+ public FillResponse buildResponse(HashMap<String, FieldTypeWithHeuristics> fieldTypesByAutofillHint,
+ List<DatasetWithFilledAutofillFields> datasets, boolean datasetAuth) {
+ FillResponse.Builder responseBuilder = new FillResponse.Builder();
+ if (datasets != null) {
+ for (DatasetWithFilledAutofillFields datasetWithFilledAutofillFields : datasets) {
+ if (datasetWithFilledAutofillFields != null) {
+ Dataset dataset;
+ String datasetName = datasetWithFilledAutofillFields.autofillDataset
+ .getDatasetName();
+ if (datasetAuth) {
+ IntentSender intentSender = AuthActivity.getAuthIntentSenderForDataset(
+ mContext, datasetName);
+ RemoteViews remoteViews = RemoteViewsHelper.viewsWithAuth(
+ mPackageName, datasetName);
+ dataset = mDatasetAdapter.buildDataset(fieldTypesByAutofillHint,
+ datasetWithFilledAutofillFields, remoteViews, intentSender);
+ } else {
+ RemoteViews remoteViews = RemoteViewsHelper.viewsWithNoAuth(
+ mPackageName, datasetName);
+ dataset = mDatasetAdapter.buildDataset(fieldTypesByAutofillHint,
+ datasetWithFilledAutofillFields, remoteViews);
+ }
+ if (dataset != null) {
+ responseBuilder.addDataset(dataset);
+ }
+ }
+ }
+ }
+ int saveType = mClientViewMetadata.getSaveType();
+ AutofillId[] autofillIds = mClientViewMetadata.getAutofillIds();
+ if (autofillIds != null && autofillIds.length > 0) {
+ SaveInfo saveInfo = new SaveInfo.Builder(saveType, autofillIds).build();
+ responseBuilder.setSaveInfo(saveInfo);
+ return responseBuilder.build();
+ } else {
+ return null;
+ }
+ }
+
+ public FillResponse buildResponse(IntentSender sender, RemoteViews remoteViews) {
+ FillResponse.Builder responseBuilder = new FillResponse.Builder();
+ int saveType = mClientViewMetadata.getSaveType();
+ AutofillId[] autofillIds = mClientViewMetadata.getAutofillIds();
+ if (autofillIds != null && autofillIds.length > 0) {
+ SaveInfo saveInfo = new SaveInfo.Builder(saveType, autofillIds).build();
+ responseBuilder.setSaveInfo(saveInfo);
+ responseBuilder.setAuthentication(autofillIds, sender, remoteViews);
+ return responseBuilder.build();
+ } else {
+ return null;
+ }
+ }
+
+ public FillResponse buildManualResponse(IntentSender sender, RemoteViews remoteViews) {
+ FillResponse.Builder responseBuilder = new FillResponse.Builder();
+ int saveType = mClientViewMetadata.getSaveType();
+ AutofillId[] focusedIds = mClientViewMetadata.getFocusedIds();
+ if (focusedIds != null && focusedIds.length > 0) {
+ SaveInfo saveInfo = new SaveInfo.Builder(saveType, focusedIds).build();
+ return responseBuilder.setSaveInfo(saveInfo)
+ .setAuthentication(focusedIds, sender, remoteViews)
+ .build();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/AutofillDataSource.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/AutofillDataSource.java
new file mode 100644
index 0000000..e3579dd
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/AutofillDataSource.java
@@ -0,0 +1,75 @@
+/*
+ * 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.autofill.service.data.source;
+
+import com.example.android.autofill.service.data.DataCallback;
+import com.example.android.autofill.service.model.DatasetWithFilledAutofillFields;
+import com.example.android.autofill.service.model.FieldType;
+import com.example.android.autofill.service.model.FieldTypeWithHeuristics;
+import com.example.android.autofill.service.model.FilledAutofillField;
+import com.example.android.autofill.service.model.ResourceIdHeuristic;
+
+import java.util.HashMap;
+import java.util.List;
+
+public interface AutofillDataSource {
+
+ /**
+ * Asynchronously gets saved list of {@link DatasetWithFilledAutofillFields} that contains some
+ * objects that can autofill fields with these {@code autofillHints}.
+ */
+ void getAutofillDatasets(List<String> allAutofillHints,
+ DataCallback<List<DatasetWithFilledAutofillFields>> datasetsCallback);
+
+ void getAllAutofillDatasets(
+ DataCallback<List<DatasetWithFilledAutofillFields>> datasetsCallback);
+
+ /**
+ * Asynchronously gets a saved {@link DatasetWithFilledAutofillFields} for a specific
+ * {@code datasetName} that contains some objects that can autofill fields with these
+ * {@code autofillHints}.
+ */
+ void getAutofillDataset(List<String> allAutofillHints,
+ String datasetName, DataCallback<DatasetWithFilledAutofillFields> datasetsCallback);
+
+ /**
+ * Stores a collection of Autofill fields.
+ */
+ void saveAutofillDatasets(List<DatasetWithFilledAutofillFields>
+ datasetsWithFilledAutofillFields);
+
+ void saveResourceIdHeuristic(ResourceIdHeuristic resourceIdHeuristic);
+
+ /**
+ * Gets all autofill field types.
+ */
+ void getFieldTypes(DataCallback<List<FieldTypeWithHeuristics>> fieldTypesCallback);
+
+ /**
+ * Gets all autofill field types.
+ */
+ void getFieldType(String typeName, DataCallback<FieldType> fieldTypeCallback);
+
+ void getFieldTypeByAutofillHints(
+ DataCallback<HashMap<String, FieldTypeWithHeuristics>> fieldTypeMapCallback);
+
+ void getFilledAutofillField(String datasetId, String fieldTypeName, DataCallback<FilledAutofillField> fieldCallback);
+
+ /**
+ * Clears all data.
+ */
+ void clear();
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/DalService.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/DalService.java
new file mode 100644
index 0000000..b9cf695
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/DalService.java
@@ -0,0 +1,31 @@
+/*
+ * 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.autofill.service.data.source;
+
+import com.example.android.autofill.service.model.DalCheck;
+
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Query;
+
+public interface DalService {
+ @GET("/v1/assetlinks:check")
+ Call<DalCheck> check(@Query("source.web.site") String webDomain,
+ @Query("relation") String permission,
+ @Query("target.android_app.package_name") String packageName,
+ @Query("target.android_app.certificate.sha256_fingerprint") String fingerprint);
+}
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/DefaultFieldTypesSource.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/DefaultFieldTypesSource.java
new file mode 100644
index 0000000..22cbead
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/DefaultFieldTypesSource.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2018 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.autofill.service.data.source;
+
+import com.example.android.autofill.service.model.DefaultFieldTypeWithHints;
+
+import java.util.List;
+
+public interface DefaultFieldTypesSource {
+ List<DefaultFieldTypeWithHints> getDefaultFieldTypes();
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/DigitalAssetLinksDataSource.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/DigitalAssetLinksDataSource.java
new file mode 100644
index 0000000..00661a7
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/DigitalAssetLinksDataSource.java
@@ -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.autofill.service.data.source;
+
+import com.example.android.autofill.service.data.DataCallback;
+import com.example.android.autofill.service.model.DalCheck;
+import com.example.android.autofill.service.model.DalInfo;
+
+import static com.example.android.autofill.service.util.Util.DalCheckRequirement;
+
+/**
+ * Data source for
+ * <a href="https://developers.google.com/digital-asset-links/">Digital Asset Links</a>.
+ */
+public interface DigitalAssetLinksDataSource {
+
+ /**
+ * Checks if the association between a web domain and a package is valid.
+ */
+ void checkValid(DalCheckRequirement dalCheckRequirement, DalInfo dalInfo,
+ DataCallback<DalCheck> dalCheckCallback);
+
+ /**
+ * Clears all cached data.
+ */
+ void clear();
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/PackageVerificationDataSource.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/PackageVerificationDataSource.java
new file mode 100644
index 0000000..7e271e0
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/PackageVerificationDataSource.java
@@ -0,0 +1,36 @@
+/*
+ * 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.autofill.service.data.source;
+
+public interface PackageVerificationDataSource {
+
+ /**
+ * Verifies that the signatures in the passed {@code Context} match what is currently in
+ * storage. If there are no current signatures in storage for this packageName, it will store
+ * the signatures from the passed {@code Context}.
+ *
+ * @return {@code true} if signatures for this packageName are not currently in storage
+ * or if the signatures in the passed {@code Context} match what is currently in storage;
+ * {@code false} if the signatures in the passed {@code Context} do not match what is
+ * currently in storage or if an {@code Exception} was thrown while generating the signatures.
+ */
+ boolean putPackageSignatures(String packageName);
+
+ /**
+ * Clears all signature data currently in storage.
+ */
+ void clear();
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/DefaultFieldTypesLocalJsonSource.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/DefaultFieldTypesLocalJsonSource.java
new file mode 100644
index 0000000..d403f32
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/DefaultFieldTypesLocalJsonSource.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2018 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.autofill.service.data.source.local;
+
+import android.content.res.Resources;
+
+import com.example.android.autofill.service.R;
+import com.example.android.autofill.service.data.source.DefaultFieldTypesSource;
+import com.example.android.autofill.service.model.DefaultFieldTypeWithHints;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.util.List;
+
+import static com.example.android.autofill.service.util.Util.loge;
+
+public class DefaultFieldTypesLocalJsonSource implements DefaultFieldTypesSource {
+ private static DefaultFieldTypesLocalJsonSource sInstance;
+
+ private final Resources mResources;
+ private final Gson mGson;
+
+ private DefaultFieldTypesLocalJsonSource(Resources resources, Gson gson) {
+ mResources = resources;
+ mGson = gson;
+ }
+
+ public static DefaultFieldTypesLocalJsonSource getInstance(Resources resources, Gson gson) {
+ if (sInstance == null) {
+ sInstance = new DefaultFieldTypesLocalJsonSource(resources, gson);
+ }
+ return sInstance;
+ }
+
+ @Override
+ public List<DefaultFieldTypeWithHints> getDefaultFieldTypes() {
+ Type fieldTypeListType = TypeToken.getParameterized(List.class,
+ DefaultFieldTypeWithHints.class).getType();
+ InputStream is = mResources.openRawResource(R.raw.default_field_types);
+ List<DefaultFieldTypeWithHints> fieldTypes = null;
+ try(Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) {
+ fieldTypes = mGson.fromJson(reader, fieldTypeListType);
+ } catch (IOException e) {
+ loge(e, "Exception during deserialization of FieldTypes.");
+ }
+ return fieldTypes;
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/DigitalAssetLinksRepository.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/DigitalAssetLinksRepository.java
new file mode 100644
index 0000000..6303a1e
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/DigitalAssetLinksRepository.java
@@ -0,0 +1,159 @@
+/*
+ * 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.autofill.service.data.source.local;
+
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.support.annotation.NonNull;
+
+import com.example.android.autofill.service.data.DataCallback;
+import com.example.android.autofill.service.data.source.DalService;
+import com.example.android.autofill.service.data.source.DigitalAssetLinksDataSource;
+import com.example.android.autofill.service.model.DalCheck;
+import com.example.android.autofill.service.model.DalInfo;
+import com.example.android.autofill.service.util.SecurityHelper;
+import com.google.common.net.InternetDomainName;
+
+import java.util.HashMap;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+
+import static com.example.android.autofill.service.util.Util.DalCheckRequirement;
+import static com.example.android.autofill.service.util.Util.DalCheckRequirement.AllUrls;
+import static com.example.android.autofill.service.util.Util.DalCheckRequirement.Disabled;
+import static com.example.android.autofill.service.util.Util.DalCheckRequirement.LoginOnly;
+import static com.example.android.autofill.service.util.Util.logd;
+
+
+/**
+ * Singleton repository that caches the result of Digital Asset Links checks.
+ */
+public class DigitalAssetLinksRepository implements DigitalAssetLinksDataSource {
+ private static final String DAL_BASE_URL = "https://digitalassetlinks.googleapis.com";
+ private static final String PERMISSION_GET_LOGIN_CREDS = "common.get_login_creds";
+ private static final String PERMISSION_HANDLE_ALL_URLS = "common.handle_all_urls";
+ private static DigitalAssetLinksRepository sInstance;
+
+ private final PackageManager mPackageManager;
+ private final DalService mDalService;
+ private final HashMap<DalInfo, DalCheck> mCache;
+
+ private DigitalAssetLinksRepository(PackageManager packageManager) {
+ mPackageManager = packageManager;
+ mCache = new HashMap<>();
+ mDalService = new Retrofit.Builder()
+ .baseUrl(DAL_BASE_URL)
+ .build()
+ .create(DalService.class);
+ }
+
+ public static DigitalAssetLinksRepository getInstance(PackageManager packageManager) {
+ if (sInstance == null) {
+ sInstance = new DigitalAssetLinksRepository(packageManager);
+ }
+ return sInstance;
+ }
+
+ public static String getCanonicalDomain(String domain) {
+ InternetDomainName idn = InternetDomainName.from(domain);
+ while (idn != null && !idn.isTopPrivateDomain()) {
+ idn = idn.parent();
+ }
+ return idn == null ? null : idn.toString();
+ }
+
+ @Override
+ public void clear() {
+ mCache.clear();
+ }
+
+ public void checkValid(DalCheckRequirement dalCheckRequirement, DalInfo dalInfo,
+ DataCallback<DalCheck> dalCheckDataCallback) {
+ if (dalCheckRequirement.equals(Disabled)) {
+ DalCheck dalCheck = new DalCheck();
+ dalCheck.linked = true;
+ dalCheckDataCallback.onLoaded(dalCheck);
+ return;
+ }
+
+ DalCheck dalCheck = mCache.get(dalInfo);
+ if (dalCheck != null) {
+ dalCheckDataCallback.onLoaded(dalCheck);
+ return;
+ }
+ String packageName = dalInfo.getPackageName();
+ String webDomain = dalInfo.getWebDomain();
+
+ final String fingerprint;
+ try {
+ PackageInfo packageInfo = mPackageManager.getPackageInfo(packageName,
+ PackageManager.GET_SIGNATURES);
+ fingerprint = SecurityHelper.getFingerprint(packageInfo, packageName);
+ } catch (Exception e) {
+ dalCheckDataCallback.onDataNotAvailable("Error getting fingerprint for %s",
+ packageName);
+ return;
+ }
+ logd("validating domain %s for pkg %s and fingerprint %s.", webDomain,
+ packageName, fingerprint);
+ mDalService.check(webDomain, PERMISSION_GET_LOGIN_CREDS, packageName, fingerprint).enqueue(
+ new Callback<DalCheck>() {
+ @Override
+ public void onResponse(@NonNull Call<DalCheck> call,
+ @NonNull Response<DalCheck> response) {
+ DalCheck dalCheck = response.body();
+ if (dalCheck == null || !dalCheck.linked) {
+ // get_login_creds check failed, so try handle_all_urls check
+ if (dalCheckRequirement.equals(LoginOnly)) {
+ dalCheckDataCallback.onDataNotAvailable(
+ "DAL: Login creds check failed.");
+ } else if (dalCheckRequirement.equals(AllUrls)) {
+ mDalService.check(webDomain, PERMISSION_HANDLE_ALL_URLS,
+ packageName, fingerprint).enqueue(new Callback<DalCheck>() {
+ @Override
+ public void onResponse(@NonNull Call<DalCheck> call,
+ @NonNull Response<DalCheck> response) {
+ DalCheck dalCheck = response.body();
+ mCache.put(dalInfo, dalCheck);
+ dalCheckDataCallback.onLoaded(dalCheck);
+ }
+
+ @Override
+ public void onFailure(@NonNull Call<DalCheck> call,
+ @NonNull Throwable t) {
+ dalCheckDataCallback.onDataNotAvailable(t.getMessage());
+ }
+ });
+ }
+ } else {
+ // get_login_creds check succeeded, so we're finished.
+ mCache.put(dalInfo, dalCheck);
+ dalCheckDataCallback.onLoaded(dalCheck);
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call<DalCheck> call, @NonNull Throwable t) {
+ // get_login_creds check failed, so try handle_all_urls check.
+ mDalService.check(webDomain, PERMISSION_HANDLE_ALL_URLS, packageName,
+ fingerprint);
+ }
+ });
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/LocalAutofillDataSource.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/LocalAutofillDataSource.java
new file mode 100644
index 0000000..e58e18b
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/LocalAutofillDataSource.java
@@ -0,0 +1,257 @@
+/*
+ * 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.autofill.service.data.source.local;
+
+import android.content.SharedPreferences;
+import android.service.autofill.Dataset;
+
+import com.example.android.autofill.service.data.DataCallback;
+import com.example.android.autofill.service.data.source.AutofillDataSource;
+import com.example.android.autofill.service.data.source.local.dao.AutofillDao;
+import com.example.android.autofill.service.model.AutofillDataset;
+import com.example.android.autofill.service.model.AutofillHint;
+import com.example.android.autofill.service.model.DatasetWithFilledAutofillFields;
+import com.example.android.autofill.service.model.FieldType;
+import com.example.android.autofill.service.model.FieldTypeWithHeuristics;
+import com.example.android.autofill.service.model.FilledAutofillField;
+import com.example.android.autofill.service.model.ResourceIdHeuristic;
+import com.example.android.autofill.service.util.AppExecutors;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.example.android.autofill.service.util.Util.logw;
+
+public class LocalAutofillDataSource implements AutofillDataSource {
+ public static final String SHARED_PREF_KEY = "com.example.android.autofill"
+ + ".service.datasource.LocalAutofillDataSource";
+ private static final String DATASET_NUMBER_KEY = "datasetNumber";
+ private static final Object sLock = new Object();
+
+ private static LocalAutofillDataSource sInstance;
+
+ private final AutofillDao mAutofillDao;
+ private final SharedPreferences mSharedPreferences;
+ private final AppExecutors mAppExecutors;
+
+ private LocalAutofillDataSource(SharedPreferences sharedPreferences, AutofillDao autofillDao,
+ AppExecutors appExecutors) {
+ mSharedPreferences = sharedPreferences;
+ mAutofillDao = autofillDao;
+ mAppExecutors = appExecutors;
+ }
+
+ public static LocalAutofillDataSource getInstance(SharedPreferences sharedPreferences,
+ AutofillDao autofillDao, AppExecutors appExecutors) {
+ synchronized (sLock) {
+ if (sInstance == null) {
+ sInstance = new LocalAutofillDataSource(sharedPreferences, autofillDao,
+ appExecutors);
+ }
+ return sInstance;
+ }
+ }
+
+ public static void clearInstance() {
+ synchronized (sLock) {
+ sInstance = null;
+ }
+ }
+
+ @Override
+ public void getAutofillDatasets(List<String> allAutofillHints,
+ DataCallback<List<DatasetWithFilledAutofillFields>> datasetsCallback) {
+ mAppExecutors.diskIO().execute(() -> {
+ final List<String> typeNames = getFieldTypesForAutofillHints(allAutofillHints)
+ .stream()
+ .map(FieldTypeWithHeuristics::getFieldType)
+ .map(FieldType::getTypeName)
+ .collect(Collectors.toList());
+ List<DatasetWithFilledAutofillFields> datasetsWithFilledAutofillFields =
+ mAutofillDao.getDatasets(typeNames);
+ mAppExecutors.mainThread().execute(() ->
+ datasetsCallback.onLoaded(datasetsWithFilledAutofillFields)
+ );
+ });
+ }
+
+ @Override
+ public void getAllAutofillDatasets(
+ DataCallback<List<DatasetWithFilledAutofillFields>> datasetsCallback) {
+ mAppExecutors.diskIO().execute(() -> {
+ List<DatasetWithFilledAutofillFields> datasetsWithFilledAutofillFields =
+ mAutofillDao.getAllDatasets();
+ mAppExecutors.mainThread().execute(() ->
+ datasetsCallback.onLoaded(datasetsWithFilledAutofillFields)
+ );
+ });
+ }
+
+ @Override
+ public void getAutofillDataset(List<String> allAutofillHints, String datasetName,
+ DataCallback<DatasetWithFilledAutofillFields> datasetsCallback) {
+ mAppExecutors.diskIO().execute(() -> {
+ // Room does not support TypeConverters for collections.
+ List<DatasetWithFilledAutofillFields> autofillDatasetFields =
+ mAutofillDao.getDatasetsWithName(allAutofillHints, datasetName);
+ if (autofillDatasetFields != null && !autofillDatasetFields.isEmpty()) {
+ if (autofillDatasetFields.size() > 1) {
+ logw("More than 1 dataset with name %s", datasetName);
+ }
+ DatasetWithFilledAutofillFields dataset = autofillDatasetFields.get(0);
+
+ mAppExecutors.mainThread().execute(() ->
+ datasetsCallback.onLoaded(dataset)
+ );
+ } else {
+ mAppExecutors.mainThread().execute(() ->
+ datasetsCallback.onDataNotAvailable("No data found.")
+ );
+ }
+ });
+ }
+
+
+ @Override
+ public void saveAutofillDatasets(List<DatasetWithFilledAutofillFields>
+ datasetsWithFilledAutofillFields) {
+ mAppExecutors.diskIO().execute(() -> {
+ for (DatasetWithFilledAutofillFields datasetWithFilledAutofillFields :
+ datasetsWithFilledAutofillFields) {
+ List<FilledAutofillField> filledAutofillFields =
+ datasetWithFilledAutofillFields.filledAutofillFields;
+ AutofillDataset autofillDataset = datasetWithFilledAutofillFields.autofillDataset;
+ mAutofillDao.insertAutofillDataset(autofillDataset);
+ mAutofillDao.insertFilledAutofillFields(filledAutofillFields);
+ }
+ });
+ incrementDatasetNumber();
+ }
+
+ @Override
+ public void saveResourceIdHeuristic(ResourceIdHeuristic resourceIdHeuristic) {
+ mAppExecutors.diskIO().execute(() -> {
+ mAutofillDao.insertResourceIdHeuristic(resourceIdHeuristic);
+ });
+ }
+
+ @Override
+ public void getFieldTypes(DataCallback<List<FieldTypeWithHeuristics>> fieldTypesCallback) {
+ mAppExecutors.diskIO().execute(() -> {
+ List<FieldTypeWithHeuristics> fieldTypeWithHints = mAutofillDao.getFieldTypesWithHints();
+ mAppExecutors.mainThread().execute(() -> {
+ if (fieldTypeWithHints != null) {
+ fieldTypesCallback.onLoaded(fieldTypeWithHints);
+ } else {
+ fieldTypesCallback.onDataNotAvailable("Field Types not found.");
+ }
+ });
+ });
+ }
+
+ @Override
+ public void getFieldTypeByAutofillHints(
+ DataCallback<HashMap<String, FieldTypeWithHeuristics>> fieldTypeMapCallback) {
+ mAppExecutors.diskIO().execute(() -> {
+ HashMap<String, FieldTypeWithHeuristics> hintMap = getFieldTypeByAutofillHints();
+ mAppExecutors.mainThread().execute(() -> {
+ if (hintMap != null) {
+ fieldTypeMapCallback.onLoaded(hintMap);
+ } else {
+ fieldTypeMapCallback.onDataNotAvailable("FieldTypes not found");
+ }
+ });
+ });
+ }
+
+ @Override
+ public void getFilledAutofillField(String datasetId, String fieldTypeName, DataCallback<FilledAutofillField> fieldCallback) {
+ mAppExecutors.diskIO().execute(() -> {
+ FilledAutofillField filledAutofillField = mAutofillDao.getFilledAutofillField(datasetId, fieldTypeName);
+ mAppExecutors.mainThread().execute(() -> {
+ fieldCallback.onLoaded(filledAutofillField);
+ });
+ });
+ }
+
+ @Override
+ public void getFieldType(String fieldTypeName, DataCallback<FieldType> fieldTypeCallback) {
+ mAppExecutors.diskIO().execute(() -> {
+ FieldType fieldType = mAutofillDao.getFieldType(fieldTypeName);
+ mAppExecutors.mainThread().execute(() -> {
+ fieldTypeCallback.onLoaded(fieldType);
+ });
+ });
+ }
+
+ public void getAutofillDatasetWithId(String datasetId,
+ DataCallback<DatasetWithFilledAutofillFields> callback) {
+ mAppExecutors.diskIO().execute(() -> {
+ DatasetWithFilledAutofillFields dataset =
+ mAutofillDao.getAutofillDatasetWithId(datasetId);
+ mAppExecutors.mainThread().execute(() -> {
+ callback.onLoaded(dataset);
+ });
+ });
+ }
+
+ private HashMap<String, FieldTypeWithHeuristics> getFieldTypeByAutofillHints() {
+ HashMap<String, FieldTypeWithHeuristics> hintMap = new HashMap<>();
+ List<FieldTypeWithHeuristics> fieldTypeWithHints =
+ mAutofillDao.getFieldTypesWithHints();
+ if (fieldTypeWithHints != null) {
+ for (FieldTypeWithHeuristics fieldType : fieldTypeWithHints) {
+ for (AutofillHint hint : fieldType.autofillHints) {
+ hintMap.put(hint.mAutofillHint, fieldType);
+ }
+ }
+ return hintMap;
+ } else {
+ return null;
+ }
+ }
+
+ private List<FieldTypeWithHeuristics> getFieldTypesForAutofillHints(List<String> autofillHints) {
+ return mAutofillDao.getFieldTypesForAutofillHints(autofillHints);
+ }
+
+ @Override
+ public void clear() {
+ mAppExecutors.diskIO().execute(() -> {
+ mAutofillDao.clearAll();
+ mSharedPreferences.edit().putInt(DATASET_NUMBER_KEY, 0).apply();
+ });
+ }
+
+ /**
+ * For simplicity, {@link Dataset}s will be named in the form {@code dataset-X.P} where
+ * {@code X} means this was the Xth group of datasets saved, and {@code P} refers to the dataset
+ * partition number. This method returns the appropriate {@code X}.
+ */
+ public int getDatasetNumber() {
+ return mSharedPreferences.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 void incrementDatasetNumber() {
+ mSharedPreferences.edit().putInt(DATASET_NUMBER_KEY, getDatasetNumber() + 1).apply();
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/SharedPrefsPackageVerificationRepository.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/SharedPrefsPackageVerificationRepository.java
new file mode 100644
index 0000000..0d43c1f
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/SharedPrefsPackageVerificationRepository.java
@@ -0,0 +1,86 @@
+/*
+ * 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.autofill.service.data.source.local;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+import com.example.android.autofill.service.data.source.PackageVerificationDataSource;
+import com.example.android.autofill.service.util.SecurityHelper;
+
+import static com.example.android.autofill.service.util.Util.logd;
+import static com.example.android.autofill.service.util.Util.logw;
+
+public class SharedPrefsPackageVerificationRepository implements PackageVerificationDataSource {
+
+ private static final String SHARED_PREF_KEY = "com.example.android.autofill.service"
+ + ".datasource.PackageVerificationDataSource";
+ private static PackageVerificationDataSource sInstance;
+
+ private final SharedPreferences mSharedPrefs;
+ private final Context mContext;
+
+ private SharedPrefsPackageVerificationRepository(Context context) {
+ mSharedPrefs = context.getApplicationContext()
+ .getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE);
+ mContext = context.getApplicationContext();
+ }
+
+ public static PackageVerificationDataSource getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new SharedPrefsPackageVerificationRepository(
+ context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ @Override
+ public void clear() {
+ mSharedPrefs.edit().clear().apply();
+ }
+
+ @Override
+ public boolean putPackageSignatures(String packageName) {
+ String hash;
+ try {
+ PackageManager pm = mContext.getPackageManager();
+ PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
+ hash = SecurityHelper.getFingerprint(packageInfo, packageName);
+ logd("Hash for %s: %s", packageName, hash);
+ } catch (Exception e) {
+ logw(e, "Error getting hash for %s.", packageName);
+ return false;
+ }
+
+ if (!containsSignatureForPackage(packageName)) {
+ // Storage does not yet contain signature for this package name.
+ mSharedPrefs.edit().putString(packageName, hash).apply();
+ return true;
+ }
+ return containsMatchingSignatureForPackage(packageName, hash);
+ }
+
+ private boolean containsSignatureForPackage(String packageName) {
+ return mSharedPrefs.contains(packageName);
+ }
+
+ private boolean containsMatchingSignatureForPackage(String packageName,
+ String hash) {
+ return hash.equals(mSharedPrefs.getString(packageName, null));
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/dao/AutofillDao.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/dao/AutofillDao.java
new file mode 100644
index 0000000..084532c
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/dao/AutofillDao.java
@@ -0,0 +1,126 @@
+/*
+ * 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.autofill.service.data.source.local.dao;
+
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Insert;
+import android.arch.persistence.room.OnConflictStrategy;
+import android.arch.persistence.room.Query;
+
+import com.example.android.autofill.service.model.AutofillDataset;
+import com.example.android.autofill.service.model.AutofillHint;
+import com.example.android.autofill.service.model.DatasetWithFilledAutofillFields;
+import com.example.android.autofill.service.model.FieldType;
+import com.example.android.autofill.service.model.FieldTypeWithHeuristics;
+import com.example.android.autofill.service.model.FilledAutofillField;
+import com.example.android.autofill.service.model.ResourceIdHeuristic;
+
+import java.util.Collection;
+import java.util.List;
+
+@Dao
+public interface AutofillDao {
+ /**
+ * Fetches a list of datasets associated to autofill fields on the page.
+ *
+ * @param allAutofillHints Filtering parameter; represents all of the hints associated with
+ * all of the views on the page.
+ */
+ @Query("SELECT DISTINCT id, datasetName FROM FilledAutofillField, AutofillDataset" +
+ " WHERE AutofillDataset.id = FilledAutofillField.datasetId" +
+ " AND FilledAutofillField.fieldTypeName IN (:allAutofillHints)")
+ List<DatasetWithFilledAutofillFields> getDatasets(List<String> allAutofillHints);
+
+ @Query("SELECT DISTINCT id, datasetName FROM FilledAutofillField, AutofillDataset" +
+ " WHERE AutofillDataset.id = FilledAutofillField.datasetId")
+ List<DatasetWithFilledAutofillFields> getAllDatasets();
+
+ /**
+ * Fetches a list of datasets associated to autofill fields. It should only return a dataset
+ * if that dataset has an autofill field associate with the view the user is focused on, and
+ * if that dataset's name matches the name passed in.
+ *
+ * @param fieldTypes Filtering parameter; represents all of the field types associated with
+ * all of the views on the page.
+ * @param datasetName Filtering parameter; only return datasets with this name.
+ */
+ @Query("SELECT DISTINCT id, datasetName FROM FilledAutofillField, AutofillDataset" +
+ " WHERE AutofillDataset.id = FilledAutofillField.datasetId" +
+ " AND AutofillDataset.datasetName = (:datasetName)" +
+ " AND FilledAutofillField.fieldTypeName IN (:fieldTypes)")
+ List<DatasetWithFilledAutofillFields> getDatasetsWithName(
+ List<String> fieldTypes, String datasetName);
+
+ @Query("SELECT DISTINCT typeName, autofillTypes, saveInfo, partition, strictExampleSet, " +
+ "textTemplate, dateTemplate" +
+ " FROM FieldType, AutofillHint" +
+ " WHERE FieldType.typeName = AutofillHint.fieldTypeName" +
+ " UNION " +
+ "SELECT DISTINCT typeName, autofillTypes, saveInfo, partition, strictExampleSet, " +
+ "textTemplate, dateTemplate" +
+ " FROM FieldType, ResourceIdHeuristic" +
+ " WHERE FieldType.typeName = ResourceIdHeuristic.fieldTypeName")
+ List<FieldTypeWithHeuristics> getFieldTypesWithHints();
+
+ @Query("SELECT DISTINCT typeName, autofillTypes, saveInfo, partition, strictExampleSet, " +
+ "textTemplate, dateTemplate" +
+ " FROM FieldType, AutofillHint" +
+ " WHERE FieldType.typeName = AutofillHint.fieldTypeName" +
+ " AND AutofillHint.autofillHint IN (:autofillHints)" +
+ " UNION " +
+ "SELECT DISTINCT typeName, autofillTypes, saveInfo, partition, strictExampleSet, " +
+ "textTemplate, dateTemplate" +
+ " FROM FieldType, ResourceIdHeuristic" +
+ " WHERE FieldType.typeName = ResourceIdHeuristic.fieldTypeName")
+ List<FieldTypeWithHeuristics> getFieldTypesForAutofillHints(List<String> autofillHints);
+
+ @Query("SELECT DISTINCT id, datasetName FROM FilledAutofillField, AutofillDataset" +
+ " WHERE AutofillDataset.id = FilledAutofillField.datasetId" +
+ " AND AutofillDataset.id = (:datasetId)")
+ DatasetWithFilledAutofillFields getAutofillDatasetWithId(String datasetId);
+
+ @Query("SELECT * FROM FilledAutofillField" +
+ " WHERE FilledAutofillField.datasetId = (:datasetId)" +
+ " AND FilledAutofillField.fieldTypeName = (:fieldTypeName)")
+ FilledAutofillField getFilledAutofillField(String datasetId, String fieldTypeName);
+
+ @Query("SELECT * FROM FieldType" +
+ " WHERE FieldType.typeName = (:fieldTypeName)")
+ FieldType getFieldType(String fieldTypeName);
+
+ /**
+ * @param autofillFields Collection of autofill fields to be saved to the db.
+ */
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insertFilledAutofillFields(Collection<FilledAutofillField> autofillFields);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insertAutofillDataset(AutofillDataset datasets);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insertAutofillHints(List<AutofillHint> autofillHints);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insertResourceIdHeuristic(ResourceIdHeuristic resourceIdHeuristic);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insertFieldTypes(List<FieldType> fieldTypes);
+
+
+ @Query("DELETE FROM AutofillDataset")
+ void clearAll();
+}
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/db/AutofillDatabase.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/db/AutofillDatabase.java
new file mode 100644
index 0000000..fe007ce
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/db/AutofillDatabase.java
@@ -0,0 +1,114 @@
+/*
+ * 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.autofill.service.data.source.local.db;
+
+import android.arch.persistence.db.SupportSQLiteDatabase;
+import android.arch.persistence.room.Database;
+import android.arch.persistence.room.Room;
+import android.arch.persistence.room.RoomDatabase;
+import android.arch.persistence.room.TypeConverters;
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import com.example.android.autofill.service.data.source.DefaultFieldTypesSource;
+import com.example.android.autofill.service.data.source.local.dao.AutofillDao;
+import com.example.android.autofill.service.model.AutofillDataset;
+import com.example.android.autofill.service.model.AutofillHint;
+import com.example.android.autofill.service.model.DefaultFieldTypeWithHints;
+import com.example.android.autofill.service.model.FakeData;
+import com.example.android.autofill.service.model.FieldType;
+import com.example.android.autofill.service.model.FilledAutofillField;
+import com.example.android.autofill.service.model.ResourceIdHeuristic;
+import com.example.android.autofill.service.util.AppExecutors;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.example.android.autofill.service.data.source.local.db.Converters.IntList;
+import static java.util.stream.Collectors.toList;
+
+@Database(entities = {
+ FilledAutofillField.class,
+ AutofillDataset.class,
+ FieldType.class,
+ AutofillHint.class,
+ ResourceIdHeuristic.class
+}, version = 1)
+@TypeConverters({Converters.class})
+public abstract class AutofillDatabase extends RoomDatabase {
+
+ private static final Object sLock = new Object();
+ private static AutofillDatabase sInstance;
+
+ public static AutofillDatabase getInstance(Context context,
+ DefaultFieldTypesSource defaultFieldTypesSource,
+ AppExecutors appExecutors) {
+ if (sInstance == null) {
+ synchronized (sLock) {
+ if (sInstance == null) {
+ sInstance = Room.databaseBuilder(context.getApplicationContext(),
+ AutofillDatabase.class, "AutofillSample.db")
+ .addCallback(new RoomDatabase.Callback() {
+ @Override
+ public void onCreate(@NonNull SupportSQLiteDatabase db) {
+ appExecutors.diskIO().execute(() -> {
+ List<DefaultFieldTypeWithHints> fieldTypes =
+ defaultFieldTypesSource.getDefaultFieldTypes();
+ AutofillDatabase autofillDatabase =
+ getInstance(context, defaultFieldTypesSource,
+ appExecutors);
+ autofillDatabase.saveDefaultFieldTypes(fieldTypes);
+ });
+ }
+
+ @Override
+ public void onOpen(@NonNull SupportSQLiteDatabase db) {
+ super.onOpen(db);
+ }
+ })
+ .build();
+ }
+ }
+ }
+ return sInstance;
+ }
+
+ private void saveDefaultFieldTypes(List<DefaultFieldTypeWithHints> defaultFieldTypes) {
+ List<FieldType> storedFieldTypes = new ArrayList<>();
+ List<AutofillHint> storedAutofillHints = new ArrayList<>();
+ for (DefaultFieldTypeWithHints defaultType : defaultFieldTypes) {
+ DefaultFieldTypeWithHints.DefaultFieldType defaultFieldType = defaultType.fieldType;
+ List<String> autofillHints = defaultType.autofillHints;
+ IntList autofillTypes = new IntList(defaultFieldType.autofillTypes);
+ DefaultFieldTypeWithHints.DefaultFakeData defaultFakeData = defaultType.fieldType.fakeData;
+ FakeData fakeData = new FakeData(new Converters.StringList(
+ defaultFakeData.strictExampleSet), defaultFakeData.textTemplate,
+ defaultFakeData.dateTemplate);
+ FieldType storedFieldType = new FieldType(defaultFieldType.typeName, autofillTypes,
+ defaultFieldType.saveInfo, defaultFieldType.partition, fakeData);
+ storedFieldTypes.add(storedFieldType);
+ storedAutofillHints.addAll(autofillHints.stream()
+ .map((autofillHint) -> new AutofillHint(autofillHint,
+ storedFieldType.getTypeName())).collect(toList()));
+ }
+ AutofillDao autofillDao = autofillDao();
+ autofillDao.insertFieldTypes(storedFieldTypes);
+ autofillDao.insertAutofillHints(storedAutofillHints);
+ }
+
+ public abstract AutofillDao autofillDao();
+}
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/db/Converters.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/db/Converters.java
new file mode 100644
index 0000000..2829f8b
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/source/local/db/Converters.java
@@ -0,0 +1,100 @@
+/*
+ * 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.autofill.service.data.source.local.db;
+
+import android.arch.persistence.room.TypeConverter;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static java.util.stream.Collectors.toList;
+
+/**
+ * Type converter for Room database.
+ */
+public class Converters {
+
+ /**
+ * If database returns a {@link String} containing a comma delimited list of ints, this converts
+ * the {@link String} to an {@link IntList}.
+ */
+ @TypeConverter
+ public static IntList storedStringToIntList(String value) {
+ List<String> strings = Arrays.asList(value.split("\\s*,\\s*"));
+ List<Integer> ints = strings.stream().map(Integer::parseInt).collect(toList());
+ return new IntList(ints);
+ }
+
+ /**
+ * Converts the {@link IntList} back into a String containing a comma delimited list of
+ * ints.
+ */
+ @TypeConverter
+ public static String intListToStoredString(IntList list) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (Integer integer : list.ints) {
+ stringBuilder.append(integer).append(",");
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * If database returns a {@link String} containing a comma delimited list of Strings, this
+ * converts the {@link String} to a {@link StringList}.
+ */
+ @TypeConverter
+ public static StringList storedStringToStringList(String value) {
+ List<String> strings = Arrays.asList(value.split("\\s*,\\s*"));
+ return new StringList(strings);
+ }
+
+
+ /**
+ * Converts the {@link StringList} back into a {@link String} containing a comma delimited
+ * list of {@link String}s.
+ */
+ @TypeConverter
+ public static String stringListToStoredString(StringList list) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (String string : list.strings) {
+ stringBuilder.append(string).append(",");
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Wrapper class for {@code List<Integer>} so it can work with Room type converters.
+ */
+ public static class IntList {
+ public final List<Integer> ints;
+
+ public IntList(List<Integer> ints) {
+ this.ints = ints;
+ }
+ }
+
+ /**
+ * Wrapper class for {@code List<String>} so it can work with Room type converters.
+ */
+ public static class StringList {
+ public final List<String> strings;
+
+ public StringList(List<String> ints) {
+ this.strings = ints;
+ }
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/AutofillDataset.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/AutofillDataset.java
new file mode 100644
index 0000000..6b6596c
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/AutofillDataset.java
@@ -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.autofill.service.model;
+
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Entity;
+import android.support.annotation.NonNull;
+
+@Entity(primaryKeys = {"id"})
+public class AutofillDataset {
+ @NonNull
+ @ColumnInfo(name = "id")
+ private final String mId;
+
+ @NonNull
+ @ColumnInfo(name = "datasetName")
+ private final String mDatasetName;
+
+ @NonNull
+ @ColumnInfo(name = "packageName")
+ private final String mPackageName;
+
+ public AutofillDataset(@NonNull String id, @NonNull String datasetName,
+ @NonNull String packageName) {
+ mId = id;
+ mDatasetName = datasetName;
+ mPackageName = packageName;
+ }
+
+ @NonNull
+ public String getId() {
+ return mId;
+ }
+
+ @NonNull
+ public String getDatasetName() {
+ return mDatasetName;
+ }
+
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ AutofillDataset that = (AutofillDataset) o;
+
+ if (!mId.equals(that.mId)) return false;
+ if (!mDatasetName.equals(that.mDatasetName)) return false;
+ return mPackageName.equals(that.mPackageName);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mId.hashCode();
+ result = 31 * result + mDatasetName.hashCode();
+ result = 31 * result + mPackageName.hashCode();
+ return result;
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/AutofillHint.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/AutofillHint.java
new file mode 100644
index 0000000..be4aa7d
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/AutofillHint.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 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.autofill.service.model;
+
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Entity;
+import android.arch.persistence.room.ForeignKey;
+import android.support.annotation.NonNull;
+
+@Entity(primaryKeys = {"autofillHint"}, foreignKeys = @ForeignKey(
+ entity = FieldType.class, parentColumns = "typeName", childColumns = "fieldTypeName",
+ onDelete = ForeignKey.CASCADE))
+public class AutofillHint {
+
+ @NonNull
+ @ColumnInfo(name = "autofillHint")
+ public String mAutofillHint;
+
+ @NonNull
+ @ColumnInfo(name = "fieldTypeName")
+ public String mFieldTypeName;
+
+ public AutofillHint(@NonNull String autofillHint, @NonNull String fieldTypeName) {
+ this.mAutofillHint = autofillHint;
+ this.mFieldTypeName = fieldTypeName;
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/DalCheck.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/DalCheck.java
new file mode 100644
index 0000000..ede77ba
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/DalCheck.java
@@ -0,0 +1,23 @@
+/*
+ * 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.autofill.service.model;
+
+public class DalCheck {
+ public boolean linked;
+ public String maxAge;
+ public String debugString;
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/DalInfo.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/DalInfo.java
new file mode 100644
index 0000000..44002ca
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/DalInfo.java
@@ -0,0 +1,67 @@
+/*
+ * 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.autofill.service.model;
+
+import static com.example.android.autofill.service.data.source.local.DigitalAssetLinksRepository.getCanonicalDomain;
+
+public class DalInfo {
+ private final String mWebDomain;
+ private final String mPackageName;
+
+ public DalInfo(String webDomain, String packageName) {
+ String canonicalDomain = getCanonicalDomain(webDomain);
+ final String fullDomain;
+ if (!webDomain.startsWith("http:") && !webDomain.startsWith("https:")) {
+ // Unfortunately AssistStructure.ViewNode does not tell what the domain is, so let's
+ // assume it's https
+ fullDomain = "https://" + canonicalDomain;
+ } else {
+ fullDomain = canonicalDomain;
+ }
+ mWebDomain = fullDomain;
+ mPackageName = packageName;
+ }
+
+ public String getWebDomain() {
+ return mWebDomain;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ DalInfo dalInfo = (DalInfo) o;
+
+ if (mWebDomain != null ? !mWebDomain.equals(dalInfo.mWebDomain) :
+ dalInfo.mWebDomain != null)
+ return false;
+ return mPackageName != null ? mPackageName.equals(dalInfo.mPackageName) :
+ dalInfo.mPackageName == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mWebDomain != null ? mWebDomain.hashCode() : 0;
+ result = 31 * result + (mPackageName != null ? mPackageName.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/DatasetWithFilledAutofillFields.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/DatasetWithFilledAutofillFields.java
new file mode 100644
index 0000000..06b529f
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/DatasetWithFilledAutofillFields.java
@@ -0,0 +1,60 @@
+/*
+ * 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.autofill.service.model;
+
+import android.arch.persistence.room.Embedded;
+import android.arch.persistence.room.Relation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DatasetWithFilledAutofillFields {
+ @Embedded
+ public AutofillDataset autofillDataset;
+
+ @Relation(parentColumn = "id", entityColumn = "datasetId", entity = FilledAutofillField.class)
+ public List<FilledAutofillField> filledAutofillFields;
+
+ public void add(FilledAutofillField filledAutofillField) {
+ if (filledAutofillFields == null) {
+ this.filledAutofillFields = new ArrayList<>();
+ }
+ this.filledAutofillFields.add(filledAutofillField);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ DatasetWithFilledAutofillFields that = (DatasetWithFilledAutofillFields) o;
+
+ if (autofillDataset != null ? !autofillDataset.equals(that.autofillDataset) :
+ that.autofillDataset != null)
+ return false;
+ return filledAutofillFields != null ?
+ filledAutofillFields.equals(that.filledAutofillFields) :
+ that.filledAutofillFields == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = autofillDataset != null ? autofillDataset.hashCode() : 0;
+ result = 31 * result + (filledAutofillFields != null ? filledAutofillFields.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/DefaultFieldTypeWithHints.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/DefaultFieldTypeWithHints.java
new file mode 100644
index 0000000..70ad382
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/DefaultFieldTypeWithHints.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2018 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.autofill.service.model;
+
+import java.util.List;
+
+/**
+ * JSON model class, representing an autofillable field type. It is called "Default" because only
+ * default field types will be included in the packaged JSON. After the JSON is initially read and
+ * written to the DB, the field types can be dynamically added, modified, and removed.
+ *<p>
+ * It contains all of the metadata about the field type. For example, if the field type is
+ * "country", this is the JSON object associated with it:
+ <pre class="prettyprint">
+ {
+ "autofillHints": [
+ "country"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "countryseed"
+ },
+ "partition": 1,
+ "saveInfo": 2,
+ "typeName": "country"
+ }
+ }
+ </pre>
+ */
+public class DefaultFieldTypeWithHints {
+ public DefaultFieldType fieldType;
+ public List<String> autofillHints;
+
+ public static class DefaultFieldType {
+ public String typeName;
+ public List<Integer> autofillTypes;
+ public int saveInfo;
+ public int partition;
+ public DefaultFakeData fakeData;
+ }
+
+ public static class DefaultFakeData {
+ public List<String> strictExampleSet;
+ public String textTemplate;
+ public String dateTemplate;
+
+ public DefaultFakeData(List<String> strictExampleSet, String textTemplate,
+ String dateTemplate) {
+ this.strictExampleSet = strictExampleSet;
+ this.textTemplate = textTemplate;
+ this.dateTemplate = dateTemplate;
+ }
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/FakeData.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/FakeData.java
new file mode 100644
index 0000000..5d0837b
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/FakeData.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2018 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.autofill.service.model;
+
+import com.example.android.autofill.service.data.source.local.db.Converters;
+
+public class FakeData {
+ public Converters.StringList strictExampleSet;
+ public String textTemplate;
+ public String dateTemplate;
+
+ public FakeData(Converters.StringList strictExampleSet, String textTemplate, String dateTemplate) {
+ this.strictExampleSet = strictExampleSet;
+ this.textTemplate = textTemplate;
+ this.dateTemplate = dateTemplate;
+ }
+}
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/FieldType.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/FieldType.java
new file mode 100644
index 0000000..4d285f3
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/FieldType.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 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.autofill.service.model;
+
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Embedded;
+import android.arch.persistence.room.Entity;
+import android.support.annotation.NonNull;
+
+import static com.example.android.autofill.service.data.source.local.db.Converters.IntList;
+
+@Entity(primaryKeys = {"typeName"})
+public class FieldType {
+ @NonNull
+ @ColumnInfo(name = "typeName")
+ private final String mTypeName;
+
+ @NonNull
+ @ColumnInfo(name = "autofillTypes")
+ private final IntList mAutofillTypes;
+
+ @NonNull
+ @ColumnInfo(name = "saveInfo")
+ private final Integer mSaveInfo;
+
+ @NonNull
+ @ColumnInfo(name = "partition")
+ private final Integer mPartition;
+
+ @NonNull
+ @Embedded
+ private final FakeData mFakeData;
+
+ public FieldType(@NonNull String typeName, @NonNull IntList autofillTypes,
+ @NonNull Integer saveInfo, @NonNull Integer partition, @NonNull FakeData fakeData) {
+ mTypeName = typeName;
+ mAutofillTypes = autofillTypes;
+ mSaveInfo = saveInfo;
+ mPartition = partition;
+ mFakeData = fakeData;
+ }
+
+ @NonNull
+ public String getTypeName() {
+ return mTypeName;
+ }
+
+ @NonNull
+ public IntList getAutofillTypes() {
+ return mAutofillTypes;
+ }
+
+ @NonNull
+ public Integer getSaveInfo() {
+ return mSaveInfo;
+ }
+
+ @NonNull
+ public Integer getPartition() {
+ return mPartition;
+ }
+
+ @NonNull
+ public FakeData getFakeData() {
+ return mFakeData;
+ }
+}
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/FieldTypeWithHeuristics.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/FieldTypeWithHeuristics.java
new file mode 100644
index 0000000..97a646e
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/FieldTypeWithHeuristics.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2018 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.autofill.service.model;
+
+import android.arch.persistence.room.Embedded;
+import android.arch.persistence.room.Relation;
+
+import java.util.List;
+
+public class FieldTypeWithHeuristics {
+ @Embedded
+ public FieldType fieldType;
+
+ @Relation(parentColumn = "typeName", entityColumn = "fieldTypeName", entity = AutofillHint.class)
+ public List<AutofillHint> autofillHints;
+
+ @Relation(parentColumn = "typeName", entityColumn = "fieldTypeName", entity = ResourceIdHeuristic.class)
+ public List<ResourceIdHeuristic> resourceIdHeuristics;
+
+ public FieldType getFieldType() {
+ return fieldType;
+ }
+
+ public List<AutofillHint> getAutofillHints() {
+ return autofillHints;
+ }
+
+ public List<ResourceIdHeuristic> getResourceIdHeuristics() {
+ return resourceIdHeuristics;
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/ResourceIdHeuristic.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/ResourceIdHeuristic.java
new file mode 100644
index 0000000..81c9abe
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/model/ResourceIdHeuristic.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 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.autofill.service.model;
+
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Entity;
+import android.arch.persistence.room.ForeignKey;
+import android.support.annotation.NonNull;
+
+@Entity(primaryKeys = {"resourceIdHeuristic", "packageName"}, foreignKeys = @ForeignKey(
+ entity = FieldType.class, parentColumns = "typeName", childColumns = "fieldTypeName",
+ onDelete = ForeignKey.CASCADE))
+public class ResourceIdHeuristic {
+
+ @NonNull
+ @ColumnInfo(name = "resourceIdHeuristic")
+ public String mResourceIdHeuristic;
+
+ @NonNull
+ @ColumnInfo(name = "packageName")
+ public String mPackageName;
+
+ @NonNull
+ @ColumnInfo(name = "fieldTypeName")
+ public String mFieldTypeName;
+
+ public ResourceIdHeuristic(@NonNull String resourceIdHeuristic, @NonNull String fieldTypeName,
+ @NonNull String packageName) {
+ mResourceIdHeuristic = resourceIdHeuristic;
+ mFieldTypeName = fieldTypeName;
+ mPackageName = packageName;
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/BasicService.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/BasicService.java
new file mode 100644
index 0000000..2b952e7
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/BasicService.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2018 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.autofill.service.simple;
+
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.os.CancellationSignal;
+import android.service.autofill.AutofillService;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillCallback;
+import android.service.autofill.FillContext;
+import android.service.autofill.FillRequest;
+import android.service.autofill.FillResponse;
+import android.service.autofill.SaveCallback;
+import android.service.autofill.SaveInfo;
+import android.service.autofill.SaveRequest;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.util.ArrayMap;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+import android.widget.Toast;
+
+import com.example.android.autofill.service.MyAutofillService;
+import com.example.android.autofill.service.R;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * A very basic {@link AutofillService} implementation that only shows dynamic-generated datasets
+ * and don't persist the saved data.
+ *
+ * <p>The goal of this class is to provide a simple autofill service implementation that is easy
+ * to understand and extend, but it should <strong>not</strong> be used as-is on real apps because
+ * it lacks fundamental security requirements such as data partitioning and package verification
+ * &mdashthese requirements are fullfilled by {@link MyAutofillService}.
+ */
+public final class BasicService extends AutofillService {
+
+ private static final String TAG = "BasicService";
+
+ /**
+ * Number of datasets sent on each request - we're simple, that value is hardcoded in our DNA!
+ */
+ private static final int NUMBER_DATASETS = 4;
+
+ @Override
+ public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
+ FillCallback callback) {
+ Log.d(TAG, "onFillRequest()");
+
+ // Find autofillable fields
+ AssistStructure structure = getLatestAssistStructure(request);
+ Map<String, AutofillId> fields = getAutofillableFields(structure);
+ Log.d(TAG, "autofillable fields:" + fields);
+
+ if (fields.isEmpty()) {
+ toast("No autofill hints found");
+ callback.onSuccess(null);
+ return;
+ }
+
+ // Create the base response
+ FillResponse.Builder response = new FillResponse.Builder();
+
+ // 1.Add the dynamic datasets
+ String packageName = getApplicationContext().getPackageName();
+ for (int i = 1; i <= NUMBER_DATASETS; i++) {
+ Dataset.Builder dataset = new Dataset.Builder();
+ for (Entry<String, AutofillId> field : fields.entrySet()) {
+ String hint = field.getKey();
+ AutofillId id = field.getValue();
+ String value = i + "-" + hint;
+ // We're simple - our dataset values are hardcoded as "N-hint" (for example,
+ // "1-username", "2-username") and they're displayed as such, except if they're a
+ // password
+ String displayValue = hint.contains("password") ? "password for #" + i : value;
+ RemoteViews presentation = newDatasetPresentation(packageName, displayValue);
+ dataset.setValue(id, AutofillValue.forText(value), presentation);
+ }
+ response.addDataset(dataset.build());
+ }
+
+ // 2.Add save info
+ Collection<AutofillId> ids = fields.values();
+ AutofillId[] requiredIds = new AutofillId[ids.size()];
+ ids.toArray(requiredIds);
+ response.setSaveInfo(
+ // We're simple, so we're generic
+ new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_GENERIC, requiredIds).build());
+
+ // 3.Profit!
+ callback.onSuccess(response.build());
+ }
+
+ @Override
+ public void onSaveRequest(SaveRequest request, SaveCallback callback) {
+ Log.d(TAG, "onSaveRequest()");
+ toast("Save not supported");
+ callback.onSuccess();
+ }
+
+ /**
+ * Parses the {@link AssistStructure} representing the activity being autofilled, and returns a
+ * map of autofillable fields (represented by their autofill ids) mapped by the hint associate
+ * with them.
+ *
+ * <p>An autofillable field is a {@link ViewNode} whose {@link #getHint(ViewNode)} metho
+ */
+ @NonNull
+ private Map<String, AutofillId> getAutofillableFields(@NonNull AssistStructure structure) {
+ Map<String, AutofillId> fields = new ArrayMap<>();
+ int nodes = structure.getWindowNodeCount();
+ for (int i = 0; i < nodes; i++) {
+ ViewNode node = structure.getWindowNodeAt(i).getRootViewNode();
+ addAutofillableFields(fields, node);
+ }
+ return fields;
+ }
+
+ /**
+ * Adds any autofillable view from the {@link ViewNode} and its descendants to the map.
+ */
+ private void addAutofillableFields(@NonNull Map<String, AutofillId> fields,
+ @NonNull ViewNode node) {
+ String[] hints = node.getAutofillHints();
+ if (hints != null) {
+ // We're simple, we only care about the first hint
+ String hint = hints[0].toLowerCase();
+
+ if (hint != null) {
+ AutofillId id = node.getAutofillId();
+ if (!fields.containsKey(hint)) {
+ Log.v(TAG, "Setting hint '" + hint + "' on " + id);
+ fields.put(hint, id);
+ } else {
+ Log.v(TAG, "Ignoring hint '" + hint + "' on " + id
+ + " because it was already set");
+ }
+ }
+ }
+ int childrenSize = node.getChildCount();
+ for (int i = 0; i < childrenSize; i++) {
+ addAutofillableFields(fields, node.getChildAt(i));
+ }
+ }
+
+ /**
+ * Helper method to get the {@link AssistStructure} associated with the latest request
+ * in an autofill context.
+ */
+ @NonNull
+ static AssistStructure getLatestAssistStructure(@NonNull FillRequest request) {
+ List<FillContext> fillContexts = request.getFillContexts();
+ return fillContexts.get(fillContexts.size() - 1).getStructure();
+ }
+
+ /**
+ * Helper method to create a dataset presentation with the given text.
+ */
+ @NonNull
+ static RemoteViews newDatasetPresentation(@NonNull String packageName,
+ @NonNull CharSequence text) {
+ RemoteViews presentation =
+ new RemoteViews(packageName, R.layout.multidataset_service_list_item);
+ presentation.setTextViewText(R.id.text, text);
+ presentation.setImageViewResource(R.id.icon, R.mipmap.ic_launcher);
+ return presentation;
+ }
+
+ /**
+ * Displays a toast with the given message.
+ */
+ private void toast(@NonNull CharSequence message) {
+ Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/HeuristicsService.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/HeuristicsService.java
new file mode 100644
index 0000000..2cc6572
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/HeuristicsService.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2018 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.autofill.service.simple;
+
+import static com.example.android.autofill.service.simple.BasicService.getLatestAssistStructure;
+import static com.example.android.autofill.service.simple.BasicService.newDatasetPresentation;
+
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.content.Context;
+import android.content.IntentSender;
+import android.os.CancellationSignal;
+import android.service.autofill.AutofillService;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillCallback;
+import android.service.autofill.FillRequest;
+import android.service.autofill.FillResponse;
+import android.service.autofill.SaveCallback;
+import android.service.autofill.SaveInfo;
+import android.service.autofill.SaveRequest;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.View;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+import android.widget.Toast;
+
+import com.example.android.autofill.service.MyAutofillService;
+import com.example.android.autofill.service.settings.MyPreferences;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * A basic service that uses some rudimentary heuristics to identify fields that are not explicitly
+ * marked with autofill hints.
+ *
+ * <p>The goal of this class is to provide a simple autofill service implementation that is easy
+ * to understand and extend, but it should <strong>not</strong> be used as-is on real apps because
+ * it lacks fundamental security requirements such as data partitioning and package verification
+ * &mdashthese requirements are fullfilled by {@link MyAutofillService}.
+ */
+public class HeuristicsService extends AutofillService {
+
+ private static final String TAG = "HeuristicsService";
+
+ private boolean mAuthenticateResponses;
+ private boolean mAuthenticateDatasets;
+ private int mNumberDatasets = 4;
+
+ @Override
+ public void onConnected() {
+ super.onConnected();
+
+ // TODO(b/114236837): use its own preferences?
+ MyPreferences pref = MyPreferences.getInstance(getApplicationContext());
+ mAuthenticateResponses = pref.isResponseAuth();
+ mAuthenticateDatasets = pref.isDatasetAuth();
+ // TODO(b/114236837): get number dataset from preferences
+
+ Log.d(TAG, "onConnected(): numberDatasets=" + mNumberDatasets
+ + ", authResponses=" + mAuthenticateResponses
+ + ", authDatasets=" + mAuthenticateDatasets);
+ }
+
+ @Override
+ public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
+ FillCallback callback) {
+ Log.d(TAG, "onFillRequest()");
+
+ // Find autofillable fields
+ AssistStructure structure = getLatestAssistStructure(request);
+ ArrayMap<String, AutofillId> fields = getAutofillableFields(structure);
+ Log.d(TAG, "autofillable fields:" + fields);
+
+ if (fields.isEmpty()) {
+ toast("No autofill hints found");
+ callback.onSuccess(null);
+ return;
+ }
+
+ // Create response...
+ FillResponse response;
+ if (mAuthenticateResponses) {
+ int size = fields.size();
+ String[] hints = new String[size];
+ AutofillId[] ids = new AutofillId[size];
+ for (int i = 0; i < size; i++) {
+ hints[i] = fields.keyAt(i);
+ ids[i] = fields.valueAt(i);
+ }
+
+ IntentSender authentication = SimpleAuthActivity.newIntentSenderForResponse(this, hints,
+ ids, mAuthenticateDatasets);
+ RemoteViews presentation = newDatasetPresentation(getPackageName(),
+ "Tap to auth response");
+
+ response = new FillResponse.Builder()
+ .setAuthentication(ids, authentication, presentation).build();
+ } else {
+ response = createResponse(this, fields, mNumberDatasets,mAuthenticateDatasets);
+ }
+
+ // ... and return it
+ callback.onSuccess(response);
+ }
+
+ @Override
+ public void onSaveRequest(SaveRequest request, SaveCallback callback) {
+ Log.d(TAG, "onSaveRequest()");
+ toast("Save not supported");
+ callback.onSuccess();
+ }
+
+ /**
+ * Parses the {@link AssistStructure} representing the activity being autofilled, and returns a
+ * map of autofillable fields (represented by their autofill ids) mapped by the hint associate
+ * with them.
+ *
+ * <p>An autofillable field is a {@link ViewNode} whose {@link #getHint(ViewNode)} metho
+ */
+ @NonNull
+ private ArrayMap<String, AutofillId> getAutofillableFields(@NonNull AssistStructure structure) {
+ ArrayMap<String, AutofillId> fields = new ArrayMap<>();
+ int nodes = structure.getWindowNodeCount();
+ for (int i = 0; i < nodes; i++) {
+ ViewNode node = structure.getWindowNodeAt(i).getRootViewNode();
+ addAutofillableFields(fields, node);
+ }
+ return fields;
+ }
+
+ /**
+ * Adds any autofillable view from the {@link ViewNode} and its descendants to the map.
+ */
+ private void addAutofillableFields(@NonNull Map<String, AutofillId> fields,
+ @NonNull ViewNode node) {
+ String hint = getHint(node);
+ if (hint != null) {
+ AutofillId id = node.getAutofillId();
+ if (!fields.containsKey(hint)) {
+ Log.v(TAG, "Setting hint '" + hint + "' on " + id);
+ fields.put(hint, id);
+ } else {
+ Log.v(TAG, "Ignoring hint '" + hint + "' on " + id
+ + " because it was already set");
+ }
+ }
+ int childrenSize = node.getChildCount();
+ for (int i = 0; i < childrenSize; i++) {
+ addAutofillableFields(fields, node.getChildAt(i));
+ }
+ }
+
+ @Nullable
+ protected String getHint(@NonNull ViewNode node) {
+
+ // First try the explicit autofill hints...
+
+ String[] hints = node.getAutofillHints();
+ if (hints != null) {
+ // We're simple, we only care about the first hint
+ return hints[0].toLowerCase();
+ }
+
+ // Then try some rudimentary heuristics based on other node properties
+
+ String viewHint = node.getHint();
+ String hint = inferHint(node, viewHint);
+ if (hint != null) {
+ Log.d(TAG, "Found hint using view hint(" + viewHint + "): " + hint);
+ return hint;
+ } else if (!TextUtils.isEmpty(viewHint)) {
+ Log.v(TAG, "No hint using view hint: " + viewHint);
+ }
+
+ String resourceId = node.getIdEntry();
+ hint = inferHint(node, resourceId);
+ if (hint != null) {
+ Log.d(TAG, "Found hint using resourceId(" + resourceId + "): " + hint);
+ return hint;
+ } else if (!TextUtils.isEmpty(resourceId)) {
+ Log.v(TAG, "No hint using resourceId: " + resourceId);
+ }
+
+ CharSequence text = node.getText();
+ CharSequence className = node.getClassName();
+ if (text != null && className != null && className.toString().contains("EditText")) {
+ hint = inferHint(node, text.toString());
+ if (hint != null) {
+ // NODE: text should not be logged, as it could contain PII
+ Log.d(TAG, "Found hint using text(" + text + "): " + hint);
+ return hint;
+ }
+ } else if (!TextUtils.isEmpty(text)) {
+ // NODE: text should not be logged, as it could contain PII
+ Log.v(TAG, "No hint using text: " + text + " and class " + className);
+ }
+ return null;
+ }
+
+ /**
+ * Uses heuristics to infer an autofill hint from a {@code string}.
+ *
+ * @return standard autofill hint, or {@code null} when it could not be inferred.
+ */
+ @Nullable
+ protected String inferHint(ViewNode node, @Nullable String string) {
+ if (string == null) return null;
+
+ string = string.toLowerCase();
+ if (string.contains("label")) {
+ Log.v(TAG, "Ignoring 'label' hint: " + string);
+ return null;
+ }
+ if (string.contains("password")) return View.AUTOFILL_HINT_PASSWORD;
+ if (string.contains("username")
+ || (string.contains("login") && string.contains("id")))
+ return View.AUTOFILL_HINT_USERNAME;
+ if (string.contains("email")) return View.AUTOFILL_HINT_EMAIL_ADDRESS;
+ if (string.contains("name")) return View.AUTOFILL_HINT_NAME;
+ if (string.contains("phone")) return View.AUTOFILL_HINT_PHONE;
+
+ // When everything else fails, return the full string - this is helpful to help app
+ // developers visualize when autofill is triggered when it shouldn't (for example, in a
+ // chat conversation window), so they can mark the root view of such activities with
+ // android:importantForAutofill=noExcludeDescendants
+ if (node.isEnabled() && node.getAutofillType() != View.AUTOFILL_TYPE_NONE) {
+ Log.v(TAG, "Falling back to " + string);
+ return string;
+ }
+ return null;
+ }
+
+ static FillResponse createResponse(@NonNull Context context,
+ @NonNull ArrayMap<String, AutofillId> fields, int numDatasets,
+ boolean authenticateDatasets) {
+ String packageName = context.getPackageName();
+ FillResponse.Builder response = new FillResponse.Builder();
+ // 1.Add the dynamic datasets
+ for (int i = 1; i <= numDatasets; i++) {
+ Dataset unlockedDataset = newUnlockedDataset(fields, packageName, i);
+ if (authenticateDatasets) {
+ Dataset.Builder lockedDataset = new Dataset.Builder();
+ for (Entry<String, AutofillId> field : fields.entrySet()) {
+ String hint = field.getKey();
+ AutofillId id = field.getValue();
+ String value = i + "-" + hint;
+ IntentSender authentication =
+ SimpleAuthActivity.newIntentSenderForDataset(context, unlockedDataset);
+ RemoteViews presentation = newDatasetPresentation(packageName,
+ "Tap to auth " + value);
+ lockedDataset.setValue(id, null, presentation)
+ .setAuthentication(authentication);
+ }
+ response.addDataset(lockedDataset.build());
+ } else {
+ response.addDataset(unlockedDataset);
+ }
+ }
+
+ // 2.Add save info
+ Collection<AutofillId> ids = fields.values();
+ AutofillId[] requiredIds = new AutofillId[ids.size()];
+ ids.toArray(requiredIds);
+ response.setSaveInfo(
+ // We're simple, so we're generic
+ new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_GENERIC, requiredIds).build());
+
+ // 3.Profit!
+ return response.build();
+ }
+
+ static Dataset newUnlockedDataset(@NonNull Map<String, AutofillId> fields,
+ @NonNull String packageName, int i) {
+ Dataset.Builder dataset = new Dataset.Builder();
+ for (Entry<String, AutofillId> field : fields.entrySet()) {
+ String hint = field.getKey();
+ AutofillId id = field.getValue();
+ String value = i + "-" + hint;
+
+ // We're simple - our dataset values are hardcoded as "N-hint" (for example,
+ // "1-username", "2-username") and they're displayed as such, except if they're a
+ // password
+ String displayValue = hint.contains("password") ? "password for #" + i : value;
+ RemoteViews presentation = newDatasetPresentation(packageName, displayValue);
+ dataset.setValue(id, AutofillValue.forText(value), presentation);
+ }
+
+ return dataset.build();
+ }
+
+ /**
+ * Displays a toast with the given message.
+ */
+ private void toast(@NonNull CharSequence message) {
+ Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/SimpleAuthActivity.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/SimpleAuthActivity.java
new file mode 100644
index 0000000..4ff97a7
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/SimpleAuthActivity.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2018 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.autofill.service.simple;
+
+import static android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE;
+import static android.view.autofill.AutofillManager.EXTRA_AUTHENTICATION_RESULT;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillResponse;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.ArrayMap;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+
+import com.example.android.autofill.service.R;
+
+import java.util.Map.Entry;
+
+/**
+ * Activity used for autofill authentication, it simply sets the dataste upon tapping OK.
+ */
+// TODO(b/114236837): should display a small dialog, not take the full screen
+public class SimpleAuthActivity extends Activity {
+
+ private static final String EXTRA_DATASET = "dataset";
+ private static final String EXTRA_HINTS = "hints";
+ private static final String EXTRA_IDS = "ids";
+ private static final String EXTRA_AUTH_DATASETS = "auth_datasets";
+
+ private static int sPendingIntentId = 0;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.simple_service_auth_activity);
+ findViewById(R.id.yes).setOnClickListener((view) -> onYes());
+ findViewById(R.id.no).setOnClickListener((view) -> onNo());
+ }
+
+ private void onYes() {
+ Intent myIntent = getIntent();
+ Intent replyIntent = new Intent();
+ Dataset dataset = myIntent.getParcelableExtra(EXTRA_DATASET);
+ if (dataset != null) {
+ replyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, dataset);
+ } else {
+ String[] hints = myIntent.getStringArrayExtra(EXTRA_HINTS);
+ Parcelable[] ids = myIntent.getParcelableArrayExtra(EXTRA_IDS);
+ boolean authenticateDatasets = myIntent.getBooleanExtra(EXTRA_AUTH_DATASETS, false);
+ int size = hints.length;
+ ArrayMap<String, AutofillId> fields = new ArrayMap<>(size);
+ for (int i = 0; i < size; i++) {
+ fields.put(hints[i], (AutofillId) ids[i]);
+ }
+ FillResponse response =
+ HeuristicsService.createResponse(this, fields, 1, authenticateDatasets);
+ replyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, response);
+
+ }
+ setResult(RESULT_OK, replyIntent);
+ finish();
+ }
+
+ private void onNo() {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ public static IntentSender newIntentSenderForDataset(@NonNull Context context,
+ @NonNull Dataset dataset) {
+ return newIntentSender(context, dataset, null, null, false);
+ }
+
+ public static IntentSender newIntentSenderForResponse(@NonNull Context context,
+ @NonNull String[] hints, @NonNull AutofillId[] ids, boolean authenticateDatasets) {
+ return newIntentSender(context, null, hints, ids, authenticateDatasets);
+ }
+
+ private static IntentSender newIntentSender(@NonNull Context context,
+ @Nullable Dataset dataset, @Nullable String[] hints, @Nullable AutofillId[] ids,
+ boolean authenticateDatasets) {
+ Intent intent = new Intent(context, SimpleAuthActivity.class);
+ if (dataset != null) {
+ intent.putExtra(EXTRA_DATASET, dataset);
+ } else {
+ intent.putExtra(EXTRA_HINTS, hints);
+ intent.putExtra(EXTRA_IDS, ids);
+ intent.putExtra(EXTRA_AUTH_DATASETS, authenticateDatasets);
+ }
+
+ return PendingIntent.getActivity(context, ++sPendingIntentId, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT).getIntentSender();
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/util/AppExecutors.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/util/AppExecutors.java
new file mode 100644
index 0000000..9befaeb
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/util/AppExecutors.java
@@ -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.autofill.service.util;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/**
+ * Global executor pools for the whole application.
+ * <p>
+ * Grouping tasks like this avoids the effects of task starvation (e.g. disk reads don't wait behind
+ * webservice requests).
+ */
+public class AppExecutors {
+
+ private static final int THREAD_COUNT = 3;
+
+ private final Executor diskIO;
+
+ private final Executor networkIO;
+
+ private final Executor mainThread;
+
+ @VisibleForTesting
+ AppExecutors(Executor diskIO, Executor networkIO, Executor mainThread) {
+ this.diskIO = diskIO;
+ this.networkIO = networkIO;
+ this.mainThread = mainThread;
+ }
+
+ public AppExecutors() {
+ this(new DiskIOThreadExecutor(), Executors.newFixedThreadPool(THREAD_COUNT),
+ new MainThreadExecutor());
+ }
+
+ public Executor diskIO() {
+ return diskIO;
+ }
+
+ public Executor networkIO() {
+ return networkIO;
+ }
+
+ public Executor mainThread() {
+ return mainThread;
+ }
+
+ private static class MainThreadExecutor implements Executor {
+ private Handler mainThreadHandler = new Handler(Looper.getMainLooper());
+
+ @Override
+ public void execute(@NonNull Runnable command) {
+ mainThreadHandler.post(command);
+ }
+ }
+
+ /**
+ * Executor that runs a task on a new background thread.
+ */
+ private static class DiskIOThreadExecutor implements Executor {
+
+ private final Executor mDiskIO;
+
+ public DiskIOThreadExecutor() {
+ mDiskIO = Executors.newSingleThreadExecutor();
+ }
+
+ @Override
+ public void execute(@NonNull Runnable command) {
+ mDiskIO.execute(command);
+ }
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/util/SecurityHelper.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/util/SecurityHelper.java
new file mode 100644
index 0000000..5311a5f
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/util/SecurityHelper.java
@@ -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.autofill.service.util;
+
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+/**
+ * Helper class for security checks.
+ */
+public final class SecurityHelper {
+
+ private SecurityHelper() {
+ throw new UnsupportedOperationException("Provides static methods only.");
+ }
+
+ /**
+ * Gets the fingerprint of the signed certificate of a package.
+ */
+ public static String getFingerprint(PackageInfo packageInfo, String packageName) throws
+ PackageManager.NameNotFoundException, IOException, NoSuchAlgorithmException,
+ CertificateException {
+ Signature[] signatures = packageInfo.signatures;
+ if (signatures.length != 1) {
+ throw new SecurityException(packageName + " has " + signatures.length + " signatures");
+ }
+ byte[] cert = signatures[0].toByteArray();
+ try (InputStream input = new ByteArrayInputStream(cert)) {
+ CertificateFactory factory = CertificateFactory.getInstance("X509");
+ X509Certificate x509 = (X509Certificate) factory.generateCertificate(input);
+ MessageDigest md = MessageDigest.getInstance("SHA256");
+ byte[] publicKey = md.digest(x509.getEncoded());
+ return toHexFormat(publicKey);
+ }
+ }
+
+ private static String toHexFormat(byte[] bytes) {
+ StringBuilder builder = new StringBuilder(bytes.length * 2);
+ for (int i = 0; i < bytes.length; i++) {
+ String hex = Integer.toHexString(bytes[i]);
+ int length = hex.length();
+ if (length == 1) {
+ hex = "0" + hex;
+ }
+ if (length > 2) {
+ hex = hex.substring(length - 2, length);
+ }
+ builder.append(hex.toUpperCase());
+ if (i < (bytes.length - 1)) {
+ builder.append(':');
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/util/Util.java b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/util/Util.java
new file mode 100644
index 0000000..a86deea
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/util/Util.java
@@ -0,0 +1,312 @@
+/*
+ * 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.autofill.service.util;
+
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.app.assist.AssistStructure.WindowNode;
+import android.os.Bundle;
+import android.service.autofill.FillContext;
+import android.service.autofill.SaveInfo;
+import android.support.annotation.NonNull;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewStructure.HtmlInfo;
+import android.view.autofill.AutofillValue;
+
+import com.google.common.base.Joiner;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+public final class Util {
+
+ public static final String EXTRA_DATASET_NAME = "dataset_name";
+ public static final String EXTRA_FOR_RESPONSE = "for_response";
+ public static final NodeFilter AUTOFILL_ID_FILTER = (node, id) ->
+ id.equals(node.getAutofillId());
+ private static final String TAG = "AutofillSample";
+ public static LogLevel sLoggingLevel = LogLevel.Off;
+
+ private static void bundleToString(StringBuilder builder, Bundle data) {
+ final Set<String> keySet = data.keySet();
+ builder.append("[Bundle with ").append(keySet.size()).append(" keys:");
+ for (String key : keySet) {
+ builder.append(' ').append(key).append('=');
+ Object value = data.get(key);
+ if ((value instanceof Bundle)) {
+ bundleToString(builder, (Bundle) value);
+ } else {
+ builder.append((value instanceof Object[])
+ ? Arrays.toString((Object[]) value) : value);
+ }
+ }
+ builder.append(']');
+ }
+
+ public static String bundleToString(Bundle data) {
+ if (data == null) {
+ return "N/A";
+ }
+ final StringBuilder builder = new StringBuilder();
+ bundleToString(builder, data);
+ return builder.toString();
+ }
+
+ public static String getTypeAsString(int type) {
+ switch (type) {
+ case View.AUTOFILL_TYPE_TEXT:
+ return "TYPE_TEXT";
+ case View.AUTOFILL_TYPE_LIST:
+ return "TYPE_LIST";
+ case View.AUTOFILL_TYPE_NONE:
+ return "TYPE_NONE";
+ case View.AUTOFILL_TYPE_TOGGLE:
+ return "TYPE_TOGGLE";
+ case View.AUTOFILL_TYPE_DATE:
+ return "TYPE_DATE";
+ }
+ return "UNKNOWN_TYPE";
+ }
+
+ private static String getAutofillValueAndTypeAsString(AutofillValue value) {
+ if (value == null) return "null";
+
+ StringBuilder builder = new StringBuilder(value.toString()).append('(');
+ if (value.isText()) {
+ builder.append("isText");
+ } else if (value.isDate()) {
+ builder.append("isDate");
+ } else if (value.isToggle()) {
+ builder.append("isToggle");
+ } else if (value.isList()) {
+ builder.append("isList");
+ }
+ return builder.append(')').toString();
+ }
+
+ public static void dumpStructure(AssistStructure structure) {
+ if (logVerboseEnabled()) {
+ int nodeCount = structure.getWindowNodeCount();
+ logv("dumpStructure(): component=%s numberNodes=%d",
+ structure.getActivityComponent(), nodeCount);
+ for (int i = 0; i < nodeCount; i++) {
+ logv("node #%d", i);
+ WindowNode node = structure.getWindowNodeAt(i);
+ dumpNode(new StringBuilder(), " ", node.getRootViewNode(), 0);
+ }
+ }
+ }
+
+ private static void dumpNode(StringBuilder builder, String prefix, ViewNode node, int childNumber) {
+ builder.append(prefix)
+ .append("child #").append(childNumber).append("\n");
+
+ builder.append(prefix)
+ .append("autoFillId: ").append(node.getAutofillId())
+ .append("\tidEntry: ").append(node.getIdEntry())
+ .append("\tid: ").append(node.getId())
+ .append("\tclassName: ").append(node.getClassName())
+ .append('\n');
+
+ builder.append(prefix)
+ .append("focused: ").append(node.isFocused())
+ .append("\tvisibility").append(node.getVisibility())
+ .append("\tchecked: ").append(node.isChecked())
+ .append("\twebDomain: ").append(node.getWebDomain())
+ .append("\thint: ").append(node.getHint())
+ .append('\n');
+
+ HtmlInfo htmlInfo = node.getHtmlInfo();
+
+ if (htmlInfo != null) {
+ builder.append(prefix)
+ .append("HTML TAG: ").append(htmlInfo.getTag())
+ .append(" attrs: ").append(htmlInfo.getAttributes())
+ .append('\n');
+ }
+
+ String[] afHints = node.getAutofillHints();
+ CharSequence[] options = node.getAutofillOptions();
+ builder.append(prefix).append("afType: ").append(getTypeAsString(node.getAutofillType()))
+ .append("\tafValue:")
+ .append(getAutofillValueAndTypeAsString(node.getAutofillValue()))
+ .append("\tafOptions:").append(options == null ? "N/A" : Arrays.toString(options))
+ .append("\tafHints: ").append(afHints == null ? "N/A" : Arrays.toString(afHints))
+ .append("\tinputType:").append(node.getInputType())
+ .append('\n');
+
+ int numberChildren = node.getChildCount();
+ builder.append(prefix).append("# children: ").append(numberChildren)
+ .append("\ttext: ").append(node.getText())
+ .append('\n');
+
+ final String prefix2 = prefix + " ";
+ for (int i = 0; i < numberChildren; i++) {
+ dumpNode(builder, prefix2, node.getChildAt(i), i);
+ }
+ logv(builder.toString());
+ }
+
+ public static String getSaveTypeAsString(int type) {
+ List<String> types = new ArrayList<>();
+ if ((type & SaveInfo.SAVE_DATA_TYPE_ADDRESS) != 0) {
+ types.add("ADDRESS");
+ }
+ if ((type & SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD) != 0) {
+ types.add("CREDIT_CARD");
+ }
+ if ((type & SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS) != 0) {
+ types.add("EMAIL_ADDRESS");
+ }
+ if ((type & SaveInfo.SAVE_DATA_TYPE_USERNAME) != 0) {
+ types.add("USERNAME");
+ }
+ if ((type & SaveInfo.SAVE_DATA_TYPE_PASSWORD) != 0) {
+ types.add("PASSWORD");
+ }
+ if (types.isEmpty()) {
+ return "UNKNOWN(" + type + ")";
+ }
+ return Joiner.on('|').join(types);
+ }
+
+ /**
+ * Gets a node if it matches the filter criteria for the given id.
+ */
+ public static ViewNode findNodeByFilter(@NonNull List<FillContext> contexts, @NonNull Object id,
+ @NonNull NodeFilter filter) {
+ for (FillContext context : contexts) {
+ ViewNode node = findNodeByFilter(context.getStructure(), id, filter);
+ if (node != null) {
+ return node;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets a node if it matches the filter criteria for the given id.
+ */
+ public static ViewNode findNodeByFilter(@NonNull AssistStructure structure, @NonNull Object id,
+ @NonNull NodeFilter filter) {
+ logv("Parsing request for activity %s", structure.getActivityComponent());
+ final int nodes = structure.getWindowNodeCount();
+ for (int i = 0; i < nodes; i++) {
+ final WindowNode windowNode = structure.getWindowNodeAt(i);
+ final ViewNode rootNode = windowNode.getRootViewNode();
+ final ViewNode node = findNodeByFilter(rootNode, id, filter);
+ if (node != null) {
+ return node;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets a node if it matches the filter criteria for the given id.
+ */
+ public static ViewNode findNodeByFilter(@NonNull ViewNode node, @NonNull Object id,
+ @NonNull NodeFilter filter) {
+ if (filter.matches(node, id)) {
+ return node;
+ }
+ final int childrenSize = node.getChildCount();
+ if (childrenSize > 0) {
+ for (int i = 0; i < childrenSize; i++) {
+ final ViewNode found = findNodeByFilter(node.getChildAt(i), id, filter);
+ if (found != null) {
+ return found;
+ }
+ }
+ }
+ return null;
+ }
+
+ public static void logd(String message, Object... params) {
+ if (logDebugEnabled()) {
+ Log.d(TAG, String.format(message, params));
+ }
+ }
+
+ public static void logv(String message, Object... params) {
+ if (logVerboseEnabled()) {
+ Log.v(TAG, String.format(message, params));
+ }
+ }
+
+ public static boolean logDebugEnabled() {
+ return sLoggingLevel.ordinal() >= LogLevel.Debug.ordinal();
+ }
+
+ public static boolean logVerboseEnabled() {
+ return sLoggingLevel.ordinal() >= LogLevel.Verbose.ordinal();
+ }
+
+ public static void logw(String message, Object... params) {
+ Log.w(TAG, String.format(message, params));
+ }
+
+ public static void logw(Throwable throwable, String message, Object... params) {
+ Log.w(TAG, String.format(message, params), throwable);
+ }
+
+ public static void loge(String message, Object... params) {
+ Log.e(TAG, String.format(message, params));
+ }
+
+ public static void loge(Throwable throwable, String message, Object... params) {
+ Log.e(TAG, String.format(message, params), throwable);
+ }
+
+ public static void setLoggingLevel(LogLevel level) {
+ sLoggingLevel = level;
+ }
+
+ /**
+ * Helper method for getting the index of a CharSequence object in an array.
+ */
+ public static int indexOf(@NonNull CharSequence[] array, CharSequence charSequence) {
+ int index = -1;
+ if (charSequence == null) {
+ return index;
+ }
+ for (int i = 0; i < array.length; i++) {
+ if (charSequence.equals(array[i])) {
+ index = i;
+ break;
+ }
+ }
+ return index;
+ }
+
+ public enum LogLevel {Off, Debug, Verbose}
+
+ public enum DalCheckRequirement {Disabled, LoginOnly, AllUrls}
+
+ /**
+ * Helper interface used to filter Assist nodes.
+ */
+ public interface NodeFilter {
+ /**
+ * Returns whether the node passes the filter for such given id.
+ */
+ boolean matches(ViewNode node, Object id);
+ }
+}
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/res/drawable/list_divider.xml b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/drawable/list_divider.xml
new file mode 100644
index 0000000..3d6d162
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/drawable/list_divider.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (c) 2017 Google Inc.
+ 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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <size android:height="1dp" android:width="1dp" />
+ <solid android:color="@color/light_grey" />
+</shape>
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/activity_field_picker.xml b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/activity_field_picker.xml
new file mode 100644
index 0000000..949dbfb
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/activity_field_picker.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * Copyright (C) 2018 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.
+-->
+<android.support.constraint.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="@dimen/activity_vertical_margin">
+
+ <TextView
+ android:id="@+id/listTitle"
+ style="@style/Manual.Header"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ tools:text="@string/manual_data_picker_title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/fieldsList"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/spacing_large"
+ android:background="@null"
+ android:elevation="@dimen/card_elevation"
+ android:listDivider="@drawable/list_divider"
+ android:orientation="vertical"
+ android:scrollbarStyle="insideOverlay"
+ android:scrollbars="vertical"
+ app:layoutManager="LinearLayoutManager"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:listitem="@layout/dataset_field" />
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/dataset_field.xml b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/dataset_field.xml
new file mode 100644
index 0000000..b441dba
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/dataset_field.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * Copyright (C) 2018 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.
+-->
+<android.support.constraint.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/spacing_large"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <TextView
+ style="@style/Manual.Content"
+ android:id="@+id/fieldType"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/manual_dataset_name_label"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Username" />
+
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/dataset_suggestion.xml b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/dataset_suggestion.xml
new file mode 100644
index 0000000..030de0a
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/dataset_suggestion.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * 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.
+-->
+<android.support.constraint.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="@+id/fieldTypes"
+ android:src="@drawable/ic_person_black_24dp"/>
+
+ <TextView
+ style="@style/Manual.Header"
+ android:id="@+id/datasetNameLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/manual_dataset_name_label"
+ app:layout_constraintStart_toEndOf="@+id/icon"
+ app:layout_constraintTop_toTopOf="parent" />
+ <TextView
+ style="@style/Manual.Content"
+ android:id="@+id/datasetName"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toStartOf="@+id/datasetNameLabel"
+ app:layout_constraintTop_toBottomOf="@+id/datasetNameLabel"
+ tools:text="dataset-1"/>
+ <TextView
+ style="@style/Manual.Header"
+ android:id="@+id/fieldTypesLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/manual_field_types_label"
+ app:layout_constraintStart_toStartOf="@+id/datasetName"
+ app:layout_constraintTop_toBottomOf="@+id/datasetName"/>
+ <TextView
+ style="@style/Manual.Content"
+ android:id="@+id/fieldTypes"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toStartOf="@+id/fieldTypesLabel"
+ app:layout_constraintTop_toBottomOf="@+id/fieldTypesLabel"
+ tools:text="Username, password, and more."/>
+
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/multidataset_service_manual_activity.xml b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/multidataset_service_manual_activity.xml
new file mode 100644
index 0000000..56117aa
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/multidataset_service_manual_activity.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * Copyright (C) 2018 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.
+-->
+<android.support.constraint.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="@dimen/activity_vertical_margin"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/suggestionsList"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:background="@null"
+ android:elevation="@dimen/card_elevation"
+ android:listDivider="@drawable/list_divider"
+ android:orientation="vertical"
+ android:scrollbarStyle="insideOverlay"
+ android:scrollbars="vertical"
+ app:layoutManager="LinearLayoutManager"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ tools:listitem="@layout/dataset_suggestion" />
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/simple_service_auth_activity.xml b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/simple_service_auth_activity.xml
new file mode 100644
index 0000000..54fb675
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/layout/simple_service_auth_activity.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * Copyright (C) 2018 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:importantForAutofill="noExcludeDescendants"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:orientation="vertical" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Authenticate?" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+ <Button
+ android:id="@+id/no"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="No" />
+ <Button
+ android:id="@+id/yes"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Yes" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/res/raw/default_field_types b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/raw/default_field_types
new file mode 100644
index 0000000..a0e6b26
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/raw/default_field_types
@@ -0,0 +1,1177 @@
+[
+ {
+ "autofillHints": [
+ "country"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "countryseed"
+ },
+ "partition": 1,
+ "saveInfo": 2,
+ "typeName": "country"
+ }
+ },
+ {
+ "autofillHints": [
+ "transaction-amount"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "seed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "transaction-amount"
+ }
+ },
+ {
+ "autofillHints": [
+ "cc-family-name"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "familynameseed"
+ },
+ "partition": 3,
+ "saveInfo": 4,
+ "typeName": "cc-family-name"
+ }
+ },
+ {
+ "autofillHints": [
+ "postal-code",
+ "postalCode"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "1000seed"
+ },
+ "partition": 1,
+ "saveInfo": 2,
+ "typeName": "postalCode"
+ }
+ },
+ {
+ "autofillHints": [
+ "language"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [
+ "Bulgarian",
+ "Croatian",
+ "Czech",
+ "Danish",
+ "Dutch",
+ "English",
+ "Estonian"
+ ]
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "language"
+ }
+ },
+ {
+ "autofillHints": [
+ "tel-national"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "tel-nationalseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "tel-national"
+ }
+ },
+ {
+ "autofillHints": [
+ "transaction-currency"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [
+ "USD",
+ "CAD",
+ "KYD",
+ "CRC"
+ ]
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "transaction-currency"
+ }
+ },
+ {
+ "autofillHints": [
+ "billing"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "billingseed"
+ },
+ "partition": 1,
+ "saveInfo": 0,
+ "typeName": "billing"
+ }
+ },
+ {
+ "autofillHints": [
+ "emailAddress",
+ "email"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "emailAddressseed"
+ },
+ "partition": 2,
+ "saveInfo": 16,
+ "typeName": "emailAddress"
+ }
+ },
+ {
+ "autofillHints": [
+ "password",
+ "current-password"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "usernameseed"
+ },
+ "partition": 0,
+ "saveInfo": 1,
+ "typeName": "password"
+ }
+ },
+ {
+ "autofillHints": [
+ "honorific-suffix"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [
+ "san",
+ "kun",
+ "chan",
+ "sama"
+ ]
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "honorific-suffix"
+ }
+ },
+ {
+ "autofillHints": [
+ "shipping"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "shippingseed"
+ },
+ "partition": 1,
+ "saveInfo": 0,
+ "typeName": "shipping"
+ }
+ },
+ {
+ "autofillHints": [
+ "pager"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "pagerseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "pager"
+ }
+ },
+ {
+ "autofillHints": [
+ "impp"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "imppseed"
+ },
+ "partition": 2,
+ "saveInfo": 16,
+ "typeName": "impp"
+ }
+ },
+ {
+ "autofillHints": [
+ "bday-month"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "dateTemplate": "curr_time",
+ "strictExampleSet": [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "10",
+ "11",
+ "12"
+ ],
+ "textTemplate": "bday-monthseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "bday-month"
+ }
+ },
+ {
+ "autofillHints": [
+ "tel-local-prefix"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "tel-local-prefixseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "tel-local-prefix"
+ }
+ },
+ {
+ "autofillHints": [
+ "tel"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "telseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "tel"
+ }
+ },
+ {
+ "autofillHints": [
+ "fax"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "faxseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "fax"
+ }
+ },
+ {
+ "autofillHints": [
+ "honorific-prefix"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [
+ "Miss",
+ "Ms.",
+ "Mr.",
+ "Mx.",
+ "Sr.",
+ "Dr.",
+ "Lady",
+ "Lord"
+ ]
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "honorific-prefix"
+ }
+ },
+ {
+ "autofillHints": [
+ "bday-year"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [
+ "2000",
+ "1995",
+ "1990",
+ "1985",
+ "1980",
+ "1975",
+ "1970",
+ "1965",
+ "1960",
+ "1955",
+ "1950",
+ "1945"
+ ]
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "bday-year"
+ }
+ },
+ {
+ "autofillHints": [
+ "creditCardExpirationMonth",
+ "cc-exp-month"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3,
+ 4
+ ],
+ "fakeData": {
+ "dateTemplate": "curr_time",
+ "strictExampleSet": [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "10",
+ "11",
+ "12"
+ ]
+ },
+ "partition": 3,
+ "saveInfo": 4,
+ "typeName": "creditCardExpirationMonth"
+ }
+ },
+ {
+ "autofillHints": [
+ "work"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "workseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "work"
+ }
+ },
+ {
+ "autofillHints": [
+ "cc-csc",
+ "creditCardSecurityCode"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "seedseedseed"
+ },
+ "partition": 3,
+ "saveInfo": 4,
+ "typeName": "creditCardSecurityCode"
+ }
+ },
+ {
+ "autofillHints": [
+ "cc-type"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "cc-typeseed"
+ },
+ "partition": 3,
+ "saveInfo": 4,
+ "typeName": "cc-type"
+ }
+ },
+ {
+ "autofillHints": [
+ "bday-day"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "dateTemplate": "curr_time",
+ "strictExampleSet": [],
+ "textTemplate": "seed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "bday-day"
+ }
+ },
+ {
+ "autofillHints": [
+ "address-line2"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "Bldg. seed"
+ },
+ "partition": 1,
+ "saveInfo": 2,
+ "typeName": "address-line2"
+ }
+ },
+ {
+ "autofillHints": [
+ "address-line1"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "seed Fake Ln."
+ },
+ "partition": 1,
+ "saveInfo": 2,
+ "typeName": "address-line1"
+ }
+ },
+ {
+ "autofillHints": [
+ "postalAddress"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "seed Fake Ln, Fake, FA, FAA 10001"
+ },
+ "partition": 1,
+ "saveInfo": 2,
+ "typeName": "postalAddress"
+ }
+ },
+ {
+ "autofillHints": [
+ "address-line3"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "Suite seed"
+ },
+ "partition": 1,
+ "saveInfo": 2,
+ "typeName": "address-line3"
+ }
+ },
+ {
+ "autofillHints": [
+ "phone"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "seed2345678910"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "phone"
+ }
+ },
+ {
+ "autofillHints": [
+ "tel_extension"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "tel_extensionseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "tel_extension"
+ }
+ },
+ {
+ "autofillHints": [
+ "creditCardNumber",
+ "cc-number"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "seed234567"
+ },
+ "partition": 3,
+ "saveInfo": 4,
+ "typeName": "creditCardNumber"
+ }
+ },
+ {
+ "autofillHints": [
+ "name"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "nameseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "name"
+ }
+ },
+ {
+ "autofillHints": [
+ "additional-name"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "additional-nameseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "additional-name"
+ }
+ },
+ {
+ "autofillHints": [
+ "tel-local"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "tel-localseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "tel-local"
+ }
+ },
+ {
+ "autofillHints": [
+ "bday"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 4
+ ],
+ "fakeData": {
+ "dateTemplate": "curr_time",
+ "strictExampleSet": []
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "bday"
+ }
+ },
+ {
+ "autofillHints": [
+ "street-address"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "street-addressseed"
+ },
+ "partition": 1,
+ "saveInfo": 2,
+ "typeName": "street-address"
+ }
+ },
+ {
+ "autofillHints": [
+ "cc-given-name"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "cc-given-nameseed"
+ },
+ "partition": 3,
+ "saveInfo": 4,
+ "typeName": "cc-given-name"
+ }
+ },
+ {
+ "autofillHints": [
+ "creditCardExpirationDate",
+ "cc-exp"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 4
+ ],
+ "fakeData": {
+ "dateTemplate": "curr_time",
+ "strictExampleSet": []
+ },
+ "partition": 3,
+ "saveInfo": 4,
+ "typeName": "creditCardExpirationDate"
+ }
+ },
+ {
+ "autofillHints": [
+ "creditCardExpirationYear",
+ "cc-exp-year"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3,
+ 4
+ ],
+ "fakeData": {
+ "dateTemplate": "curr_time",
+ "strictExampleSet": [],
+ "textTemplate": "creditCardExpirationYearseed"
+ },
+ "partition": 3,
+ "saveInfo": 4,
+ "typeName": "creditCardExpirationYear"
+ }
+ },
+ {
+ "autofillHints": [
+ "organization-title"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "organization-titleseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "organization-title"
+ }
+ },
+ {
+ "autofillHints": [
+ "sex"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [
+ "Male",
+ "Female",
+ "Other"
+ ]
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "sex"
+ }
+ },
+ {
+ "autofillHints": [
+ "section-"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "section-seed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "section-"
+ }
+ },
+ {
+ "autofillHints": [
+ "country-name"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [
+ "USA",
+ "Mexico",
+ "Canada"
+ ]
+ },
+ "partition": 1,
+ "saveInfo": 2,
+ "typeName": "country-name"
+ }
+ },
+ {
+ "autofillHints": [
+ "photo"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "photoseed.jpg"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "photo"
+ }
+ },
+ {
+ "autofillHints": [
+ "tel-area-code"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "tel-area-codeseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "tel-area-code"
+ }
+ },
+ {
+ "autofillHints": [
+ "new-password"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "new-passwordseed"
+ },
+ "partition": 0,
+ "saveInfo": 1,
+ "typeName": "new-password"
+ }
+ },
+ {
+ "autofillHints": [
+ "tel-local-suffix"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "tel-local-suffixseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "tel-local-suffix"
+ }
+ },
+ {
+ "autofillHints": [
+ "family-name"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "family-nameseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "family-name"
+ }
+ },
+ {
+ "autofillHints": [
+ "url"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "https://www.google.com/?count\u003dseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "url"
+ }
+ },
+ {
+ "autofillHints": [
+ "home"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "homeseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "home"
+ }
+ },
+ {
+ "autofillHints": [
+ "address-level4"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "address-level4seed"
+ },
+ "partition": 1,
+ "saveInfo": 2,
+ "typeName": "address-level4"
+ }
+ },
+ {
+ "autofillHints": [
+ "address-level3"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "address-level3seed"
+ },
+ "partition": 1,
+ "saveInfo": 2,
+ "typeName": "address-level3"
+ }
+ },
+ {
+ "autofillHints": [
+ "organization"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "organizationseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "organization"
+ }
+ },
+ {
+ "autofillHints": [
+ "cc-additional-name"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "cc-additional-nameseed"
+ },
+ "partition": 3,
+ "saveInfo": 4,
+ "typeName": "cc-additional-name"
+ }
+ },
+ {
+ "autofillHints": [
+ "address-level2"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "address-level2seed"
+ },
+ "partition": 1,
+ "saveInfo": 2,
+ "typeName": "address-level2"
+ }
+ },
+ {
+ "autofillHints": [
+ "given-name"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "given-nameseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "given-name"
+ }
+ },
+ {
+ "autofillHints": [
+ "address-level1"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "address-level1seed"
+ },
+ "partition": 1,
+ "saveInfo": 2,
+ "typeName": "address-level1"
+ }
+ },
+ {
+ "autofillHints": [
+ "creditCardExpirationDay"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3,
+ 4
+ ],
+ "fakeData": {
+ "dateTemplate": "curr_time",
+ "strictExampleSet": [],
+ "textTemplate": "creditCardExpirationDayseed"
+ },
+ "partition": 3,
+ "saveInfo": 4,
+ "typeName": "creditCardExpirationDay"
+ }
+ },
+ {
+ "autofillHints": [
+ "cc-name"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "Firstnameseed Lastnameseed"
+ },
+ "partition": 3,
+ "saveInfo": 4,
+ "typeName": "cc-name"
+ }
+ },
+ {
+ "autofillHints": [
+ "username"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "usernameseed"
+ },
+ "partition": 0,
+ "saveInfo": 8,
+ "typeName": "username"
+ }
+ },
+ {
+ "autofillHints": [
+ "tel-country-code"
+ ],
+ "fieldType": {
+ "autofillTypes": [
+ 1,
+ 3
+ ],
+ "fakeData": {
+ "strictExampleSet": [],
+ "textTemplate": "tel-country-codeseed"
+ },
+ "partition": 0,
+ "saveInfo": 0,
+ "typeName": "tel-country-code"
+ }
+ }
+]
diff --git a/prebuilts/gradle/AutofillFramework/afservice/src/main/res/xml/heuristics_service.xml b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/xml/heuristics_service.xml
new file mode 100644
index 0000000..fed1c47
--- /dev/null
+++ b/prebuilts/gradle/AutofillFramework/afservice/src/main/res/xml/heuristics_service.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * Copyright (C) 2018 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.
+-->
+
+<!-- TODO(b/114236837): use its own Settings Activity -->
+<autofill-service xmlns:android="http://schemas.android.com/apk/res/android"
+ android:settingsActivity="com.example.android.autofill.service.settings.SettingsActivity">
+
+ <!-- sample app -->
+ <compatibility-package
+ android:name="com.example.android.autofill.app"
+ android:maxLongVersionCode="10000000000"/>
+
+ <!-- well-known browswers, alphabetical order -->
+ <compatibility-package
+ android:name="com.android.chrome"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.chrome.beta"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.chrome.dev"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.chrome.canary"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.microsoft.emmx"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.opera.browser"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.opera.browser.beta"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.opera.mini.native"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.opera.mini.native.beta"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.sec.android.app.sbrowser"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.sec.android.app.sbrowser.beta"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="org.chromium.chrome"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="org.mozilla.fennec_aurora"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="org.mozilla.firefox"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="org.mozilla.firefox_beta"
+ android:maxLongVersionCode="10000000000"/>
+</autofill-service>
diff --git a/prebuilts/gradle/EmojiCompat/README.md b/prebuilts/gradle/EmojiCompat/README.md
index e283cbc..b6e5a1b 100644
--- a/prebuilts/gradle/EmojiCompat/README.md
+++ b/prebuilts/gradle/EmojiCompat/README.md
@@ -96,7 +96,7 @@
--------------
- Android SDK 27
-- Android Build Tools v27.0.2
+- Android Build Tools v27.0.3
- Android Support Repository
Screenshots
diff --git a/prebuilts/gradle/EmojiCompat/app/src/main/AndroidManifest.xml b/prebuilts/gradle/EmojiCompat/app/src/main/AndroidManifest.xml
index 4f2453d..22076c1 100644
--- a/prebuilts/gradle/EmojiCompat/app/src/main/AndroidManifest.xml
+++ b/prebuilts/gradle/EmojiCompat/app/src/main/AndroidManifest.xml
@@ -19,8 +19,12 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.example.android.emojicompat">
+ <!-- Adding this meta-data will download the font when application is installed from
+ Google Play Store. This way the font will be downloaded and ready when your app starts
+ for the first time. -->
+ <meta-data android:name="fontProviderRequests" android:value="Noto Color Emoji Compat"/>
+
<application
- android:name=".EmojiCompatApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
diff --git a/prebuilts/gradle/EmojiCompat/app/src/main/java/com/example/android/emojicompat/EmojiCompatApplication.java b/prebuilts/gradle/EmojiCompat/app/src/main/java/com/example/android/emojicompat/EmojiCompatApplication.java
deleted file mode 100644
index 81543ea..0000000
--- a/prebuilts/gradle/EmojiCompat/app/src/main/java/com/example/android/emojicompat/EmojiCompatApplication.java
+++ /dev/null
@@ -1,70 +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.emojicompat;
-
-import android.app.Application;
-import android.support.annotation.Nullable;
-import android.support.text.emoji.EmojiCompat;
-import android.support.text.emoji.FontRequestEmojiCompatConfig;
-import android.support.text.emoji.bundled.BundledEmojiCompatConfig;
-import android.support.v4.provider.FontRequest;
-import android.util.Log;
-
-
-/**
- * This application uses EmojiCompat.
- */
-public class EmojiCompatApplication extends Application {
-
- private static final String TAG = "EmojiCompatApplication";
-
- /** Change this to {@code false} when you want to use the downloadable Emoji font. */
- private static final boolean USE_BUNDLED_EMOJI = true;
-
- @Override
- public void onCreate() {
- super.onCreate();
-
- final EmojiCompat.Config config;
- if (USE_BUNDLED_EMOJI) {
- // Use the bundled font for EmojiCompat
- config = new BundledEmojiCompatConfig(getApplicationContext());
- } else {
- // Use a downloadable font for EmojiCompat
- final FontRequest fontRequest = new FontRequest(
- "com.google.android.gms.fonts",
- "com.google.android.gms",
- "Noto Color Emoji Compat",
- R.array.com_google_android_gms_fonts_certs);
- config = new FontRequestEmojiCompatConfig(getApplicationContext(), fontRequest)
- .setReplaceAll(true)
- .registerInitCallback(new EmojiCompat.InitCallback() {
- @Override
- public void onInitialized() {
- Log.i(TAG, "EmojiCompat initialized");
- }
-
- @Override
- public void onFailed(@Nullable Throwable throwable) {
- Log.e(TAG, "EmojiCompat initialization failed", throwable);
- }
- });
- }
- EmojiCompat.init(config);
- }
-
-}
diff --git a/prebuilts/gradle/EmojiCompat/app/src/main/java/com/example/android/emojicompat/MainActivity.java b/prebuilts/gradle/EmojiCompat/app/src/main/java/com/example/android/emojicompat/MainActivity.java
index 2f93c54..4769271 100644
--- a/prebuilts/gradle/EmojiCompat/app/src/main/java/com/example/android/emojicompat/MainActivity.java
+++ b/prebuilts/gradle/EmojiCompat/app/src/main/java/com/example/android/emojicompat/MainActivity.java
@@ -18,8 +18,13 @@
import android.content.Context;
import android.os.Bundle;
+import android.support.annotation.Nullable;
import android.support.text.emoji.EmojiCompat;
+import android.support.text.emoji.FontRequestEmojiCompatConfig;
+import android.support.text.emoji.bundled.BundledEmojiCompatConfig;
+import android.support.v4.provider.FontRequest;
import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
import android.widget.TextView;
import java.lang.ref.WeakReference;
@@ -27,6 +32,11 @@
public class MainActivity extends AppCompatActivity {
+ private static final String TAG = "MainActivity";
+
+ /** Change this to {@code false} when you want to use the downloadable Emoji font. */
+ private static final boolean USE_BUNDLED_EMOJI = true;
+
// [U+1F469] (WOMAN) + [U+200D] (ZERO WIDTH JOINER) + [U+1F4BB] (PERSONAL COMPUTER)
private static final String WOMAN_TECHNOLOGIST = "\uD83D\uDC69\u200D\uD83D\uDCBB";
@@ -38,6 +48,9 @@
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
+ initEmojiCompat();
+
setContentView(R.layout.activity_main);
// TextView variant provided by EmojiCompat library
@@ -61,6 +74,37 @@
customTextView.setText(getString(R.string.custom_text_view, EMOJI));
}
+ private void initEmojiCompat() {
+ final EmojiCompat.Config config;
+ if (USE_BUNDLED_EMOJI) {
+ // Use the bundled font for EmojiCompat
+ config = new BundledEmojiCompatConfig(getApplicationContext());
+ } else {
+ // Use a downloadable font for EmojiCompat
+ final FontRequest fontRequest = new FontRequest(
+ "com.google.android.gms.fonts",
+ "com.google.android.gms",
+ "Noto Color Emoji Compat",
+ R.array.com_google_android_gms_fonts_certs);
+ config = new FontRequestEmojiCompatConfig(getApplicationContext(), fontRequest);
+ }
+
+ config.setReplaceAll(true)
+ .registerInitCallback(new EmojiCompat.InitCallback() {
+ @Override
+ public void onInitialized() {
+ Log.i(TAG, "EmojiCompat initialized");
+ }
+
+ @Override
+ public void onFailed(@Nullable Throwable throwable) {
+ Log.e(TAG, "EmojiCompat initialization failed", throwable);
+ }
+ });
+
+ EmojiCompat.init(config);
+ }
+
private static class InitCallback extends EmojiCompat.InitCallback {
private final WeakReference<TextView> mRegularTextViewRef;
diff --git a/prebuilts/gradle/EmojiCompat/build.gradle b/prebuilts/gradle/EmojiCompat/build.gradle
index 5d5882d..97c6ae3 100644
--- a/prebuilts/gradle/EmojiCompat/build.gradle
+++ b/prebuilts/gradle/EmojiCompat/build.gradle
@@ -16,6 +16,7 @@
buildscript {
repositories {
+ google()
jcenter()
}
dependencies {
@@ -25,8 +26,8 @@
allprojects {
repositories {
- jcenter()
google()
+ jcenter()
}
}