1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.example.android.autofill.service.simple; 17 18 import android.app.assist.AssistStructure; 19 import android.app.assist.AssistStructure.ViewNode; 20 import android.os.CancellationSignal; 21 import android.service.autofill.AutofillService; 22 import android.service.autofill.Dataset; 23 import android.service.autofill.FillCallback; 24 import android.service.autofill.FillContext; 25 import android.service.autofill.FillRequest; 26 import android.service.autofill.FillResponse; 27 import android.service.autofill.SaveCallback; 28 import android.service.autofill.SaveInfo; 29 import android.service.autofill.SaveRequest; 30 import android.support.annotation.NonNull; 31 import android.support.annotation.Nullable; 32 import android.support.v4.util.ArrayMap; 33 import android.util.Log; 34 import android.view.autofill.AutofillId; 35 import android.view.autofill.AutofillValue; 36 import android.widget.RemoteViews; 37 import android.widget.Toast; 38 39 import com.example.android.autofill.service.MyAutofillService; 40 import com.example.android.autofill.service.R; 41 42 import java.util.Collection; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Map.Entry; 46 47 /** 48 * A very basic {@link AutofillService} implementation that only shows dynamic-generated datasets 49 * and don't persist the saved data. 50 * 51 * <p>The goal of this class is to provide a simple autofill service implementation that is easy 52 * to understand and extend, but it should <strong>not</strong> be used as-is on real apps because 53 * it lacks fundamental security requirements such as data partitioning and package verification 54 * &mdashthese requirements are fullfilled by {@link MyAutofillService}. 55 */ 56 public final class BasicService extends AutofillService { 57 58 private static final String TAG = "BasicService"; 59 60 /** 61 * Number of datasets sent on each request - we're simple, that value is hardcoded in our DNA! 62 */ 63 private static final int NUMBER_DATASETS = 4; 64 65 @Override onFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback)66 public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal, 67 FillCallback callback) { 68 Log.d(TAG, "onFillRequest()"); 69 70 // Find autofillable fields 71 AssistStructure structure = getLatestAssistStructure(request); 72 Map<String, AutofillId> fields = getAutofillableFields(structure); 73 Log.d(TAG, "autofillable fields:" + fields); 74 75 if (fields.isEmpty()) { 76 toast("No autofill hints found"); 77 callback.onSuccess(null); 78 return; 79 } 80 81 // Create the base response 82 FillResponse.Builder response = new FillResponse.Builder(); 83 84 // 1.Add the dynamic datasets 85 String packageName = getApplicationContext().getPackageName(); 86 for (int i = 1; i <= NUMBER_DATASETS; i++) { 87 Dataset.Builder dataset = new Dataset.Builder(); 88 for (Entry<String, AutofillId> field : fields.entrySet()) { 89 String hint = field.getKey(); 90 AutofillId id = field.getValue(); 91 String value = i + "-" + hint; 92 // We're simple - our dataset values are hardcoded as "N-hint" (for example, 93 // "1-username", "2-username") and they're displayed as such, except if they're a 94 // password 95 String displayValue = hint.contains("password") ? "password for #" + i : value; 96 RemoteViews presentation = newDatasetPresentation(packageName, displayValue); 97 dataset.setValue(id, AutofillValue.forText(value), presentation); 98 } 99 response.addDataset(dataset.build()); 100 } 101 102 // 2.Add save info 103 Collection<AutofillId> ids = fields.values(); 104 AutofillId[] requiredIds = new AutofillId[ids.size()]; 105 ids.toArray(requiredIds); 106 response.setSaveInfo( 107 // We're simple, so we're generic 108 new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_GENERIC, requiredIds).build()); 109 110 // 3.Profit! 111 callback.onSuccess(response.build()); 112 } 113 114 @Override onSaveRequest(SaveRequest request, SaveCallback callback)115 public void onSaveRequest(SaveRequest request, SaveCallback callback) { 116 Log.d(TAG, "onSaveRequest()"); 117 toast("Save not supported"); 118 callback.onSuccess(); 119 } 120 121 /** 122 * Parses the {@link AssistStructure} representing the activity being autofilled, and returns a 123 * map of autofillable fields (represented by their autofill ids) mapped by the hint associate 124 * with them. 125 * 126 * <p>An autofillable field is a {@link ViewNode} whose {@link #getHint(ViewNode)} metho 127 */ 128 @NonNull getAutofillableFields(@onNull AssistStructure structure)129 private Map<String, AutofillId> getAutofillableFields(@NonNull AssistStructure structure) { 130 Map<String, AutofillId> fields = new ArrayMap<>(); 131 int nodes = structure.getWindowNodeCount(); 132 for (int i = 0; i < nodes; i++) { 133 ViewNode node = structure.getWindowNodeAt(i).getRootViewNode(); 134 addAutofillableFields(fields, node); 135 } 136 return fields; 137 } 138 139 /** 140 * Adds any autofillable view from the {@link ViewNode} and its descendants to the map. 141 */ addAutofillableFields(@onNull Map<String, AutofillId> fields, @NonNull ViewNode node)142 private void addAutofillableFields(@NonNull Map<String, AutofillId> fields, 143 @NonNull ViewNode node) { 144 String[] hints = node.getAutofillHints(); 145 if (hints != null) { 146 // We're simple, we only care about the first hint 147 String hint = hints[0].toLowerCase(); 148 149 if (hint != null) { 150 AutofillId id = node.getAutofillId(); 151 if (!fields.containsKey(hint)) { 152 Log.v(TAG, "Setting hint '" + hint + "' on " + id); 153 fields.put(hint, id); 154 } else { 155 Log.v(TAG, "Ignoring hint '" + hint + "' on " + id 156 + " because it was already set"); 157 } 158 } 159 } 160 int childrenSize = node.getChildCount(); 161 for (int i = 0; i < childrenSize; i++) { 162 addAutofillableFields(fields, node.getChildAt(i)); 163 } 164 } 165 166 /** 167 * Helper method to get the {@link AssistStructure} associated with the latest request 168 * in an autofill context. 169 */ 170 @NonNull getLatestAssistStructure(@onNull FillRequest request)171 static AssistStructure getLatestAssistStructure(@NonNull FillRequest request) { 172 List<FillContext> fillContexts = request.getFillContexts(); 173 return fillContexts.get(fillContexts.size() - 1).getStructure(); 174 } 175 176 /** 177 * Helper method to create a dataset presentation with the given text. 178 */ 179 @NonNull newDatasetPresentation(@onNull String packageName, @NonNull CharSequence text)180 static RemoteViews newDatasetPresentation(@NonNull String packageName, 181 @NonNull CharSequence text) { 182 RemoteViews presentation = 183 new RemoteViews(packageName, R.layout.multidataset_service_list_item); 184 presentation.setTextViewText(R.id.text, text); 185 presentation.setImageViewResource(R.id.icon, R.mipmap.ic_launcher); 186 return presentation; 187 } 188 189 /** 190 * Displays a toast with the given message. 191 */ toast(@onNull CharSequence message)192 private void toast(@NonNull CharSequence message) { 193 Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); 194 } 195 } 196