/* * 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 android.service.watchdog; import static android.os.Parcelable.Creator; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.app.Service; import android.content.Intent; import android.content.pm.PackageManager; import android.crashrecovery.flags.Flags; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteCallback; import android.os.RemoteException; import android.util.Log; import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** * A service to provide packages supporting explicit health checks and route checks to these * packages on behalf of the package watchdog. * *

To extend this class, you must declare the service in your manifest file with the * {@link android.Manifest.permission#BIND_EXPLICIT_HEALTH_CHECK_SERVICE} permission, * and include an intent filter with the {@link #SERVICE_INTERFACE} action. In adddition, * your implementation must live in * {@link PackageManager#getServicesSystemSharedLibraryPackageName()}. * For example:

*
 *     <service android:name=".FooExplicitHealthCheckService"
 *             android:exported="true"
 *             android:priority="100"
 *             android:permission="android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE">
 *         <intent-filter>
 *             <action android:name="android.service.watchdog.ExplicitHealthCheckService" />
 *         </intent-filter>
 *     </service>
 * 
* @hide */ @SystemApi public abstract class ExplicitHealthCheckService extends Service { private static final String TAG = "ExplicitHealthCheckService"; /** * {@link Bundle} key for a {@link List} of {@link PackageConfig} value. * * {@hide} */ public static final String EXTRA_SUPPORTED_PACKAGES = "android.service.watchdog.extra.supported_packages"; /** * {@link Bundle} key for a {@link List} of {@link String} value. * * {@hide} */ public static final String EXTRA_REQUESTED_PACKAGES = "android.service.watchdog.extra.requested_packages"; /** * {@link Bundle} key for a {@link String} value. */ @FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) public static final String EXTRA_HEALTH_CHECK_PASSED_PACKAGE = "android.service.watchdog.extra.HEALTH_CHECK_PASSED_PACKAGE"; /** * The Intent action that a service must respond to. Add it to the intent filter of the service * in its manifest. */ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) public static final String SERVICE_INTERFACE = "android.service.watchdog.ExplicitHealthCheckService"; /** * The permission that a service must require to ensure that only Android system can bind to it. * If this permission is not enforced in the AndroidManifest of the service, the system will * skip that service. */ public static final String BIND_PERMISSION = "android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE"; private final ExplicitHealthCheckServiceWrapper mWrapper = new ExplicitHealthCheckServiceWrapper(); /** * Called when the system requests an explicit health check for {@code packageName}. * *

When {@code packageName} passes the check, implementors should call * {@link #notifyHealthCheckPassed} to inform the system. * *

It could take many hours before a {@code packageName} passes a check and implementors * should never drop requests unless {@link onCancel} is called or the service dies. * *

Requests should not be queued and additional calls while expecting a result for * {@code packageName} should have no effect. */ public abstract void onRequestHealthCheck(@NonNull String packageName); /** * Called when the system cancels the explicit health check request for {@code packageName}. * Should do nothing if there are is no active request for {@code packageName}. */ public abstract void onCancelHealthCheck(@NonNull String packageName); /** * Called when the system requests for all the packages supporting explicit health checks. The * system may request an explicit health check for any of these packages with * {@link #onRequestHealthCheck}. * * @return all packages supporting explicit health checks */ @NonNull public abstract List onGetSupportedPackages(); /** * Called when the system requests for all the packages that it has currently requested * an explicit health check for. * * @return all packages expecting an explicit health check result */ @NonNull public abstract List onGetRequestedPackages(); private final Handler mHandler = Handler.createAsync(Looper.getMainLooper()); @Nullable private Consumer mHealthCheckResultCallback; @Nullable private Executor mCallbackExecutor; @Override @NonNull public final IBinder onBind(@NonNull Intent intent) { return mWrapper; } /** * Sets a callback to be invoked when an explicit health check passes for a package. *

* The callback will receive a {@link Bundle} containing the package name that passed the * health check, identified by the key {@link #EXTRA_HEALTH_CHECK_PASSED_PACKAGE}. *

* Note: This API is primarily intended for testing purposes. Calling this outside of a * test environment will override the default callback mechanism used to notify the system * about health check results. Use with caution in production code. * * @param executor The executor on which the callback should be invoked. If {@code null}, the * callback will be executed on the main thread. * @param callback A callback that receives a {@link Bundle} containing the package name that * passed the health check. */ @FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) public final void setHealthCheckPassedCallback(@CallbackExecutor @Nullable Executor executor, @Nullable Consumer callback) { mCallbackExecutor = executor; mHealthCheckResultCallback = callback; } private void executeCallback(@NonNull String packageName) { if (mHealthCheckResultCallback != null) { Objects.requireNonNull(packageName, "Package passing explicit health check must be non-null"); Bundle bundle = new Bundle(); bundle.putString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE, packageName); mHealthCheckResultCallback.accept(bundle); } else { Log.wtf(TAG, "System missed explicit health check result for " + packageName); } } /** * Implementors should call this to notify the system when explicit health check passes * for {@code packageName}; */ public final void notifyHealthCheckPassed(@NonNull String packageName) { if (mCallbackExecutor != null) { mCallbackExecutor.execute(() -> executeCallback(packageName)); } else { mHandler.post(() -> executeCallback(packageName)); } } /** * A PackageConfig contains a package supporting explicit health checks and the * timeout in {@link System#uptimeMillis} across reboots after which health * check requests from clients are failed. * * @hide */ @SystemApi public static final class PackageConfig implements Parcelable { private static final long DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(1); private final String mPackageName; private final long mHealthCheckTimeoutMillis; /** * Creates a new instance. * * @param packageName the package name * @param durationMillis the duration in milliseconds, must be greater than or * equal to 0. If it is 0, it will use a system default value. */ public PackageConfig(@NonNull String packageName, long healthCheckTimeoutMillis) { mPackageName = Preconditions.checkNotNull(packageName); if (healthCheckTimeoutMillis == 0) { mHealthCheckTimeoutMillis = DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS; } else { mHealthCheckTimeoutMillis = Preconditions.checkArgumentNonnegative( healthCheckTimeoutMillis); } } private PackageConfig(Parcel parcel) { mPackageName = parcel.readString(); mHealthCheckTimeoutMillis = parcel.readLong(); } /** * Gets the package name. * * @return the package name */ public @NonNull String getPackageName() { return mPackageName; } /** * Gets the timeout in milliseconds to evaluate an explicit health check result after a * request. * * @return the duration in {@link System#uptimeMillis} across reboots */ public long getHealthCheckTimeoutMillis() { return mHealthCheckTimeoutMillis; } @NonNull @Override public String toString() { return "PackageConfig{" + mPackageName + ", " + mHealthCheckTimeoutMillis + "}"; } @Override public boolean equals(@Nullable Object other) { if (other == this) { return true; } if (!(other instanceof PackageConfig)) { return false; } PackageConfig otherInfo = (PackageConfig) other; return Objects.equals(otherInfo.getHealthCheckTimeoutMillis(), mHealthCheckTimeoutMillis) && Objects.equals(otherInfo.getPackageName(), mPackageName); } @Override public int hashCode() { return Objects.hash(mPackageName, mHealthCheckTimeoutMillis); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@SuppressLint({"MissingNullability"}) Parcel parcel, int flags) { parcel.writeString(mPackageName); parcel.writeLong(mHealthCheckTimeoutMillis); } public static final @NonNull Creator CREATOR = new Creator() { @Override public PackageConfig createFromParcel(Parcel source) { return new PackageConfig(source); } @Override public PackageConfig[] newArray(int size) { return new PackageConfig[size]; } }; } private class ExplicitHealthCheckServiceWrapper extends IExplicitHealthCheckService.Stub { @Override public void setCallback(RemoteCallback callback) throws RemoteException { mHandler.post(() -> mHealthCheckResultCallback = callback::sendResult); } @Override public void request(String packageName) throws RemoteException { mHandler.post(() -> ExplicitHealthCheckService.this.onRequestHealthCheck(packageName)); } @Override public void cancel(String packageName) throws RemoteException { mHandler.post(() -> ExplicitHealthCheckService.this.onCancelHealthCheck(packageName)); } @Override public void getSupportedPackages(RemoteCallback callback) throws RemoteException { mHandler.post(() -> { List packages = ExplicitHealthCheckService.this.onGetSupportedPackages(); Objects.requireNonNull(packages, "Supported package list must be non-null"); Bundle bundle = new Bundle(); bundle.putParcelableArrayList(EXTRA_SUPPORTED_PACKAGES, new ArrayList<>(packages)); callback.sendResult(bundle); }); } @Override public void getRequestedPackages(RemoteCallback callback) throws RemoteException { mHandler.post(() -> { List packages = ExplicitHealthCheckService.this.onGetRequestedPackages(); Objects.requireNonNull(packages, "Requested package list must be non-null"); Bundle bundle = new Bundle(); bundle.putStringArrayList(EXTRA_REQUESTED_PACKAGES, new ArrayList<>(packages)); callback.sendResult(bundle); }); } } }