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.tv;
  
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.ServiceConnection;
  import android.media.tv.ITvRemoteProvider;
  import android.media.tv.ITvRemoteServiceInput;
  import android.os.Binder;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.RemoteException;
  import android.os.UserHandle;
  import android.util.Log;
  import android.util.Slog;
  
  import java.io.PrintWriter;
  import java.lang.ref.WeakReference;
  
  /**
   * Maintains a connection to a tv remote provider service.
   */
  final class TvRemoteProviderProxy implements ServiceConnection {
      private static final String TAG = "TvRemoteProvProxy";  // max. 23 chars
      private static final boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
      private static final boolean DEBUG_KEY = false;
  
  
      // This should match TvRemoteProvider.ACTION_TV_REMOTE_PROVIDER
      protected static final String SERVICE_INTERFACE =
              "com.android.media.tv.remoteprovider.TvRemoteProvider";
      private final Context mContext;
      private final ComponentName mComponentName;
      private final int mUserId;
      private final int mUid;
      private final Handler mHandler;
  
      /**
       * State guarded by mLock.
       *  This is the first lock in sequence for an incoming call.
       *  The second lock is always {@link TvRemoteService#mLock}
       *
       *  There are currently no methods that break this sequence.
       */
      private final Object mLock = new Object();
  
      private ProviderMethods mProviderMethods;
      // Connection state
      private boolean mRunning;
      private boolean mBound;
      private Connection mActiveConnection;
      private boolean mConnectionReady;
  
      public TvRemoteProviderProxy(Context context, ComponentName componentName, int userId,
                                   int uid) {
          mContext = context;
          mComponentName = componentName;
          mUserId = userId;
          mUid = uid;
          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);
      }
  
      public void setProviderSink(ProviderMethods provider) {
          mProviderMethods = provider;
      }
  
      public boolean hasComponentName(String packageName, String className) {
          return mComponentName.getPackageName().equals(packageName)
                  && mComponentName.getClassName().equals(className);
      }
  
      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() {
          synchronized (mLock) {
              if (mActiveConnection == null && shouldBind()) {
                  unbind();
                  bind();
              }
          }
      }
  
      private void updateBinding() {
          if (shouldBind()) {
              bind();
          } else {
              unbind();
          }
      }
  
      private boolean shouldBind() {
          return mRunning;
      }
  
      private void bind() {
          if (!mBound) {
              if (DEBUG) {
                  Slog.d(TAG, this + ": Binding");
              }
  
              Intent service = new Intent(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 + ": onServiceConnected()");
          }
  
          if (mBound) {
              disconnect();
  
              ITvRemoteProvider provider = ITvRemoteProvider.Stub.asInterface(service);
              if (provider != null) {
                  Connection connection = new Connection(provider);
                  if (connection.register()) {
                      synchronized (mLock) {
                          mActiveConnection = connection;
                      }
                      if (DEBUG) {
                          Slog.d(TAG, this + ": Connected successfully.");
                      }
                  } else {
                      if (DEBUG) {
                          Slog.d(TAG, this + ": Registration failed");
                      }
                  }
              } else {
                  Slog.e(TAG, this + ": Service returned invalid remote-control provider binder");
              }
          }
      }
  
      @Override
      public void onServiceDisconnected(ComponentName name) {
          if (DEBUG) Slog.d(TAG, this + ": Service disconnected");
          disconnect();
      }
  
  
      private void onConnectionReady(Connection connection) {
          synchronized (mLock) {
              if (DEBUG) Slog.d(TAG, "onConnectionReady");
              if (mActiveConnection == connection) {
                  if (DEBUG) Slog.d(TAG, "mConnectionReady = true");
                  mConnectionReady = true;
              }
          }
      }
  
      private void onConnectionDied(Connection connection) {
          if (mActiveConnection == connection) {
              if (DEBUG) Slog.d(TAG, this + ": Service connection died");
              disconnect();
          }
      }
  
      private void disconnect() {
          synchronized (mLock) {
              if (mActiveConnection != null) {
                  mConnectionReady = false;
                  mActiveConnection.dispose();
                  mActiveConnection = null;
              }
          }
      }
  
      // Provider helpers
      public void inputBridgeConnected(IBinder token) {
          synchronized (mLock) {
              if (DEBUG) Slog.d(TAG, this + ": inputBridgeConnected token: " + token);
              if (mConnectionReady) {
                  mActiveConnection.onInputBridgeConnected(token);
              }
          }
      }
  
      public interface ProviderMethods {
          // InputBridge
          void openInputBridge(TvRemoteProviderProxy provider, IBinder token, String name,
                               int width, int height, int maxPointers);
  
          void closeInputBridge(TvRemoteProviderProxy provider, IBinder token);
  
          void clearInputBridge(TvRemoteProviderProxy provider, IBinder token);
  
          void sendTimeStamp(TvRemoteProviderProxy provider, IBinder token, long timestamp);
  
          void sendKeyDown(TvRemoteProviderProxy provider, IBinder token, int keyCode);
  
          void sendKeyUp(TvRemoteProviderProxy provider, IBinder token, int keyCode);
  
          void sendPointerDown(TvRemoteProviderProxy provider, IBinder token, int pointerId, int x,
                               int y);
  
          void sendPointerUp(TvRemoteProviderProxy provider, IBinder token, int pointerId);
  
          void sendPointerSync(TvRemoteProviderProxy provider, IBinder token);
      }
  
      private final class Connection implements IBinder.DeathRecipient {
          private final ITvRemoteProvider mTvRemoteProvider;
          private final RemoteServiceInputProvider mServiceInputProvider;
  
          public Connection(ITvRemoteProvider provider) {
              mTvRemoteProvider = provider;
              mServiceInputProvider = new RemoteServiceInputProvider(this);
          }
  
          public boolean register() {
              if (DEBUG) Slog.d(TAG, "Connection::register()");
              try {
                  mTvRemoteProvider.asBinder().linkToDeath(this, 0);
                  mTvRemoteProvider.setRemoteServiceInputSink(mServiceInputProvider);
                  mHandler.post(new Runnable() {
                      @Override
                      public void run() {
                          onConnectionReady(Connection.this);
                      }
                  });
                  return true;
              } catch (RemoteException ex) {
                  binderDied();
              }
              return false;
          }
  
          public void dispose() {
              if (DEBUG) Slog.d(TAG, "Connection::dispose()");
              mTvRemoteProvider.asBinder().unlinkToDeath(this, 0);
              mServiceInputProvider.dispose();
          }
  
  
          public void onInputBridgeConnected(IBinder token) {
              if (DEBUG) Slog.d(TAG, this + ": onInputBridgeConnected");
              try {
                  mTvRemoteProvider.onInputBridgeConnected(token);
              } catch (RemoteException ex) {
                  Slog.e(TAG, "Failed to deliver onInputBridgeConnected. ", ex);
              }
          }
  
          @Override
          public void binderDied() {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      onConnectionDied(Connection.this);
                  }
              });
          }
  
          void openInputBridge(final IBinder token, final String name, final int width,
                               final int height, final int maxPointers) {
              synchronized (mLock) {
                  if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
                      if (DEBUG) {
                          Slog.d(TAG, this + ": openInputBridge," +
                                  " token=" + token + ", name=" + name);
                      }
                      final long idToken = Binder.clearCallingIdentity();
                      try {
                          if (mProviderMethods != null) {
                              mProviderMethods.openInputBridge(TvRemoteProviderProxy.this, token,
                                      name, width, height, maxPointers);
                          }
                      } finally {
                          Binder.restoreCallingIdentity(idToken);
                      }
                  } else {
                      if (DEBUG) {
                          Slog.w(TAG,
                                  "openInputBridge, Invalid connection or incorrect uid: " + Binder
                                          .getCallingUid());
                      }
                  }
              }
          }
  
          void closeInputBridge(final IBinder token) {
              synchronized (mLock) {
                  if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
                      if (DEBUG) {
                          Slog.d(TAG, this + ": closeInputBridge," +
                                  " token=" + token);
                      }
                      final long idToken = Binder.clearCallingIdentity();
                      try {
                          if (mProviderMethods != null) {
                              mProviderMethods.closeInputBridge(TvRemoteProviderProxy.this, token);
                          }
                      } finally {
                          Binder.restoreCallingIdentity(idToken);
                      }
                  } else {
                      if (DEBUG) {
                          Slog.w(TAG,
                                  "closeInputBridge, Invalid connection or incorrect uid: " +
                                          Binder.getCallingUid());
                      }
                  }
              }
          }
  
          void clearInputBridge(final IBinder token) {
              synchronized (mLock) {
                  if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
                      if (DEBUG) {
                          Slog.d(TAG, this + ": clearInputBridge," +
                                  " token=" + token);
                      }
                      final long idToken = Binder.clearCallingIdentity();
                      try {
                          if (mProviderMethods != null) {
                              mProviderMethods.clearInputBridge(TvRemoteProviderProxy.this, token);
                          }
                      } finally {
                          Binder.restoreCallingIdentity(idToken);
                      }
                  } else {
                      if (DEBUG) {
                          Slog.w(TAG,
                                  "clearInputBridge, Invalid connection or incorrect uid: " +
                                          Binder.getCallingUid());
                      }
                  }
              }
          }
  
          void sendTimestamp(final IBinder token, final long timestamp) {
              synchronized (mLock) {
                  if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
                      final long idToken = Binder.clearCallingIdentity();
                      try {
                          if (mProviderMethods != null) {
                              mProviderMethods.sendTimeStamp(TvRemoteProviderProxy.this, token,
                                      timestamp);
                          }
                      } finally {
                          Binder.restoreCallingIdentity(idToken);
                      }
                  } else {
                      if (DEBUG) {
                          Slog.w(TAG,
                                  "sendTimeStamp, Invalid connection or incorrect uid: " + Binder
                                          .getCallingUid());
                      }
                  }
              }
          }
  
          void sendKeyDown(final IBinder token, final int keyCode) {
              synchronized (mLock) {
                  if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
                      if (DEBUG_KEY) {
                          Slog.d(TAG, this + ": sendKeyDown," +
                                  " token=" + token + ", keyCode=" + keyCode);
                      }
                      final long idToken = Binder.clearCallingIdentity();
                      try {
                          if (mProviderMethods != null) {
                              mProviderMethods.sendKeyDown(TvRemoteProviderProxy.this, token,
                                      keyCode);
                          }
                      } finally {
                          Binder.restoreCallingIdentity(idToken);
                      }
                  } else {
                      if (DEBUG) {
                          Slog.w(TAG,
                                  "sendKeyDown, Invalid connection or incorrect uid: " + Binder
                                          .getCallingUid());
                      }
                  }
              }
          }
  
          void sendKeyUp(final IBinder token, final int keyCode) {
              synchronized (mLock) {
                  if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
                      if (DEBUG_KEY) {
                          Slog.d(TAG, this + ": sendKeyUp," +
                                  " token=" + token + ", keyCode=" + keyCode);
                      }
                      final long idToken = Binder.clearCallingIdentity();
                      try {
                          if (mProviderMethods != null) {
                              mProviderMethods.sendKeyUp(TvRemoteProviderProxy.this, token, keyCode);
                          }
                      } finally {
                          Binder.restoreCallingIdentity(idToken);
                      }
                  } else {
                      if (DEBUG) {
                          Slog.w(TAG,
                                  "sendKeyUp, Invalid connection or incorrect uid: " + Binder
                                          .getCallingUid());
                      }
                  }
              }
          }
  
          void sendPointerDown(final IBinder token, final int pointerId, final int x, final int y) {
              synchronized (mLock) {
                  if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
                      if (DEBUG_KEY) {
                          Slog.d(TAG, this + ": sendPointerDown," +
                                  " token=" + token + ", pointerId=" + pointerId);
                      }
                      final long idToken = Binder.clearCallingIdentity();
                      try {
                          if (mProviderMethods != null) {
                              mProviderMethods.sendPointerDown(TvRemoteProviderProxy.this, token,
                                      pointerId, x, y);
                          }
                      } finally {
                          Binder.restoreCallingIdentity(idToken);
                      }
                  } else {
                      if (DEBUG) {
                          Slog.w(TAG,
                                  "sendPointerDown, Invalid connection or incorrect uid: " + Binder
                                          .getCallingUid());
                      }
                  }
              }
          }
  
          void sendPointerUp(final IBinder token, final int pointerId) {
              synchronized (mLock) {
                  if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
                      if (DEBUG_KEY) {
                          Slog.d(TAG, this + ": sendPointerUp," +
                                  " token=" + token + ", pointerId=" + pointerId);
                      }
                      final long idToken = Binder.clearCallingIdentity();
                      try {
                          if (mProviderMethods != null) {
                              mProviderMethods.sendPointerUp(TvRemoteProviderProxy.this, token,
                                      pointerId);
                          }
                      } finally {
                          Binder.restoreCallingIdentity(idToken);
                      }
                  } else {
                      if (DEBUG) {
                          Slog.w(TAG,
                                  "sendPointerUp, Invalid connection or incorrect uid: " + Binder
                                          .getCallingUid());
                      }
                  }
              }
          }
  
          void sendPointerSync(final IBinder token) {
              synchronized (mLock) {
                  if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
                      if (DEBUG_KEY) {
                          Slog.d(TAG, this + ": sendPointerSync," +
                                  " token=" + token);
                      }
                      final long idToken = Binder.clearCallingIdentity();
                      try {
                          if (mProviderMethods != null) {
                              mProviderMethods.sendPointerSync(TvRemoteProviderProxy.this, token);
                          }
                      } finally {
                          Binder.restoreCallingIdentity(idToken);
                      }
                  } else {
                      if (DEBUG) {
                          Slog.w(TAG,
                                  "sendPointerSync, Invalid connection or incorrect uid: " + Binder
                                          .getCallingUid());
                      }
                  }
              }
          }
      }
  
      /**
       * Receives events from the connected provider.
       * <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 RemoteServiceInputProvider extends ITvRemoteServiceInput.Stub {
          private final WeakReference<Connection> mConnectionRef;
  
          public RemoteServiceInputProvider(Connection connection) {
              mConnectionRef = new WeakReference<Connection>(connection);
          }
  
          public void dispose() {
              // Terminate the connection.
              mConnectionRef.clear();
          }
  
          @Override
          public void openInputBridge(IBinder token, String name, int width,
                                      int height, int maxPointers) throws RemoteException {
              Connection connection = mConnectionRef.get();
              if (connection != null) {
                  connection.openInputBridge(token, name, width, height, maxPointers);
              }
          }
  
          @Override
          public void closeInputBridge(IBinder token) throws RemoteException {
              Connection connection = mConnectionRef.get();
              if (connection != null) {
                  connection.closeInputBridge(token);
              }
          }
  
          @Override
          public void clearInputBridge(IBinder token) throws RemoteException {
              Connection connection = mConnectionRef.get();
              if (connection != null) {
                  connection.clearInputBridge(token);
              }
          }
  
          @Override
          public void sendTimestamp(IBinder token, long timestamp) throws RemoteException {
              Connection connection = mConnectionRef.get();
              if (connection != null) {
                  connection.sendTimestamp(token, timestamp);
              }
          }
  
          @Override
          public void sendKeyDown(IBinder token, int keyCode) throws RemoteException {
              Connection connection = mConnectionRef.get();
              if (connection != null) {
                  connection.sendKeyDown(token, keyCode);
              }
          }
  
          @Override
          public void sendKeyUp(IBinder token, int keyCode) throws RemoteException {
              Connection connection = mConnectionRef.get();
              if (connection != null) {
                  connection.sendKeyUp(token, keyCode);
              }
          }
  
          @Override
          public void sendPointerDown(IBinder token, int pointerId, int x, int y)
                  throws RemoteException {
              Connection connection = mConnectionRef.get();
              if (connection != null) {
                  connection.sendPointerDown(token, pointerId, x, y);
              }
          }
  
          @Override
          public void sendPointerUp(IBinder token, int pointerId) throws RemoteException {
              Connection connection = mConnectionRef.get();
              if (connection != null) {
                  connection.sendPointerUp(token, pointerId);
              }
          }
  
          @Override
          public void sendPointerSync(IBinder token) throws RemoteException {
              Connection connection = mConnectionRef.get();
              if (connection != null) {
                  connection.sendPointerSync(token);
              }
          }
      }
  }
  