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.backup.transport;
  
  import static com.android.server.backup.transport.TransportUtils.formatMessage;
  
  import android.annotation.IntDef;
  import android.annotation.Nullable;
  import android.annotation.UserIdInt;
  import android.annotation.WorkerThread;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.ServiceConnection;
  import android.os.DeadObjectException;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.Looper;
  import android.os.SystemClock;
  import android.os.UserHandle;
  import android.text.format.DateFormat;
  import android.util.ArrayMap;
  import android.util.EventLog;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.backup.IBackupTransport;
  import com.android.internal.util.Preconditions;
  import com.android.server.EventLogTags;
  import com.android.server.backup.TransportManager;
  import com.android.server.backup.transport.TransportUtils.Priority;
  
  import dalvik.system.CloseGuard;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.lang.ref.WeakReference;
  import java.util.Collections;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.concurrent.CompletableFuture;
  import java.util.concurrent.ExecutionException;
  
  /**
   * A {@link TransportClient} manages the connection to an {@link IBackupTransport} service, obtained
   * via the {@param bindIntent} parameter provided in the constructor. A {@link TransportClient} is
   * responsible for only one connection to the transport service, not more.
   *
   * <p>After retrieved using {@link TransportManager#getTransportClient(String, String)}, you can
   * call either {@link #connect(String)}, if you can block your thread, or {@link
   * #connectAsync(TransportConnectionListener, String)}, otherwise, to obtain a {@link
   * IBackupTransport} instance. It's meant to be passed around as a token to a connected transport.
   * When the connection is not needed anymore you should call {@link #unbind(String)} or indirectly
   * via {@link TransportManager#disposeOfTransportClient(TransportClient, String)}.
   *
   * <p>DO NOT forget to unbind otherwise there will be dangling connections floating around.
   *
   * <p>This class is thread-safe.
   *
   * @see TransportManager
   */
  public class TransportClient {
      @VisibleForTesting static final String TAG = "TransportClient";
      private static final int LOG_BUFFER_SIZE = 5;
  
      private final @UserIdInt int mUserId;
      private final Context mContext;
      private final TransportStats mTransportStats;
      private final Intent mBindIntent;
      private final ServiceConnection mConnection;
      private final String mIdentifier;
      private final String mCreatorLogString;
      private final ComponentName mTransportComponent;
      private final Handler mListenerHandler;
      private final String mPrefixForLog;
      private final Object mStateLock = new Object();
      private final Object mLogBufferLock = new Object();
      private final CloseGuard mCloseGuard = CloseGuard.get();
  
      @GuardedBy("mLogBufferLock")
      private final List<String> mLogBuffer = new LinkedList<>();
  
      @GuardedBy("mStateLock")
      private final Map<TransportConnectionListener, String> mListeners = new ArrayMap<>();
  
      @GuardedBy("mStateLock")
      @State
      private int mState = State.IDLE;
  
      @GuardedBy("mStateLock")
      private volatile IBackupTransport mTransport;
  
      TransportClient(
              @UserIdInt int userId,
              Context context,
              TransportStats transportStats,
              Intent bindIntent,
              ComponentName transportComponent,
              String identifier,
              String caller) {
          this(
                  userId,
                  context,
                  transportStats,
                  bindIntent,
                  transportComponent,
                  identifier,
                  caller,
                  new Handler(Looper.getMainLooper()));
      }
  
      @VisibleForTesting
      TransportClient(
              @UserIdInt int userId,
              Context context,
              TransportStats transportStats,
              Intent bindIntent,
              ComponentName transportComponent,
              String identifier,
              String caller,
              Handler listenerHandler) {
          mUserId = userId;
          mContext = context;
          mTransportStats = transportStats;
          mTransportComponent = transportComponent;
          mBindIntent = bindIntent;
          mIdentifier = identifier;
          mCreatorLogString = caller;
          mListenerHandler = listenerHandler;
          mConnection = new TransportConnection(context, this);
  
          // For logging
          String classNameForLog = mTransportComponent.getShortClassName().replaceFirst(".*\\.", "");
          mPrefixForLog = classNameForLog + "#" + mIdentifier + ":";
  
          mCloseGuard.open("markAsDisposed");
      }
  
      public ComponentName getTransportComponent() {
          return mTransportComponent;
      }
  
      /**
       * Attempts to connect to the transport (if needed).
       *
       * <p>Note that being bound is not the same as connected. To be connected you also need to be
       * bound. You go from nothing to bound, then to bound and connected. To have a usable transport
       * binder instance you need to be connected. This method will attempt to connect and return an
       * usable transport binder regardless of the state of the object, it may already be connected,
       * or bound but not connected, not bound at all or even unusable.
       *
       * <p>So, a {@link Context#bindServiceAsUser(Intent, ServiceConnection, int, UserHandle)} (or
       * one of its variants) can be called or not depending on the inner state. However, it won't be
       * called again if we're already bound. For example, if one was already requested but the
       * framework has not yet returned (meaning we're bound but still trying to connect) it won't
       * trigger another one, just piggyback on the original request.
       *
       * <p>It's guaranteed that you are going to get a call back to {@param listener} after this
       * call. However, the {@param IBackupTransport} parameter, the transport binder, is not
       * guaranteed to be non-null, or if it's non-null it's not guaranteed to be usable - i.e. it can
       * throw {@link DeadObjectException}s on method calls. You should check for both in your code.
       * The reasons for a null transport binder are:
       *
       * <ul>
       *   <li>Some code called {@link #unbind(String)} before you got a callback.
       *   <li>The framework had already called {@link
       *       ServiceConnection#onServiceDisconnected(ComponentName)} or {@link
       *       ServiceConnection#onBindingDied(ComponentName)} on this object's connection before.
       *       Check the documentation of those methods for when that happens.
       *   <li>The framework returns false for {@link Context#bindServiceAsUser(Intent,
       *       ServiceConnection, int, UserHandle)} (or one of its variants). Check documentation for
       *       when this happens.
       * </ul>
       *
       * For unusable transport binders check {@link DeadObjectException}.
       *
       * @param listener The listener that will be called with the (possibly null or unusable) {@link
       *     IBackupTransport} instance and this {@link TransportClient} object.
       * @param caller A {@link String} identifying the caller for logging/debugging purposes. This
       *     should be a human-readable short string that is easily identifiable in the logs. Ideally
       *     TAG.methodName(), where TAG is the one used in logcat. In cases where this is is not very
       *     descriptive like MyHandler.handleMessage() you should put something that someone reading
       *     the code would understand, like MyHandler/MSG_FOO.
       * @see #connect(String)
       * @see DeadObjectException
       * @see ServiceConnection#onServiceConnected(ComponentName, IBinder)
       * @see ServiceConnection#onServiceDisconnected(ComponentName)
       * @see Context#bindServiceAsUser(Intent, ServiceConnection, int, UserHandle)
       */
      public void connectAsync(TransportConnectionListener listener, String caller) {
          synchronized (mStateLock) {
              checkStateIntegrityLocked();
  
              switch (mState) {
                  case State.UNUSABLE:
                      log(Priority.WARN, caller, "Async connect: UNUSABLE client");
                      notifyListener(listener, null, caller);
                      break;
                  case State.IDLE:
                      boolean hasBound =
                              mContext.bindServiceAsUser(
                                      mBindIntent,
                                      mConnection,
                                      Context.BIND_AUTO_CREATE,
                                      UserHandle.of(mUserId));
                      if (hasBound) {
                          // We don't need to set a time-out because we are guaranteed to get a call
                          // back in ServiceConnection, either an onServiceConnected() or
                          // onBindingDied().
                          log(Priority.DEBUG, caller, "Async connect: service bound, connecting");
                          setStateLocked(State.BOUND_AND_CONNECTING, null);
                          mListeners.put(listener, caller);
                      } else {
                          log(Priority.ERROR, "Async connect: bindService returned false");
                          // mState remains State.IDLE
                          mContext.unbindService(mConnection);
                          notifyListener(listener, null, caller);
                      }
                      break;
                  case State.BOUND_AND_CONNECTING:
                      log(
                              Priority.DEBUG,
                              caller,
                              "Async connect: already connecting, adding listener");
                      mListeners.put(listener, caller);
                      break;
                  case State.CONNECTED:
                      log(Priority.DEBUG, caller, "Async connect: reusing transport");
                      notifyListener(listener, mTransport, caller);
                      break;
              }
          }
      }
  
      /**
       * Removes the transport binding.
       *
       * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
       *     {@link #connectAsync(TransportConnectionListener, String)} for more details.
       */
      public void unbind(String caller) {
          synchronized (mStateLock) {
              checkStateIntegrityLocked();
  
              log(Priority.DEBUG, caller, "Unbind requested (was " + stateToString(mState) + ")");
              switch (mState) {
                  case State.UNUSABLE:
                  case State.IDLE:
                      break;
                  case State.BOUND_AND_CONNECTING:
                      setStateLocked(State.IDLE, null);
                      // After unbindService() no calls back to mConnection
                      mContext.unbindService(mConnection);
                      notifyListenersAndClearLocked(null);
                      break;
                  case State.CONNECTED:
                      setStateLocked(State.IDLE, null);
                      mContext.unbindService(mConnection);
                      break;
              }
          }
      }
  
      /** Marks this TransportClient as disposed, allowing it to be GC'ed without warnings. */
      public void markAsDisposed() {
          synchronized (mStateLock) {
              Preconditions.checkState(
                      mState < State.BOUND_AND_CONNECTING, "Can't mark as disposed if still bound");
              mCloseGuard.close();
          }
      }
  
      /**
       * Attempts to connect to the transport (if needed) and returns it.
       *
       * <p>Synchronous version of {@link #connectAsync(TransportConnectionListener, String)}. The
       * same observations about state are valid here. Also, what was said about the {@link
       * IBackupTransport} parameter of {@link TransportConnectionListener} now apply to the return
       * value of this method.
       *
       * <p>This is a potentially blocking operation, so be sure to call this carefully on the correct
       * threads. You can't call this from the process main-thread (it throws an exception if you do
       * so).
       *
       * <p>In most cases only the first call to this method will block, the following calls should
       * return instantly. However, this is not guaranteed.
       *
       * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
       *     {@link #connectAsync(TransportConnectionListener, String)} for more details.
       * @return A {@link IBackupTransport} transport binder instance or null. If it's non-null it can
       *     still be unusable - throws {@link DeadObjectException} on method calls
       */
      @WorkerThread
      @Nullable
      public IBackupTransport connect(String caller) {
          // If called on the main-thread this could deadlock waiting because calls to
          // ServiceConnection are on the main-thread as well
          Preconditions.checkState(
                  !Looper.getMainLooper().isCurrentThread(), "Can't call connect() on main thread");
  
          IBackupTransport transport = mTransport;
          if (transport != null) {
              log(Priority.DEBUG, caller, "Sync connect: reusing transport");
              return transport;
          }
  
          // If it's already UNUSABLE we return straight away, no need to go to main-thread
          synchronized (mStateLock) {
              if (mState == State.UNUSABLE) {
                  log(Priority.WARN, caller, "Sync connect: UNUSABLE client");
                  return null;
              }
          }
  
          CompletableFuture<IBackupTransport> transportFuture = new CompletableFuture<>();
          TransportConnectionListener requestListener =
                  (requestedTransport, transportClient) ->
                          transportFuture.complete(requestedTransport);
  
          long requestTime = SystemClock.elapsedRealtime();
          log(Priority.DEBUG, caller, "Sync connect: calling async");
          connectAsync(requestListener, caller);
  
          try {
              transport = transportFuture.get();
              long time = SystemClock.elapsedRealtime() - requestTime;
              mTransportStats.registerConnectionTime(mTransportComponent, time);
              log(Priority.DEBUG, caller, String.format(Locale.US, "Connect took %d ms", time));
              return transport;
          } catch (InterruptedException | ExecutionException e) {
              String error = e.getClass().getSimpleName();
              log(Priority.ERROR, caller, error + " while waiting for transport: " + e.getMessage());
              return null;
          }
      }
  
      /**
       * Tries to connect to the transport, if it fails throws {@link TransportNotAvailableException}.
       *
       * <p>Same as {@link #connect(String)} except it throws instead of returning null.
       *
       * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
       *     {@link #connectAsync(TransportConnectionListener, String)} for more details.
       * @return A {@link IBackupTransport} transport binder instance.
       * @see #connect(String)
       * @throws TransportNotAvailableException if connection attempt fails.
       */
      @WorkerThread
      public IBackupTransport connectOrThrow(String caller) throws TransportNotAvailableException {
          IBackupTransport transport = connect(caller);
          if (transport == null) {
              log(Priority.ERROR, caller, "Transport connection failed");
              throw new TransportNotAvailableException();
          }
          return transport;
      }
  
      /**
       * If the {@link TransportClient} is already connected to the transport, returns the transport,
       * otherwise throws {@link TransportNotAvailableException}.
       *
       * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
       *     {@link #connectAsync(TransportConnectionListener, String)} for more details.
       * @return A {@link IBackupTransport} transport binder instance.
       * @throws TransportNotAvailableException if not connected.
       */
      public IBackupTransport getConnectedTransport(String caller)
              throws TransportNotAvailableException {
          IBackupTransport transport = mTransport;
          if (transport == null) {
              log(Priority.ERROR, caller, "Transport not connected");
              throw new TransportNotAvailableException();
          }
          return transport;
      }
  
      @Override
      public String toString() {
          return "TransportClient{"
                  + mTransportComponent.flattenToShortString()
                  + "#"
                  + mIdentifier
                  + "}";
      }
  
      @Override
      protected void finalize() throws Throwable {
          synchronized (mStateLock) {
              mCloseGuard.warnIfOpen();
              if (mState >= State.BOUND_AND_CONNECTING) {
                  String callerLogString = "TransportClient.finalize()";
                  log(
                          Priority.ERROR,
                          callerLogString,
                          "Dangling TransportClient created in [" + mCreatorLogString + "] being "
                                  + "GC'ed. Left bound, unbinding...");
                  try {
                      unbind(callerLogString);
                  } catch (IllegalStateException e) {
                      // May throw because there may be a race between this method being called and
                      // the framework calling any method on the connection with the weak reference
                      // there already cleared. In this case the connection will unbind before this
                      // is called. This is fine.
                  }
              }
          }
      }
  
      private void onServiceConnected(IBinder binder) {
          IBackupTransport transport = IBackupTransport.Stub.asInterface(binder);
          synchronized (mStateLock) {
              checkStateIntegrityLocked();
  
              if (mState != State.UNUSABLE) {
                  log(Priority.DEBUG, "Transport connected");
                  setStateLocked(State.CONNECTED, transport);
                  notifyListenersAndClearLocked(transport);
              }
          }
      }
  
      /**
       * If we are called here the TransportClient becomes UNUSABLE. After one of these calls, if a
       * binding happen again the new service can be a different instance. Since transports are
       * stateful, we don't want a new instance responding for an old instance's state.
       */
      private void onServiceDisconnected() {
          synchronized (mStateLock) {
              log(Priority.ERROR, "Service disconnected: client UNUSABLE");
              setStateLocked(State.UNUSABLE, null);
              try {
                  // After unbindService() no calls back to mConnection
                  mContext.unbindService(mConnection);
              } catch (IllegalArgumentException e) {
                  // TODO: Investigate why this is happening
                  // We're UNUSABLE, so any calls to mConnection will be no-op, so it's safe to
                  // swallow this one
                  log(
                          Priority.WARN,
                          "Exception trying to unbind onServiceDisconnected(): " + e.getMessage());
              }
          }
      }
  
      /**
       * If we are called here the TransportClient becomes UNUSABLE for the same reason as in {@link
       * #onServiceDisconnected()}.
       */
      private void onBindingDied() {
          synchronized (mStateLock) {
              checkStateIntegrityLocked();
  
              log(Priority.ERROR, "Binding died: client UNUSABLE");
              // After unbindService() no calls back to mConnection
              switch (mState) {
                  case State.UNUSABLE:
                      break;
                  case State.IDLE:
                      log(Priority.ERROR, "Unexpected state transition IDLE => UNUSABLE");
                      setStateLocked(State.UNUSABLE, null);
                      break;
                  case State.BOUND_AND_CONNECTING:
                      setStateLocked(State.UNUSABLE, null);
                      mContext.unbindService(mConnection);
                      notifyListenersAndClearLocked(null);
                      break;
                  case State.CONNECTED:
                      setStateLocked(State.UNUSABLE, null);
                      mContext.unbindService(mConnection);
                      break;
              }
          }
      }
  
      private void notifyListener(
              TransportConnectionListener listener,
              @Nullable IBackupTransport transport,
              String caller) {
          String transportString = (transport != null) ? "IBackupTransport" : "null";
          log(Priority.INFO, "Notifying [" + caller + "] transport = " + transportString);
          mListenerHandler.post(() -> listener.onTransportConnectionResult(transport, this));
      }
  
      @GuardedBy("mStateLock")
      private void notifyListenersAndClearLocked(@Nullable IBackupTransport transport) {
          for (Map.Entry<TransportConnectionListener, String> entry : mListeners.entrySet()) {
              TransportConnectionListener listener = entry.getKey();
              String caller = entry.getValue();
              notifyListener(listener, transport, caller);
          }
          mListeners.clear();
      }
  
      @GuardedBy("mStateLock")
      private void setStateLocked(@State int state, @Nullable IBackupTransport transport) {
          log(Priority.VERBOSE, "State: " + stateToString(mState) + " => " + stateToString(state));
          onStateTransition(mState, state);
          mState = state;
          mTransport = transport;
      }
  
      private void onStateTransition(int oldState, int newState) {
          String transport = mTransportComponent.flattenToShortString();
          int bound = transitionThroughState(oldState, newState, State.BOUND_AND_CONNECTING);
          int connected = transitionThroughState(oldState, newState, State.CONNECTED);
          if (bound != Transition.NO_TRANSITION) {
              int value = (bound == Transition.UP) ? 1 : 0; // 1 is bound, 0 is not bound
              EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, transport, value);
          }
          if (connected != Transition.NO_TRANSITION) {
              int value = (connected == Transition.UP) ? 1 : 0; // 1 is connected, 0 is not connected
              EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_CONNECTION, transport, value);
          }
      }
  
      /**
       * Returns:
       *
       * <ul>
       *   <li>{@link Transition#UP}, if oldState < stateReference <= newState
       *   <li>{@link Transition#DOWN}, if oldState >= stateReference > newState
       *   <li>{@link Transition#NO_TRANSITION}, otherwise
       */
      @Transition
      private int transitionThroughState(
              @State int oldState, @State int newState, @State int stateReference) {
          if (oldState < stateReference && stateReference <= newState) {
              return Transition.UP;
          }
          if (oldState >= stateReference && stateReference > newState) {
              return Transition.DOWN;
          }
          return Transition.NO_TRANSITION;
      }
  
      @GuardedBy("mStateLock")
      private void checkStateIntegrityLocked() {
          switch (mState) {
              case State.UNUSABLE:
                  checkState(mListeners.isEmpty(), "Unexpected listeners when state = UNUSABLE");
                  checkState(
                          mTransport == null, "Transport expected to be null when state = UNUSABLE");
              case State.IDLE:
                  checkState(mListeners.isEmpty(), "Unexpected listeners when state = IDLE");
                  checkState(mTransport == null, "Transport expected to be null when state = IDLE");
                  break;
              case State.BOUND_AND_CONNECTING:
                  checkState(
                          mTransport == null,
                          "Transport expected to be null when state = BOUND_AND_CONNECTING");
                  break;
              case State.CONNECTED:
                  checkState(mListeners.isEmpty(), "Unexpected listeners when state = CONNECTED");
                  checkState(
                          mTransport != null,
                          "Transport expected to be non-null when state = CONNECTED");
                  break;
              default:
                  checkState(false, "Unexpected state = " + stateToString(mState));
          }
      }
  
      private void checkState(boolean assertion, String message) {
          if (!assertion) {
              log(Priority.ERROR, message);
          }
      }
  
      private String stateToString(@State int state) {
          switch (state) {
              case State.UNUSABLE:
                  return "UNUSABLE";
              case State.IDLE:
                  return "IDLE";
              case State.BOUND_AND_CONNECTING:
                  return "BOUND_AND_CONNECTING";
              case State.CONNECTED:
                  return "CONNECTED";
              default:
                  return "<UNKNOWN = " + state + ">";
          }
      }
  
      private void log(int priority, String message) {
          TransportUtils.log(priority, TAG, formatMessage(mPrefixForLog, null, message));
          saveLogEntry(formatMessage(null, null, message));
      }
  
      private void log(int priority, String caller, String message) {
          TransportUtils.log(priority, TAG, formatMessage(mPrefixForLog, caller, message));
          saveLogEntry(formatMessage(null, caller, message));
      }
  
      private void saveLogEntry(String message) {
          CharSequence time = DateFormat.format("yyyy-MM-dd HH:mm:ss", System.currentTimeMillis());
          message = time + " " + message;
          synchronized (mLogBufferLock) {
              if (mLogBuffer.size() == LOG_BUFFER_SIZE) {
                  mLogBuffer.remove(mLogBuffer.size() - 1);
              }
              mLogBuffer.add(0, message);
          }
      }
  
      List<String> getLogBuffer() {
          synchronized (mLogBufferLock) {
              return Collections.unmodifiableList(mLogBuffer);
          }
      }
  
      @IntDef({Transition.DOWN, Transition.NO_TRANSITION, Transition.UP})
      @Retention(RetentionPolicy.SOURCE)
      private @interface Transition {
          int DOWN = -1;
          int NO_TRANSITION = 0;
          int UP = 1;
      }
  
      @IntDef({State.UNUSABLE, State.IDLE, State.BOUND_AND_CONNECTING, State.CONNECTED})
      @Retention(RetentionPolicy.SOURCE)
      private @interface State {
          // Constant values MUST be in order
          int UNUSABLE = 0;
          int IDLE = 1;
          int BOUND_AND_CONNECTING = 2;
          int CONNECTED = 3;
      }
  
      /**
       * This class is a proxy to TransportClient methods that doesn't hold a strong reference to the
       * TransportClient, allowing it to be GC'ed. If the reference was lost it logs a message.
       */
      private static class TransportConnection implements ServiceConnection {
          private final Context mContext;
          private final WeakReference<TransportClient> mTransportClientRef;
  
          private TransportConnection(Context context, TransportClient transportClient) {
              mContext = context;
              mTransportClientRef = new WeakReference<>(transportClient);
          }
  
          @Override
          public void onServiceConnected(ComponentName transportComponent, IBinder binder) {
              TransportClient transportClient = mTransportClientRef.get();
              if (transportClient == null) {
                  referenceLost("TransportConnection.onServiceConnected()");
                  return;
              }
              transportClient.onServiceConnected(binder);
          }
  
          @Override
          public void onServiceDisconnected(ComponentName transportComponent) {
              TransportClient transportClient = mTransportClientRef.get();
              if (transportClient == null) {
                  referenceLost("TransportConnection.onServiceDisconnected()");
                  return;
              }
              transportClient.onServiceDisconnected();
          }
  
          @Override
          public void onBindingDied(ComponentName transportComponent) {
              TransportClient transportClient = mTransportClientRef.get();
              if (transportClient == null) {
                  referenceLost("TransportConnection.onBindingDied()");
                  return;
              }
              transportClient.onBindingDied();
          }
  
          /** @see TransportClient#finalize() */
          private void referenceLost(String caller) {
              mContext.unbindService(this);
              TransportUtils.log(
                      Priority.INFO,
                      TAG,
                      caller + " called but TransportClient reference has been GC'ed");
          }
      }
  }
  