content: /**
   * Copyright (c) 2016, 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.utils;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.app.PendingIntent;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.ServiceConnection;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.IBinder.DeathRecipient;
  import android.os.IInterface;
  import android.os.RemoteException;
  import android.os.SystemClock;
  import android.os.UserHandle;
  import android.util.Slog;
  
  import java.text.SimpleDateFormat;
  import java.util.Objects;
  import java.util.Date;
  
  /**
   * Manages the lifecycle of an application-provided service bound from system server.
   *
   * @hide
   */
  public class ManagedApplicationService {
      private final String TAG = getClass().getSimpleName();
  
      /**
       * Attempt to reconnect service forever if an onBindingDied or onServiceDisconnected event
       * is received.
       */
      public static final int RETRY_FOREVER = 1;
  
      /**
       * Never attempt to reconnect the service - a single onBindingDied or onServiceDisconnected
       * event will cause this to fully unbind the service and never attempt to reconnect.
       */
      public static final int RETRY_NEVER = 2;
  
      /**
       * Attempt to reconnect the service until the maximum number of retries is reached, then stop.
       *
       * The first retry will occur MIN_RETRY_DURATION_MS after the disconnection, and each
       * subsequent retry will occur after 2x the duration used for the previous retry up to the
       * MAX_RETRY_DURATION_MS duration.
       *
       * In this case, retries mean a full unbindService/bindService pair to handle cases when the
       * usual service re-connection logic in ActiveServices has very high backoff times or when the
       * serviceconnection has fully died due to a package update or similar.
       */
      public static final int RETRY_BEST_EFFORT = 3;
  
      // Maximum number of retries before giving up (for RETRY_BEST_EFFORT).
      private static final int MAX_RETRY_COUNT = 4;
      // Max time between retry attempts.
      private static final long MAX_RETRY_DURATION_MS = 16000;
      // Min time between retry attempts.
      private static final long MIN_RETRY_DURATION_MS = 2000;
      // Time since the last retry attempt after which to clear the retry attempt counter.
      private static final long RETRY_RESET_TIME_MS = MAX_RETRY_DURATION_MS * 4;
  
      private final Context mContext;
      private final int mUserId;
      private final ComponentName mComponent;
      private final int mClientLabel;
      private final String mSettingsAction;
      private final BinderChecker mChecker;
      private final boolean mIsImportant;
      private final int mRetryType;
      private final Handler mHandler;
      private final Runnable mRetryRunnable = this::doRetry;
      private final EventCallback mEventCb;
  
      private final Object mLock = new Object();
  
      // State protected by mLock
      private ServiceConnection mConnection;
      private IInterface mBoundInterface;
      private PendingEvent mPendingEvent;
      private int mRetryCount;
      private long mLastRetryTimeMs;
      private long mNextRetryDurationMs = MIN_RETRY_DURATION_MS;
      private boolean mRetrying;
  
      public static interface LogFormattable {
         String toLogString(SimpleDateFormat dateFormat);
      }
  
      /**
       * Lifecycle event of this managed service.
       */
      public static class LogEvent implements LogFormattable {
          public static final int EVENT_CONNECTED = 1;
          public static final int EVENT_DISCONNECTED = 2;
          public static final int EVENT_BINDING_DIED = 3;
          public static final int EVENT_STOPPED_PERMANENTLY = 4;
  
          // Time of the events in "current time ms" timebase.
          public final long timestamp;
          // Name of the component for this system service.
          public final ComponentName component;
          // ID of the event that occurred.
          public final int event;
  
          public LogEvent(long timestamp, ComponentName component, int event) {
              this.timestamp = timestamp;
              this.component = component;
              this.event = event;
          }
  
          @Override
          public String toLogString(SimpleDateFormat dateFormat) {
              return dateFormat.format(new Date(timestamp)) + "   " + eventToString(event)
                      + " Managed Service: "
                      + ((component == null) ? "None" : component.flattenToString());
          }
  
          public static String eventToString(int event) {
              switch (event) {
                  case EVENT_CONNECTED:
                      return "Connected";
                  case EVENT_DISCONNECTED:
                      return "Disconnected";
                  case EVENT_BINDING_DIED:
                      return "Binding Died For";
                  case EVENT_STOPPED_PERMANENTLY:
                      return "Permanently Stopped";
                  default:
                      return "Unknown Event Occurred";
              }
          }
      }
  
      private ManagedApplicationService(final Context context, final ComponentName component,
              final int userId, int clientLabel, String settingsAction,
              BinderChecker binderChecker, boolean isImportant, int retryType, Handler handler,
              EventCallback eventCallback) {
          mContext = context;
          mComponent = component;
          mUserId = userId;
          mClientLabel = clientLabel;
          mSettingsAction = settingsAction;
          mChecker = binderChecker;
          mIsImportant = isImportant;
          mRetryType = retryType;
          mHandler = handler;
          mEventCb = eventCallback;
      }
  
      /**
       * Implement to validate returned IBinder instance.
       */
      public interface BinderChecker {
          IInterface asInterface(IBinder binder);
          boolean checkType(IInterface service);
      }
  
      /**
       * Implement to call IInterface methods after service is connected.
       */
      public interface PendingEvent {
          void runEvent(IInterface service) throws RemoteException;
      }
  
      /**
       * Implement to be notified about any problems with remote service.
       */
      public interface EventCallback {
          /**
           * Called when an sevice lifecycle event occurs.
           */
          void onServiceEvent(LogEvent event);
      }
  
      /**
       * Create a new ManagedApplicationService object but do not yet bind to the user service.
       *
       * @param context a Context to use for binding the application service.
       * @param component the {@link ComponentName} of the application service to bind.
       * @param userId the user ID of user to bind the application service as.
       * @param clientLabel the resource ID of a label displayed to the user indicating the
       *      binding service, or 0 if none is desired.
       * @param settingsAction an action that can be used to open the Settings UI to enable/disable
       *      binding to these services, or null if none is desired.
       * @param binderChecker an interface used to validate the returned binder object, or null if
       *      this interface is unchecked.
       * @param isImportant bind the user service with BIND_IMPORTANT.
       * @param retryType reconnect behavior to have when bound service is disconnected.
       * @param handler the Handler to use for retries and delivering EventCallbacks.
       * @param eventCallback a callback used to deliver disconnection events, or null if you
       *      don't care.
       * @return a ManagedApplicationService instance.
       */
      public static ManagedApplicationService build(@NonNull final Context context,
              @NonNull final ComponentName component, final int userId, int clientLabel,
              @Nullable String settingsAction, @Nullable BinderChecker binderChecker,
              boolean isImportant, int retryType, @NonNull Handler handler,
              @Nullable EventCallback eventCallback) {
          return new ManagedApplicationService(context, component, userId, clientLabel,
              settingsAction, binderChecker, isImportant, retryType, handler, eventCallback);
      }
  
  
      /**
       * @return the user ID of the user that owns the bound service.
       */
      public int getUserId() {
          return mUserId;
      }
  
      /**
       * @return the component of the bound service.
       */
      public ComponentName getComponent() {
          return mComponent;
      }
  
      /**
       * Asynchronously unbind from the application service if the bound service component and user
       * does not match the given signature.
       *
       * @param componentName the component that must match.
       * @param userId the user ID that must match.
       * @return {@code true} if not matching.
       */
      public boolean disconnectIfNotMatching(final ComponentName componentName, final int userId) {
          if (matches(componentName, userId)) {
              return false;
          }
          disconnect();
          return true;
      }
  
      /**
       * Send an event to run as soon as the binder interface is available.
       *
       * @param event a {@link PendingEvent} to send.
       */
      public void sendEvent(@NonNull PendingEvent event) {
          IInterface iface;
          synchronized (mLock) {
              iface = mBoundInterface;
              if (iface == null) {
                  mPendingEvent = event;
              }
          }
  
          if (iface != null) {
              try {
                  event.runEvent(iface);
              } catch (RuntimeException | RemoteException ex) {
                  Slog.e(TAG, "Received exception from user service: ", ex);
              }
          }
      }
  
      /**
       * Asynchronously unbind from the application service if bound.
       */
      public void disconnect() {
          synchronized (mLock) {
              // Unbind existing connection, if it exists
              if (mConnection == null) {
                  return;
              }
  
              mContext.unbindService(mConnection);
              mConnection = null;
              mBoundInterface = null;
          }
      }
  
      /**
       * Asynchronously bind to the application service if not bound.
       */
      public void connect() {
          synchronized (mLock) {
              if (mConnection != null) {
                  // We're already connected or are trying to connect
                  return;
              }
  
              Intent intent  = new Intent().setComponent(mComponent);
              if (mClientLabel != 0) {
                  intent.putExtra(Intent.EXTRA_CLIENT_LABEL, mClientLabel);
              }
              if (mSettingsAction != null) {
                  intent.putExtra(Intent.EXTRA_CLIENT_INTENT,
                          PendingIntent.getActivity(mContext, 0, new Intent(mSettingsAction), 0));
              }
  
              mConnection = new ServiceConnection() {
                  @Override
                  public void onBindingDied(ComponentName componentName) {
                      final long timestamp = System.currentTimeMillis();
                      Slog.w(TAG, "Service binding died: " + componentName);
                      synchronized (mLock) {
                          if (mConnection != this) {
                              return;
                          }
                          mHandler.post(() -> {
                              mEventCb.onServiceEvent(new LogEvent(timestamp, mComponent,
                                    LogEvent.EVENT_BINDING_DIED));
                          });
  
                          mBoundInterface = null;
                          startRetriesLocked();
                      }
                  }
  
                  @Override
                  public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
                      final long timestamp = System.currentTimeMillis();
                      Slog.i(TAG, "Service connected: " + componentName);
                      IInterface iface = null;
                      PendingEvent pendingEvent = null;
                      synchronized (mLock) {
                          if (mConnection != this) {
                              // Must've been unbound.
                              return;
                          }
                          mHandler.post(() -> {
                              mEventCb.onServiceEvent(new LogEvent(timestamp, mComponent,
                                    LogEvent.EVENT_CONNECTED));
                          });
  
                          stopRetriesLocked();
  
                          mBoundInterface = null;
                          if (mChecker != null) {
                              mBoundInterface = mChecker.asInterface(iBinder);
                              if (!mChecker.checkType(mBoundInterface)) {
                                  // Received an invalid binder, disconnect.
                                  mBoundInterface = null;
                                  Slog.w(TAG, "Invalid binder from " + componentName);
                                  startRetriesLocked();
                                  return;
                              }
                              iface = mBoundInterface;
                              pendingEvent = mPendingEvent;
                              mPendingEvent = null;
                          }
                      }
                      if (iface != null && pendingEvent != null) {
                          try {
                              pendingEvent.runEvent(iface);
                          } catch (RuntimeException | RemoteException ex) {
                              Slog.e(TAG, "Received exception from user service: ", ex);
                              startRetriesLocked();
                          }
                      }
                  }
  
                  @Override
                  public void onServiceDisconnected(ComponentName componentName) {
                      final long timestamp = System.currentTimeMillis();
                      Slog.w(TAG, "Service disconnected: " + componentName);
                      synchronized (mLock) {
                          if (mConnection != this) {
                              return;
                          }
  
                          mHandler.post(() -> {
                              mEventCb.onServiceEvent(new LogEvent(timestamp, mComponent,
                                    LogEvent.EVENT_DISCONNECTED));
                          });
  
                          mBoundInterface = null;
                          startRetriesLocked();
                      }
                  }
              };
  
              int flags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE;
              if (mIsImportant) {
                  flags |= Context.BIND_IMPORTANT;
              }
              try {
                  if (!mContext.bindServiceAsUser(intent, mConnection, flags,
                          new UserHandle(mUserId))) {
                      Slog.w(TAG, "Unable to bind service: " + intent);
                      startRetriesLocked();
                  }
              } catch (SecurityException e) {
                  Slog.w(TAG, "Unable to bind service: " + intent, e);
                  startRetriesLocked();
              }
          }
      }
  
      private boolean matches(final ComponentName component, final int userId) {
          return Objects.equals(mComponent, component) && mUserId == userId;
      }
  
      private void startRetriesLocked() {
          if (checkAndDeliverServiceDiedCbLocked()) {
              // If we delivered the service callback, disconnect and stop retrying.
              disconnect();
              return;
          }
  
          if (mRetrying) {
              // Retry already queued, don't queue a new one.
              return;
          }
          mRetrying = true;
          queueRetryLocked();
      }
  
      private void stopRetriesLocked() {
          mRetrying = false;
          mHandler.removeCallbacks(mRetryRunnable);
      }
  
      private void queueRetryLocked() {
          long now = SystemClock.uptimeMillis();
          if ((now - mLastRetryTimeMs) > RETRY_RESET_TIME_MS) {
              // It's been longer than the reset time since we last had to retry.  Re-initialize.
              mNextRetryDurationMs = MIN_RETRY_DURATION_MS;
              mRetryCount = 0;
          }
          mLastRetryTimeMs = now;
          mHandler.postDelayed(mRetryRunnable, mNextRetryDurationMs);
          mNextRetryDurationMs = Math.min(2 * mNextRetryDurationMs, MAX_RETRY_DURATION_MS);
          mRetryCount++;
      }
  
      private boolean checkAndDeliverServiceDiedCbLocked() {
  
         if (mRetryType == RETRY_NEVER || (mRetryType == RETRY_BEST_EFFORT
                  && mRetryCount >= MAX_RETRY_COUNT)) {
              // If we never retry, or we've exhausted our retries, post the onServiceDied callback.
              Slog.e(TAG, "Service " + mComponent + " has died too much, not retrying.");
              if (mEventCb != null) {
                  final long timestamp = System.currentTimeMillis();
                  mHandler.post(() -> {
                    mEventCb.onServiceEvent(new LogEvent(timestamp, mComponent,
                          LogEvent.EVENT_STOPPED_PERMANENTLY));
                  });
              }
              return true;
          }
          return false;
      }
  
      private void doRetry() {
          synchronized (mLock) {
              if (mConnection == null) {
                  // We disconnected for good.  Don't attempt to retry.
                  return;
              }
              if (!mRetrying) {
                  // We successfully connected.  Don't attempt to retry.
                  return;
              }
              Slog.i(TAG, "Attempting to reconnect " + mComponent + "...");
              // While frameworks may restart the remote Service if we stay bound, we have little
              // control of the backoff timing for reconnecting the service.  In the event of a
              // process crash, the backoff time can be very large (1-30 min), which is not
              // acceptable for the types of services this is used for.  Instead force an unbind/bind
              // sequence to cause a more immediate retry.
              disconnect();
              if (checkAndDeliverServiceDiedCbLocked()) {
                  // No more retries.
                  return;
              }
              queueRetryLocked();
              connect();
          }
      }
  }
  