content: /*
   * Copyright (C) 2013 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.media;
  
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.ServiceConnection;
  import android.media.IRemoteDisplayCallback;
  import android.media.IRemoteDisplayProvider;
  import android.media.RemoteDisplayState;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.RemoteException;
  import android.os.IBinder.DeathRecipient;
  import android.os.UserHandle;
  import android.util.Log;
  import android.util.Slog;
  
  import java.io.PrintWriter;
  import java.lang.ref.WeakReference;
  import java.util.Objects;
  
  /**
   * Maintains a connection to a particular remote display provider service.
   */
  final class RemoteDisplayProviderProxy implements ServiceConnection {
      private static final String TAG = "RemoteDisplayProvider";  // max. 23 chars
      private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
  
      private final Context mContext;
      private final ComponentName mComponentName;
      private final int mUserId;
      private final Handler mHandler;
  
      private Callback mDisplayStateCallback;
  
      // Connection state
      private boolean mRunning;
      private boolean mBound;
      private Connection mActiveConnection;
      private boolean mConnectionReady;
  
      // Logical state
      private int mDiscoveryMode;
      private String mSelectedDisplayId;
      private RemoteDisplayState mDisplayState;
      private boolean mScheduledDisplayStateChangedCallback;
  
      public RemoteDisplayProviderProxy(Context context, ComponentName componentName,
              int userId) {
          mContext = context;
          mComponentName = componentName;
          mUserId = userId;
          mHandler = new Handler();
      }
  
      public void dump(PrintWriter pw, String prefix) {
          pw.println(prefix + "Proxy");
          pw.println(prefix + "  mUserId=" + mUserId);
          pw.println(prefix + "  mRunning=" + mRunning);
          pw.println(prefix + "  mBound=" + mBound);
          pw.println(prefix + "  mActiveConnection=" + mActiveConnection);
          pw.println(prefix + "  mConnectionReady=" + mConnectionReady);
          pw.println(prefix + "  mDiscoveryMode=" + mDiscoveryMode);
          pw.println(prefix + "  mSelectedDisplayId=" + mSelectedDisplayId);
          pw.println(prefix + "  mDisplayState=" + mDisplayState);
      }
  
      public void setCallback(Callback callback) {
          mDisplayStateCallback = callback;
      }
  
      public RemoteDisplayState getDisplayState() {
          return mDisplayState;
      }
  
      public void setDiscoveryMode(int mode) {
          if (mDiscoveryMode != mode) {
              mDiscoveryMode = mode;
              if (mConnectionReady) {
                  mActiveConnection.setDiscoveryMode(mode);
              }
              updateBinding();
          }
      }
  
      public void setSelectedDisplay(String id) {
          if (!Objects.equals(mSelectedDisplayId, id)) {
              if (mConnectionReady && mSelectedDisplayId != null) {
                  mActiveConnection.disconnect(mSelectedDisplayId);
              }
              mSelectedDisplayId = id;
              if (mConnectionReady && id != null) {
                  mActiveConnection.connect(id);
              }
              updateBinding();
          }
      }
  
      public void setDisplayVolume(int volume) {
          if (mConnectionReady && mSelectedDisplayId != null) {
              mActiveConnection.setVolume(mSelectedDisplayId, volume);
          }
      }
  
      public void adjustDisplayVolume(int delta) {
          if (mConnectionReady && mSelectedDisplayId != null) {
              mActiveConnection.adjustVolume(mSelectedDisplayId, delta);
          }
      }
  
      public boolean hasComponentName(String packageName, String className) {
          return mComponentName.getPackageName().equals(packageName)
                  && mComponentName.getClassName().equals(className);
      }
  
      public String getFlattenedComponentName() {
          return mComponentName.flattenToShortString();
      }
  
      public void start() {
          if (!mRunning) {
              if (DEBUG) {
                  Slog.d(TAG, this + ": Starting");
              }
  
              mRunning = true;
              updateBinding();
          }
      }
  
      public void stop() {
          if (mRunning) {
              if (DEBUG) {
                  Slog.d(TAG, this + ": Stopping");
              }
  
              mRunning = false;
              updateBinding();
          }
      }
  
      public void rebindIfDisconnected() {
          if (mActiveConnection == null && shouldBind()) {
              unbind();
              bind();
          }
      }
  
      private void updateBinding() {
          if (shouldBind()) {
              bind();
          } else {
              unbind();
          }
      }
  
      private boolean shouldBind() {
          if (mRunning) {
              // Bind whenever there is a discovery request or selected display.
              if (mDiscoveryMode != RemoteDisplayState.DISCOVERY_MODE_NONE
                      || mSelectedDisplayId != null) {
                  return true;
              }
          }
          return false;
      }
  
      private void bind() {
          if (!mBound) {
              if (DEBUG) {
                  Slog.d(TAG, this + ": Binding");
              }
  
              Intent service = new Intent(RemoteDisplayState.SERVICE_INTERFACE);
              service.setComponent(mComponentName);
              try {
                  mBound = mContext.bindServiceAsUser(service, this,
                          Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
                          new UserHandle(mUserId));
                  if (!mBound && DEBUG) {
                      Slog.d(TAG, this + ": Bind failed");
                  }
              } catch (SecurityException ex) {
                  if (DEBUG) {
                      Slog.d(TAG, this + ": Bind failed", ex);
                  }
              }
          }
      }
  
      private void unbind() {
          if (mBound) {
              if (DEBUG) {
                  Slog.d(TAG, this + ": Unbinding");
              }
  
              mBound = false;
              disconnect();
              mContext.unbindService(this);
          }
      }
  
      @Override
      public void onServiceConnected(ComponentName name, IBinder service) {
          if (DEBUG) {
              Slog.d(TAG, this + ": Connected");
          }
  
          if (mBound) {
              disconnect();
  
              IRemoteDisplayProvider provider = IRemoteDisplayProvider.Stub.asInterface(service);
              if (provider != null) {
                  Connection connection = new Connection(provider);
                  if (connection.register()) {
                      mActiveConnection = connection;
                  } else {
                      if (DEBUG) {
                          Slog.d(TAG, this + ": Registration failed");
                      }
                  }
              } else {
                  Slog.e(TAG, this + ": Service returned invalid remote display provider binder");
              }
          }
      }
  
      @Override
      public void onServiceDisconnected(ComponentName name) {
          if (DEBUG) {
              Slog.d(TAG, this + ": Service disconnected");
          }
          disconnect();
      }
  
      private void onConnectionReady(Connection connection) {
          if (mActiveConnection == connection) {
              mConnectionReady = true;
  
              if (mDiscoveryMode != RemoteDisplayState.DISCOVERY_MODE_NONE) {
                  mActiveConnection.setDiscoveryMode(mDiscoveryMode);
              }
              if (mSelectedDisplayId != null) {
                  mActiveConnection.connect(mSelectedDisplayId);
              }
          }
      }
  
      private void onConnectionDied(Connection connection) {
          if (mActiveConnection == connection) {
              if (DEBUG) {
                  Slog.d(TAG, this + ": Service connection died");
              }
              disconnect();
          }
      }
  
      private void onDisplayStateChanged(Connection connection, RemoteDisplayState state) {
          if (mActiveConnection == connection) {
              if (DEBUG) {
                  Slog.d(TAG, this + ": State changed, state=" + state);
              }
              setDisplayState(state);
          }
      }
  
      private void disconnect() {
          if (mActiveConnection != null) {
              if (mSelectedDisplayId != null) {
                  mActiveConnection.disconnect(mSelectedDisplayId);
              }
              mConnectionReady = false;
              mActiveConnection.dispose();
              mActiveConnection = null;
              setDisplayState(null);
          }
      }
  
      private void setDisplayState(RemoteDisplayState state) {
          if (!Objects.equals(mDisplayState, state)) {
              mDisplayState = state;
              if (!mScheduledDisplayStateChangedCallback) {
                  mScheduledDisplayStateChangedCallback = true;
                  mHandler.post(mDisplayStateChanged);
              }
          }
      }
  
      @Override
      public String toString() {
          return "Service connection " + mComponentName.flattenToShortString();
      }
  
      private final Runnable mDisplayStateChanged = new Runnable() {
          @Override
          public void run() {
              mScheduledDisplayStateChangedCallback = false;
              if (mDisplayStateCallback != null) {
                  mDisplayStateCallback.onDisplayStateChanged(
                          RemoteDisplayProviderProxy.this, mDisplayState);
              }
          }
      };
  
      public interface Callback {
          void onDisplayStateChanged(RemoteDisplayProviderProxy provider, RemoteDisplayState state);
      }
  
      private final class Connection implements DeathRecipient {
          private final IRemoteDisplayProvider mProvider;
          private final ProviderCallback mCallback;
  
          public Connection(IRemoteDisplayProvider provider) {
              mProvider = provider;
              mCallback = new ProviderCallback(this);
          }
  
          public boolean register() {
              try {
                  mProvider.asBinder().linkToDeath(this, 0);
                  mProvider.setCallback(mCallback);
                  mHandler.post(new Runnable() {
                      @Override
                      public void run() {
                          onConnectionReady(Connection.this);
                      }
                  });
                  return true;
              } catch (RemoteException ex) {
                  binderDied();
              }
              return false;
          }
  
          public void dispose() {
              mProvider.asBinder().unlinkToDeath(this, 0);
              mCallback.dispose();
          }
  
          public void setDiscoveryMode(int mode) {
              try {
                  mProvider.setDiscoveryMode(mode);
              } catch (RemoteException ex) {
                  Slog.e(TAG, "Failed to deliver request to set discovery mode.", ex);
              }
          }
  
          public void connect(String id) {
              try {
                  mProvider.connect(id);
              } catch (RemoteException ex) {
                  Slog.e(TAG, "Failed to deliver request to connect to display.", ex);
              }
          }
  
          public void disconnect(String id) {
              try {
                  mProvider.disconnect(id);
              } catch (RemoteException ex) {
                  Slog.e(TAG, "Failed to deliver request to disconnect from display.", ex);
              }
          }
  
          public void setVolume(String id, int volume) {
              try {
                  mProvider.setVolume(id, volume);
              } catch (RemoteException ex) {
                  Slog.e(TAG, "Failed to deliver request to set display volume.", ex);
              }
          }
  
          public void adjustVolume(String id, int volume) {
              try {
                  mProvider.adjustVolume(id, volume);
              } catch (RemoteException ex) {
                  Slog.e(TAG, "Failed to deliver request to adjust display volume.", ex);
              }
          }
  
          @Override
          public void binderDied() {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      onConnectionDied(Connection.this);
                  }
              });
          }
  
          void postStateChanged(final RemoteDisplayState state) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      onDisplayStateChanged(Connection.this, state);
                  }
              });
          }
      }
  
      /**
       * Receives callbacks from the service.
       * <p>
       * This inner class is static and only retains a weak reference to the connection
       * to prevent the client from being leaked in case the service is holding an
       * active reference to the client's callback.
       * </p>
       */
      private static final class ProviderCallback extends IRemoteDisplayCallback.Stub {
          private final WeakReference<Connection> mConnectionRef;
  
          public ProviderCallback(Connection connection) {
              mConnectionRef = new WeakReference<Connection>(connection);
          }
  
          public void dispose() {
              mConnectionRef.clear();
          }
  
          @Override
          public void onStateChanged(RemoteDisplayState state) throws RemoteException {
              Connection connection = mConnectionRef.get();
              if (connection != null) {
                  connection.postStateChanged(state);
              }
          }
      }
  }
  