/*
|
* 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;
|
}
|
}
|