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