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 */
16package com.android.server.autofill;
17
18import static com.android.server.autofill.Helper.sDebug;
19import static com.android.server.autofill.Helper.sVerbose;
20import static android.service.autofill.AutofillFieldClassificationService.SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS;
21import static android.service.autofill.AutofillFieldClassificationService.SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM;
22
23import android.Manifest;
24import android.annotation.MainThread;
25import android.annotation.NonNull;
26import android.annotation.Nullable;
27import android.content.ComponentName;
28import android.content.Context;
29import android.content.Intent;
30import android.content.ServiceConnection;
31import android.content.pm.PackageManager;
32import android.content.pm.ResolveInfo;
33import android.content.pm.ServiceInfo;
34import android.content.res.Resources;
35import android.os.Binder;
36import android.os.Bundle;
37import android.os.IBinder;
38import android.os.RemoteCallback;
39import android.os.RemoteException;
40import android.os.UserHandle;
41import android.service.autofill.AutofillFieldClassificationService;
42import android.service.autofill.IAutofillFieldClassificationService;
43import android.util.Log;
44import android.util.Slog;
45import android.view.autofill.AutofillValue;
46
47import com.android.internal.annotations.GuardedBy;
48
49import java.io.PrintWriter;
50import java.util.ArrayList;
51import java.util.Arrays;
52import java.util.List;
53
54/**
55 * Strategy used to bridge the field classification algorithms provided by a service in an external
56 * package.
57 */
58//TODO(b/70291841): add unit tests ?
59final class FieldClassificationStrategy {
60
61    private static final String TAG = "FieldClassificationStrategy";
62
63    private final Context mContext;
64    private final Object mLock = new Object();
65    private final int mUserId;
66
67    @GuardedBy("mLock")
68    private ServiceConnection mServiceConnection;
69
70    @GuardedBy("mLock")
71    private IAutofillFieldClassificationService mRemoteService;
72
73    @GuardedBy("mLock")
74    private ArrayList<Command> mQueuedCommands;
75
76    public FieldClassificationStrategy(Context context, int userId) {
77        mContext = context;
78        mUserId = userId;
79    }
80
81    @Nullable
82    ServiceInfo getServiceInfo() {
83        final String packageName =
84                mContext.getPackageManager().getServicesSystemSharedLibraryPackageName();
85        if (packageName == null) {
86            Slog.w(TAG, "no external services package!");
87            return null;
88        }
89
90        final Intent intent = new Intent(AutofillFieldClassificationService.SERVICE_INTERFACE);
91        intent.setPackage(packageName);
92        final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
93                PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
94        if (resolveInfo == null || resolveInfo.serviceInfo == null) {
95            Slog.w(TAG, "No valid components found.");
96            return null;
97        }
98        return resolveInfo.serviceInfo;
99    }
100
101    @Nullable
102    private ComponentName getServiceComponentName() {
103        final ServiceInfo serviceInfo = getServiceInfo();
104        if (serviceInfo == null) return null;
105
106        final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name);
107        if (!Manifest.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE
108                .equals(serviceInfo.permission)) {
109            Slog.w(TAG, name.flattenToShortString() + " does not require permission "
110                    + Manifest.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE);
111            return null;
112        }
113
114        if (sVerbose) Slog.v(TAG, "getServiceComponentName(): " + name);
115        return name;
116    }
117
118    void reset() {
119        synchronized (mLock) {
120            if (mServiceConnection != null) {
121                if (sDebug) Slog.d(TAG, "reset(): unbinding service.");
122                mContext.unbindService(mServiceConnection);
123                mServiceConnection = null;
124            } else {
125                if (sDebug) Slog.d(TAG, "reset(): service is not bound. Do nothing.");
126            }
127        }
128    }
129
130    /**
131     * Run a command, starting the service connection if necessary.
132     */
133    private void connectAndRun(@NonNull Command command) {
134        synchronized (mLock) {
135            if (mRemoteService != null) {
136                try {
137                    if (sVerbose) Slog.v(TAG, "running command right away");
138                    command.run(mRemoteService);
139                } catch (RemoteException e) {
140                    Slog.w(TAG, "exception calling service: " + e);
141                }
142                return;
143            } else {
144                if (sDebug) Slog.d(TAG, "service is null; queuing command");
145                if (mQueuedCommands == null) {
146                    mQueuedCommands = new ArrayList<>(1);
147                }
148                mQueuedCommands.add(command);
149                // If we're already connected, don't create a new connection, just leave - the
150                // command will be run when the service connects
151                if (mServiceConnection != null) return;
152            }
153
154            if (sVerbose) Slog.v(TAG, "creating connection");
155
156            // Create the connection
157            mServiceConnection = new ServiceConnection() {
158                @Override
159                public void onServiceConnected(ComponentName name, IBinder service) {
160                    if (sVerbose) Slog.v(TAG, "onServiceConnected(): " + name);
161                    synchronized (mLock) {
162                        mRemoteService = IAutofillFieldClassificationService.Stub
163                                .asInterface(service);
164                        if (mQueuedCommands != null) {
165                            final int size = mQueuedCommands.size();
166                            if (sDebug) Slog.d(TAG, "running " + size + " queued commands");
167                            for (int i = 0; i < size; i++) {
168                                final Command queuedCommand = mQueuedCommands.get(i);
169                                try {
170                                    if (sVerbose) Slog.v(TAG, "running queued command #" + i);
171                                    queuedCommand.run(mRemoteService);
172                                } catch (RemoteException e) {
173                                    Slog.w(TAG, "exception calling " + name + ": " + e);
174                                }
175                            }
176                            mQueuedCommands = null;
177                        } else if (sDebug) Slog.d(TAG, "no queued commands");
178                    }
179                }
180
181                @Override
182                @MainThread
183                public void onServiceDisconnected(ComponentName name) {
184                    if (sVerbose) Slog.v(TAG, "onServiceDisconnected(): " + name);
185                    synchronized (mLock) {
186                        mRemoteService = null;
187                    }
188                }
189
190                @Override
191                public void onBindingDied(ComponentName name) {
192                    if (sVerbose) Slog.v(TAG, "onBindingDied(): " + name);
193                    synchronized (mLock) {
194                        mRemoteService = null;
195                    }
196                }
197
198                @Override
199                public void onNullBinding(ComponentName name) {
200                    if (sVerbose) Slog.v(TAG, "onNullBinding(): " + name);
201                    synchronized (mLock) {
202                        mRemoteService = null;
203                    }
204                }
205            };
206
207            final ComponentName component = getServiceComponentName();
208            if (sVerbose) Slog.v(TAG, "binding to: " + component);
209            if (component != null) {
210                final Intent intent = new Intent();
211                intent.setComponent(component);
212                final long token = Binder.clearCallingIdentity();
213                try {
214                    mContext.bindServiceAsUser(intent, mServiceConnection, Context.BIND_AUTO_CREATE,
215                            UserHandle.of(mUserId));
216                    if (sVerbose) Slog.v(TAG, "bound");
217                } finally {
218                    Binder.restoreCallingIdentity(token);
219                }
220            }
221        }
222    }
223
224    /**
225     * Gets the name of all available algorithms.
226     */
227    @Nullable
228    String[] getAvailableAlgorithms() {
229        return getMetadataValue(SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS,
230                (res, id) -> res.getStringArray(id));
231    }
232
233    /**
234     * Gets the default algorithm that's used when an algorithm is not specified or is invalid.
235     */
236    @Nullable
237    String getDefaultAlgorithm() {
238        return getMetadataValue(SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM, (res, id) -> res.getString(id));
239    }
240
241    @Nullable
242    private <T> T getMetadataValue(String field, MetadataParser<T> parser) {
243        final ServiceInfo serviceInfo = getServiceInfo();
244        if (serviceInfo == null) return null;
245
246        final PackageManager pm = mContext.getPackageManager();
247
248        final Resources res;
249        try {
250            res = pm.getResourcesForApplication(serviceInfo.applicationInfo);
251        } catch (PackageManager.NameNotFoundException e) {
252            Log.e(TAG, "Error getting application resources for " + serviceInfo, e);
253            return null;
254        }
255
256        final int resourceId = serviceInfo.metaData.getInt(field);
257        return parser.get(res, resourceId);
258    }
259
260    //TODO(b/70291841): rename this method (and all others in the chain) to something like
261    // calculateScores() ?
262    void getScores(RemoteCallback callback, @Nullable String algorithmName,
263            @Nullable Bundle algorithmArgs, @NonNull List<AutofillValue> actualValues,
264            @NonNull String[] userDataValues) {
265        connectAndRun((service) -> service.getScores(callback, algorithmName,
266                algorithmArgs, actualValues, userDataValues));
267    }
268
269    void dump(String prefix, PrintWriter pw) {
270        final ComponentName impl = getServiceComponentName();
271        pw.print(prefix); pw.print("User ID: "); pw.println(mUserId);
272        pw.print(prefix); pw.print("Queued commands: ");
273        if (mQueuedCommands == null) {
274            pw.println("N/A");
275        } else {
276            pw.println(mQueuedCommands.size());
277        }
278        pw.print(prefix); pw.print("Implementation: ");
279        if (impl == null) {
280            pw.println("N/A");
281            return;
282        }
283        pw.println(impl.flattenToShortString());
284
285        pw.print(prefix); pw.print("Available algorithms: ");
286        pw.println(Arrays.toString(getAvailableAlgorithms()));
287        pw.print(prefix); pw.print("Default algorithm: "); pw.println(getDefaultAlgorithm());
288    }
289
290    private static interface Command {
291        void run(IAutofillFieldClassificationService service) throws RemoteException;
292    }
293
294    private static interface MetadataParser<T> {
295        T get(Resources res, int resId);
296    }
297}
298