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,
194                        Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
195                        new UserHandle(mUserId));
196                if (!mBound && DEBUG) {
197                    Slog.d(TAG, this + ": Bind failed");
198                }
199            } catch (SecurityException ex) {
200                if (DEBUG) {
201                    Slog.d(TAG, this + ": Bind failed", ex);
202                }
203            }
204        }
205    }
206
207    private void unbind() {
208        if (mBound) {
209            if (DEBUG) {
210                Slog.d(TAG, this + ": Unbinding");
211            }
212
213            mBound = false;
214            disconnect();
215            mContext.unbindService(this);
216        }
217    }
218
219    @Override
220    public void onServiceConnected(ComponentName name, IBinder service) {
221        if (DEBUG) {
222            Slog.d(TAG, this + ": Connected");
223        }
224
225        if (mBound) {
226            disconnect();
227
228            IRemoteDisplayProvider provider = IRemoteDisplayProvider.Stub.asInterface(service);
229            if (provider != null) {
230                Connection connection = new Connection(provider);
231                if (connection.register()) {
232                    mActiveConnection = connection;
233                } else {
234                    if (DEBUG) {
235                        Slog.d(TAG, this + ": Registration failed");
236                    }
237                }
238            } else {
239                Slog.e(TAG, this + ": Service returned invalid remote display provider binder");
240            }
241        }
242    }
243
244    @Override
245    public void onServiceDisconnected(ComponentName name) {
246        if (DEBUG) {
247            Slog.d(TAG, this + ": Service disconnected");
248        }
249        disconnect();
250    }
251
252    private void onConnectionReady(Connection connection) {
253        if (mActiveConnection == connection) {
254            mConnectionReady = true;
255
256            if (mDiscoveryMode != RemoteDisplayState.DISCOVERY_MODE_NONE) {
257                mActiveConnection.setDiscoveryMode(mDiscoveryMode);
258            }
259            if (mSelectedDisplayId != null) {
260                mActiveConnection.connect(mSelectedDisplayId);
261            }
262        }
263    }
264
265    private void onConnectionDied(Connection connection) {
266        if (mActiveConnection == connection) {
267            if (DEBUG) {
268                Slog.d(TAG, this + ": Service connection died");
269            }
270            disconnect();
271        }
272    }
273
274    private void onDisplayStateChanged(Connection connection, RemoteDisplayState state) {
275        if (mActiveConnection == connection) {
276            if (DEBUG) {
277                Slog.d(TAG, this + ": State changed, state=" + state);
278            }
279            setDisplayState(state);
280        }
281    }
282
283    private void disconnect() {
284        if (mActiveConnection != null) {
285            if (mSelectedDisplayId != null) {
286                mActiveConnection.disconnect(mSelectedDisplayId);
287            }
288            mConnectionReady = false;
289            mActiveConnection.dispose();
290            mActiveConnection = null;
291            setDisplayState(null);
292        }
293    }
294
295    private void setDisplayState(RemoteDisplayState state) {
296        if (!Objects.equals(mDisplayState, state)) {
297            mDisplayState = state;
298            if (!mScheduledDisplayStateChangedCallback) {
299                mScheduledDisplayStateChangedCallback = true;
300                mHandler.post(mDisplayStateChanged);
301            }
302        }
303    }
304
305    @Override
306    public String toString() {
307        return "Service connection " + mComponentName.flattenToShortString();
308    }
309
310    private final Runnable mDisplayStateChanged = new Runnable() {
311        @Override
312        public void run() {
313            mScheduledDisplayStateChangedCallback = false;
314            if (mDisplayStateCallback != null) {
315                mDisplayStateCallback.onDisplayStateChanged(
316                        RemoteDisplayProviderProxy.this, mDisplayState);
317            }
318        }
319    };
320
321    public interface Callback {
322        void onDisplayStateChanged(RemoteDisplayProviderProxy provider, RemoteDisplayState state);
323    }
324
325    private final class Connection implements DeathRecipient {
326        private final IRemoteDisplayProvider mProvider;
327        private final ProviderCallback mCallback;
328
329        public Connection(IRemoteDisplayProvider provider) {
330            mProvider = provider;
331            mCallback = new ProviderCallback(this);
332        }
333
334        public boolean register() {
335            try {
336                mProvider.asBinder().linkToDeath(this, 0);
337                mProvider.setCallback(mCallback);
338                mHandler.post(new Runnable() {
339                    @Override
340                    public void run() {
341                        onConnectionReady(Connection.this);
342                    }
343                });
344                return true;
345            } catch (RemoteException ex) {
346                binderDied();
347            }
348            return false;
349        }
350
351        public void dispose() {
352            mProvider.asBinder().unlinkToDeath(this, 0);
353            mCallback.dispose();
354        }
355
356        public void setDiscoveryMode(int mode) {
357            try {
358                mProvider.setDiscoveryMode(mode);
359            } catch (RemoteException ex) {
360                Slog.e(TAG, "Failed to deliver request to set discovery mode.", ex);
361            }
362        }
363
364        public void connect(String id) {
365            try {
366                mProvider.connect(id);
367            } catch (RemoteException ex) {
368                Slog.e(TAG, "Failed to deliver request to connect to display.", ex);
369            }
370        }
371
372        public void disconnect(String id) {
373            try {
374                mProvider.disconnect(id);
375            } catch (RemoteException ex) {
376                Slog.e(TAG, "Failed to deliver request to disconnect from display.", ex);
377            }
378        }
379
380        public void setVolume(String id, int volume) {
381            try {
382                mProvider.setVolume(id, volume);
383            } catch (RemoteException ex) {
384                Slog.e(TAG, "Failed to deliver request to set display volume.", ex);
385            }
386        }
387
388        public void adjustVolume(String id, int volume) {
389            try {
390                mProvider.adjustVolume(id, volume);
391            } catch (RemoteException ex) {
392                Slog.e(TAG, "Failed to deliver request to adjust display volume.", ex);
393            }
394        }
395
396        @Override
397        public void binderDied() {
398            mHandler.post(new Runnable() {
399                @Override
400                public void run() {
401                    onConnectionDied(Connection.this);
402                }
403            });
404        }
405
406        void postStateChanged(final RemoteDisplayState state) {
407            mHandler.post(new Runnable() {
408                @Override
409                public void run() {
410                    onDisplayStateChanged(Connection.this, state);
411                }
412            });
413        }
414    }
415
416    /**
417     * Receives callbacks from the service.
418     * <p>
419     * This inner class is static and only retains a weak reference to the connection
420     * to prevent the client from being leaked in case the service is holding an
421     * active reference to the client's callback.
422     * </p>
423     */
424    private static final class ProviderCallback extends IRemoteDisplayCallback.Stub {
425        private final WeakReference<Connection> mConnectionRef;
426
427        public ProviderCallback(Connection connection) {
428            mConnectionRef = new WeakReference<Connection>(connection);
429        }
430
431        public void dispose() {
432            mConnectionRef.clear();
433        }
434
435        @Override
436        public void onStateChanged(RemoteDisplayState state) throws RemoteException {
437            Connection connection = mConnectionRef.get();
438            if (connection != null) {
439                connection.postStateChanged(state);
440            }
441        }
442    }
443}
444