שירות מילוי אוטומטי הוא אפליקציה שמקלה על המשתמשים למלא טפסים על ידי הוספת נתונים לתצוגות של אפליקציות אחרות. שירותי מילוי אוטומטי יכולים גם לאחזר נתוני משתמשים מהתצוגות באפליקציה ולאחסן אותם לשימוש מאוחר יותר. שירותי מילוי אוטומטי מסופקים בדרך כלל על ידי אפליקציות שמנהלות נתוני משתמשים, כמו מנהלי סיסמאות.
ב-Android, קל יותר למלא טפסים באמצעות מסגרת המילוי האוטומטי שזמינה ב-Android 8.0 (API ברמה 26) ואילך. המשתמשים יכולים ליהנות מהתכונות של המילוי האוטומטי רק אם יש במכשיר שלהם אפליקציה שמספקת שירותי מילוי אוטומטי.
בדף הזה מוסבר איך להטמיע שירות מילוי אוטומטי באפליקציה. אם אתם מחפשים דוגמת קוד שמראה איך להטמיע שירות, תוכלו לעיין בדוגמה AutofillFramework ב-Java או ב-Kotlin.
פרטים נוספים על אופן הפעולה של שירותי המילוי האוטומטי זמינים בדפי ההפניה של המחלקות AutofillService
ו-AutofillManager
.
הצהרות והרשאות במניפסט
אפליקציות שמספקות שירותים של מילוי אוטומטי חייבות לכלול הצהרה שמתארת את ההטמעה של השירות. כדי לציין את ההצהרה, צריך לכלול אלמנט <service>
במניפסט האפליקציה. הרכיב
<service>
חייב לכלול את המאפיינים והרכיבים הבאים:
- מאפיין
android:name
שמפנה למחלקת המשנה שלAutofillService
באפליקציה שמטמיעה את השירות. - מאפיין
android:permission
שמצהיר על ההרשאהBIND_AUTOFILL_SERVICE
. - רכיב
<intent-filter>
שרכיב הצאצא שלו<action>
מציין את הפעולהandroid.service.autofill.AutofillService
. - רכיב
<meta-data>
אופציונלי שאפשר להשתמש בו כדי לספק פרמטרים נוספים להגדרת השירות.
בדוגמה הבאה מוצגת הצהרה על שירות מילוי אוטומטי:
<service
android:name=".MyAutofillService"
android:label="My Autofill Service"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<intent-filter>
<action android:name="android.service.autofill.AutofillService" />
</intent-filter>
<meta-data
android:name="android.autofill"
android:resource="@xml/service_configuration" />
</service>
הרכיב <meta-data>
כולל את המאפיין android:resource
שמפנה למשאב XML עם פרטים נוספים על השירות.
המשאב service_configuration
בדוגמה הקודמת מציין פעילות שמאפשרת למשתמשים להגדיר את השירות. בדוגמה הבאה מוצג משאב ה-XML של service_configuration
:
<autofill-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="com.example.android.SettingsActivity" />
מידע נוסף על משאבי XML זמין במאמר סקירה כללית על משאבי אפליקציות.
הנחיה להפעלת השירות
אפליקציה משמשת כשירות למילוי אוטומטי אחרי שהיא מצהירה על הרשאת BIND_AUTOFILL_SERVICE
והמשתמש מפעיל אותה בהגדרות המכשיר. אפליקציה יכולה לאמת אם היא השירות שמופעל כרגע על ידי קריאה לשיטה hasEnabledAutofillServices()
של המחלקה AutofillManager
.
אם האפליקציה לא מוגדרת כרגע כשירות המילוי האוטומטי, היא יכולה לבקש מהמשתמש לשנות את הגדרות המילוי האוטומטי באמצעות ה-Intent ACTION_REQUEST_SET_AUTOFILL_SERVICE
. הכוונה מחזירה ערך של RESULT_OK
אם המשתמש בוחר שירות מילוי אוטומטי שתואם לחבילה של המתקשר.
מילוי תצוגות הלקוח
שירות המילוי האוטומטי מקבל בקשות למילוי תצוגות של לקוחות כשהמשתמש מקיים אינטראקציה עם אפליקציות אחרות. אם בשירות המילוי האוטומטי יש נתוני משתמש שעונים על הבקשה, הוא שולח את הנתונים בתגובה. מערכת Android מציגה ממשק משתמש למילוי אוטומטי עם הנתונים הזמינים, כמו שמוצג באיור 1:
איור 1. ממשק משתמש של מילוי אוטומטי שבו מוצג מערך נתונים.
מסגרת המילוי האוטומטי מגדירה תהליך עבודה למילוי תצוגות, שמיועד לצמצום הזמן שבו מערכת Android קשורה לשירות המילוי האוטומטי. בכל בקשה, מערכת Android שולחת אובייקט AssistStructure
לשירות על ידי קריאה ל-method onFillRequest()
.
שירות המילוי האוטומטי בודק אם הוא יכול למלא את הבקשה באמצעות נתוני משתמש שהוא שמר בעבר. אם השירות יכול למלא את הבקשה, הוא אורז את הנתונים באובייקטים של Dataset
. השירות קורא ל-method onSuccess()
ומעביר אובייקט FillResponse
שמכיל את האובייקטים Dataset
. אם אין לשירות נתונים שיכולים לספק את הבקשה, הוא מעביר את null
לשיטה onSuccess()
.
אם יש שגיאה בעיבוד הבקשה, השירות קורא לשיטה onFailure()
. הסבר מפורט על תהליך העבודה מופיע בתיאור שבAutofillService
דף ההפניה.
הקוד הבא מציג דוגמה לשימוש בשיטה onFillRequest()
:
Kotlin
override fun onFillRequest( request: FillRequest, cancellationSignal: CancellationSignal, callback: FillCallback ) { // Get the structure from the request val context: List<FillContext> = request.fillContexts val structure: AssistStructure = context[context.size - 1].structure // Traverse the structure looking for nodes to fill out val parsedStructure: ParsedStructure = parseStructure(structure) // Fetch user data that matches the fields val (username: String, password: String) = fetchUserData(parsedStructure) // Build the presentation of the datasets val usernamePresentation = RemoteViews(packageName, android.R.layout.simple_list_item_1) usernamePresentation.setTextViewText(android.R.id.text1, "my_username") val passwordPresentation = RemoteViews(packageName, android.R.layout.simple_list_item_1) passwordPresentation.setTextViewText(android.R.id.text1, "Password for my_username") // Add a dataset to the response val fillResponse: FillResponse = FillResponse.Builder() .addDataset(Dataset.Builder() .setValue( parsedStructure.usernameId, AutofillValue.forText(username), usernamePresentation ) .setValue( parsedStructure.passwordId, AutofillValue.forText(password), passwordPresentation ) .build()) .build() // If there are no errors, call onSuccess() and pass the response callback.onSuccess(fillResponse) } data class ParsedStructure(var usernameId: AutofillId, var passwordId: AutofillId) data class UserData(var username: String, var password: String)
Java
@Override public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback) { // Get the structure from the request List<FillContext> context = request.getFillContexts(); AssistStructure structure = context.get(context.size() - 1).getStructure(); // Traverse the structure looking for nodes to fill out ParsedStructure parsedStructure = parseStructure(structure); // Fetch user data that matches the fields UserData userData = fetchUserData(parsedStructure); // Build the presentation of the datasets RemoteViews usernamePresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1); usernamePresentation.setTextViewText(android.R.id.text1, "my_username"); RemoteViews passwordPresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1); passwordPresentation.setTextViewText(android.R.id.text1, "Password for my_username"); // Add a dataset to the response FillResponse fillResponse = new FillResponse.Builder() .addDataset(new Dataset.Builder() .setValue(parsedStructure.usernameId, AutofillValue.forText(userData.username), usernamePresentation) .setValue(parsedStructure.passwordId, AutofillValue.forText(userData.password), passwordPresentation) .build()) .build(); // If there are no errors, call onSuccess() and pass the response callback.onSuccess(fillResponse); } class ParsedStructure { AutofillId usernameId; AutofillId passwordId; } class UserData { String username; String password; }
יכול להיות שלשירות יהיו כמה מערכי נתונים שעומדים בדרישות הבקשה. במקרה הזה, מערכת Android מציגה כמה אפשרויות – אחת לכל מערך נתונים – בממשק המשתמש של המילוי האוטומטי. בדוגמה הבאה של קוד אפשר לראות איך מספקים כמה מערכי נתונים בתגובה:
Kotlin
// Add multiple datasets to the response val fillResponse: FillResponse = FillResponse.Builder() .addDataset(Dataset.Builder() .setValue(parsedStructure.usernameId, AutofillValue.forText(user1Data.username), username1Presentation) .setValue(parsedStructure.passwordId, AutofillValue.forText(user1Data.password), password1Presentation) .build()) .addDataset(Dataset.Builder() .setValue(parsedStructure.usernameId, AutofillValue.forText(user2Data.username), username2Presentation) .setValue(parsedStructure.passwordId, AutofillValue.forText(user2Data.password), password2Presentation) .build()) .build()
Java
// Add multiple datasets to the response FillResponse fillResponse = new FillResponse.Builder() .addDataset(new Dataset.Builder() .setValue(parsedStructure.usernameId, AutofillValue.forText(user1Data.username), username1Presentation) .setValue(parsedStructure.passwordId, AutofillValue.forText(user1Data.password), password1Presentation) .build()) .addDataset(new Dataset.Builder() .setValue(parsedStructure.usernameId, AutofillValue.forText(user2Data.username), username2Presentation) .setValue(parsedStructure.passwordId, AutofillValue.forText(user2Data.password), password2Presentation) .build()) .build();
שירותי המילוי האוטומטי יכולים לנווט באובייקטים ViewNode
ב-AssistStructure
כדי לאחזר את נתוני המילוי האוטומטי שנדרשים לביצוע הבקשה. שירות יכול לאחזר נתונים למילוי אוטומטי באמצעות שיטות של המחלקה ViewNode
, כמו getAutofillId()
.
שירות צריך להיות מסוגל לתאר את התוכן של תצוגה כדי לבדוק אם הוא יכול לספק את הבקשה. הגישה הראשונה שבה שירות צריך להשתמש כדי לתאר את התוכן של תצוגה היא באמצעות המאפיין autofillHints
. עם זאת,
אפליקציות לקוח חייבות לספק את המאפיין באופן מפורש בתצוגות שלהן לפני שהוא זמין לשירות.
אם אפליקציית לקוח לא מספקת את מאפיין autofillHints
, שירות צריך להשתמש בהיוריסטיקה משלו כדי לתאר את התוכן.
השירות יכול להשתמש ב-methods ממחלקות אחרות, כמו getText()
או getHint()
, כדי לקבל מידע על התוכן של התצוגה.
מידע נוסף זמין במאמר בנושא הוספת רמזים למילוי אוטומטי.
בדוגמה הבאה מוצג אופן המעבר ב-AssistStructure
והשליפה של נתוני מילוי אוטומטי מאובייקט ViewNode
:
Kotlin
fun traverseStructure(structure: AssistStructure) { val windowNodes: List<AssistStructure.WindowNode> = structure.run { (0 until windowNodeCount).map { getWindowNodeAt(it) } } windowNodes.forEach { windowNode: AssistStructure.WindowNode -> val viewNode: ViewNode? = windowNode.rootViewNode traverseNode(viewNode) } } fun traverseNode(viewNode: ViewNode?) { if (viewNode?.autofillHints?.isNotEmpty() == true) { // If the client app provides autofill hints, you can obtain them using // viewNode.getAutofillHints(); } else { // Or use your own heuristics to describe the contents of a view // using methods such as getText() or getHint() } val children: List<ViewNode>? = viewNode?.run { (0 until childCount).map { getChildAt(it) } } children?.forEach { childNode: ViewNode -> traverseNode(childNode) } }
Java
public void traverseStructure(AssistStructure structure) { int nodes = structure.getWindowNodeCount(); for (int i = 0; i < nodes; i++) { WindowNode windowNode = structure.getWindowNodeAt(i); ViewNode viewNode = windowNode.getRootViewNode(); traverseNode(viewNode); } } public void traverseNode(ViewNode viewNode) { if(viewNode.getAutofillHints() != null && viewNode.getAutofillHints().length > 0) { // If the client app provides autofill hints, you can obtain them using // viewNode.getAutofillHints(); } else { // Or use your own heuristics to describe the contents of a view // using methods such as getText() or getHint() } for(int i = 0; i < viewNode.getChildCount(); i++) { ViewNode childNode = viewNode.getChildAt(i); traverseNode(childNode); } }
שמירת נתוני המשתמשים
שירות מילוי אוטומטי צריך נתוני משתמשים כדי למלא תצוגות באפליקציות. כשמשתמשים ממלאים תצוגה באופן ידני, הם מתבקשים לשמור את הנתונים בשירות המילוי האוטומטי הנוכחי, כמו שמוצג באיור 2.
איור 2. ממשק משתמש לשמירה לצורך מילוי אוטומטי.
כדי לשמור את הנתונים, השירות צריך לציין שהוא מעוניין לאחסן את הנתונים לשימוש עתידי. לפני שמערכת Android שולחת בקשה לשמירת הנתונים, נשלחת בקשת מילוי שבה השירות יכול למלא את התצוגות. כדי לציין שהשירות מעוניין לשמור את הנתונים, הוא כולל אובייקט SaveInfo
בתשובה לבקשת המילוי. האובייקט SaveInfo
מכיל לפחות את הנתונים הבאים:
- סוג נתוני המשתמשים שנשמרים. רשימה של הערכים הזמינים של
SAVE_DATA
מופיעה במאמרSaveInfo
. - המספר המינימלי של תצוגות שצריך לשנות כדי להפעיל בקשת שמירה.
לדוגמה, בטופס התחברות, המשתמש צריך לעדכן את התצוגות
username
ו-password
כדי להפעיל בקשת שמירה.
אובייקט SaveInfo
משויך לאובייקט FillResponse
, כמו בדוגמת הקוד הבאה:
Kotlin
override fun onFillRequest( request: FillRequest, cancellationSignal: CancellationSignal, callback: FillCallback ) { ... // Builder object requires a non-null presentation val notUsed = RemoteViews(packageName, android.R.layout.simple_list_item_1) val fillResponse: FillResponse = FillResponse.Builder() .addDataset( Dataset.Builder() .setValue(parsedStructure.usernameId, null, notUsed) .setValue(parsedStructure.passwordId, null, notUsed) .build() ) .setSaveInfo( SaveInfo.Builder( SaveInfo.SAVE_DATA_TYPE_USERNAME or SaveInfo.SAVE_DATA_TYPE_PASSWORD, arrayOf(parsedStructure.usernameId, parsedStructure.passwordId) ).build() ) .build() ... }
Java
@Override public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback) { ... // Builder object requires a non-null presentation RemoteViews notUsed = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1); FillResponse fillResponse = new FillResponse.Builder() .addDataset(new Dataset.Builder() .setValue(parsedStructure.usernameId, null, notUsed) .setValue(parsedStructure.passwordId, null, notUsed) .build()) .setSaveInfo(new SaveInfo.Builder( SaveInfo.SAVE_DATA_TYPE_USERNAME | SaveInfo.SAVE_DATA_TYPE_PASSWORD, new AutofillId[] {parsedStructure.usernameId, parsedStructure.passwordId}) .build()) .build(); ... }
שירות המילוי האוטומטי יכול להטמיע לוגיקה כדי לשמור את נתוני המשתמש בשיטה onSaveRequest()
, שמופעלת בדרך כלל אחרי שהפעילות של הלקוח מסתיימת או כשאפליקציית הלקוח מפעילה את commit()
.
הקוד הבא מציג דוגמה לשימוש בשיטה onSaveRequest()
:
Kotlin
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { // Get the structure from the request val context: List<FillContext> = request.fillContexts val structure: AssistStructure = context[context.size - 1].structure // Traverse the structure looking for data to save traverseStructure(structure) // Persist the data - if there are no errors, call onSuccess() callback.onSuccess() }
Java
@Override public void onSaveRequest(SaveRequest request, SaveCallback callback) { // Get the structure from the request List<FillContext> context = request.getFillContexts(); AssistStructure structure = context.get(context.size() - 1).getStructure(); // Traverse the structure looking for data to save traverseStructure(structure); // Persist the data - if there are no errors, call onSuccess() callback.onSuccess(); }
שירותים למילוי אוטומטי צריכים להצפין נתונים רגישים לפני שהם נשמרים. עם זאת, נתוני המשתמשים יכולים לכלול תוויות או נתונים לא רגישים. לדוגמה, חשבון משתמש יכול לכלול תווית שמסמנת את הנתונים כחשבון עבודה או כחשבון אישי. אסור להצפין תוויות של שירותים. אם לא מצפינים את התוויות, שירותים יכולים להשתמש בהן בתצוגות של מצגות אם המשתמש לא עבר אימות. לאחר מכן, השירותים יכולים להחליף את התוויות בנתונים בפועל אחרי שהמשתמש מאומת.
דחיית ממשק המשתמש של שמירת המילוי האוטומטי
החל מ-Android 10, אם משתמשים בכמה מסכים כדי להטמיע תהליך עבודה של מילוי אוטומטי – לדוגמה, מסך אחד לשדה של שם המשתמש ומסך אחר לסיסמה – אפשר לדחות את ממשק המשתמש של שמירת המילוי האוטומטי באמצעות הדגל SaveInfo.FLAG_DELAY_SAVE
.
אם הדגל הזה מוגדר, ממשק המשתמש של השמירה של המילוי האוטומטי לא מופעל כשמתבצעת פעולת commit של הקשר של המילוי האוטומטי שמשויך לתגובה SaveInfo
. במקום זאת, אפשר להשתמש בפעילות נפרדת באותה משימה כדי להעביר בקשות עתידיות למילוי שטחי פרסום, ואז להציג את ממשק המשתמש באמצעות בקשת שמירה. מידע נוסף זמין במאמר SaveInfo.FLAG_DELAY_SAVE
.
דרישת אימות משתמשים
שירותי מילוי אוטומטי יכולים לספק רמת אבטחה נוספת בכך שהם דורשים מהמשתמש לבצע אימות לפני שהם ממלאים תצוגות. התרחישים הבאים הם דוגמאות טובות להטמעה של אימות משתמשים:
- כדי לבטל את הנעילה של נתוני המשתמש באפליקציה, צריך להשתמש בסיסמה ראשית או בסריקת טביעת אצבע.
- צריך לבטל את הנעילה של מערך נתונים ספציפי, כמו פרטי כרטיס אשראי, באמצעות קוד אימות כרטיס (CVC).
בתרחיש שבו השירות דורש אימות משתמש לפני פתיחת הנתונים, השירות יכול להציג נתונים סטנדרטיים או תווית ולציין את Intent
שאחראי על האימות. אם אתם צריכים נתונים נוספים כדי לעבד את הבקשה אחרי השלמת תהליך האימות, אתם יכולים להוסיף את הנתונים האלה לכוונת המשתמש. אחרי האימות, פעילות האימות יכולה להחזיר את הנתונים למחלקה AutofillService
באפליקציה.
בדוגמת הקוד הבאה אפשר לראות איך מציינים שהבקשה דורשת אימות:
Kotlin
val authPresentation = RemoteViews(packageName, android.R.layout.simple_list_item_1).apply { setTextViewText(android.R.id.text1, "requires authentication") } val authIntent = Intent(this, AuthActivity::class.java).apply { // Send any additional data required to complete the request putExtra(MY_EXTRA_DATASET_NAME, "my_dataset") } val intentSender: IntentSender = PendingIntent.getActivity( this, 1001, authIntent, PendingIntent.FLAG_CANCEL_CURRENT ).intentSender // Build a FillResponse object that requires authentication val fillResponse: FillResponse = FillResponse.Builder() .setAuthentication(autofillIds, intentSender, authPresentation) .build()
Java
RemoteViews authPresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1); authPresentation.setTextViewText(android.R.id.text1, "requires authentication"); Intent authIntent = new Intent(this, AuthActivity.class); // Send any additional data required to complete the request authIntent.putExtra(MY_EXTRA_DATASET_NAME, "my_dataset"); IntentSender intentSender = PendingIntent.getActivity( this, 1001, authIntent, PendingIntent.FLAG_CANCEL_CURRENT ).getIntentSender(); // Build a FillResponse object that requires authentication FillResponse fillResponse = new FillResponse.Builder() .setAuthentication(autofillIds, intentSender, authPresentation) .build();
אחרי שהפעילות משלימה את תהליך האימות, היא צריכה לקרוא למתודה setResult()
, להעביר ערך של RESULT_OK
ולהגדיר את EXTRA_AUTHENTICATION_RESULT
extra לאובייקט FillResponse
שכולל את מערך הנתונים שאוכלס. בדוגמת הקוד הבאה אפשר לראות איך להחזיר את התוצאה אחרי שתהליכי האימות מסתיימים:
Kotlin
// The data sent by the service and the structure are included in the intent val datasetName: String? = intent.getStringExtra(MY_EXTRA_DATASET_NAME) val structure: AssistStructure = intent.getParcelableExtra(EXTRA_ASSIST_STRUCTURE) val parsedStructure: ParsedStructure = parseStructure(structure) val (username, password) = fetchUserData(parsedStructure) // Build the presentation of the datasets val usernamePresentation = RemoteViews(packageName, android.R.layout.simple_list_item_1).apply { setTextViewText(android.R.id.text1, "my_username") } val passwordPresentation = RemoteViews(packageName, android.R.layout.simple_list_item_1).apply { setTextViewText(android.R.id.text1, "Password for my_username") } // Add the dataset to the response val fillResponse: FillResponse = FillResponse.Builder() .addDataset(Dataset.Builder() .setValue( parsedStructure.usernameId, AutofillValue.forText(username), usernamePresentation ) .setValue( parsedStructure.passwordId, AutofillValue.forText(password), passwordPresentation ) .build() ).build() val replyIntent = Intent().apply { // Send the data back to the service putExtra(MY_EXTRA_DATASET_NAME, datasetName) putExtra(EXTRA_AUTHENTICATION_RESULT, fillResponse) } setResult(Activity.RESULT_OK, replyIntent)
Java
Intent intent = getIntent(); // The data sent by the service and the structure are included in the intent String datasetName = intent.getStringExtra(MY_EXTRA_DATASET_NAME); AssistStructure structure = intent.getParcelableExtra(EXTRA_ASSIST_STRUCTURE); ParsedStructure parsedStructure = parseStructure(structure); UserData userData = fetchUserData(parsedStructure); // Build the presentation of the datasets RemoteViews usernamePresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1); usernamePresentation.setTextViewText(android.R.id.text1, "my_username"); RemoteViews passwordPresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1); passwordPresentation.setTextViewText(android.R.id.text1, "Password for my_username"); // Add the dataset to the response FillResponse fillResponse = new FillResponse.Builder() .addDataset(new Dataset.Builder() .setValue(parsedStructure.usernameId, AutofillValue.forText(userData.username), usernamePresentation) .setValue(parsedStructure.passwordId, AutofillValue.forText(userData.password), passwordPresentation) .build()) .build(); Intent replyIntent = new Intent(); // Send the data back to the service replyIntent.putExtra(MY_EXTRA_DATASET_NAME, datasetName); replyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, fillResponse); setResult(RESULT_OK, replyIntent);
בתרחיש שבו צריך לבטל את הנעילה של מערך נתונים של כרטיסי אשראי, השירות יכול להציג ממשק משתמש שבו המשתמש מתבקש להזין את קוד האימות (CVC). אפשר להסתיר את הנתונים עד שערכת הנתונים תיפתח באמצעות הצגת נתונים סטנדרטיים, כמו שם הבנק וארבע הספרות האחרונות של מספר כרטיס האשראי. בדוגמה הבאה מוצג איך לדרוש אימות של מערך נתונים ולהסתיר את הנתונים עד שהמשתמש יזין את קוד ה-CVC:
Kotlin
// Parse the structure and fetch payment data val parsedStructure: ParsedStructure = parseStructure(structure) val paymentData: Payment = fetchPaymentData(parsedStructure) // Build the presentation that shows the bank and the last four digits of the // credit card number, such as 'Bank-1234' val maskedPresentation: String = "${paymentData.bank}-" + paymentData.creditCardNumber.substring(paymentData.creditCardNumber.length - 4) val authPresentation = RemoteViews(packageName, android.R.layout.simple_list_item_1).apply { setTextViewText(android.R.id.text1, maskedPresentation) } // Prepare an intent that displays the UI that asks for the CVC val cvcIntent = Intent(this, CvcActivity::class.java) val cvcIntentSender: IntentSender = PendingIntent.getActivity( this, 1001, cvcIntent, PendingIntent.FLAG_CANCEL_CURRENT ).intentSender // Build a FillResponse object that includes a Dataset that requires authentication val fillResponse: FillResponse = FillResponse.Builder() .addDataset( Dataset.Builder() // The values in the dataset are replaced by the actual // data once the user provides the CVC .setValue(parsedStructure.creditCardId, null, authPresentation) .setValue(parsedStructure.expDateId, null, authPresentation) .setAuthentication(cvcIntentSender) .build() ).build()
Java
// Parse the structure and fetch payment data ParsedStructure parsedStructure = parseStructure(structure); Payment paymentData = fetchPaymentData(parsedStructure); // Build the presentation that shows the bank and the last four digits of the // credit card number, such as 'Bank-1234' String maskedPresentation = paymentData.bank + "-" + paymentData.creditCardNumber.subString(paymentData.creditCardNumber.length - 4); RemoteViews authPresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1); authPresentation.setTextViewText(android.R.id.text1, maskedPresentation); // Prepare an intent that displays the UI that asks for the CVC Intent cvcIntent = new Intent(this, CvcActivity.class); IntentSender cvcIntentSender = PendingIntent.getActivity( this, 1001, cvcIntent, PendingIntent.FLAG_CANCEL_CURRENT ).getIntentSender(); // Build a FillResponse object that includes a Dataset that requires authentication FillResponse fillResponse = new FillResponse.Builder() .addDataset(new Dataset.Builder() // The values in the dataset are replaced by the actual // data once the user provides the CVC .setValue(parsedStructure.creditCardId, null, authPresentation) .setValue(parsedStructure.expDateId, null, authPresentation) .setAuthentication(cvcIntentSender) .build()) .build();
אחרי שהפעילות מאמתת את קוד האימות (CVC), היא צריכה להפעיל את השיטה setResult()
, להעביר ערך RESULT_OK
ולהגדיר את התוסף EXTRA_AUTHENTICATION_RESULT
לאובייקט Dataset
שמכיל את מספר כרטיס האשראי ואת תאריך התפוגה. מערך הנתונים החדש מחליף את מערך הנתונים שנדרש לאימות, והתצוגות המפורטות מתמלאות באופן מיידי. בדוגמת הקוד הבאה מוצג איך להחזיר את מערך הנתונים אחרי שהמשתמש מספק את קוד ה-CVC:
Kotlin
// Parse the structure and fetch payment data. val parsedStructure: ParsedStructure = parseStructure(structure) val paymentData: Payment = fetchPaymentData(parsedStructure) // Build a non-null RemoteViews object to use as the presentation when // creating the Dataset object. This presentation isn't actually used, but the // Builder object requires a non-null presentation. val notUsed = RemoteViews(packageName, android.R.layout.simple_list_item_1) // Create a dataset with the credit card number and expiration date. val responseDataset: Dataset = Dataset.Builder() .setValue( parsedStructure.creditCardId, AutofillValue.forText(paymentData.creditCardNumber), notUsed ) .setValue( parsedStructure.expDateId, AutofillValue.forText(paymentData.expirationDate), notUsed ) .build() val replyIntent = Intent().apply { putExtra(EXTRA_AUTHENTICATION_RESULT, responseDataset) }
Java
// Parse the structure and fetch payment data. ParsedStructure parsedStructure = parseStructure(structure); Payment paymentData = fetchPaymentData(parsedStructure); // Build a non-null RemoteViews object to use as the presentation when // creating the Dataset object. This presentation isn't actually used, but the // Builder object requires a non-null presentation. RemoteViews notUsed = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1); // Create a dataset with the credit card number and expiration date. Dataset responseDataset = new Dataset.Builder() .setValue(parsedStructure.creditCardId, AutofillValue.forText(paymentData.creditCardNumber), notUsed) .setValue(parsedStructure.expDateId, AutofillValue.forText(paymentData.expirationDate), notUsed) .build(); Intent replyIntent = new Intent(); replyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, responseDataset);
ארגון הנתונים בקבוצות לוגיות
שירותי המילוי האוטומטי צריכים לארגן את הנתונים בקבוצות לוגיות שמבודדות מושגים מדומיינים שונים. בדף הזה, הקבוצות הלוגיות האלה נקראות מחיצות. ברשימה הבאה מוצגות דוגמאות אופייניות למחיצות ולשדות:
- פרטי כניסה, שכוללים את השדות של שם המשתמש והסיסמה.
- כתובת, שכוללת שדות של רחוב, עיר, מדינה ומיקוד.
- פרטי תשלום, כולל מספר כרטיס אשראי, תאריך תפוגה ושדות קוד האימות.
שירות מילוי אוטומטי שמחלק את הנתונים בצורה נכונה יכול להגן טוב יותר על הנתונים של המשתמשים שלו, כי הוא לא חושף נתונים מיותר ממחיצה אחת במערך נתונים. לדוגמה, מערך נתונים שכולל פרטי כניסה לא צריך לכלול פרטי תשלום. ארגון הנתונים במחיצות מאפשר לשירות לחשוף את הכמות המינימלית של מידע רלוונטי שנדרשת כדי למלא בקשה.
ארגון הנתונים במחיצות מאפשר לשירותים למלא פעילויות שכוללות תצוגות ממחיצות שונות, תוך שליחת כמות מינימלית של נתונים רלוונטיים לאפליקציית הלקוח. לדוגמה, נניח שיש פעילות שכוללת תצוגות של שם משתמש, סיסמה, רחוב ועיר, ושירות מילוי אוטומטי שיש לו את הנתונים הבאים:
מחיצה | שדה 1 | שדה 2 |
---|---|---|
פרטי כניסה | work_username | work_password |
personal_username | personal_password | |
כתובת | work_street | work_city |
personal_street | personal_city |
השירות יכול להכין מערך נתונים שכולל את מחיצת פרטי הכניסה גם לחשבון העבודה וגם לחשבון האישי. כשהמשתמש בוחר מערך נתונים, תגובה של מילוי אוטומטי יכולה לספק את כתובת העבודה או הכתובת האישית, בהתאם לבחירה הראשונה של המשתמש.
שירות יכול לזהות את השדה שממנו הגיעה הבקשה על ידי הפעלת השיטה isFocused()
בזמן המעבר על האובייקט AssistStructure
. כך השירות יכול להכין FillResponse
עם נתוני המחיצה המתאימים.
מילוי אוטומטי של קוד חד-פעמי ב-SMS
שירות המילוי האוטומטי יכול לעזור למשתמש למלא קודים חד-פעמיים שנשלחים באמצעות SMS, באמצעות SMS Retriever API.
כדי להשתמש בתכונה הזו, צריך לעמוד בדרישות הבאות:
- שירות המילוי האוטומטי פועל ב-Android מגרסה 9 (API ברמה 28) ואילך.
- המשתמש נותן הסכמה לשירות המילוי האוטומטי לקרוא קודים חד-פעמיים מ-SMS.
- האפליקציה שאתם מספקים לה מילוי אוטומטי לא משתמשת כבר ב-SMS Retriever API כדי לקרוא קודים חד-פעמיים.
שירות המילוי האוטומטי יכול להשתמש ב-SmsCodeAutofillClient
, שאפשר לגשת אליו באמצעות קריאה ל-SmsCodeRetriever.getAutofillClient()
מ-Google Play Services בגרסה 19.0.56 ומעלה.
השלבים העיקריים לשימוש ב-API הזה בשירות מילוי אוטומטי הם:
- בשירות המילוי האוטומטי, משתמשים ב-
hasOngoingSmsRequest
מ-SmsCodeAutofillClient
כדי לקבוע אם יש בקשות פעילות לשם החבילה של האפליקציה שבה מתבצע מילוי אוטומטי. שירות המילוי האוטומטי יציג הנחיה להצעה רק אם הפונקציה הזו תחזירfalse
. - בשירות המילוי האוטומטי, משתמשים ב-
checkPermissionState
מ-SmsCodeAutofillClient
כדי לבדוק אם לשירות המילוי האוטומטי יש הרשאה למלא באופן אוטומטי קודים חד-פעמיים. מצב ההרשאה יכול להיותNONE
,GRANTED
אוDENIED
. שירות המילוי האוטומטי צריך להציג הנחיה להצעה במצביםNONE
ו-GRANTED
. - בפעילות האימות של המילוי האוטומטי, משתמשים בהרשאה
SmsRetriever.SEND_PERMISSION
כדי לרשוםBroadcastReceiver
להאזנה ל-SmsCodeRetriever.SMS_CODE_RETRIEVED_ACTION
כדי לקבל את התוצאה של קוד ה-SMS כשהיא זמינה. מתקשרים למספר
startSmsCodeRetriever
בטלפוןSmsCodeAutofillClient
כדי להתחיל להאזין לקודים חד-פעמיים שנשלחים באמצעות SMS. אם המשתמש מעניק הרשאות לשירות המילוי האוטומטי שלכם לאחזר קודים חד-פעמיים מ-SMS, השירות יחפש הודעות SMS שהתקבלו בטווח של דקה עד חמש דקות מהרגע הנוכחי.אם שירות המילוי האוטומטי צריך לבקש הרשאת משתמש לקריאת קודים חד-פעמיים, יכול להיות שהערך
Task
שמוחזר על ידיstartSmsCodeRetriever
ייכשל עם הערךResolvableApiException
שמוחזר. במקרה כזה, צריך להפעיל את השיטהResolvableApiException.startResolutionForResult()
כדי להציג תיבת דו-שיח לבקשת הסכמה להרשאה.מקבלים את התוצאה של קוד ה-SMS מה-Intent ואז מחזירים את קוד ה-SMS כתשובה למילוי אוטומטי.
הפעלת מילוי אוטומטי ב-Chrome
Chrome מאפשר לשירותים של צד שלישי למילוי אוטומטי למלא טפסים באופן מקורי, וכך מספק למשתמשים חוויית משתמש חלקה ופשוטה יותר. כדי להשתמש בשירותי מילוי אוטומטי של צד שלישי למילוי אוטומטי של סיסמאות, מפתחות גישה ומידע אחר כמו כתובות ופרטי תשלום, המשתמשים צריכים לבחור באפשרות מילוי אוטומטי באמצעות שירות אחר בהגדרות של Chrome.
כדי להבטיח שהמשתמשים ייהנו מחוויית המילוי האוטומטי הטובה ביותר האפשרית באמצעות השירות שלכם ו-Chrome ב-Android, ספקי שירותי מילוי אוטומטי צריכים לעודד את המשתמשים שלהם לציין את ספק השירות המועדף עליהם בהגדרות של Chrome.
כדי לעזור למשתמשים להפעיל את המתג, המפתחים יכולים:
- שאילתת הגדרות Chrome כדי לדעת אם המשתמש רוצה להשתמש בשירות מילוי אוטומטי של צד שלישי.
- קישור עמוק לדף ההגדרות של Chrome שבו המשתמשים יכולים להפעיל שירותי מילוי אוטומטי של צד שלישי.
קריאת הגדרות Chrome
כל אפליקציה יכולה לקרוא אם Chrome משתמש במצב מילוי אוטומטי של צד שלישי שמאפשר לו להשתמש במילוי האוטומטי של Android. Chrome משתמש ב-ContentProvider
של Android כדי להעביר את המידע הזה. מצהירים בקובץ המניפסט של Android אילו ערוצים רוצים לקרוא מההגדרות:
<uses-permission android:name="android.permission.READ_USER_DICTIONARY"/>
<queries>
<!-- To Query Chrome Beta: -->
<package android:name="com.chrome.beta" />
<!-- To Query Chrome Stable: -->
<package android:name="com.android.chrome" />
</queries>
לאחר מכן, משתמשים ב-ContentResolver
של Android כדי לבקש את המידע הזה על ידי יצירת URI של תוכן:
Kotlin
val CHROME_CHANNEL_PACKAGE = "com.android.chrome" // Chrome Stable. val CONTENT_PROVIDER_NAME = ".AutofillThirdPartyModeContentProvider" val THIRD_PARTY_MODE_COLUMN = "autofill_third_party_state" val THIRD_PARTY_MODE_ACTIONS_URI_PATH = "autofill_third_party_mode" val uri = Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(CHROME_CHANNEL_PACKAGE + CONTENT_PROVIDER_NAME) .path(THIRD_PARTY_MODE_ACTIONS_URI_PATH) .build() val cursor = contentResolver.query( uri, arrayOf(THIRD_PARTY_MODE_COLUMN), // projection null, // selection null, // selectionArgs null // sortOrder ) if (cursor == null) { // Terminate now! Older versions of Chromium don't provide this information. } cursor?.use { // Use the safe call operator and the use function for auto-closing if (it.moveToFirst()) { // Check if the cursor has any rows val index = it.getColumnIndex(THIRD_PARTY_MODE_COLUMN) if (index != -1) { // Check if the column exists val value = it.getInt(index) if (0 == value) { // 0 means that the third party mode is turned off. Chrome uses its built-in // password manager. This is the default for new users. } else { // 1 means that the third party mode is turned on. Chrome uses forwards all // autofill requests to Android Autofill. Users have to opt-in for this. } } else { // Handle the case where the column doesn't exist. Log a warning, perhaps. Log.w("Autofill", "Column $THIRD_PARTY_MODE_COLUMN not found in cursor") } } } // The cursor is automatically closed here
Java
final String CHROME_CHANNEL_PACKAGE = "com.android.chrome"; // Chrome Stable. final String CONTENT_PROVIDER_NAME = ".AutofillThirdPartyModeContentProvider"; final String THIRD_PARTY_MODE_COLUMN = "autofill_third_party_state"; final String THIRD_PARTY_MODE_ACTIONS_URI_PATH = "autofill_third_party_mode"; final Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(CHROME_CHANNEL_PACKAGE + CONTENT_PROVIDER_NAME) .path(THIRD_PARTY_MODE_ACTIONS_URI_PATH) .build(); final Cursor cursor = getContentResolver().query( uri, /*projection=*/new String[] {THIRD_PARTY_MODE_COLUMN}, /*selection=*/ null, /*selectionArgs=*/ null, /*sortOrder=*/ null); if (cursor == null) { // Terminate now! Older versions of Chromium don't provide this information. } cursor.moveToFirst(); // Retrieve the result; int index = cursor.getColumnIndex(THIRD_PARTY_MODE_COLUMN); if (0 == cursor.getInt(index)) { // 0 means that the third party mode is turned off. Chrome uses its built-in // password manager. This is the default for new users. } else { // 1 means that the third party mode is turned on. Chrome uses forwards all // autofill requests to Android Autofill. Users have to opt-in for this. }
קישור עומק להגדרות Chrome
כדי ליצור קישור עמוק לדף ההגדרות של Chrome שבו המשתמשים יכולים להפעיל שירותי מילוי אוטומטי של צד שלישי, צריך להשתמש ב-Intent
של Android.
חשוב להגדיר את הפעולה והקטגוריות כמו בדוגמה הזו:
Kotlin
val autofillSettingsIntent = Intent(Intent.ACTION_APPLICATION_PREFERENCES) autofillSettingsIntent.addCategory(Intent.CATEGORY_DEFAULT) autofillSettingsIntent.addCategory(Intent.CATEGORY_APP_BROWSER) autofillSettingsIntent.addCategory(Intent.CATEGORY_PREFERENCE) // Invoking the intent with a chooser allows users to select the channel they // want to configure. If only one browser reacts to the intent, the chooser is // skipped. val chooser = Intent.createChooser(autofillSettingsIntent, "Pick Chrome Channel") startActivity(chooser) // If the caller knows which Chrome channel they want to configure, // they can instead add a package hint to the intent, e.g. val specificChromeIntent = Intent(Intent.ACTION_APPLICATION_PREFERENCES) // Create a *new* intent specificChromeIntent.addCategory(Intent.CATEGORY_DEFAULT) specificChromeIntent.addCategory(Intent.CATEGORY_APP_BROWSER) specificChromeIntent.addCategory(Intent.CATEGORY_PREFERENCE) specificChromeIntent.setPackage("com.android.chrome") // Set the package on the *new* intent startActivity(specificChromeIntent) // Start the *new* intent
Java
Intent autofillSettingsIntent = new Intent(Intent.ACTION_APPLICATION_PREFERENCES); autofillSettingsIntent.addCategory(Intent.CATEGORY_DEFAULT); autofillSettingsIntent.addCategory(Intent.CATEGORY_APP_BROWSER); autofillSettingsIntent.addCategory(Intent.CATEGORY_PREFERENCE); // Invoking the intent with a chooser allows users to select the channel they // want to configure. If only one browser reacts to the intent, the chooser is // skipped. Intent chooser = Intent.createChooser(autofillSettingsIntent, "Pick Chrome Channel"); startActivity(chooser); // If the caller knows which Chrome channel they want to configure, // they can instead add a package hint to the intent, e.g. autofillSettingsIntent.setPackage("com.android.chrome"); startActivity(autofillSettingsInstent);
תרחישים מתקדמים של מילוי אוטומטי
- שילוב עם המקלדת
- החל מ-Android 11, הפלטפורמה מאפשרת למקלדות ולעורכי שיטות קלט (IME) אחרים להציג הצעות למילוי אוטומטי בשורה, במקום להשתמש בתפריט נפתח. מידע נוסף על האופן שבו שירות המילוי האוטומטי יכול לתמוך בפונקציונליות הזו זמין במאמר בנושא שילוב מילוי אוטומטי עם מקלדות.
- עימוד של מערכי נתונים
- תשובה גדולה למילוי אוטומטי יכולה לחרוג מהגודל המקסימלי של טרנזקציה באובייקט
Binder
שמייצג את האובייקט שניתן להעברה מרחוק שנדרש לעיבוד הבקשה. כדי למנוע ממערכת Android לזרוק חריג בתרחישים האלה, אפשר להקפיד על כך ש-FillResponse
יהיה קטן ולא להוסיף יותר מ-20 אובייקטים שלDataset
בכל פעם. אם התשובה צריכה לכלול עוד מערכי נתונים, אפשר להוסיף מערך נתונים שמודיע למשתמשים שיש עוד מידע, ומאחזר את הקבוצה הבאה של מערכי הנתונים כשבוחרים בו. מידע נוסף זמיןaddDataset(Dataset)
. - שמירת פיצול הנתונים במספר מסכים
לעתים קרובות, אפליקציות מפצלות את נתוני המשתמשים למסכים מרובים באותה פעילות, במיוחד בפעילויות שמשמשות ליצירת חשבון משתמש חדש. לדוגמה, במסך הראשון המשתמש מתבקש להזין שם משתמש, ואם שם המשתמש זמין, במסך השני הוא מתבקש להזין סיסמה. במקרים כאלה, שירות המילוי האוטומטי צריך להמתין עד שהמשתמש יזין את שני השדות לפני שיוצג ממשק המשתמש של שמירת המילוי האוטומטי. כדי לטפל בתרחישים כאלה, פועלים לפי השלבים הבאים:
- בבקשת המילוי הראשונה, מוסיפים חבילת מצב לקוח בתגובה שמכילה את מזהי המילוי האוטומטי של השדות החלקיים שמוצגים במסך.
- בבקשת המילוי השנייה, מאחזרים את חבילת מצב הלקוח, מקבלים את מזהי המילוי האוטומטי שהוגדרו בבקשה הקודמת ממצב הלקוח, ומוסיפים את המזהים האלה ואת הדגל
FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE
לאובייקטSaveInfo
שמשמש בתגובה השנייה. - בבקשת השמירה, משתמשים באובייקטים המתאימים של
FillContext
כדי לקבל את הערך של כל שדה. יש הקשר מילוי אחד לכל בקשת מילוי.
מידע נוסף זמין במאמר שמירה כשנתונים מחולקים למספר מסכים.
- הגדרת לוגיקה של הפעלה וסגירה לכל בקשה
בכל פעם שיש בקשה למילוי אוטומטי, מערכת Android מתחברת לשירות ומפעילה את השיטה
onConnected()
. אחרי שהשירות מעבד את הבקשה, מערכת Android קוראת לשיטהonDisconnected()
ומבטלת את הקישור לשירות. אפשר להטמיע אתonConnected()
כדי לספק קוד שמופעל לפני עיבוד הבקשה ואתonDisconnected()
כדי לספק קוד שמופעל אחרי עיבוד הבקשה.- התאמה אישית של ממשק המשתמש של השמירה של המילוי האוטומטי
שירותי מילוי אוטומטי יכולים להתאים אישית את ממשק המשתמש של שמירת המילוי האוטומטי כדי לעזור למשתמשים להחליט אם הם רוצים לאפשר לשירות לשמור את הנתונים שלהם. שירותים יכולים לספק מידע נוסף על מה שנשמר, באמצעות טקסט פשוט או באמצעות תצוגה מותאמת אישית. שירותים יכולים גם לשנות את המראה של הכפתור לביטול בקשת השמירה ולקבל התראה כשהמשתמש מקיש על הכפתור הזה. מידע נוסף מופיע בדף העזרה בנושא
SaveInfo
.- מצב תאימות
מצב התאימות מאפשר לשירותי המילוי האוטומטי להשתמש במבנה הווירטואלי של הנגישות למטרות מילוי אוטומטי. הוא שימושי במיוחד כדי לספק פונקציונליות של מילוי אוטומטי בדפדפנים שלא מטמיעים במפורש את ממשקי ה-API של המילוי האוטומטי.
כדי לבדוק את שירות המילוי האוטומטי באמצעות מצב תאימות, צריך להוסיף במפורש לרשימת ההיתרים את הדפדפן או האפליקציה שדורשים מצב תאימות. כדי לבדוק אילו חבילות כבר נכללות ברשימת ההיתרים, מריצים את הפקודה הבאה:
$ adb shell settings get global autofill_compat_mode_allowed_packages
אם החבילה שאתם בודקים לא מופיעה ברשימה, מוסיפים אותה על ידי הרצת הפקודה הבאה, כאשר
pkgX
היא החבילה של האפליקציה:$ adb shell settings put global autofill_compat_mode_allowed_packages pkg1[resId1]:pkg2[resId1,resId2]
אם האפליקציה היא דפדפן, צריך להשתמש ב-
resIdx
כדי לציין את מזהה המשאב של שדה הקלט שמכיל את כתובת ה-URL של הדף שעבר רינדור.
למצב תאימות יש את המגבלות הבאות:
- בקשת שמירה מופעלת כשהשירות משתמש בדגל
FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE
או כשהשיטהsetTrigger()
מופעלת. כברירת מחדל,FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE
מוגדר כשמשתמשים במצב תאימות. - יכול להיות שערך הטקסט של הצמתים לא יהיה זמין בשיטה
onSaveRequest(SaveRequest, SaveCallback)
.
מידע נוסף על מצב תאימות, כולל המגבלות שקשורות אליו, זמין במאמר בנושא AutofillService
.