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