content: /*
   * Copyright (C) 2017 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.am;
  
  import android.annotation.NonNull;
  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.SystemClock;
  import android.os.UserHandle;
  import android.util.Log;
  import android.util.TimeUtils;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  
  import java.io.PrintWriter;
  
  /**
   * Connects to a given service component on a given user.
   *
   * - Call {@link #bind()} to create a connection.
   * - Call {@link #unbind()} to disconnect.  Make sure to disconnect when the user stops.
   *
   * Add onConnected/onDisconnected callbacks as needed.
   *
   * When the target process gets killed (by OOM-killer, etc), then the activity manager will
   * re-connect the connection automatically, in which case onServiceDisconnected() gets called
   * and then onServiceConnected().
   *
   * However sometimes the activity manager just "kills" the connection -- like when the target
   * package gets updated or the target process crashes multiple times in a row, in which case
   * onBindingDied() gets called.  This class handles this case by re-connecting in the time
   * {@link #mRebindBackoffMs}.  If this happens again, this class increases the back-off time
   * by {@link #mRebindBackoffIncrease} and retry.  The back-off time is capped at
   * {@link #mRebindMaxBackoffMs}.
   *
   * The back-off time will never be reset until {@link #unbind()} and {@link #bind()} are called
   * explicitly.
   *
   * NOTE: This class does *not* handle package-updates -- i.e. even if the binding dies due to
   * the target package being updated, this class won't reconnect.  This is because this class doesn't
   * know what to do when the service component has gone missing, for example.  If the user of this
   * class wants to restore the connection, then it should call {@link #unbind()} and {@link #bind}
   * explicitly.
   *
   * atest ${ANDROID_BUILD_TOP}/frameworks/base/services/tests/mockingservicestests/src/com/android/server/am/PersistentConnectionTest.java
   */
  public abstract class PersistentConnection<T> {
      private final Object mLock = new Object();
  
      private final static boolean DEBUG = false;
  
      private final String mTag;
      private final Context mContext;
      private final Handler mHandler;
      private final int mUserId;
      private final ComponentName mComponentName;
  
      private long mNextBackoffMs;
  
      private final long mRebindBackoffMs;
      private final double mRebindBackoffIncrease;
      private final long mRebindMaxBackoffMs;
      private final long mResetBackoffDelay;
  
      private long mReconnectTime;
  
      // TODO too many booleans... Should clean up.
  
      @GuardedBy("mLock")
      private boolean mBound;
  
      /**
       * Whether {@link #bind()} has been called and {@link #unbind()} hasn't been yet; meaning this
       * is the expected bind state from the caller's point of view.
       */
      @GuardedBy("mLock")
      private boolean mShouldBeBound;
  
      @GuardedBy("mLock")
      private boolean mRebindScheduled;
  
      @GuardedBy("mLock")
      private boolean mIsConnected;
  
      @GuardedBy("mLock")
      private T mService;
  
      @GuardedBy("mLock")
      private int mNumConnected;
  
      @GuardedBy("mLock")
      private int mNumDisconnected;
  
      @GuardedBy("mLock")
      private int mNumBindingDied;
  
      @GuardedBy("mLock")
      private long mLastConnectedTime;
  
      private final ServiceConnection mServiceConnection = new ServiceConnection() {
          @Override
          public void onServiceConnected(ComponentName name, IBinder service) {
              synchronized (mLock) {
                  if (!mBound) {
                      // Callback came in after PersistentConnection.unbind() was called.
                      // We just ignore this.
                      // (We've already called unbindService() already in unbind)
                      Log.w(mTag, "Connected: " + mComponentName.flattenToShortString()
                              + " u" + mUserId + " but not bound, ignore.");
                      return;
                  }
                  Log.i(mTag, "Connected: " + mComponentName.flattenToShortString()
                          + " u" + mUserId);
  
                  mNumConnected++;
  
                  mIsConnected = true;
                  mLastConnectedTime = injectUptimeMillis();
                  mService = asInterface(service);
  
                  scheduleStableCheckLocked();
              }
          }
  
          @Override
          public void onServiceDisconnected(ComponentName name) {
              synchronized (mLock) {
                  Log.i(mTag, "Disconnected: " + mComponentName.flattenToShortString()
                          + " u" + mUserId);
  
                  mNumDisconnected++;
  
                  cleanUpConnectionLocked();
  
                  // Note we won't increase the rebind timeout here, because we don't explicitly
                  // rebind in this case.
              }
          }
  
          @Override
          public void onBindingDied(ComponentName name) {
              // Activity manager gave up; we'll schedule a re-connect by ourselves.
              synchronized (mLock) {
                  if (!mBound) {
                      // Callback came in late?
                      Log.w(mTag, "Binding died: " + mComponentName.flattenToShortString()
                              + " u" + mUserId + " but not bound, ignore.");
                      return;
                  }
  
                  Log.w(mTag, "Binding died: " + mComponentName.flattenToShortString()
                          + " u" + mUserId);
  
                  mNumBindingDied++;
  
                  scheduleRebindLocked();
              }
          }
      };
  
      private final Runnable mBindForBackoffRunnable = () -> bindForBackoff();
  
      public PersistentConnection(@NonNull String tag, @NonNull Context context,
              @NonNull Handler handler, int userId, @NonNull ComponentName componentName,
              long rebindBackoffSeconds, double rebindBackoffIncrease, long rebindMaxBackoffSeconds,
              long resetBackoffDelay) {
          mTag = tag;
          mContext = context;
          mHandler = handler;
          mUserId = userId;
          mComponentName = componentName;
  
          mRebindBackoffMs = rebindBackoffSeconds * 1000;
          mRebindBackoffIncrease = rebindBackoffIncrease;
          mRebindMaxBackoffMs = rebindMaxBackoffSeconds * 1000;
          mResetBackoffDelay = resetBackoffDelay * 1000;
  
          mNextBackoffMs = mRebindBackoffMs;
      }
  
      public final ComponentName getComponentName() {
          return mComponentName;
      }
  
      public final int getUserId() {
          return mUserId;
      }
  
      protected abstract int getBindFlags();
  
      /**
       * @return whether {@link #bind()} has been called and {@link #unbind()} hasn't.
       *
       * Note when the AM gives up on connection, this class detects it and un-bind automatically,
       * and schedule rebind, and {@link #isBound} returns false when it's waiting for a retry.
       */
      public final boolean isBound() {
          synchronized (mLock) {
              return mBound;
          }
      }
  
      /**
       * @return whether re-bind is scheduled after the AM gives up on a connection.
       */
      public final boolean isRebindScheduled() {
          synchronized (mLock) {
              return mRebindScheduled;
          }
      }
  
      /**
       * @return whether connected.
       */
      public final boolean isConnected() {
          synchronized (mLock) {
              return mIsConnected;
          }
      }
  
      /**
       * @return the service binder interface.
       */
      public final T getServiceBinder() {
          synchronized (mLock) {
              return mService;
          }
      }
  
      /**
       * Connects to the service.
       */
      public final void bind() {
          synchronized (mLock) {
              mShouldBeBound = true;
  
              bindInnerLocked(/* resetBackoff= */ true);
          }
      }
  
      /** Return the next back-off time */
      public long getNextBackoffMs() {
          synchronized (mLock) {
              return mNextBackoffMs;
          }
      }
  
      /** Return the number of times the connected callback called. */
      public int getNumConnected() {
          synchronized (mLock) {
              return mNumConnected;
          }
      }
  
      /** Return the number of times the disconnected callback called. */
      public int getNumDisconnected() {
          synchronized (mLock) {
              return mNumDisconnected;
          }
      }
  
      /** Return the number of times the binding died callback called. */
      public int getNumBindingDied() {
          synchronized (mLock) {
              return mNumBindingDied;
          }
      }
  
      @GuardedBy("mLock")
      private void resetBackoffLocked() {
          if (mNextBackoffMs != mRebindBackoffMs) {
              mNextBackoffMs = mRebindBackoffMs;
              Log.i(mTag, "Backoff reset to " + mNextBackoffMs);
          }
      }
  
      @GuardedBy("mLock")
      public final void bindInnerLocked(boolean resetBackoff) {
          unscheduleRebindLocked();
  
          if (mBound) {
              return;
          }
          mBound = true;
  
          unscheduleStableCheckLocked();
  
          if (resetBackoff) {
              resetBackoffLocked();
          }
  
          final Intent service = new Intent().setComponent(mComponentName);
  
          if (DEBUG) {
              Log.d(mTag, "Attempting to connect to " + mComponentName);
          }
  
          final boolean success = mContext.bindServiceAsUser(service, mServiceConnection,
                  Context.BIND_AUTO_CREATE | getBindFlags(),
                  mHandler, UserHandle.of(mUserId));
  
          if (!success) {
              Log.e(mTag, "Binding: " + service.getComponent() + " u" + mUserId
                      + " failed.");
          }
      }
  
      final void bindForBackoff() {
          synchronized (mLock) {
              if (!mShouldBeBound) {
                  // Race condition -- by the time we got here, unbind() has already been called.
                  return;
              }
  
              bindInnerLocked(/* resetBackoff= */ false);
          }
      }
  
      @GuardedBy("mLock")
      private void cleanUpConnectionLocked() {
          mIsConnected = false;
          mService = null;
  
          unscheduleStableCheckLocked();
      }
  
      /**
       * Disconnect from the service.
       */
      public final void unbind() {
          synchronized (mLock) {
              mShouldBeBound = false;
  
              unbindLocked();
              unscheduleStableCheckLocked();
          }
      }
  
      @GuardedBy("mLock")
      private final void unbindLocked() {
          unscheduleRebindLocked();
  
          if (!mBound) {
              return;
          }
          Log.i(mTag, "Stopping: " + mComponentName.flattenToShortString() + " u" + mUserId);
          mBound = false;
          mContext.unbindService(mServiceConnection);
  
          cleanUpConnectionLocked();
      }
  
      @GuardedBy("mLock")
      void unscheduleRebindLocked() {
          injectRemoveCallbacks(mBindForBackoffRunnable);
          mRebindScheduled = false;
      }
  
      @GuardedBy("mLock")
      void scheduleRebindLocked() {
          unbindLocked();
  
          if (!mRebindScheduled) {
              Log.i(mTag, "Scheduling to reconnect in " + mNextBackoffMs + " ms (uptime)");
  
              mReconnectTime = injectUptimeMillis() + mNextBackoffMs;
  
              injectPostAtTime(mBindForBackoffRunnable, mReconnectTime);
  
              mNextBackoffMs = Math.min(mRebindMaxBackoffMs,
                      (long) (mNextBackoffMs * mRebindBackoffIncrease));
  
              mRebindScheduled = true;
          }
      }
  
      private final Runnable mStableCheck = this::stableConnectionCheck;
  
      private void stableConnectionCheck() {
          synchronized (mLock) {
              final long now = injectUptimeMillis();
              final long timeRemaining = (mLastConnectedTime + mResetBackoffDelay) - now;
              if (DEBUG) {
                  Log.d(mTag, "stableConnectionCheck: bound=" + mBound + " connected=" + mIsConnected
                          + " remaining=" + timeRemaining);
              }
              if (mBound && mIsConnected && timeRemaining <= 0) {
                  resetBackoffLocked();
              }
          }
      }
  
      @GuardedBy("mLock")
      private void unscheduleStableCheckLocked() {
          injectRemoveCallbacks(mStableCheck);
      }
  
      @GuardedBy("mLock")
      private void scheduleStableCheckLocked() {
          unscheduleStableCheckLocked();
          injectPostAtTime(mStableCheck, injectUptimeMillis() + mResetBackoffDelay);
      }
  
      /** Must be implemented by a subclass to convert an {@link IBinder} to a stub. */
      protected abstract T asInterface(IBinder binder);
  
      public void dump(String prefix, PrintWriter pw) {
          synchronized (mLock) {
              pw.print(prefix);
              pw.print(mComponentName.flattenToShortString());
              pw.print(" u");
              pw.print(mUserId);
              pw.print(mBound ? " [bound]" : " [not bound]");
              pw.print(mIsConnected ? " [connected]" : " [not connected]");
              if (mRebindScheduled) {
                  pw.print(" reconnect in ");
                  TimeUtils.formatDuration((mReconnectTime - injectUptimeMillis()), pw);
              }
              pw.println();
  
              pw.print(prefix);
              pw.print("  Next backoff(sec): ");
              pw.print(mNextBackoffMs / 1000);
              pw.println();
  
              pw.print(prefix);
              pw.print("  Connected: ");
              pw.print(mNumConnected);
              pw.print("  Disconnected: ");
              pw.print(mNumDisconnected);
              pw.print("  Died: ");
              pw.print(mNumBindingDied);
              if (mIsConnected) {
                  pw.print("  Duration: ");
                  TimeUtils.formatDuration((injectUptimeMillis() - mLastConnectedTime), pw);
              }
              pw.println();
          }
      }
  
      @VisibleForTesting
      void injectRemoveCallbacks(Runnable r) {
          mHandler.removeCallbacks(r);
      }
  
      @VisibleForTesting
      void injectPostAtTime(Runnable r, long uptimeMillis) {
          mHandler.postAtTime(r, uptimeMillis);
      }
  
      @VisibleForTesting
      long injectUptimeMillis() {
          return SystemClock.uptimeMillis();
      }
  
      @VisibleForTesting
      long getNextBackoffMsForTest() {
          return mNextBackoffMs;
      }
  
      @VisibleForTesting
      long getReconnectTimeForTest() {
          return mReconnectTime;
      }
  
      @VisibleForTesting
      ServiceConnection getServiceConnectionForTest() {
          return mServiceConnection;
      }
  
      @VisibleForTesting
      Runnable getBindForBackoffRunnableForTest() {
          return mBindForBackoffRunnable;
      }
  
      @VisibleForTesting
      Runnable getStableCheckRunnableForTest() {
          return mStableCheck;
      }
  
      @VisibleForTesting
      boolean shouldBeBoundForTest() {
          return mShouldBeBound;
      }
  }
  