| /* |
| * Copyright (C) 2019 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; |
| import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_HEALTH_CHECK_PASSED_PACKAGE; |
| import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_REQUESTED_PACKAGES; |
| import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_SUPPORTED_PACKAGES; |
| import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig; |
| |
| import android.Manifest; |
| import android.annotation.MainThread; |
| 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.os.IBinder; |
| import android.os.RemoteCallback; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.service.watchdog.ExplicitHealthCheckService; |
| import android.service.watchdog.IExplicitHealthCheckService; |
| import android.text.TextUtils; |
| import android.util.ArraySet; |
| import android.util.Slog; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.util.Preconditions; |
| |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.function.Consumer; |
| |
| // TODO(b/120598832): Add tests |
| /** |
| * Controls the connections with {@link ExplicitHealthCheckService}. |
| */ |
| class ExplicitHealthCheckController { |
| private static final String TAG = "ExplicitHealthCheckController"; |
| private final Object mLock = new Object(); |
| private final Context mContext; |
| |
| // Called everytime a package passes the health check, so the watchdog is notified of the |
| // passing check. In practice, should never be null after it has been #setEnabled. |
| // To prevent deadlocks between the controller and watchdog threads, we have |
| // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. |
| // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer. |
| @GuardedBy("mLock") @Nullable private Consumer<String> mPassedConsumer; |
| // Called everytime after a successful #syncRequest call, so the watchdog can receive packages |
| // supporting health checks and update its internal state. In practice, should never be null |
| // after it has been #setEnabled. |
| // To prevent deadlocks between the controller and watchdog threads, we have |
| // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. |
| // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer. |
| @GuardedBy("mLock") @Nullable private Consumer<List<PackageConfig>> mSupportedConsumer; |
| // Called everytime we need to notify the watchdog to sync requests between itself and the |
| // health check service. In practice, should never be null after it has been #setEnabled. |
| // To prevent deadlocks between the controller and watchdog threads, we have |
| // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. |
| // It's easier to just NOT hold #mLock when calling into watchdog code on this runnable. |
| @GuardedBy("mLock") @Nullable private Runnable mNotifySyncRunnable; |
| // Actual binder object to the explicit health check service. |
| @GuardedBy("mLock") @Nullable private IExplicitHealthCheckService mRemoteService; |
| // Connection to the explicit health check service, necessary to unbind. |
| // We should only try to bind if mConnection is null, non-null indicates we |
| // are connected or at least connecting. |
| @GuardedBy("mLock") @Nullable private ServiceConnection mConnection; |
| // Bind state of the explicit health check service. |
| @GuardedBy("mLock") private boolean mEnabled; |
| |
| ExplicitHealthCheckController(Context context) { |
| mContext = context; |
| } |
| |
| /** Enables or disables explicit health checks. */ |
| public void setEnabled(boolean enabled) { |
| synchronized (mLock) { |
| Slog.i(TAG, "Explicit health checks " + (enabled ? "enabled." : "disabled.")); |
| mEnabled = enabled; |
| } |
| } |
| |
| /** |
| * Sets callbacks to listen to important events from the controller. |
| * |
| * <p> Should be called once at initialization before any other calls to the controller to |
| * ensure a happens-before relationship of the set parameters and visibility on other threads. |
| */ |
| public void setCallbacks(Consumer<String> passedConsumer, |
| Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable) { |
| synchronized (mLock) { |
| if (mPassedConsumer != null || mSupportedConsumer != null |
| || mNotifySyncRunnable != null) { |
| Slog.wtf(TAG, "Resetting health check controller callbacks"); |
| } |
| |
| mPassedConsumer = Preconditions.checkNotNull(passedConsumer); |
| mSupportedConsumer = Preconditions.checkNotNull(supportedConsumer); |
| mNotifySyncRunnable = Preconditions.checkNotNull(notifySyncRunnable); |
| } |
| } |
| |
| /** |
| * Calls the health check service to request or cancel packages based on |
| * {@code newRequestedPackages}. |
| * |
| * <p> Supported packages in {@code newRequestedPackages} that have not been previously |
| * requested will be requested while supported packages not in {@code newRequestedPackages} |
| * but were previously requested will be cancelled. |
| * |
| * <p> This handles binding and unbinding to the health check service as required. |
| * |
| * <p> Note, calling this may modify {@code newRequestedPackages}. |
| * |
| * <p> Note, this method is not thread safe, all calls should be serialized. |
| */ |
| public void syncRequests(Set<String> newRequestedPackages) { |
| boolean enabled; |
| synchronized (mLock) { |
| enabled = mEnabled; |
| } |
| |
| if (!enabled) { |
| Slog.i(TAG, "Health checks disabled, no supported packages"); |
| // Call outside lock |
| mSupportedConsumer.accept(Collections.emptyList()); |
| return; |
| } |
| |
| getSupportedPackages(supportedPackageConfigs -> { |
| // Notify the watchdog without lock held |
| mSupportedConsumer.accept(supportedPackageConfigs); |
| getRequestedPackages(previousRequestedPackages -> { |
| synchronized (mLock) { |
| // Hold lock so requests and cancellations are sent atomically. |
| // It is important we don't mix requests from multiple threads. |
| |
| Set<String> supportedPackages = new ArraySet<>(); |
| for (PackageConfig config : supportedPackageConfigs) { |
| supportedPackages.add(config.getPackageName()); |
| } |
| // Note, this may modify newRequestedPackages |
| newRequestedPackages.retainAll(supportedPackages); |
| |
| // Cancel packages no longer requested |
| actOnDifference(previousRequestedPackages, |
| newRequestedPackages, p -> cancel(p)); |
| // Request packages not yet requested |
| actOnDifference(newRequestedPackages, |
| previousRequestedPackages, p -> request(p)); |
| |
| if (newRequestedPackages.isEmpty()) { |
| Slog.i(TAG, "No more health check requests, unbinding..."); |
| unbindService(); |
| return; |
| } |
| } |
| }); |
| }); |
| } |
| |
| private void actOnDifference(Collection<String> collection1, Collection<String> collection2, |
| Consumer<String> action) { |
| Iterator<String> iterator = collection1.iterator(); |
| while (iterator.hasNext()) { |
| String packageName = iterator.next(); |
| if (!collection2.contains(packageName)) { |
| action.accept(packageName); |
| } |
| } |
| } |
| |
| /** |
| * Requests an explicit health check for {@code packageName}. |
| * After this request, the callback registered on {@link #setCallbacks} can receive explicit |
| * health check passed results. |
| */ |
| private void request(String packageName) { |
| synchronized (mLock) { |
| if (!prepareServiceLocked("request health check for " + packageName)) { |
| return; |
| } |
| |
| Slog.i(TAG, "Requesting health check for package " + packageName); |
| try { |
| mRemoteService.request(packageName); |
| } catch (RemoteException e) { |
| Slog.w(TAG, "Failed to request health check for package " + packageName, e); |
| } |
| } |
| } |
| |
| /** |
| * Cancels all explicit health checks for {@code packageName}. |
| * After this request, the callback registered on {@link #setCallbacks} can no longer receive |
| * explicit health check passed results. |
| */ |
| private void cancel(String packageName) { |
| synchronized (mLock) { |
| if (!prepareServiceLocked("cancel health check for " + packageName)) { |
| return; |
| } |
| |
| Slog.i(TAG, "Cancelling health check for package " + packageName); |
| try { |
| mRemoteService.cancel(packageName); |
| } catch (RemoteException e) { |
| // Do nothing, if the service is down, when it comes up, we will sync requests, |
| // if there's some other error, retrying wouldn't fix anyways. |
| Slog.w(TAG, "Failed to cancel health check for package " + packageName, e); |
| } |
| } |
| } |
| |
| /** |
| * Returns the packages that we can request explicit health checks for. |
| * The packages will be returned to the {@code consumer}. |
| */ |
| private void getSupportedPackages(Consumer<List<PackageConfig>> consumer) { |
| synchronized (mLock) { |
| if (!prepareServiceLocked("get health check supported packages")) { |
| return; |
| } |
| |
| Slog.d(TAG, "Getting health check supported packages"); |
| try { |
| mRemoteService.getSupportedPackages(new RemoteCallback(result -> { |
| List<PackageConfig> packages = |
| result.getParcelableArrayList(EXTRA_SUPPORTED_PACKAGES); |
| Slog.i(TAG, "Explicit health check supported packages " + packages); |
| consumer.accept(packages); |
| })); |
| } catch (RemoteException e) { |
| // Request failed, treat as if all observed packages are supported, if any packages |
| // expire during this period, we may incorrectly treat it as failing health checks |
| // even if we don't support health checks for the package. |
| Slog.w(TAG, "Failed to get health check supported packages", e); |
| } |
| } |
| } |
| |
| /** |
| * Returns the packages for which health checks are currently in progress. |
| * The packages will be returned to the {@code consumer}. |
| */ |
| private void getRequestedPackages(Consumer<List<String>> consumer) { |
| synchronized (mLock) { |
| if (!prepareServiceLocked("get health check requested packages")) { |
| return; |
| } |
| |
| Slog.d(TAG, "Getting health check requested packages"); |
| try { |
| mRemoteService.getRequestedPackages(new RemoteCallback(result -> { |
| List<String> packages = result.getStringArrayList(EXTRA_REQUESTED_PACKAGES); |
| Slog.i(TAG, "Explicit health check requested packages " + packages); |
| consumer.accept(packages); |
| })); |
| } catch (RemoteException e) { |
| // Request failed, treat as if we haven't requested any packages, if any packages |
| // were actually requested, they will not be cancelled now. May be cancelled later |
| Slog.w(TAG, "Failed to get health check requested packages", e); |
| } |
| } |
| } |
| |
| /** |
| * Binds to the explicit health check service if the controller is enabled and |
| * not already bound. |
| */ |
| private void bindService() { |
| synchronized (mLock) { |
| if (!mEnabled || mConnection != null || mRemoteService != null) { |
| if (!mEnabled) { |
| Slog.i(TAG, "Not binding to service, service disabled"); |
| } else if (mRemoteService != null) { |
| Slog.i(TAG, "Not binding to service, service already connected"); |
| } else { |
| Slog.i(TAG, "Not binding to service, service already connecting"); |
| } |
| return; |
| } |
| ComponentName component = getServiceComponentNameLocked(); |
| if (component == null) { |
| Slog.wtf(TAG, "Explicit health check service not found"); |
| return; |
| } |
| |
| Intent intent = new Intent(); |
| intent.setComponent(component); |
| mConnection = new ServiceConnection() { |
| @Override |
| public void onServiceConnected(ComponentName name, IBinder service) { |
| Slog.i(TAG, "Explicit health check service is connected " + name); |
| initState(service); |
| } |
| |
| @Override |
| @MainThread |
| public void onServiceDisconnected(ComponentName name) { |
| // Service crashed or process was killed, #onServiceConnected will be called. |
| // Don't need to re-bind. |
| Slog.i(TAG, "Explicit health check service is disconnected " + name); |
| synchronized (mLock) { |
| mRemoteService = null; |
| } |
| } |
| |
| @Override |
| public void onBindingDied(ComponentName name) { |
| // Application hosting service probably got updated |
| // Need to re-bind. |
| Slog.i(TAG, "Explicit health check service binding is dead. Rebind: " + name); |
| unbindService(); |
| bindService(); |
| } |
| |
| @Override |
| public void onNullBinding(ComponentName name) { |
| // Should never happen. Service returned null from #onBind. |
| Slog.wtf(TAG, "Explicit health check service binding is null?? " + name); |
| } |
| }; |
| |
| mContext.bindServiceAsUser(intent, mConnection, |
| Context.BIND_AUTO_CREATE, UserHandle.of(UserHandle.USER_SYSTEM)); |
| Slog.i(TAG, "Explicit health check service is bound"); |
| } |
| } |
| |
| /** Unbinds the explicit health check service. */ |
| private void unbindService() { |
| synchronized (mLock) { |
| if (mRemoteService != null) { |
| mContext.unbindService(mConnection); |
| mRemoteService = null; |
| mConnection = null; |
| } |
| Slog.i(TAG, "Explicit health check service is unbound"); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| @Nullable |
| private ServiceInfo getServiceInfoLocked() { |
| final String packageName = |
| mContext.getPackageManager().getServicesSystemSharedLibraryPackageName(); |
| if (packageName == null) { |
| Slog.w(TAG, "no external services package!"); |
| return null; |
| } |
| |
| final Intent intent = new Intent(ExplicitHealthCheckService.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; |
| } |
| |
| @GuardedBy("mLock") |
| @Nullable |
| private ComponentName getServiceComponentNameLocked() { |
| final ServiceInfo serviceInfo = getServiceInfoLocked(); |
| if (serviceInfo == null) { |
| return null; |
| } |
| |
| final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name); |
| if (!Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE |
| .equals(serviceInfo.permission)) { |
| Slog.w(TAG, name.flattenToShortString() + " does not require permission " |
| + Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE); |
| return null; |
| } |
| return name; |
| } |
| |
| private void initState(IBinder service) { |
| synchronized (mLock) { |
| if (!mEnabled) { |
| Slog.w(TAG, "Attempting to connect disabled service?? Unbinding..."); |
| // Very unlikely, but we disabled the service after binding but before we connected |
| unbindService(); |
| return; |
| } |
| mRemoteService = IExplicitHealthCheckService.Stub.asInterface(service); |
| try { |
| mRemoteService.setCallback(new RemoteCallback(result -> { |
| String packageName = result.getString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE); |
| if (!TextUtils.isEmpty(packageName)) { |
| if (mPassedConsumer == null) { |
| Slog.wtf(TAG, "Health check passed for package " + packageName |
| + "but no consumer registered."); |
| } else { |
| // Call without lock held |
| mPassedConsumer.accept(packageName); |
| } |
| } else { |
| Slog.wtf(TAG, "Empty package passed explicit health check?"); |
| } |
| })); |
| Slog.i(TAG, "Service initialized, syncing requests"); |
| } catch (RemoteException e) { |
| Slog.wtf(TAG, "Could not setCallback on explicit health check service"); |
| } |
| } |
| // Calling outside lock |
| mNotifySyncRunnable.run(); |
| } |
| |
| /** |
| * Prepares the health check service to receive requests. |
| * |
| * @return {@code true} if it is ready and we can proceed with a request, |
| * {@code false} otherwise. If it is not ready, and the service is enabled, |
| * we will bind and the request should be automatically attempted later. |
| */ |
| @GuardedBy("mLock") |
| private boolean prepareServiceLocked(String action) { |
| if (mRemoteService != null && mEnabled) { |
| return true; |
| } |
| Slog.i(TAG, "Service not ready to " + action |
| + (mEnabled ? ". Binding..." : ". Disabled")); |
| if (mEnabled) { |
| bindService(); |
| } |
| return false; |
| } |
| } |