Android 自動填入 Autofill Framework

Android 8 (Oreo, SDK 26)增加了自動填入的框架,使我們在填寫表單時更快速較不易出錯。

本篇中使用基礎元件(EditText, SharedPreferences),來完成自動填入的功能。

Step 1: 畫面布局

在主畫面中,我們只使用了一個EditText來實做。

activity_main.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
tools:context=".MainActivity">

<EditText
android:id="@+id/editText"
android:layout_width="348dp"
android:layout_height="79dp"
android:ems="10"
android:inputType="textPersonName"
android:text="Name"
android:autofillHints="user_name"
android:importantForAutofill="yes"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

值得注意的是android:autofillHints, android:importantForAutofill這兩個屬性。

android:autofillHint: 註明autofill的tag,tag內容可以自己定義,也可以使用Android 官方文件HintConstants所定義的tag, 共有38個種類。

android:importantForAutofill : 自動填入服務
此屬性內容值說明如下

  • IMPORTANT_FOR_AUTOFILL_AUTO 預設值, 由系統決定是否使用自動填入
  • IMPORTANT_FOR_AUTOFILL_NO 不使用自動填入,但是子view可以使用
  • IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS 不使用自動填入,子view也不使用
  • IMPORTANT_FOR_AUTOFILL_YES 使用自動填入,包括子view
  • IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS 使用自動填入,但子view不使用

Step 2: 創建AutofillService

新增class MyAutofillService繼承AutofillService,並實現兩個函式onFillRequest及onSaveRequest

onFillRequest為執行自動填入時所呼叫的函式,在此文章中會介紹當執行自動填入時,顯示視窗讓使用者選擇填入的值。

onSaveRequest為保存自動填入的內容值所呼叫。

MyAutofillService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyAutofillService extends AutofillService {
private String TAG = "Nick AutofillService";

//執行自動填入
@Override
public void onFillRequest(@NonNull FillRequest request, @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback fillCallback) {
Log.e(TAG, "onFillRequest");
}

//保存自動填入的值
@Override
public void onSaveRequest(@NonNull SaveRequest request, @NonNull SaveCallback callback) {

}
}

在AndroidManifest.xml中宣告此Service

AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.nickthomas55.autofill.test">

<application
... >
<activity android:name=".MainActivity">
...
</activity>
<service android:name=".MyAutofillService"
android:label="Autofill Service Test"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">

<meta-data android:name="android.autofill"
android:resource="@xml/autofill_filler"/>
<intent-filter>
<action android:name="android.service.autofill.AutofillService"/>
</intent-filter>
</service>
</application>
</manifest>

在使用自動填入服務之前,我們必須先開啟自動填入服務的功能。
從[設定->系統->語言與輸入設定->進階->自動填入服務]

在此頁面中可看見我們在AndroidManifest.xml中所定義的android:label名稱,選取此服務為自動填入服務。

在Project res資料夾中新增xml 資料夾,並在此資料夾中新增autofill_filler.xml
內容如下

autofill_filler.xml
1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<autofill-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="[your package name].MainActivity"/>

Step 3: 初始化

當程式一開始時, 我們先在SharedPreferences中加入自動填入的內容。
宣告AutofillManager和AutofillCallback,在onResume與onPause註冊與解註冊callback。

MainActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class MainActivity extends AppCompatActivity {

private AutofillManager mAutofillManager;
private MyAutofillCallback mAutofillCallback;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

init();

mAutofillManager = this.getSystemService(AutofillManager.class);
mAutofillCallback = new MyAutofillCallback();
}

@Override
protected void onResume() {
super.onResume();
mAutofillManager.registerCallback(mAutofillCallback);
}

@Override
protected void onPause() {
super.onPause();
mAutofillManager.unregisterCallback(mAutofillCallback);
}

private void init() {
SharedPreferences.Editor editor = getSharedPreferences("People_Info", MODE_PRIVATE).edit();
editor.putString("user_name", "nick");
editor.commit();
}


private class MyAutofillCallback extends AutofillManager.AutofillCallback {
@Override
public void onAutofillEvent(@NonNull View view, int event) {
super.onAutofillEvent(view, event);

}

@Override
public void onAutofillEvent(@NonNull View view, int virtualId, int event) {
super.onAutofillEvent(view, virtualId, event);
}
}
}

Step 4: 取得自動填入的元件

在執行自動填入和保存自動填入資料時,我們必須先取得自動填入的元件。

MyAutofillService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
 public class MyAutofillService extends AutofillService {
private String TAG = "Nick AutofillService";

//執行自動填入
@Override
public void onFillRequest(@NonNull FillRequest request, @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback fillCallback) {
Log.e(TAG, "onFillRequest");
//取得畫面中所有元件
List<FillContext> context = request.getFillContexts();
AssistStructure structure = context.get(context.size() - 1).getStructure();

//在所有元件中, 收集需要自動填入的元件
List<AssistStructure.ViewNode> fields = new ArrayList<>();
traverseStructure(structure, fields);
}

//保存自動填入的值
@Override
public void onSaveRequest(@NonNull SaveRequest request, @NonNull SaveCallback callback) {
Log.e(TAG, "onSaveRequest");
//取得畫面中所有元件
List<FillContext> context = request.getFillContexts();
AssistStructure structure = context.get(context.size() - 1).getStructure();
//在所有元件中, 收集自動填入的元件
List<AssistStructure.ViewNode> fields = new ArrayList<>();
traverseStructure(structure, fields);
}

//掃描畫面上所有元件
private void traverseStructure(AssistStructure structure, List<AssistStructure.ViewNode> fields) {
int nodes = structure.getWindowNodeCount();

for (int i = 0; i < nodes; i++) {
AssistStructure.WindowNode windowNode = structure.getWindowNodeAt(i);
AssistStructure.ViewNode viewNode = windowNode.getRootViewNode();
traverseNode(viewNode, fields);
}
}

//收集自動填入的元件
private void traverseNode(AssistStructure.ViewNode viewNode, List<AssistStructure.ViewNode> fields) {
if (viewNode == null || viewNode.getClassName() == null)
return;

if (viewNode.getAutofillHints() != null && viewNode.getAutofillHints().length > 0) {
fields.add(viewNode);
}

for (int i = 0; i < viewNode.getChildCount(); i++) {
AssistStructure.ViewNode childNode = viewNode.getChildAt(i);
traverseNode(childNode, fields);
}
}
}

