/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.autofill; import static android.service.autofill.AutofillFieldClassificationService.SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS; import static android.service.autofill.AutofillFieldClassificationService.SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM; import static com.android.server.autofill.Helper.sDebug; import static com.android.server.autofill.Helper.sVerbose; import android.Manifest; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.UserHandle; import android.service.autofill.AutofillFieldClassificationService; import android.service.autofill.IAutofillFieldClassificationService; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import android.view.autofill.AutofillValue; import com.android.internal.annotations.GuardedBy; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Strategy used to bridge the field classification algorithms provided by a service in an external * package. */ //TODO(b/70291841): add unit tests ? final class FieldClassificationStrategy { private static final String TAG = "FieldClassificationStrategy"; private final Context mContext; private final Object mLock = new Object(); private final int mUserId; @GuardedBy("mLock") private ServiceConnection mServiceConnection; @GuardedBy("mLock") private IAutofillFieldClassificationService mRemoteService; @GuardedBy("mLock") private ArrayList mQueuedCommands; public FieldClassificationStrategy(Context context, int userId) { mContext = context; mUserId = userId; } @Nullable ServiceInfo getServiceInfo() { final String packageName = mContext.getPackageManager().getServicesSystemSharedLibraryPackageName(); if (packageName == null) { Slog.w(TAG, "no external services package!"); return null; } final Intent intent = new Intent(AutofillFieldClassificationService.SERVICE_INTERFACE); intent.setPackage(packageName); final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); if (resolveInfo == null || resolveInfo.serviceInfo == null) { Slog.w(TAG, "No valid components found."); return null; } return resolveInfo.serviceInfo; } @Nullable private ComponentName getServiceComponentName() { final ServiceInfo serviceInfo = getServiceInfo(); if (serviceInfo == null) return null; final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name); if (!Manifest.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE .equals(serviceInfo.permission)) { Slog.w(TAG, name.flattenToShortString() + " does not require permission " + Manifest.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE); return null; } if (sVerbose) Slog.v(TAG, "getServiceComponentName(): " + name); return name; } void reset() { synchronized (mLock) { if (mServiceConnection != null) { if (sDebug) Slog.d(TAG, "reset(): unbinding service."); try { mContext.unbindService(mServiceConnection); } catch (IllegalArgumentException e) { // no-op, just log the error message. Slog.w(TAG, "reset(): " + e.getMessage()); } mServiceConnection = null; } else { if (sDebug) Slog.d(TAG, "reset(): service is not bound. Do nothing."); } } } /** * Run a command, starting the service connection if necessary. */ private void connectAndRun(@NonNull Command command) { synchronized (mLock) { if (mRemoteService != null) { try { if (sVerbose) Slog.v(TAG, "running command right away"); command.run(mRemoteService); } catch (RemoteException e) { Slog.w(TAG, "exception calling service: " + e); } return; } else { if (sDebug) Slog.d(TAG, "service is null; queuing command"); if (mQueuedCommands == null) { mQueuedCommands = new ArrayList<>(1); } mQueuedCommands.add(command); // If we're already connected, don't create a new connection, just leave - the // command will be run when the service connects if (mServiceConnection != null) return; } if (sVerbose) Slog.v(TAG, "creating connection"); // Create the connection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { if (sVerbose) Slog.v(TAG, "onServiceConnected(): " + name); synchronized (mLock) { mRemoteService = IAutofillFieldClassificationService.Stub .asInterface(service); if (mQueuedCommands != null) { final int size = mQueuedCommands.size(); if (sDebug) Slog.d(TAG, "running " + size + " queued commands"); for (int i = 0; i < size; i++) { final Command queuedCommand = mQueuedCommands.get(i); try { if (sVerbose) Slog.v(TAG, "running queued command #" + i); queuedCommand.run(mRemoteService); } catch (RemoteException e) { Slog.w(TAG, "exception calling " + name + ": " + e); } } mQueuedCommands = null; } else if (sDebug) Slog.d(TAG, "no queued commands"); } } @Override @MainThread public void onServiceDisconnected(ComponentName name) { if (sVerbose) Slog.v(TAG, "onServiceDisconnected(): " + name); synchronized (mLock) { mRemoteService = null; } } @Override public void onBindingDied(ComponentName name) { if (sVerbose) Slog.v(TAG, "onBindingDied(): " + name); synchronized (mLock) { mRemoteService = null; } } @Override public void onNullBinding(ComponentName name) { if (sVerbose) Slog.v(TAG, "onNullBinding(): " + name); synchronized (mLock) { mRemoteService = null; } } }; final ComponentName component = getServiceComponentName(); if (sVerbose) Slog.v(TAG, "binding to: " + component); if (component != null) { final Intent intent = new Intent(); intent.setComponent(component); final long token = Binder.clearCallingIdentity(); try { mContext.bindServiceAsUser(intent, mServiceConnection, Context.BIND_AUTO_CREATE, UserHandle.of(mUserId)); if (sVerbose) Slog.v(TAG, "bound"); } finally { Binder.restoreCallingIdentity(token); } } } } /** * Gets the name of all available algorithms. */ @Nullable String[] getAvailableAlgorithms() { return getMetadataValue(SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS, (res, id) -> res.getStringArray(id)); } /** * Gets the default algorithm that's used when an algorithm is not specified or is invalid. */ @Nullable String getDefaultAlgorithm() { return getMetadataValue(SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM, (res, id) -> res.getString(id)); } @Nullable private T getMetadataValue(String field, MetadataParser parser) { final ServiceInfo serviceInfo = getServiceInfo(); if (serviceInfo == null) return null; final PackageManager pm = mContext.getPackageManager(); final Resources res; try { res = pm.getResourcesForApplication(serviceInfo.applicationInfo); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Error getting application resources for " + serviceInfo, e); return null; } final int resourceId = serviceInfo.metaData.getInt(field); return parser.get(res, resourceId); } void calculateScores(RemoteCallback callback, @NonNull List actualValues, @NonNull String[] userDataValues, @NonNull String[] categoryIds, @Nullable String defaultAlgorithm, @Nullable Bundle defaultArgs, @Nullable ArrayMap algorithms, @Nullable ArrayMap args) { connectAndRun((service) -> service.calculateScores(callback, actualValues, userDataValues, categoryIds, defaultAlgorithm, defaultArgs, algorithms, args)); } void dump(String prefix, PrintWriter pw) { final ComponentName impl = getServiceComponentName(); pw.print(prefix); pw.print("User ID: "); pw.println(mUserId); pw.print(prefix); pw.print("Queued commands: "); if (mQueuedCommands == null) { pw.println("N/A"); } else { pw.println(mQueuedCommands.size()); } pw.print(prefix); pw.print("Implementation: "); if (impl == null) { pw.println("N/A"); return; } pw.println(impl.flattenToShortString()); try { pw.print(prefix); pw.print("Available algorithms: "); pw.println(Arrays.toString(getAvailableAlgorithms())); pw.print(prefix); pw.print("Default algorithm: "); pw.println(getDefaultAlgorithm()); } catch (Exception e) { pw.print("ERROR CALLING SERVICE: " ); pw.println(e); } } private static interface Command { void run(IAutofillFieldClassificationService service) throws RemoteException; } private static interface MetadataParser { T get(Resources res, int resId); } }