1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.media;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.ServiceConnection;
23import android.media.IRemoteDisplayCallback;
24import android.media.IRemoteDisplayProvider;
25import android.media.RemoteDisplayState;
26import android.os.Handler;
27import android.os.IBinder;
28import android.os.RemoteException;
29import android.os.IBinder.DeathRecipient;
30import android.os.UserHandle;
31import android.util.Log;
32import android.util.Slog;
33
34import java.io.PrintWriter;
35import java.lang.ref.WeakReference;
36import java.util.Objects;
37
38/**
39 * Maintains a connection to a particular remote display provider service.
40 */
41final class RemoteDisplayProviderProxy implements ServiceConnection {
42    private static final String TAG = "RemoteDisplayProvider";  // max. 23 chars
43    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
44
45    private final Context mContext;
46    private final ComponentName mComponentName;
47    private final int mUserId;
48    private final Handler mHandler;
49
50    private Callback mDisplayStateCallback;
51
52    // Connection state
53    private boolean mRunning;
54    private boolean mBound;
55    private Connection mActiveConnection;
56    private boolean mConnectionReady;
57
58    // Logical state
59    private int mDiscoveryMode;
60    private String mSelectedDisplayId;
61    private RemoteDisplayState mDisplayState;
62    private boolean mScheduledDisplayStateChangedCallback;
63
64    public RemoteDisplayProviderProxy(Context context, ComponentName componentName,
65            int userId) {
66        mContext = context;
67        mComponentName = componentName;
68        mUserId = userId;
69        mHandler = new Handler();
70    }
71
72    public void dump(PrintWriter pw, String prefix) {
73        pw.println(prefix + "Proxy");
74        pw.println(prefix + "  mUserId=" + mUserId);
75        pw.println(prefix + "  mRunning=" + mRunning);
76        pw.println(prefix + "  mBound=" + mBound);
77        pw.println(prefix + "  mActiveConnection=" + mActiveConnection);
78        pw.println(prefix + "  mConnectionReady=" + mConnectionReady);
79        pw.println(prefix + "  mDiscoveryMode=" + mDiscoveryMode);
80        pw.println(prefix + "  mSelectedDisplayId=" + mSelectedDisplayId);
81        pw.println(prefix + "  mDisplayState=" + mDisplayState);
82    }
83
84    public void setCallback(Callback callback) {
85        mDisplayStateCallback = callback;
86    }
87
88    public RemoteDisplayState getDisplayState() {
89        return mDisplayState;
90    }
91
92    public void setDiscoveryMode(int mode) {
93        if (mDiscoveryMode != mode) {
94            mDiscoveryMode = mode;
95            if (mConnectionReady) {
96                mActiveConnection.setDiscoveryMode(mode);
97            }
98            updateBinding();
99        }
100    }
101
102    public void setSelectedDisplay(String id) {
103        if (!Objects.equals(mSelectedDisplayId, id)) {
104            if (mConnectionReady && mSelectedDisplayId != null) {
105                mActiveConnection.disconnect(mSelectedDisplayId);
106            }
107            mSelectedDisplayId = id;
108            if (mConnectionReady && id != null) {
109                mActiveConnection.connect(id);
110            }
111            updateBinding();
112        }
113    }
114
115    public void setDisplayVolume(int volume) {
116        if (mConnectionReady && mSelectedDisplayId != null) {
117            mActiveConnection.setVolume(mSelectedDisplayId, volume);
118        }
119    }
120
121    public void adjustDisplayVolume(int delta) {
122        if (mConnectionReady && mSelectedDisplayId != null) {
123            mActiveConnection.adjustVolume(mSelectedDisplayId, delta);
124        }
125    }
126
127    public boolean hasComponentName(String packageName, String className) {
128        return mComponentName.getPackageName().equals(packageName)
129                && mComponentName.getClassName().equals(className);
130    }
131
132    public String getFlattenedComponentName() {
133        return mComponentName.flattenToShortString();
134    }
135
136    public void start() {
137        if (!mRunning) {
138            if (DEBUG) {
139                Slog.d(TAG, this + ": Starting");
140            }
141
142            mRunning = true;
143            updateBinding();
144        }
145    }
146
147    public void stop() {
148        if (mRunning) {
149            if (DEBUG) {
150                Slog.d(TAG, this + ": Stopping");
151            }
152
153            mRunning = false;
154            updateBinding();
155        }
156    }
157
158    public void rebindIfDisconnected() {
159        if (mActiveConnection == null && shouldBind()) {
160            unbind();
161            bind();
162        }
163    }
164
165    private void updateBinding() {
166        if (shouldBind()) {
167            bind();
168        } else {
169            unbind();
170        }
171    }
172
173    private boolean shouldBind() {
174        if (mRunning) {
175            // Bind whenever there is a discovery request or selected display.
176            if (mDiscoveryMode != RemoteDisplayState.DISCOVERY_MODE_NONE
177                    || mSelectedDisplayId != null) {
178                return true;
179            }
180        }
181        return false;
182    }
183
184    private void bind() {
185        if (!mBound) {
186            if (DEBUG) {
187                Slog.d(TAG, this + ": Binding");
188            }
189
190            Intent service = new Intent(RemoteDisplayState.SERVICE_INTERFACE);
191            service.setComponent(mComponentName);
192            try {
193                mBound = mContext.bindServiceAsUser(service, this, Context.BIND_AUTO_CREATE,
194                        new UserHandle(mUserId));
195                if (!mBound && DEBUG) {
196                    Slog.d(TAG, this + ": Bind failed");
197                }
198            } catch (SecurityException ex) {
199                if (DEBUG) {
200                    Slog.d(TAG, this + ": Bind failed", ex);
201                }
202            }
203        }
204    }
205
206    private void unbind() {
207        if (mBound) {
208            if (DEBUG) {
209                Slog.d(TAG, this + ": Unbinding");
210            }
211
212            mBound = false;
213            disconnect();
214            mContext.unbindService(this);
215        }
216    }
217
218    @Override
219    public void onServiceConnected(ComponentName name, IBinder service) {
220        if (DEBUG) {
221            Slog.d(TAG, this + ": Connected");
222        }
223
224        if (mBound) {
225            disconnect();
226
227            IRemoteDisplayProvider provider = IRemoteDisplayProvider.Stub.asInterface(service);
228            if (provider != null) {
229                Connection connection = new Connection(provider);
230                if (connection.register()) {
231                    mActiveConnection = connection;
232                } else {
233                    if (DEBUG) {
234                        Slog.d(TAG, this + ": Registration failed");
235                    }
236                }
237            } else {
238                Slog.e(TAG, this + ": Service returned invalid remote display provider binder");
239            }
240        }
241    }
242
243    @Override
244    public void onServiceDisconnected(ComponentName name) {
245        if (DEBUG) {
246            Slog.d(TAG, this + ": Service disconnected");
247        }
248        disconnect();
249    }
250
251    private void onConnectionReady(Connection connection) {
252        if (mActiveConnection == connection) {
253            mConnectionReady = true;
254
255            if (mDiscoveryMode != RemoteDisplayState.DISCOVERY_MODE_NONE) {
256                mActiveConnection.setDiscoveryMode(mDiscoveryMode);
257            }
258            if (mSelectedDisplayId != null) {
259                mActiveConnection.connect(mSelectedDisplayId);
260            }
261        }
262    }
263
264    private void onConnectionDied(Connection connection) {
265        if (mActiveConnection == connection) {
266            if (DEBUG) {
267                Slog.d(TAG, this + ": Service connection died");
268            }
269            disconnect();
270        }
271    }
272
273    private void onDisplayStateChanged(Connection connection, RemoteDisplayState state) {
274        if (mActiveConnection == connection) {
275            if (DEBUG) {
276                Slog.d(TAG, this + ": State changed, state=" + state);
277            }
278            setDisplayState(state);
279        }
280    }
281
282    private void disconnect() {
283        if (mActiveConnection != null) {
284            if (mSelectedDisplayId != null) {
285                mActiveConnection.disconnect(mSelectedDisplayId);
286            }
287            mConnectionReady = false;
288            mActiveConnection.dispose();
289            mActiveConnection = null;
290            setDisplayState(null);
291        }
292    }
293
294    private void setDisplayState(RemoteDisplayState state) {
295        if (!Objects.equals(mDisplayState, state)) {
296            mDisplayState = state;
297            if (!mScheduledDisplayStateChangedCallback) {
298                mScheduledDisplayStateChangedCallback = true;
299                mHandler.post(mDisplayStateChanged);
300            }
301        }
302    }
303
304    @Override
305    public String toString() {
306        return "Service connection " + mComponentName.flattenToShortString();
307    }
308
309    private final Runnable mDisplayStateChanged = new Runnable() {
310        @Override
311        public void run() {
312            mScheduledDisplayStateChangedCallback = false;
313            if (mDisplayStateCallback != null) {
314                mDisplayStateCallback.onDisplayStateChanged(
315                        RemoteDisplayProviderProxy.this, mDisplayState);
316            }
317        }
318    };
319
320    public interface Callback {
321        void onDisplayStateChanged(RemoteDisplayProviderProxy provider, RemoteDisplayState state);
322    }
323
324    private final class Connection implements DeathRecipient {
325        private final IRemoteDisplayProvider mProvider;
326        private final ProviderCallback mCallback;
327
328        public Connection(IRemoteDisplayProvider provider) {
329            mProvider = provider;
330            mCallback = new ProviderCallback(this);
331        }
332
333        public boolean register() {
334            try {
335                mProvider.asBinder().linkToDeath(this, 0);
336                mProvider.setCallback(mCallback);
337                mHandler.post(new Runnable() {
338                    @Override
339                    public void run() {
340                        onConnectionReady(Connection.this);
341                    }
342                });
343                return true;
344            } catch (RemoteException ex) {
345                binderDied();
346            }
347            return false;
348        }
349
350        public void dispose() {
351            mProvider.asBinder().unlinkToDeath(this, 0);
352            mCallback.dispose();
353        }
354
355        public void setDiscoveryMode(int mode) {
356            try {
357                mProvider.setDiscoveryMode(mode);
358            } catch (RemoteException ex) {
359                Slog.e(TAG, "Failed to deliver request to set discovery mode.", ex);
360            }
361        }
362
363        public void connect(String id) {
364            try {
365                mProvider.connect(id);
366            } catch (RemoteException ex) {
367                Slog.e(TAG, "Failed to deliver request to connect to display.", ex);
368            }
369        }
370
371        public void disconnect(String id) {
372            try {
373                mProvider.disconnect(id);
374            } catch (RemoteException ex) {
375                Slog.e(TAG, "Failed to deliver request to disconnect from display.", ex);
376            }
377        }
378
379        public void setVolume(String id, int volume) {
380            try {
381                mProvider.setVolume(id, volume);
382            } catch (RemoteException ex) {
383                Slog.e(TAG, "Failed to deliver request to set display volume.", ex);
384            }
385        }
386
387        public void adjustVolume(String id, int volume) {
388            try {
389                mProvider.adjustVolume(id, volume);
390            } catch (RemoteException ex) {
391                Slog.e(TAG, "Failed to deliver request to adjust display volume.", ex);
392            }
393        }
394
395        @Override
396        public void binderDied() {
397            mHandler.post(new Runnable() {
398                @Override
399                public void run() {
400                    onConnectionDied(Connection.this);
401                }
402            });
403        }
404
405        void postStateChanged(final RemoteDisplayState state) {
406            mHandler.post(new Runnable() {
407                @Override
408                public void run() {
409                    onDisplayStateChanged(Connection.this, state);
410                }
411            });
412        }
413    }
414
415    /**
416     * Receives callbacks from the service.
417     * <p>
418     * This inner class is static and only retains a weak reference to the connection
419     * to prevent the client from being leaked in case the service is holding an
420     * active reference to the client's callback.
421     * </p>
422     */
423    private static final class ProviderCallback extends IRemoteDisplayCallback.Stub {
424        private final WeakReference<Connection> mConnectionRef;
425
426        public ProviderCallback(Connection connection) {
427            mConnectionRef = new WeakReference<Connection>(connection);
428        }
429
430        public void dispose() {
431            mConnectionRef.clear();
432        }
433
434        @Override
435        public void onStateChanged(RemoteDisplayState state) throws RemoteException {
436            Connection connection = mConnectionRef.get();
437            if (connection != null) {
438                connection.postStateChanged(state);
439            }
440        }
441    }
442}
443