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