diff options
author | Felipe Leme <felipeal@google.com> | 2018-10-04 13:39:26 -0700 |
---|---|---|
committer | Felipe Leme <felipeal@google.com> | 2018-10-10 11:58:36 -0700 |
commit | 5367691cf1d3f357b70f2488234e5b2442885c14 (patch) | |
tree | 9781e6d512eff71cdaaad571eeb71c111327fc27 /input | |
parent | b271c51d0a116d5cf21d4a25f0bd67d507f4c308 (diff) | |
download | android-5367691cf1d3f357b70f2488234e5b2442885c14.tar.gz |
Moar changes on Autofill samples.
- Renamed HeuristicsService to DebugService.
- Removed multi-steps support from MyService (it was broken anyways)
- Created a new simple, temporary service to handle multi-step logins.
- Added anti-pattern example for login without hints.
- Added anti-pattern example for autocomplete without using autofill callbacks.
- Added anti-pattern example for multi-step login using activities.
- Added edge-case example for custom theme that changes autofilled text.
Bug: 114236837
Test: manual verification
Change-Id: I7f0f63c090507c247f16eda0075cc20e6dc35139
Diffstat (limited to 'input')
23 files changed, 1159 insertions, 304 deletions
diff --git a/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml b/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml index 624eaee1..59368c6c 100644 --- a/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml +++ b/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml @@ -53,7 +53,12 @@ <activity android:name="com.example.android.autofill.app.edgecases.MultipleStepsCreditCardActivity" /> <activity android:name="com.example.android.autofill.app.commoncases.RecyclerViewActivity" /> <activity android:name="com.example.android.autofill.app.antipatterns.BadViewStructureCreationSignInActivity" /> - + <activity android:name="com.example.android.autofill.app.antipatterns.UsernameOnlyActivity" /> + <activity android:name="com.example.android.autofill.app.antipatterns.PasswordOnlyActivity" /> + <activity android:name="com.example.android.autofill.app.antipatterns.HintlessSignInActivity" /> + <activity android:name="com.example.android.autofill.app.antipatterns.CallbackLessAutoCompleteSignInActivity" /> + <activity android:name="com.example.android.autofill.app.edgecases.CustomThemeSignInActivity" + android:theme="@style/CustomAutofilledHighlightTheme" /> </application> </manifest> diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/CallbackLessAutoCompleteSignInActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/CallbackLessAutoCompleteSignInActivity.java new file mode 100644 index 00000000..9f28912f --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/CallbackLessAutoCompleteSignInActivity.java @@ -0,0 +1,109 @@ +/* + * 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.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 com.example.android.autofill.app.view.widget.InfoButton; + +import static com.example.android.autofill.app.Util.TAG; + +public class CallbackLessAutoCompleteSignInActivity extends AppCompatActivity { + private AutoCompleteTextView mUsernameAutoCompleteField; + private TextView mPasswordField; + private TextView mLoginButton; + private TextView mClearButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.login_with_autocomplete_activity); + + TextView title = findViewById(R.id.standard_login_header); + title.setText(R.string.navigation_button_anti_pattern_callbackless_autocomplete_login_label); + + InfoButton info = findViewById(R.id.imageButton); + info.setInfoText(getString(R.string.anti_pattern_callbackless_autocomplete_login_info)); + + + mLoginButton = findViewById(R.id.login); + mClearButton = findViewById(R.id.clear); + mUsernameAutoCompleteField = findViewById(R.id.usernameField); + mPasswordField = findViewById(R.id.passwordField); + mLoginButton.setOnClickListener((v) -> login()); + mClearButton.setOnClickListener((v) -> { + AutofillManager afm = getSystemService(AutofillManager.class); + if (afm != null) { + afm.cancel(); + } + resetFields(); + }); + ArrayAdapter<CharSequence> mockAutocompleteAdapter = ArrayAdapter.createFromResource + (this, R.array.mock_autocomplete_sign_in_suggestions, + android.R.layout.simple_dropdown_item_1line); + mUsernameAutoCompleteField.setAdapter(mockAutocompleteAdapter); + mUsernameAutoCompleteField.setThreshold(1); + + // Show it right away + mUsernameAutoCompleteField.setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus) { + mUsernameAutoCompleteField.showDropDown(); + } + }); + } + + 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(CallbackLessAutoCompleteSignInActivity.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); + } +}
\ No newline at end of file diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/HintlessSignInActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/HintlessSignInActivity.java new file mode 100644 index 00000000..dfb6d094 --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/HintlessSignInActivity.java @@ -0,0 +1,27 @@ +/* + * 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 com.example.android.autofill.app.commoncases.StandardSignInActivity; +import com.example.android.autofill.app.R; + +public class HintlessSignInActivity extends StandardSignInActivity { + + @Override + protected int getContentView() { + return R.layout.hintless_login_activity; + } +} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/PasswordOnlyActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/PasswordOnlyActivity.java new file mode 100644 index 00000000..44400baf --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/PasswordOnlyActivity.java @@ -0,0 +1,56 @@ +/* + * 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.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import com.example.android.autofill.app.R; +import com.example.android.autofill.app.WelcomeActivity; + +/** + * This activity is the second step in a multi-screen login workflow that uses 2 distinct activities + * for username and password, which causes 2 Autofill Save UI to be shown. + * + * <p>This is a bad pattern anyways—apps should use Fragments in such scenarios. + */ +/* + * TODO list + * - use ConstraintLayout + * - use strings.xml insteaf of hardcoded strings + * - add icon with information + * - extend AppCompatActivity + */ +public final class PasswordOnlyActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.password_only_activity); + + findViewById(R.id.login).setOnClickListener((v) -> login()); + } + + protected int getContentView() { + return R.layout.password_only_activity; + } + + void login() { + startActivity(new Intent(this, WelcomeActivity.class)); + finish(); + } +} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/UsernameOnlyActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/UsernameOnlyActivity.java new file mode 100644 index 00000000..dc5dde19 --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/antipatterns/UsernameOnlyActivity.java @@ -0,0 +1,53 @@ +/* + * 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.app.Activity; +import android.content.Intent; +import android.util.Log; +import android.view.autofill.AutofillManager; + +import com.example.android.autofill.app.R; + +/** + * This activity is the first step in a multi-screen login workflow that uses 2 distinct activities + * for username and password, which causes 2 Autofill Save UI to be shown. + * + * <p>This is a bad pattern anyways—apps should use Fragments in such scenarios. + */ + +/* + * TODO list + * - use ConstraintLayout + * - use strings.xml insteaf of hardcoded strings + * - add icon with information + * - extend AppCompatActivity + */ +public class UsernameOnlyActivity extends Activity { + + @Override + protected void onStart() { + super.onStart(); + + setContentView(R.layout.username_only_activity); + findViewById(R.id.next).setOnClickListener((v) -> next()); + } + + private void next() { + startActivity(new Intent(this, PasswordOnlyActivity.class)); + finish(); + } +} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/StandardSignInActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/StandardSignInActivity.java index c333bce1..3e3b5e9f 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/StandardSignInActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/commoncases/StandardSignInActivity.java @@ -36,7 +36,7 @@ public class StandardSignInActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.login_activity); + setContentView(getContentView()); mUsernameEditText = findViewById(R.id.usernameField); mPasswordEditText = findViewById(R.id.passwordField); findViewById(R.id.login).setOnClickListener(new View.OnClickListener() { @@ -57,6 +57,10 @@ public class StandardSignInActivity extends AppCompatActivity { }); } + protected int getContentView() { + return R.layout.login_activity; + } + private void resetFields() { mUsernameEditText.setText(""); mPasswordEditText.setText(""); diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/AbstractMultipleStepsActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/AbstractMultipleStepsActivity.java index 2baf3351..fc12baa4 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/AbstractMultipleStepsActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/AbstractMultipleStepsActivity.java @@ -20,6 +20,7 @@ import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.view.ViewGroup; +import android.view.autofill.AutofillManager; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; @@ -134,6 +135,10 @@ abstract class AbstractMultipleStepsActivity extends AppCompatActivity { mStatus.setText(message.toString()); mContainer.removeAllViews(); mFinished = true; + AutofillManager afm = getSystemService(AutofillManager.class); + if (afm != null) { + afm.commit(); + } updateButtons(); } diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/CustomThemeSignInActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/CustomThemeSignInActivity.java new file mode 100644 index 00000000..dbb48e91 --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/CustomThemeSignInActivity.java @@ -0,0 +1,40 @@ +/* + * 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.os.Bundle; +import android.widget.TextView; + +import com.example.android.autofill.app.commoncases.StandardSignInActivity; +import com.example.android.autofill.app.view.widget.InfoButton; +import com.example.android.autofill.app.R; + +/** + * Same as {@link StandardSignInActivity}, but using a custom theme (defined in the manifest). + */ +public class CustomThemeSignInActivity extends StandardSignInActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + TextView title = findViewById(R.id.standard_login_header); + title.setText(R.string.navigation_button_custom_theme_login_label); + + InfoButton info = findViewById(R.id.imageButton); + info.setInfoText(getString(R.string.custom_theme_login_info)); + } +} diff --git a/input/autofill/AutofillFramework/Application/src/main/res/drawable/custom_autofilled_highlight.xml b/input/autofill/AutofillFramework/Application/src/main/res/drawable/custom_autofilled_highlight.xml new file mode 100644 index 00000000..f65c32a8 --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/res/drawable/custom_autofilled_highlight.xml @@ -0,0 +1,18 @@ +<!-- + * 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="#4DFF0000" /> +</shape> diff --git a/input/autofill/AutofillFramework/Application/src/main/res/layout/fragment_edge_cases.xml b/input/autofill/AutofillFramework/Application/src/main/res/layout/fragment_edge_cases.xml index 88a353d0..83aac1e0 100644 --- a/input/autofill/AutofillFramework/Application/src/main/res/layout/fragment_edge_cases.xml +++ b/input/autofill/AutofillFramework/Application/src/main/res/layout/fragment_edge_cases.xml @@ -109,5 +109,44 @@ app:labelText="@string/navigation_button_anti_pattern_bad_view_structure_label" app:destinationActivityName="com.example.android.autofill.app.antipatterns.BadViewStructureCreationSignInActivity"/> + <com.example.android.autofill.app.view.widget.NavigationItem + android:id="@+id/multistepSignInAntiPatternButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:imageColor="@android:color/holo_red_dark" + app:infoText="@string/anti_pattern_multistep_signin_info" + app:itemLogo="@drawable/ic_disabled_black_24dp" + app:labelText="@string/navigation_button_anti_pattern_multistep_signin_label" + app:destinationActivityName="com.example.android.autofill.app.antipatterns.UsernameOnlyActivity"/> + + <com.example.android.autofill.app.view.widget.NavigationItem + android:id="@+id/hintlessSignInAntiPatternButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:imageColor="@android:color/holo_red_dark" + app:infoText="@string/anti_pattern_hintless_signin_info" + app:itemLogo="@drawable/ic_disabled_black_24dp" + app:labelText="@string/navigation_button_anti_pattern_hintless_signin_label" + app:destinationActivityName="com.example.android.autofill.app.antipatterns.HintlessSignInActivity"/> + + <com.example.android.autofill.app.view.widget.NavigationItem + android:id="@+id/callbacklessSignInAntiPatternButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:imageColor="@android:color/holo_red_dark" + app:infoText="@string/anti_pattern_callbackless_autocomplete_login_info" + app:itemLogo="@drawable/ic_disabled_black_24dp" + app:labelText="@string/navigation_button_anti_pattern_callbackless_autocomplete_login_label" + app:destinationActivityName="com.example.android.autofill.app.antipatterns.CallbackLessAutoCompleteSignInActivity"/> + + <com.example.android.autofill.app.view.widget.NavigationItem + android:id="@+id/standardLoginWithCustomThemeButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:imageColor="@android:color/holo_blue_dark" + app:infoText="@string/custom_theme_login_info" + app:itemLogo="@drawable/ic_autocomplete_logo_24dp" + app:labelText="@string/navigation_button_custom_theme_login_label" + app:destinationActivityName="com.example.android.autofill.app.edgecases.CustomThemeSignInActivity"/> </LinearLayout> </ScrollView>
\ No newline at end of file diff --git a/input/autofill/AutofillFramework/Application/src/main/res/layout/hintless_login_activity.xml b/input/autofill/AutofillFramework/Application/src/main/res/layout/hintless_login_activity.xml new file mode 100644 index 00000000..433ec306 --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/res/layout/hintless_login_activity.xml @@ -0,0 +1,140 @@ +<?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:id="@+id/authLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + 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/standard_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_anti_pattern_hintless_signin_label" + app:layout_constraintEnd_toEndOf="parent" + 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:layout_marginStart="@dimen/spacing_normal" + android:background="@drawable/ic_info_black_24dp" + app:dialogText="@string/anti_pattern_hintless_signin_info" + app:layout_constraintBottom_toBottomOf="@+id/standard_login_header" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/standard_login_header" + app:layout_constraintTop_toTopOf="@+id/standard_login_header" /> + + <TextView + android:id="@+id/usernameLabel" + style="@style/TextAppearance.AppCompat.Body1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/spacing_normal" + android:layout_marginStart="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_large" + android:labelFor="@+id/obscureFieldId" + android:text="@string/username_label" + app:layout_constraintEnd_toStartOf="@+id/obscureFieldId" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/standard_login_header" /> + + <EditText + android:id="@+id/obscureFieldId" + android:layout_width="@dimen/text_field_width" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/spacing_normal" + android:layout_marginStart="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_normal" + android:inputType="text" + app:layout_constraintBottom_toBottomOf="@+id/usernameLabel" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/usernameLabel" + app:layout_constraintTop_toTopOf="@+id/usernameLabel" /> + + <TextView + android:id="@+id/passwordLabel" + style="@style/TextAppearance.AppCompat.Body1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/spacing_normal" + android:layout_marginStart="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_large" + android:labelFor="@+id/anotherObscureFieldId" + android:text="@string/password_label" + app:layout_constraintEnd_toStartOf="@+id/anotherObscureFieldId" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/usernameLabel" /> + + <EditText + android:id="@+id/anotherObscureFieldId" + android:layout_width="@dimen/text_field_width" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/spacing_normal" + android:layout_marginStart="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_normal" + android:inputType="textPassword" + app:layout_constraintBottom_toBottomOf="@+id/passwordLabel" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/passwordLabel" + app:layout_constraintTop_toTopOf="@+id/passwordLabel" /> + + <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/anotherObscureFieldId" /> + + <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/input/autofill/AutofillFramework/Application/src/main/res/layout/password_only_activity.xml b/input/autofill/AutofillFramework/Application/src/main/res/layout/password_only_activity.xml new file mode 100644 index 00000000..a0fbd1d9 --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/res/layout/password_only_activity.xml @@ -0,0 +1,56 @@ +<?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:focusable="true" + android:focusableInTouchMode="true" + android:orientation="vertical" > + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" > + + <TextView + android:id="@+id/password_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Password" /> + + <EditText + android:id="@+id/password" + android:autofillHints="password" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textPassword" /> + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" > + + <Button + android:id="@+id/login" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Login" /> + </LinearLayout> +</LinearLayout>
\ No newline at end of file diff --git a/input/autofill/AutofillFramework/Application/src/main/res/layout/username_only_activity.xml b/input/autofill/AutofillFramework/Application/src/main/res/layout/username_only_activity.xml new file mode 100644 index 00000000..5ce2887e --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/res/layout/username_only_activity.xml @@ -0,0 +1,55 @@ +<?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:focusable="true" + android:focusableInTouchMode="true" + android:orientation="vertical" > + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" > + + <TextView + android:id="@+id/username_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Username" /> + + <EditText + android:id="@+id/username" + android:autofillHints="username" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" > + + <Button + android:id="@+id/next" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Next" /> + </LinearLayout> +</LinearLayout>
\ No newline at end of file diff --git a/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml b/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml index fbd8a31d..8b40483f 100644 --- a/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml +++ b/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml @@ -34,6 +34,10 @@ <string name="navigation_button_anti_pattern_bad_view_structure_label">Bad View Structure Creation Anti Pattern</string> <string name="navigation_button_multistep_signin_label">Multi-Step Sign In</string> <string name="navigation_button_multistep_cc_label">Multi-Step Credit Card Check Out</string> + <string name="navigation_button_anti_pattern_multistep_signin_label">Multi-Step Sign In Anti Pattern</string> + <string name="navigation_button_anti_pattern_hintless_signin_label">Hintless Sign In Anti Pattern</string> + <string name="navigation_button_anti_pattern_callbackless_autocomplete_login_label">Auto Complete without Autofill Integration Anti Pattern</string> + <string name="navigation_button_custom_theme_login_label">Sign In Using Custom Theme</string> <string name="username_label">Username</string> <string name="password_label">Password</string> <string name="welcome_text">Success!</string> @@ -136,6 +140,9 @@ structure is created onStart() instead of onCreate(), which causes autofill to misbehave when the autofill service requires authentication. </string> + <string name="anti_pattern_hintless_signin_info">This is a sample login page that uses standard EditTexts + from the UI toolkit, but without annotating them with android:autofillHints. + </string> <string name="multi_step_signin_info"> <!--TODO--> @@ -145,6 +152,18 @@ <!--TODO--> </string> + <string name="anti_pattern_multistep_signin_info"> + <!--TODO--> + </string> + + <string name="anti_pattern_callbackless_autocomplete_login_info"> + <!--TODO--> + </string> + + <string name="custom_theme_login_info"> + <!--TODO--> + </string> + <string name="partition_credentials">Credentials</string> <string name="partition_credit_card">Credit Card</string> @@ -169,8 +188,7 @@ </plurals> <string-array name="mock_autocomplete_sign_in_suggestions"> - <item>user-1</item> - <item>user-2</item> + <item>app_provided_user</item> </string-array> <string-array name="month_array"> diff --git a/input/autofill/AutofillFramework/Application/src/main/res/values/styles.xml b/input/autofill/AutofillFramework/Application/src/main/res/values/styles.xml index 303a44dc..e7549acc 100644 --- a/input/autofill/AutofillFramework/Application/src/main/res/values/styles.xml +++ b/input/autofill/AutofillFramework/Application/src/main/res/values/styles.xml @@ -19,6 +19,10 @@ <!-- Customize your theme here. --> </style> + <style name="CustomAutofilledHighlightTheme" parent="AppTheme"> + <item name="android:autofilledHighlight">@drawable/custom_autofilled_highlight</item> + </style> + <style name="CustomDatePickerDialogTheme" parent="android:Theme.Material.Light.Dialog"> <item name="android:datePickerStyle">@style/MyDatePickerStyle</item> </style> diff --git a/input/autofill/AutofillFramework/afservice/src/main/AndroidManifest.xml b/input/autofill/AutofillFramework/afservice/src/main/AndroidManifest.xml index 8c0b7750..9bfff73c 100644 --- a/input/autofill/AutofillFramework/afservice/src/main/AndroidManifest.xml +++ b/input/autofill/AutofillFramework/afservice/src/main/AndroidManifest.xml @@ -36,12 +36,21 @@ </service> <service - android:name=".simple.HeuristicsService" - android:label="Heuristics Autofill Service" + android:name=".simple.DebugService" + android:label="Debug Autofill Service" android:permission="android.permission.BIND_AUTOFILL_SERVICE"> <meta-data android:name="android.autofill" - android:resource="@xml/heuristics_service"/> + android:resource="@xml/debug_service"/> + <intent-filter> + <action android:name="android.service.autofill.AutofillService" /> + </intent-filter> + </service> + + <service + android:name=".simple.MultiStepsService" + android:label="Multiple-steps Service" + android:permission="android.permission.BIND_AUTOFILL_SERVICE"> <intent-filter> <action android:name="android.service.autofill.AutofillService" /> </intent-filter> diff --git a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/settings/MyPreferences.java b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/settings/MyPreferences.java index 8770ef83..6a723f0d 100644 --- a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/settings/MyPreferences.java +++ b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/settings/MyPreferences.java @@ -29,6 +29,7 @@ public class MyPreferences { private static final String MASTER_PASSWORD_KEY = "master_password"; private static final String LOGGING_LEVEL = "logging_level"; private static final String DAL_CHECK_REQUIRED = "dal_check_required"; + private static final String NUMBER_DATASETS = "number_datasets"; private static MyPreferences sInstance; private final SharedPreferences mPrefs; @@ -107,4 +108,12 @@ public class MyPreferences { public void setDalCheckRequired(Util.DalCheckRequirement level) { mPrefs.edit().putInt(DAL_CHECK_REQUIRED, level.ordinal()).apply(); } + + public int getNumberDatasets(int defaultNumber) { + return mPrefs.getInt(NUMBER_DATASETS, defaultNumber); + } + + public void setNumberDatasets(int number) { + mPrefs.edit().putInt(NUMBER_DATASETS, number).apply(); + } } diff --git a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/settings/SettingsActivity.java b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/settings/SettingsActivity.java index 1fd87df0..500ceeca 100644 --- a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/settings/SettingsActivity.java +++ b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/settings/SettingsActivity.java @@ -206,6 +206,7 @@ public class SettingsActivity extends AppCompatActivity { .setView(numberOfDatasetsPicker) .setPositiveButton(R.string.settings_ok, (dialog, which) -> { int numOfDatasets = numberOfDatasetsPicker.getValue(); + mPreferences.setNumberDatasets(numOfDatasets); mLocalAutofillDataSource.getFieldTypes(new DataCallback<List<FieldTypeWithHeuristics>>() { @Override public void onLoaded(List<FieldTypeWithHeuristics> fieldTypes) { diff --git a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/DebugService.java b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/DebugService.java new file mode 100644 index 00000000..501801e8 --- /dev/null +++ b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/DebugService.java @@ -0,0 +1,319 @@ +/* + * 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 provides autofill data for pretty much any input field, even those not + * annotated with autfoill hints. + * + * <p>The goal of this class is to provide a simple autofill service implementation that can be used + * to debug how other apps interact with autofill, it should <strong>not</strong> be used as a + * reference for real autofill service implementations because it lacks fundamental security + * requirements such as data partitioning and package verification &mdashthese requirements are + * fullfilled by {@link MyAutofillService}. + */ +public class DebugService extends AutofillService { + + private static final String TAG = "DebugService"; + + private boolean mAuthenticateResponses; + private boolean mAuthenticateDatasets; + private int mNumberDatasets; + + @Override + public void onConnected() { + super.onConnected(); + + // TODO(b/114236837): use its own preferences? + MyPreferences pref = MyPreferences.getInstance(getApplicationContext()); + mAuthenticateResponses = pref.isResponseAuth(); + mAuthenticateDatasets = pref.isDatasetAuth(); + mNumberDatasets = pref.getNumberDatasets(4); + + 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 actualHint) { + if (actualHint == null) return null; + + String hint = actualHint.toLowerCase(); + if (hint.contains("label") || hint.contains("container")) { + Log.v(TAG, "Ignoring 'label/container' hint: " + hint); + return null; + } + + if (hint.contains("password")) return View.AUTOFILL_HINT_PASSWORD; + if (hint.contains("username") + || (hint.contains("login") && hint.contains("id"))) + return View.AUTOFILL_HINT_USERNAME; + if (hint.contains("email")) return View.AUTOFILL_HINT_EMAIL_ADDRESS; + if (hint.contains("name")) return View.AUTOFILL_HINT_NAME; + if (hint.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 " + actualHint); + return actualHint; + } + 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/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/HeuristicsService.java b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/HeuristicsService.java index 2cc65729..bddc149d 100644 --- a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/HeuristicsService.java +++ b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/HeuristicsService.java @@ -15,303 +15,11 @@ */ 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}. + * @deprecated this class was renamed to {@link DebugService}, but it's still in the project because + * it's linked from the Autofill guide docs site. */ -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(); - } +@Deprecated +public final class HeuristicsService { - /** - * Displays a toast with the given message. - */ - private void toast(@NonNull CharSequence message) { - Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); - } } diff --git a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/MultiStepsService.java b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/MultiStepsService.java new file mode 100644 index 00000000..a4bc338b --- /dev/null +++ b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/MultiStepsService.java @@ -0,0 +1,180 @@ +/* + * 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.Bundle; +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 used to demonstrate multi-steps workflows (such as + * {@code MultipleStepsSignInActivity} and {@code MultipleStepsCreditCardActivity}) by saving the + * save type from previous requests in the client state bundle that's passed along to next requests. + * + * <p>This class should <strong>not</strong> be used as a reference for real autofill service + * implementations because it lacks fundamental security requirements such as data partitioning and + * package verification &mdashthese requirements are fullfilled by {@link MyAutofillService}. + */ +public class MultiStepsService extends AutofillService { + + private static final String TAG = "MultiStepsService"; + private static final String SAVE_TYPE_KEY = "saveType"; + + @Override + public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal, + FillCallback callback) { + int saveType = SaveInfo.SAVE_DATA_TYPE_GENERIC; + Bundle clientState = request.getClientState(); + if (clientState != null) { + saveType = clientState.getInt(SAVE_TYPE_KEY, saveType); + } + Log.d(TAG, "onFillRequest(): saveType=" + saveType); + + // 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; + } + + Collection<AutofillId> ids = fields.values(); + AutofillId[] requiredIds = new AutofillId[ids.size()]; + ids.toArray(requiredIds); + for (int i = 0; i < fields.size(); i++) { + String hint = fields.keyAt(i); + switch (hint) { + case View.AUTOFILL_HINT_USERNAME: + saveType |= SaveInfo.SAVE_DATA_TYPE_USERNAME; + break; + case View.AUTOFILL_HINT_EMAIL_ADDRESS: + saveType |= SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS; + break; + case View.AUTOFILL_HINT_PASSWORD: + saveType |= SaveInfo.SAVE_DATA_TYPE_PASSWORD; + break; + case View.AUTOFILL_HINT_CREDIT_CARD_NUMBER: + case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE: + case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY: + case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH: + case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR: + case View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE: + saveType |= SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD; + break; + case View.AUTOFILL_HINT_POSTAL_ADDRESS: + case View.AUTOFILL_HINT_POSTAL_CODE: + saveType |= SaveInfo.SAVE_DATA_TYPE_ADDRESS; + break; + default: + Log.d(TAG, "Ignoring hint '" + hint + "'"); + } + } + + Log.d(TAG, "new saveType=" + saveType); + if (clientState == null) { + // Initial request + clientState = new Bundle(); + } + // NOTE: to simplify, we're saving just the saveType, but a real service implementation + // would have to save the previous values as well, so they can be used later (for example, + // it would have to save the username in the first request so it's used to save the + // username + password combo in the second request. + clientState.putInt(SAVE_TYPE_KEY, saveType); + + // Create response... + callback.onSuccess(new FillResponse.Builder() + .setClientState(clientState) + .setSaveInfo(new SaveInfo.Builder(saveType, requiredIds).build()) + .build()); + } + + @Override + public void onSaveRequest(SaveRequest request, SaveCallback callback) { + Log.d(TAG, "onSaveRequest()"); + toast("Save not supported"); + callback.onSuccess(); + } + + @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; + } + + 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]; + 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)); + } + } + + private void toast(@NonNull CharSequence message) { + Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); + } +} diff --git a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/SimpleAuthActivity.java b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/SimpleAuthActivity.java index 4ff97a77..048b140b 100644 --- a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/SimpleAuthActivity.java +++ b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/SimpleAuthActivity.java @@ -76,7 +76,7 @@ public class SimpleAuthActivity extends Activity { fields.put(hints[i], (AutofillId) ids[i]); } FillResponse response = - HeuristicsService.createResponse(this, fields, 1, authenticateDatasets); + DebugService.createResponse(this, fields, 1, authenticateDatasets); replyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, response); } diff --git a/input/autofill/AutofillFramework/afservice/src/main/res/xml/heuristics_service.xml b/input/autofill/AutofillFramework/afservice/src/main/res/xml/debug_service.xml index fed1c479..fed1c479 100644 --- a/input/autofill/AutofillFramework/afservice/src/main/res/xml/heuristics_service.xml +++ b/input/autofill/AutofillFramework/afservice/src/main/res/xml/debug_service.xml |