從SharedPreferences, DB, 或文件中取得自動填入的資料內容

MyAutofillService.java
1
2
3
 SharedPreferences sharedPreferences = getSharedPreferences("People_Info", MODE_PRIVATE);
String userName = sharedPreferences.getString("user_name", "");

設定一個浮動視窗來顯示資料內容

MyAutofillService.java
1
2
3
4
RemoteViews afRemoteView = new RemoteViews(getPackageName(), R.layout.user_suggestion);
afRemoteView.setTextViewText(R.id.user_suggestion_item, userName);
afRemoteView.setImageViewResource(R.id.icon, R.mipmap.ic_launcher_round);

user_suggestion_item.xml 畫面設定如下

user_suggestion_item.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 <?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
xmlns:android="http://schemas.android.com/apk/res/android">

<TextView
android:id="@+id/user_suggestion_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:textSize="18sp"
android:textStyle="bold"/>

<ImageView
android:id="@+id/icon"
android:layout_gravity="center"
android:layout_height="wrap_content"
android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"
android:layout_width="wrap_content"
android:src="@drawable/people" />
</LinearLayout>

將浮動視窗和自動填入元件做Mapping

MyAutofillService.java
1
2
3
4
5
6
//設定元件的顯示視窗
AssistStructure.ViewNode userField = fields.get(0);
Dataset primaryEmailDataSet = new Dataset.Builder(afRemoteView)
.setValue(userField.getAutofillId(), AutofillValue.forText(userName))
.build();

封裝返回數據並通知自動填入框架

MyAutofillService.java
1
2
3
4
5
6
7
//封裝數據
FillResponse response = new FillResponse.Builder()
.addDataset(primaryEmailDataSet)
.build();
//通知自動填入的框架
fillCallback.onSuccess(response);

Source Code

MainActivity.java請參考上面內容,以下為MyAutofillService.java完整內容。

MyAutofillService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
 public class MyAutofillService extends AutofillService {
private String TAG = "Nick AutofillService";

//執行自動填入
@Override
public void onFillRequest(@NonNull FillRequest request, @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback fillCallback) {
Log.e(TAG, "onFillRequest");
//取得畫面中所有元件
List<FillContext> context = request.getFillContexts();
AssistStructure structure = context.get(context.size() - 1).getStructure();

//在所有元件中, 收集需要自動填入的元件
List<AssistStructure.ViewNode> fields = new ArrayList<>();
traverseStructure(structure, fields);

//設定要自動填入的資料內容, 從SharedPreferences, DB, 文件中取得資料內容
SharedPreferences sharedPreferences = getSharedPreferences("People_Info", MODE_PRIVATE);
String userName = sharedPreferences.getString("user_name", "");

//用於顯示內容的視窗
RemoteViews afRemoteView = new RemoteViews(getPackageName(), R.layout.user_suggestion);
afRemoteView.setTextViewText(R.id.user_suggestion_item, userName);
afRemoteView.setImageViewResource(R.id.icon, R.mipmap.ic_launcher_round);

//設定元件的顯示視窗
AssistStructure.ViewNode userField = fields.get(0);
Dataset primaryEmailDataSet = new Dataset.Builder(afRemoteView)
.setValue(userField.getAutofillId(), AutofillValue.forText(userName)
).build();

//封裝數據
FillResponse response = new FillResponse.Builder()
.addDataset(primaryEmailDataSet)
.build();
//通知自動填入的框架
fillCallback.onSuccess(response);
}

//保存自動填入的值
@Override
public void onSaveRequest(@NonNull SaveRequest request, @NonNull SaveCallback callback) {
Log.e(TAG, "onSaveRequest");
//取得畫面中所有元件
List<FillContext> context = request.getFillContexts();
AssistStructure structure = context.get(context.size() - 1).getStructure();
//在所有元件中, 收集自動填入的元件
List<AssistStructure.ViewNode> fields = new ArrayList<>();
traverseStructure(structure, fields);
//保存於SharedPreferences, DB, 文件中
callback.onSuccess();
}

private void traverseStructure(AssistStructure structure, List<AssistStructure.ViewNode> fields) {
int nodes = structure.getWindowNodeCount();

for (int i = 0; i < nodes; i++) {
AssistStructure.WindowNode windowNode = structure.getWindowNodeAt(i);
AssistStructure.ViewNode viewNode = windowNode.getRootViewNode();
traverseNode(viewNode, fields);
}
}

private void traverseNode(AssistStructure.ViewNode viewNode, List<AssistStructure.ViewNode> fields) {
if (viewNode == null || viewNode.getClassName() == null)
return;

if (viewNode.getAutofillHints() != null && viewNode.getAutofillHints().length > 0) {
fields.add(viewNode);
}

for (int i = 0; i < viewNode.getChildCount(); i++) {
AssistStructure.ViewNode childNode = viewNode.getChildAt(i);
traverseNode(childNode, fields);
}
}
}

執行結果

作者

Nick Lin

發表於

2020-05-21

更新於

2023-01-18

許可協議


評論