Bug 1285870 - (Part 2) Implement RemotePresentationService to show PresentationView. r?snorp,sebastian draft
authorKuoE0 <kuoe0.tw@gmail.com>
Tue, 16 Aug 2016 16:50:01 +0800
changeset 420091 72851c664e975ef239df4dbf73bb94ef568aefef
parent 420090 287631996d98a4dcfb49965bfb0057e351ada04b
child 532715 f8ca7bc325f7ab169e25063522376f1dc89a861d
push id31088
push userbmo:kuoe0@mozilla.com
push dateMon, 03 Oct 2016 09:00:57 +0000
reviewerssnorp, sebastian
bugs1285870
milestone52.0a1
Bug 1285870 - (Part 2) Implement RemotePresentationService to show PresentationView. r?snorp,sebastian MozReview-Commit-ID: 8wxXH7jNjVC
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java
mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
mobile/android/base/java/org/mozilla/gecko/PresentationMediaPlayerManager.java
mobile/android/base/java/org/mozilla/gecko/RemotePresentationService.java
mobile/android/base/moz.build
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -37,16 +37,18 @@
                  android:debuggable="false">
 #endif
 
         <meta-data android:name="com.sec.android.support.multiwindow" android:value="true"/>
 
 #ifdef MOZ_NATIVE_DEVICES
         <!-- This resources comes from Google Play Services. Required for casting support. -->
         <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
+        <service android:name="org.mozilla.gecko.RemotePresentationService" android:exported="false"/>
+
 #endif
 
         <!-- This activity handles all incoming Intents and dispatches them to other activities. -->
         <activity android:name="org.mozilla.gecko.LauncherActivity"
             android:theme="@android:style/Theme.Translucent.NoTitleBar" />
 
         <!-- Fennec is shipped as the Android package named
              org.mozilla.{fennec,firefox,firefox_beta}.  The internal Java
