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 
17 package com.android.media.remotedisplay;
18 
19 import android.annotation.SystemApi;
20 import android.app.PendingIntent;
21 import android.app.Service;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.media.IRemoteDisplayCallback;
25 import android.media.IRemoteDisplayProvider;
26 import android.media.RemoteDisplayState;
27 import android.os.Handler;
28 import android.os.IBinder;
29 import android.os.Looper;
30 import android.os.Message;
31 import android.os.RemoteException;
32 import android.provider.Settings;
33 import android.util.ArrayMap;
34 
35 import java.util.Collection;
36 
37 /**
38  * Base class for remote display providers implemented as unbundled services.
39  * <p>
40  * To implement your remote display provider service, create a subclass of
41  * {@link Service} and override the {@link Service#onBind Service.onBind()} method
42  * to return the provider's binder when the {@link #SERVICE_INTERFACE} is requested.
43  * </p>
44  * <pre>
45  *   public class SampleRemoteDisplayProviderService extends Service {
46  *       private SampleProvider mProvider;
47  *
48  *       public IBinder onBind(Intent intent) {
49  *           if (intent.getAction().equals(RemoteDisplayProvider.SERVICE_INTERFACE)) {
50  *               if (mProvider == null) {
51  *                   mProvider = new SampleProvider(this);
52  *               }
53  *               return mProvider.getBinder();
54  *           }
55  *           return null;
56  *       }
57  *
58  *       class SampleProvider extends RemoteDisplayProvider {
59  *           public SampleProvider() {
60  *               super(SampleRemoteDisplayProviderService.this);
61  *           }
62  *
63  *           // --- Implementation goes here ---
64  *       }
65  *   }
66  * </pre>
67  * <p>
68  * Declare your remote display provider service in your application manifest
69  * like this:
70  * </p>
71  * <pre>
72  *   &lt;application>
73  *       &lt;uses-library android:name="com.android.media.remotedisplay" />
74  *
75  *       &lt;service android:name=".SampleRemoteDisplayProviderService"
76  *               android:label="@string/sample_remote_display_provider_service"
77  *               android:exported="true"
78  *               android:permission="android.permission.BIND_REMOTE_DISPLAY">
79  *           &lt;intent-filter>
80  *               &lt;action android:name="com.android.media.remotedisplay.RemoteDisplayProvider" />
81  *           &lt;/intent-filter>
82  *       &lt;/service>
83  *   &lt;/application>
84  * </pre>
85  * <p>
86  * This object is not thread safe.  It is only intended to be accessed on the
87  * {@link Context#getMainLooper main looper thread} of an application.
88  * </p><p>
89  * IMPORTANT: This class is effectively a public API for unbundled applications, and
90  * must remain API stable. See README.txt in the root of this package for more information.
91  * </p>
92  *
93  * @hide
94  */
95 @SystemApi
96 public abstract class RemoteDisplayProvider {
97     private static final int MSG_SET_CALLBACK = 1;
98     private static final int MSG_SET_DISCOVERY_MODE = 2;
99     private static final int MSG_CONNECT = 3;
100     private static final int MSG_DISCONNECT = 4;
101     private static final int MSG_SET_VOLUME = 5;
102     private static final int MSG_ADJUST_VOLUME = 6;
103 
104     private final Context mContext;
105     private final ProviderStub mStub;
106     private final ProviderHandler mHandler;
107     private final ArrayMap<String, RemoteDisplay> mDisplays =
108             new ArrayMap<String, RemoteDisplay>();
109     private IRemoteDisplayCallback mCallback;
110     private int mDiscoveryMode = DISCOVERY_MODE_NONE;
111 
112     private PendingIntent mSettingsPendingIntent;
113 
114     /**
115      * The {@link Intent} that must be declared as handled by the service.
116      * Put this in your manifest.
117      */
118     public static final String SERVICE_INTERFACE = RemoteDisplayState.SERVICE_INTERFACE;
119 
120     /**
121      * Discovery mode: Do not perform any discovery.
122      */
123     public static final int DISCOVERY_MODE_NONE = RemoteDisplayState.DISCOVERY_MODE_NONE;
124 
125     /**
126      * Discovery mode: Passive or low-power periodic discovery.
127      * <p>
128      * This mode indicates that an application is interested in knowing whether there
129      * are any remote displays paired or available but doesn't need the latest or
130      * most detailed information.  The provider may scan at a lower rate or rely on
131      * knowledge of previously paired devices.
132      * </p>
133      */
134     public static final int DISCOVERY_MODE_PASSIVE = RemoteDisplayState.DISCOVERY_MODE_PASSIVE;
135 
136     /**
137      * Discovery mode: Active discovery.
138      * <p>
139      * This mode indicates that the user is actively trying to connect to a route
140      * and we should perform continuous scans.  This mode may use significantly more
141      * power but is intended to be short-lived.
142      * </p>
143      */
144     public static final int DISCOVERY_MODE_ACTIVE = RemoteDisplayState.DISCOVERY_MODE_ACTIVE;
145 
146     /**
147      * Creates a remote display provider.
148      *
149      * @param context The application context for the remote display provider.
150      */
RemoteDisplayProvider(Context context)151     public RemoteDisplayProvider(Context context) {
152         mContext = context;
153         mStub = new ProviderStub();
154         mHandler = new ProviderHandler(context.getMainLooper());
155     }
156 
157     /**
158      * Gets the context of the remote display provider.
159      */
getContext()160     public final Context getContext() {
161         return mContext;
162     }
163 
164     /**
165      * Gets the Binder associated with the provider.
166      * <p>
167      * This is intended to be used for the onBind() method of a service that implements
168      * a remote display provider service.
169      * </p>
170      *
171      * @return The IBinder instance associated with the provider.
172      */
getBinder()173     public IBinder getBinder() {
174         return mStub;
175     }
176 
177     /**
178      * Called when the current discovery mode changes.
179      *
180      * @param mode The new discovery mode.
181      */
onDiscoveryModeChanged(int mode)182     public void onDiscoveryModeChanged(int mode) {
183     }
184 
185     /**
186      * Called when the system would like to connect to a display.
187      *
188      * @param display The remote display.
189      */
onConnect(RemoteDisplay display)190     public void onConnect(RemoteDisplay display) {
191     }
192 
193     /**
194      * Called when the system would like to disconnect from a display.
195      *
196      * @param display The remote display.
197      */
onDisconnect(RemoteDisplay display)198     public void onDisconnect(RemoteDisplay display) {
199     }
200 
201     /**
202      * Called when the system would like to set the volume of a display.
203      *
204      * @param display The remote display.
205      * @param volume The desired volume.
206      */
onSetVolume(RemoteDisplay display, int volume)207     public void onSetVolume(RemoteDisplay display, int volume) {
208     }
209 
210     /**
211      * Called when the system would like to adjust the volume of a display.
212      *
213      * @param display The remote display.
214      * @param delta An increment to add to the current volume, such as +1 or -1.
215      */
onAdjustVolume(RemoteDisplay display, int delta)216     public void onAdjustVolume(RemoteDisplay display, int delta) {
217     }
218 
219     /**
220      * Gets the current discovery mode.
221      *
222      * @return The current discovery mode.
223      */
getDiscoveryMode()224     public int getDiscoveryMode() {
225         return mDiscoveryMode;
226     }
227 
228     /**
229      * Gets the current collection of displays.
230      *
231      * @return The current collection of displays, which must not be modified.
232      */
getDisplays()233     public Collection<RemoteDisplay> getDisplays() {
234         return mDisplays.values();
235     }
236 
237     /**
238      * Adds the specified remote display and notifies the system.
239      *
240      * @param display The remote display that was added.
241      * @throws IllegalStateException if the argument is null, or if there is already a display with
242      *         the same id.
243      */
addDisplay(RemoteDisplay display)244     public void addDisplay(RemoteDisplay display) {
245         if (display == null) {
246             throw new IllegalArgumentException("display cannot be null");
247         }
248         String displayId = display.getId();
249         if (mDisplays.containsKey(displayId)) {
250             throw new IllegalArgumentException("display already exists with id: " + displayId);
251         }
252         mDisplays.put(displayId, display);
253         publishState();
254     }
255 
256     /**
257      * Updates information about the specified remote display and notifies the system.
258      *
259      * @param display The remote display that was added.
260      * @throws IllegalStateException if the argument is null, or if the provider is not aware of the
261      *         display.
262      */
updateDisplay(RemoteDisplay display)263     public void updateDisplay(RemoteDisplay display) {
264         if (display == null) {
265             throw new IllegalArgumentException("display cannot be null");
266         }
267         String displayId = display.getId();
268         if (mDisplays.get(displayId) != display) {
269             throw new IllegalArgumentException("unexpected display with id: " + displayId);
270         }
271         publishState();
272     }
273 
274     /**
275      * Removes the specified remote display and tells the system about it.
276      *
277      * @param display The remote display that was removed.
278      * @throws IllegalStateException if the argument is null, or if the provider is not aware of the
279      *         display.
280      */
removeDisplay(RemoteDisplay display)281     public void removeDisplay(RemoteDisplay display) {
282         if (display == null) {
283             throw new IllegalArgumentException("display cannot be null");
284         }
285         String displayId = display.getId();
286         if (mDisplays.get(displayId) != display) {
287             throw new IllegalArgumentException("unexpected display with id: " + displayId);
288         }
289         mDisplays.remove(displayId);
290         publishState();
291     }
292 
293     /**
294      * Finds the remote display with the specified id, returns null if not found.
295      *
296      * @param id Id of the remote display.
297      * @return The display, or null if none.
298      */
findRemoteDisplay(String id)299     public RemoteDisplay findRemoteDisplay(String id) {
300         return mDisplays.get(id);
301     }
302 
303     /**
304      * Gets a pending intent to launch the remote display settings activity.
305      *
306      * @return A pending intent to launch the settings activity.
307      */
getSettingsPendingIntent()308     public PendingIntent getSettingsPendingIntent() {
309         if (mSettingsPendingIntent == null) {
310             Intent settingsIntent = new Intent(Settings.ACTION_CAST_SETTINGS);
311             settingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
312                     | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
313                     | Intent.FLAG_ACTIVITY_CLEAR_TOP);
314             mSettingsPendingIntent = PendingIntent.getActivity(
315                     mContext, 0, settingsIntent, PendingIntent.FLAG_IMMUTABLE, null);
316         }
317         return mSettingsPendingIntent;
318     }
319 
setCallback(IRemoteDisplayCallback callback)320     void setCallback(IRemoteDisplayCallback callback) {
321         mCallback = callback;
322         publishState();
323     }
324 
setDiscoveryMode(int mode)325     void setDiscoveryMode(int mode) {
326         if (mDiscoveryMode != mode) {
327             mDiscoveryMode = mode;
328             onDiscoveryModeChanged(mode);
329         }
330     }
331 
publishState()332     void publishState() {
333         if (mCallback != null) {
334             RemoteDisplayState state = new RemoteDisplayState();
335             final int count = mDisplays.size();
336             for (int i = 0; i < count; i++) {
337                 final RemoteDisplay display = mDisplays.valueAt(i);
338                 state.displays.add(display.getInfo());
339             }
340             try {
341                 mCallback.onStateChanged(state);
342             } catch (RemoteException ex) {
343                 // system server died?
344             }
345         }
346     }
347 
348     final class ProviderStub extends IRemoteDisplayProvider.Stub {
349         @Override
setCallback(IRemoteDisplayCallback callback)350         public void setCallback(IRemoteDisplayCallback callback) {
351             mHandler.obtainMessage(MSG_SET_CALLBACK, callback).sendToTarget();
352         }
353 
354         @Override
setDiscoveryMode(int mode)355         public void setDiscoveryMode(int mode) {
356             mHandler.obtainMessage(MSG_SET_DISCOVERY_MODE, mode, 0).sendToTarget();
357         }
358 
359         @Override
connect(String id)360         public void connect(String id) {
361             mHandler.obtainMessage(MSG_CONNECT, id).sendToTarget();
362         }
363 
364         @Override
disconnect(String id)365         public void disconnect(String id) {
366             mHandler.obtainMessage(MSG_DISCONNECT, id).sendToTarget();
367         }
368 
369         @Override
setVolume(String id, int volume)370         public void setVolume(String id, int volume) {
371             mHandler.obtainMessage(MSG_SET_VOLUME, volume, 0, id).sendToTarget();
372         }
373 
374         @Override
adjustVolume(String id, int delta)375         public void adjustVolume(String id, int delta) {
376             mHandler.obtainMessage(MSG_ADJUST_VOLUME, delta, 0, id).sendToTarget();
377         }
378     }
379 
380     final class ProviderHandler extends Handler {
ProviderHandler(Looper looper)381         public ProviderHandler(Looper looper) {
382             super(looper, null, true);
383         }
384 
385         @Override
handleMessage(Message msg)386         public void handleMessage(Message msg) {
387             switch (msg.what) {
388                 case MSG_SET_CALLBACK: {
389                     setCallback((IRemoteDisplayCallback)msg.obj);
390                     break;
391                 }
392                 case MSG_SET_DISCOVERY_MODE: {
393                     setDiscoveryMode(msg.arg1);
394                     break;
395                 }
396                 case MSG_CONNECT: {
397                     RemoteDisplay display = findRemoteDisplay((String)msg.obj);
398                     if (display != null) {
399                         onConnect(display);
400                     }
401                     break;
402                 }
403                 case MSG_DISCONNECT: {
404                     RemoteDisplay display = findRemoteDisplay((String)msg.obj);
405                     if (display != null) {
406                         onDisconnect(display);
407                     }
408                     break;
409                 }
410                 case MSG_SET_VOLUME: {
411                     RemoteDisplay display = findRemoteDisplay((String)msg.obj);
412                     if (display != null) {
413                         onSetVolume(display, msg.arg1);
414                     }
415                     break;
416                 }
417                 case MSG_ADJUST_VOLUME: {
418                     RemoteDisplay display = findRemoteDisplay((String)msg.obj);
419                     if (display != null) {
420                         onAdjustVolume(display, msg.arg1);
421                     }
422                     break;
423                 }
424             }
425         }
426     }
427 }
428