Unbundle RemoteService on TV - part 3

- New service TVRemoteService triggered by SystemServer
- Provider service proxy and watcher for maintaining connections to unbundled
  services which have the BIND_TV_REMOTE_SERVICE permission.
- Shared library to facilitate connections between unbundled service and
  TVRemoteService.
- Unbundled service needs TV_VIRTUAL_REMOTE_CONTROLLER
  permission to be fully functional.

b/23792608

Change-Id: Ief5c6995883d1f7268a73bdd0c920c4c3f42cddb
diff --git a/services/core/java/com/android/server/tv/TvRemoteProviderProxy.java b/services/core/java/com/android/server/tv/TvRemoteProviderProxy.java
new file mode 100644
index 0000000..557e9c8
--- /dev/null
+++ b/services/core/java/com/android/server/tv/TvRemoteProviderProxy.java
@@ -0,0 +1,647 @@
+/*
+ * 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);
+            }
+        }
+    }
+}