--- a/mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java
@@ -24,25 +24,47 @@ import android.content.Intent;
 import android.support.v7.media.MediaRouter.RouteInfo;
 import android.util.Log;
 
 public class ChromeCastDisplay implements GeckoPresentationDisplay {
 
     static final String REMOTE_DISPLAY_APP_ID = "743AF7F7";
 
     private static final String LOGTAG = "GeckoChromeCastDisplay";
+    private final Context mContext;
     private final RouteInfo mRoute;
     private CastDevice mCastDevice;
+    private EventCallback mStartCallback = null;
+
+    // EventCallback which is actually a GeckoEventCallback is sometimes being invoked more
+    // than once. That causes the IllegalStateException to be thrown. To prevent a crash,
+    // catch the exception and report it as an error to the log.
+    private static void sendSuccess(final EventCallback callback, final String msg) {
+        try {
+            callback.sendSuccess(msg);
+        } catch (final IllegalStateException e) {
+            Log.e(LOGTAG, "Attempting to invoke callback.sendSuccess more than once.", e);
+        }
+    }
+
+    private static void sendError(final EventCallback callback, final String msg) {
+        try {
+            callback.sendError(msg);
+        } catch (final IllegalStateException e) {
+            Log.e(LOGTAG, "Attempting to invoke callback.sendError more than once.", e);
+        }
+    }
 
     public ChromeCastDisplay(Context context, RouteInfo route) {
         int status =  GooglePlayServicesUtil.isGooglePlayServicesAvailable(context);
         if (status != ConnectionResult.SUCCESS) {
             throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")");
         }
 
+        mContext = context;
         mRoute = route;
         mCastDevice = CastDevice.getFromBundle(mRoute.getExtras());
     }
 
     public JSONObject toJSON() {
         final JSONObject obj = new JSONObject();
         try {
             if (mCastDevice == null) {
@@ -54,13 +76,66 @@ public class ChromeCastDisplay implement
         } catch (JSONException ex) {
             Log.d(LOGTAG, "Error building route", ex);
         }
 
         return obj;
     }
 
     @Override
-    public void start(EventCallback callback) { }
+    public void start(EventCallback callback) {
+        mStartCallback = callback;
+
+        try {
+            Intent intent = new Intent(mContext, RemotePresentationService.class);
+            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+            PendingIntent notificationPendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+
+            CastRemoteDisplayLocalService.NotificationSettings settings =
+                new CastRemoteDisplayLocalService.NotificationSettings.Builder()
+                .setNotificationPendingIntent(notificationPendingIntent).build();
+
+            CastRemoteDisplayLocalService.startService(
+                    mContext,
+                    RemotePresentationService.class,
+                    REMOTE_DISPLAY_APP_ID,
+                    mCastDevice,
+                    settings,
+                    new CastRemoteDisplayLocalService.Callbacks() {
+                        public void onServiceCreated(CastRemoteDisplayLocalService service) {
+                            ((RemotePresentationService) service).setDeviceId(mRoute.getId());
+                        }
+
+                        @Override
+                        public void onRemoteDisplaySessionStarted(CastRemoteDisplayLocalService service) {
+                            Log.d(LOGTAG, "Remote presentation launched!");
+                            sendSuccess(mStartCallback, "Succeed to start presentation.");
+                            mStartCallback = null;
+                        }
+
+                        @Override
+                        public void onRemoteDisplaySessionError(Status errorReason) {
+                            int code = errorReason.getStatusCode();
+
+                            mCastDevice = null;
+
+                            sendError(mStartCallback, "Fail to start presentation. Error code: " + code);
+                            mStartCallback = null;
+                        }
+            });
+        } catch (final IllegalArgumentException e) {
+            Log.e(LOGTAG, "IllegalArgumentException", e);
+            sendError(callback, "Fail to start presentation.");
+            mStartCallback = null;
+        }
+    }
 
     @Override
-    public void stop(EventCallback callback) { }
+    public void stop(EventCallback callback) {
+        try {
+            CastRemoteDisplayLocalService.stopService();
+            sendSuccess(callback, "Succeed to stop presentation.");
+        } catch (final Exception e) {
+            Log.e(LOGTAG, "Exception", e);
+            sendError(callback, "Some exception happened when stopping presentation.");
+        }
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
@@ -40,16 +40,17 @@ public class MediaPlayerManager extends 
         if (Versions.feature17Plus) {
             return new PresentationMediaPlayerManager();
         } else {
             return new MediaPlayerManager();
         }
     }
 
     private static final String LOGTAG = "GeckoMediaPlayerManager";
+    protected boolean isPresentationMode = false; // Used to prevent mirroring when Presentation API is using.
 
     @ReflectionTarget
     public static final String MEDIA_PLAYER_TAG = "MPManagerFragment";
 
     private static final boolean SHOW_DEBUG = false;
     // Simplified debugging interfaces
     private static void debug(String msg, Exception e) {
         if (SHOW_DEBUG) {
@@ -66,74 +67,98 @@ public class MediaPlayerManager extends 
     protected MediaRouter mediaRouter = null;
     protected final Map<String, GeckoMediaPlayer> players = new HashMap<String, GeckoMediaPlayer>();
     protected final Map<String, GeckoPresentationDisplay> displays = new HashMap<String, GeckoPresentationDisplay>(); // used for Presentation API
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
-                "MediaPlayer:Load",
-                "MediaPlayer:Start",
-                "MediaPlayer:Stop",
-                "MediaPlayer:Play",
-                "MediaPlayer:Pause",
-                "MediaPlayer:End",
-                "MediaPlayer:Mirror",
-                "MediaPlayer:Message");
+                                                                  "MediaPlayer:Load",
+                                                                  "MediaPlayer:Start",
+                                                                  "MediaPlayer:Stop",
+                                                                  "MediaPlayer:Play",
+                                                                  "MediaPlayer:Pause",
+                                                                  "MediaPlayer:End",
+                                                                  "MediaPlayer:Mirror",
+                                                                  "MediaPlayer:Message",
+                                                                  "PresentationDevice:Start",
+                                                                  "PresentationDevice:Stop");
     }
 
     @Override
     @JNITarget
     public void onDestroy() {
         super.onDestroy();
         EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
                                                                     "MediaPlayer:Load",
                                                                     "MediaPlayer:Start",
                                                                     "MediaPlayer:Stop",
                                                                     "MediaPlayer:Play",
                                                                     "MediaPlayer:Pause",
                                                                     "MediaPlayer:End",
                                                                     "MediaPlayer:Mirror",
-                                                                    "MediaPlayer:Message");
+                                                                    "MediaPlayer:Message",
+                                                                    "PresentationDevice:Start",
+                                                                    "PresentationDevice:Stop");
     }
 
     // GeckoEventListener implementation
     @Override
     public void handleMessage(String event, final NativeJSObject message, final EventCallback callback) {
         debug(event);
 
-        final GeckoMediaPlayer player = players.get(message.getString("id"));
-        if (player == null) {
-            Log.e(LOGTAG, "Couldn'tplayerisplay for this id: " + message.getString("id") + " for message: " + event);
-            if (callback != null) {
-                callback.sendError(null);
+        final String eventPrefix = event.split(":")[0];
+
+        if ("MediaPlayer".equals(eventPrefix)) {
+            final GeckoMediaPlayer player = players.get(message.getString("id"));
+            if (player == null) {
+                Log.e(LOGTAG, "Couldn't find a player for this id: " + message.getString("id") + " for message: " + event);
+                if (callback != null) {
+                    callback.sendError(null);
+                }
+                return;
             }
-            return;
+
+            if ("MediaPlayer:Play".equals(event)) {
+                player.play(callback);
+            } else if ("MediaPlayer:Start".equals(event)) {
+                player.start(callback);
+            } else if ("MediaPlayer:Stop".equals(event)) {
+                player.stop(callback);
+            } else if ("MediaPlayer:Pause".equals(event)) {
+                player.pause(callback);
+            } else if ("MediaPlayer:End".equals(event)) {
+                player.end(callback);
+            } else if ("MediaPlayer:Mirror".equals(event)) {
+                player.mirror(callback);
+            } else if ("MediaPlayer:Message".equals(event) && message.has("data")) {
+                player.message(message.getString("data"), callback);
+            } else if ("MediaPlayer:Load".equals(event)) {
+                final String url = message.optString("source", "");
+                final String type = message.optString("type", "video/mp4");
+                final String title = message.optString("title", "");
+                player.load(title, url, type, callback);
+            }
         }
 
-        if ("MediaPlayer:Play".equals(event)) {
-            player.play(callback);
-        } else if ("MediaPlayer:Start".equals(event)) {
-            player.start(callback);
-        } else if ("MediaPlayer:Stop".equals(event)) {
-            player.stop(callback);
-        } else if ("MediaPlayer:Pause".equals(event)) {
-            player.pause(callback);
-        } else if ("MediaPlayer:End".equals(event)) {
-            player.end(callback);
-        } else if ("MediaPlayer:Mirror".equals(event)) {
-            player.mirror(callback);
-        } else if ("MediaPlayer:Message".equals(event) && message.has("data")) {
-            player.message(message.getString("data"), callback);
-        } else if ("MediaPlayer:Load".equals(event)) {
-            final String url = message.optString("source", "");
-            final String type = message.optString("type", "video/mp4");
-            final String title = message.optString("title", "");
-            player.load(title, url, type, callback);
+        if ("PresentationDevice".equals(eventPrefix)) {
+            final GeckoPresentationDisplay display = displays.get(message.getString("id"));
+            if (display == null) {
+                Log.e(LOGTAG, "Couldn't find a display for this id: " + message.getString("id") + " for message: " + event);
+                return;
+            }
+
+            if ("PresentationDevice:Start".equals(event)) {
+                display.start(callback);
+                isPresentationMode = true;
+            } else if ("PresentationDevice:Stop".equals(event)) {
+                display.stop(callback);
+                isPresentationMode = false;
+            }
         }
     }
 
     private final MediaRouter.Callback callback =
         new MediaRouter.Callback() {
             @Override
             public void onRouteRemoved(MediaRouter router, RouteInfo route) {
                 debug("onRouteRemoved: route=" + route);
--- a/mobile/android/base/java/org/mozilla/gecko/PresentationMediaPlayerManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/PresentationMediaPlayerManager.java
@@ -49,16 +49,20 @@ public class PresentationMediaPlayerMana
     }
 
     @Override
     protected void updatePresentation() {
         if (mediaRouter == null) {
             return;
         }
 
+        if (isPresentationMode) {
+            return;
+        }
+
         MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute();
         Display display = route != null ? route.getPresentationDisplay() : null;
 
         if (display != null) {
             if ((presentation != null) && (presentation.getDisplay() != display)) {
                 presentation.dismiss();
                 presentation = null;
             }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/RemotePresentationService.java
@@ -0,0 +1,151 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: ts=4 sw=4 expandtab:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import org.json.JSONObject;
+import org.json.JSONException;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.PresentationView;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.ScreenManagerHelper;
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.ReflectionTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+
+import com.google.android.gms.cast.CastMediaControlIntent;
+import com.google.android.gms.cast.CastPresentation;
+import com.google.android.gms.cast.CastRemoteDisplayLocalService;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GooglePlayServicesUtil;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter.RouteInfo;
+import android.support.v7.media.MediaRouter;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.widget.RelativeLayout;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/*
+ * Service to keep the remote display running even when the app goes into the background
+ */
+public class RemotePresentationService extends CastRemoteDisplayLocalService {
+
+    private static final String LOGTAG = "RemotePresentationService";
+    private CastPresentation mPresentation;
+    private Display mDisplay;
+    private String mDeviceId;
+    private int mScreenId;
+
+    public void setDeviceId(String aDeviceId) {
+        mDeviceId = aDeviceId;
+    }
+
+    public String getDeviceId() {
+        return mDeviceId;
+    }
+
+    @Override
+    public void onCreatePresentation(Display display) {
+        mDisplay = display;
+        createPresentation();
+    }
+
+    @Override
+    public void onDismissPresentation() {
+        dismissPresentation();
+    }
+
+    private void dismissPresentation() {
+        if (mPresentation != null) {
+            mPresentation.dismiss();
+            mPresentation = null;
+            ScreenManagerHelper.removeDisplay(mScreenId);
+        }
+    }
+
+    private void createPresentation() {
+        dismissPresentation();
+
+        DisplayMetrics metrics = new DisplayMetrics();
+        mDisplay.getMetrics(metrics);
+        mScreenId = ScreenManagerHelper.addDisplay(ScreenManagerHelper.DISPLAY_VIRTUAL,
+                                                   metrics.widthPixels,
+                                                   metrics.heightPixels,
+                                                   metrics.density);
+
+        VirtualPresentation presentation = new VirtualPresentation(this, mDisplay);
+        presentation.setDeviceId(mDeviceId);
+        presentation.setScreenId(mScreenId);
+        mPresentation = (CastPresentation) presentation;
+
+        try {
+            mPresentation.show();
+        } catch (WindowManager.InvalidDisplayException ex) {
+            Log.e(LOGTAG, "Unable to show presentation, display was removed.", ex);
+            dismissPresentation();
+        }
+    }
+}
+
+class VirtualPresentation extends CastPresentation {
+    private final String LOGTAG = "VirtualPresentation";
+    private RelativeLayout mLayuot;
+    private PresentationView mView;
+    private String mDeviceId;
+    private int mScreenId;
+
+    public VirtualPresentation(Context context, Display display) {
+        super(context, display);
+    }
+
+    public void setDeviceId(String aDeviceId) { mDeviceId = aDeviceId; }
+    public void setScreenId(int aScreenId) { mScreenId = aScreenId; }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        /*
+         * NOTICE: The context get from getContext() is different to the context
+         * of the application. Presentaion has its own context to get correct
+         * resources.
+         */
+
+        // Create new PresentationView
+        mView = new PresentationView(getContext());
+        mView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
+                                               LayoutParams.MATCH_PARENT));
+        mView.setDeviceId(mDeviceId);
+        mView.setScreenId(mScreenId);
+
+        // Create new layout to put the GeckoView
+        mLayuot = new RelativeLayout(getContext());
+        mLayuot.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
+                                                 LayoutParams.MATCH_PARENT));
+        mLayuot.addView(mView);
+
+        setContentView(mLayuot);
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -833,16 +833,17 @@ moz_native_devices_jars = [
 ]
 moz_native_devices_sources = ['java/org/mozilla/gecko/' + x for x in [
     'ChromeCastDisplay.java',
     'ChromeCastPlayer.java',
     'GeckoMediaPlayer.java',
     'GeckoPresentationDisplay.java',
     'MediaPlayerManager.java',
     'PresentationMediaPlayerManager.java',
+    'RemotePresentationService.java',
 ]]
 if CONFIG['MOZ_NATIVE_DEVICES']:
     gbjar.extra_jars += moz_native_devices_jars
     gbjar.sources += moz_native_devices_sources
 
     if CONFIG['ANDROID_MEDIAROUTER_V7_AAR']:
         ANDROID_EXTRA_PACKAGES += ['android.support.v7.mediarouter']
         ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_MEDIAROUTER_V7_AAR_RES']]