1 /*
2  * Copyright (C) 2016 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 android.service.autofill;
17 
18 import android.Manifest;
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.UserIdInt;
22 import android.app.AppGlobals;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageManager;
28 import android.content.pm.ResolveInfo;
29 import android.content.pm.ServiceInfo;
30 import android.content.res.Resources;
31 import android.content.res.TypedArray;
32 import android.content.res.XmlResourceParser;
33 import android.metrics.LogMaker;
34 import android.os.RemoteException;
35 import android.text.TextUtils;
36 import android.util.ArrayMap;
37 import android.util.AttributeSet;
38 import android.util.Log;
39 import android.util.Xml;
40 
41 import com.android.internal.R;
42 import com.android.internal.annotations.VisibleForTesting;
43 import com.android.internal.logging.MetricsLogger;
44 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
45 import com.android.internal.util.XmlUtils;
46 
47 import org.xmlpull.v1.XmlPullParser;
48 import org.xmlpull.v1.XmlPullParserException;
49 
50 import java.io.IOException;
51 import java.io.PrintWriter;
52 import java.util.ArrayList;
53 import java.util.List;
54 
55 /**
56  * {@link ServiceInfo} and meta-data about an {@link AutofillService}.
57  *
58  * @hide
59  */
60 public final class AutofillServiceInfo {
61     private static final String TAG = "AutofillServiceInfo";
62 
63     private static final String TAG_AUTOFILL_SERVICE = "autofill-service";
64     private static final String TAG_COMPATIBILITY_PACKAGE = "compatibility-package";
65 
getServiceInfoOrThrow(ComponentName comp, int userHandle)66     private static ServiceInfo getServiceInfoOrThrow(ComponentName comp, int userHandle)
67             throws PackageManager.NameNotFoundException {
68         try {
69             ServiceInfo si = AppGlobals.getPackageManager().getServiceInfo(
70                     comp,
71                     PackageManager.GET_META_DATA,
72                     userHandle);
73             if (si != null) {
74                 return si;
75             }
76         } catch (RemoteException e) {
77         }
78         throw new PackageManager.NameNotFoundException(comp.toString());
79     }
80 
81     @NonNull
82     private final ServiceInfo mServiceInfo;
83 
84     @Nullable
85     private final String mSettingsActivity;
86     @Nullable
87     private final String mPasswordsActivity;
88 
89     @Nullable
90     private final ArrayMap<String, Long> mCompatibilityPackages;
91 
92     private final boolean mInlineSuggestionsEnabled;
93 
AutofillServiceInfo(Context context, ComponentName comp, int userHandle)94     public AutofillServiceInfo(Context context, ComponentName comp, int userHandle)
95             throws PackageManager.NameNotFoundException {
96         this(context, getServiceInfoOrThrow(comp, userHandle));
97     }
98 
AutofillServiceInfo(Context context, ServiceInfo si)99     public AutofillServiceInfo(Context context, ServiceInfo si) {
100         // Check for permissions.
101         if (!Manifest.permission.BIND_AUTOFILL_SERVICE.equals(si.permission)) {
102             if (Manifest.permission.BIND_AUTOFILL.equals(si.permission)) {
103                 // Let it go for now...
104                 Log.w(TAG, "AutofillService from '" + si.packageName + "' uses unsupported "
105                         + "permission " + Manifest.permission.BIND_AUTOFILL + ". It works for "
106                         + "now, but might not be supported on future releases");
107                 new MetricsLogger().write(new LogMaker(MetricsEvent.AUTOFILL_INVALID_PERMISSION)
108                         .setPackageName(si.packageName));
109             } else {
110                 Log.w(TAG, "AutofillService from '" + si.packageName
111                         + "' does not require permission "
112                         + Manifest.permission.BIND_AUTOFILL_SERVICE);
113                 throw new SecurityException("Service does not require permission "
114                         + Manifest.permission.BIND_AUTOFILL_SERVICE);
115             }
116         }
117 
118         mServiceInfo = si;
119 
120         // Get the AutoFill metadata, if declared.
121         final XmlResourceParser parser = si.loadXmlMetaData(context.getPackageManager(),
122                 AutofillService.SERVICE_META_DATA);
123         if (parser == null) {
124             mSettingsActivity = null;
125             mPasswordsActivity = null;
126             mCompatibilityPackages = null;
127             mInlineSuggestionsEnabled = false;
128             return;
129         }
130 
131         String settingsActivity = null;
132         String passwordsActivity = null;
133         ArrayMap<String, Long> compatibilityPackages = null;
134         boolean inlineSuggestionsEnabled = false; // false by default.
135 
136         try {
137             final Resources resources = context.getPackageManager().getResourcesForApplication(
138                     si.applicationInfo);
139 
140             int type = 0;
141             while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
142                 type = parser.next();
143             }
144 
145             if (TAG_AUTOFILL_SERVICE.equals(parser.getName())) {
146                 final AttributeSet allAttributes = Xml.asAttributeSet(parser);
147                 TypedArray afsAttributes = null;
148                 try {
149                     afsAttributes = resources.obtainAttributes(allAttributes,
150                             com.android.internal.R.styleable.AutofillService);
151                     settingsActivity = afsAttributes.getString(
152                             R.styleable.AutofillService_settingsActivity);
153                     passwordsActivity = afsAttributes.getString(
154                             R.styleable.AutofillService_passwordsActivity);
155                     inlineSuggestionsEnabled = afsAttributes.getBoolean(
156                             R.styleable.AutofillService_supportsInlineSuggestions, false);
157                 } finally {
158                     if (afsAttributes != null) {
159                         afsAttributes.recycle();
160                     }
161                 }
162                 compatibilityPackages = parseCompatibilityPackages(parser, resources);
163             } else {
164                 Log.e(TAG, "Meta-data does not start with autofill-service tag");
165             }
166         } catch (PackageManager.NameNotFoundException | IOException | XmlPullParserException e) {
167             Log.e(TAG, "Error parsing auto fill service meta-data", e);
168         }
169 
170         mSettingsActivity = settingsActivity;
171         mPasswordsActivity = passwordsActivity;
172         mCompatibilityPackages = compatibilityPackages;
173         mInlineSuggestionsEnabled = inlineSuggestionsEnabled;
174     }
175 
parseCompatibilityPackages(XmlPullParser parser, Resources resources)176     private ArrayMap<String, Long> parseCompatibilityPackages(XmlPullParser parser,
177             Resources resources) throws IOException, XmlPullParserException {
178         ArrayMap<String, Long> compatibilityPackages = null;
179 
180         final int outerDepth = parser.getDepth();
181         int type;
182         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
183                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
184             if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
185                 continue;
186             }
187 
188             if (TAG_COMPATIBILITY_PACKAGE.equals(parser.getName())) {
189                 TypedArray cpAttributes = null;
190                 try {
191                     final AttributeSet allAttributes = Xml.asAttributeSet(parser);
192 
193                     cpAttributes = resources.obtainAttributes(allAttributes,
194                            R.styleable.AutofillService_CompatibilityPackage);
195 
196                     final String name = cpAttributes.getString(
197                             R.styleable.AutofillService_CompatibilityPackage_name);
198                     if (TextUtils.isEmpty(name)) {
199                         Log.e(TAG, "Invalid compatibility package:" + name);
200                         break;
201                     }
202 
203                     final String maxVersionCodeStr = cpAttributes.getString(
204                             R.styleable.AutofillService_CompatibilityPackage_maxLongVersionCode);
205                     final Long maxVersionCode;
206                     if (maxVersionCodeStr != null) {
207                         try {
208                             maxVersionCode = Long.parseLong(maxVersionCodeStr);
209                         } catch (NumberFormatException e) {
210                             Log.e(TAG, "Invalid compatibility max version code:"
211                                     + maxVersionCodeStr);
212                             break;
213                         }
214                         if (maxVersionCode < 0) {
215                             Log.e(TAG, "Invalid compatibility max version code:"
216                                     + maxVersionCode);
217                             break;
218                         }
219                     } else {
220                         maxVersionCode = Long.MAX_VALUE;
221                     }
222                     if (compatibilityPackages == null) {
223                         compatibilityPackages = new ArrayMap<>();
224                     }
225                     compatibilityPackages.put(name, maxVersionCode);
226                 } finally {
227                     XmlUtils.skipCurrentTag(parser);
228                     if (cpAttributes != null) {
229                         cpAttributes.recycle();
230                     }
231                 }
232             }
233         }
234 
235         return compatibilityPackages;
236     }
237 
238     /**
239      * Used by {@link TestDataBuilder}.
240      */
AutofillServiceInfo(String passwordsActivity)241     private AutofillServiceInfo(String passwordsActivity) {
242         mServiceInfo = new ServiceInfo();
243         mServiceInfo.applicationInfo = new ApplicationInfo();
244         mServiceInfo.packageName = "com.android.test";
245         mSettingsActivity = null;
246         mPasswordsActivity = passwordsActivity;
247         mCompatibilityPackages = null;
248         mInlineSuggestionsEnabled = false;
249     }
250 
251     /**
252      * Builds test data for unit tests.
253      */
254     @VisibleForTesting
255     public static final class TestDataBuilder {
256         private String mPasswordsActivity;
257 
TestDataBuilder()258         public TestDataBuilder() {
259         }
260 
setPasswordsActivity(String passwordsActivity)261         public TestDataBuilder setPasswordsActivity(String passwordsActivity) {
262             mPasswordsActivity = passwordsActivity;
263             return this;
264         }
265 
build()266         public AutofillServiceInfo build() {
267             return new AutofillServiceInfo(mPasswordsActivity);
268         }
269     }
270 
271     @NonNull
getServiceInfo()272     public ServiceInfo getServiceInfo() {
273         return mServiceInfo;
274     }
275 
276     @Nullable
getSettingsActivity()277     public String getSettingsActivity() {
278         return mSettingsActivity;
279     }
280 
281     @Nullable
getPasswordsActivity()282     public String getPasswordsActivity() {
283         return mPasswordsActivity;
284     }
285 
286     @Nullable
getCompatibilityPackages()287     public ArrayMap<String, Long> getCompatibilityPackages() {
288         return mCompatibilityPackages;
289     }
290 
isInlineSuggestionsEnabled()291     public boolean isInlineSuggestionsEnabled() {
292         return mInlineSuggestionsEnabled;
293     }
294 
295     /**
296      * Queries the valid autofill services available for the user.
297      */
getAvailableServices( Context context, @UserIdInt int user)298     public static List<AutofillServiceInfo> getAvailableServices(
299             Context context, @UserIdInt int user) {
300         final List<AutofillServiceInfo> services = new ArrayList<>();
301 
302         final List<ResolveInfo> resolveInfos =
303                 context.getPackageManager().queryIntentServicesAsUser(
304                         new Intent(AutofillService.SERVICE_INTERFACE),
305                         PackageManager.GET_META_DATA,
306                         user);
307         for (ResolveInfo resolveInfo : resolveInfos) {
308             final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
309             try {
310                 services.add(new AutofillServiceInfo(context, serviceInfo));
311             } catch (SecurityException e) {
312                 // Service does not declare the proper permission, ignore it.
313                 Log.w(TAG, "Error getting info for " + serviceInfo + ": " + e);
314             }
315         }
316         return services;
317     }
318 
319     @Override
toString()320     public String toString() {
321         final StringBuilder builder = new StringBuilder();
322         builder.append(getClass().getSimpleName());
323         builder.append("[").append(mServiceInfo);
324         builder.append(", settings:").append(mSettingsActivity);
325         builder.append(", passwords activity:").append(mPasswordsActivity);
326         builder.append(", hasCompatPckgs:").append(mCompatibilityPackages != null
327                 && !mCompatibilityPackages.isEmpty()).append("]");
328         builder.append(", inline suggestions enabled:").append(mInlineSuggestionsEnabled);
329         return builder.toString();
330     }
331 
332     /**
333      * Dumps it!
334      */
dump(String prefix, PrintWriter pw)335     public void dump(String prefix, PrintWriter pw) {
336         pw.print(prefix); pw.print("Component: "); pw.println(getServiceInfo().getComponentName());
337         pw.print(prefix); pw.print("Settings: "); pw.println(mSettingsActivity);
338         pw.print(prefix); pw.print("Passwords activity: "); pw.println(mPasswordsActivity);
339         pw.print(prefix); pw.print("Compat packages: "); pw.println(mCompatibilityPackages);
340         pw.print(prefix); pw.print("Inline Suggestions Enabled: ");
341         pw.println(mInlineSuggestionsEnabled);
342     }
343 }
344