Bug 1295087 - Implement PresentationDeviceProvider for Chromecast devices on Fennec. r=schien,smaug,snorp
authorKuoE0 <kuoe0.tw@gmail.com>
Fri, 23 Sep 2016 22:13:28 +0800
changeset 348569 0a2c98b982b9e3dd04521535e5b9a34ad9f49b9e
parent 348568 b04c5c12b2009fb0d32887aeaad68cd048ec3992
child 348570 a64e70dc94f7aa147c33c2a2c92efd9fddd006f4
push id10298
push userraliiev@mozilla.com
push dateMon, 14 Nov 2016 12:33:03 +0000
treeherdermozilla-aurora@7e29173b1641 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersschien, smaug, snorp
bugs1295087
milestone52.0a1
Bug 1295087 - Implement PresentationDeviceProvider for Chromecast devices on Fennec. r=schien,smaug,snorp MozReview-Commit-ID: 78eNnvAMCsq
dom/presentation/provider/AndroidCastDeviceProvider.js
dom/presentation/provider/AndroidCastDeviceProvider.manifest
dom/presentation/provider/moz.build
mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
mobile/android/installer/package-manifest.in
new file mode 100644
--- /dev/null
+++ b/dom/presentation/provider/AndroidCastDeviceProvider.js
@@ -0,0 +1,461 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */
+/* globals Components, dump */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+// globals XPCOMUtils
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+// globals Services
+Cu.import("resource://gre/modules/Services.jsm");
+// globals Messaging
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+function log(str) {
+  // dump("-*- AndroidCastDeviceProvider -*-: " + str + "\n");
+}
+
+// Helper function: transfer nsIPresentationChannelDescription to json
+function descriptionToString(aDescription) {
+  let json = {};
+  json.type = aDescription.type;
+  switch(aDescription.type) {
+    case Ci.nsIPresentationChannelDescription.TYPE_TCP:
+      let addresses = aDescription.tcpAddress.QueryInterface(Ci.nsIArray);
+      json.tcpAddress = [];
+      for (let idx = 0; idx < addresses.length; idx++) {
+        let address = addresses.queryElementAt(idx, Ci.nsISupportsCString);
+        json.tcpAddress.push(address.data);
+      }
+      json.tcpPort = aDescription.tcpPort;
+      break;
+    case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL:
+      json.dataChannelSDP = aDescription.dataChannelSDP;
+      break;
+  }
+  return JSON.stringify(json);
+}
+
+const TOPIC_ANDROID_CAST_DEVICE_SYNCDEVICE = "AndroidCastDevice:SyncDevice";
+const TOPIC_ANDROID_CAST_DEVICE_ADDED      = "AndroidCastDevice:Added";
+const TOPIC_ANDROID_CAST_DEVICE_REMOVED    = "AndroidCastDevice:Removed";
+const TOPIC_ANDROID_CAST_DEVICE_START      = "AndroidCastDevice:Start";
+const TOPIC_ANDROID_CAST_DEVICE_STOP       = "AndroidCastDevice:Stop";
+const TOPIC_PRESENTATION_VIEW_READY        = "presentation-view-ready";
+
+function LocalControlChannel(aProvider, aDeviceId, aRole) {
+  log("LocalControlChannel - create new LocalControlChannel for : "
+      + aRole);
+  this._provider = aProvider;
+  this._deviceId = aDeviceId;
+  this._role     = aRole;
+}
+
+LocalControlChannel.prototype = {
+  _listener: null,
+  _provider: null,
+  _deviceId: null,
+  _role: null,
+  _isOnTerminating: false,
+  _isOnDisconnecting: false,
+  _pendingConnected: false,
+  _pendingDisconnect: null,
+  _pendingOffer: null,
+  _pendingCandidate: null,
+  /* For the controller, it would be the control channel of the receiver.
+   * For the receiver, it would be the control channel of the controller. */
+  _correspondingControlChannel: null,
+
+  set correspondingControlChannel(aCorrespondingControlChannel) {
+    this._correspondingControlChannel = aCorrespondingControlChannel;
+  },
+
+  get correspondingControlChannel() {
+    return this._correspondingControlChannel;
+  },
+
+  notifyConnected: function LCC_notifyConnected() {
+    this._pendingDisconnect = null;
+
+    if (!this._listener) {
+      this._pendingConnected = true;
+    } else {
+      this._listener.notifyConnected();
+    }
+  },
+
+  onOffer: function LCC_onOffer(aOffer) {
+    if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) {
+      log("LocalControlChannel - onOffer of controller should not be called.");
+      return;
+    }
+    if (!this._listener) {
+      this._pendingOffer = aOffer;
+    } else {
+      this._listener.onOffer(aOffer);
+    }
+  },
+
+  onAnswer: function LCC_onAnswer(aAnswer) {
+    if (this._role == Ci.nsIPresentationService.ROLE_RECEIVER) {
+      log("LocalControlChannel - onAnswer of receiver should not be called.");
+      return;
+    }
+    this._listener.onAnswer(aAnswer);
+  },
+
+  notifyIceCandidate: function LCC_notifyIceCandidate(aCandidate) {
+    if (!this._listener) {
+      this._pendingCandidate = aCandidate;
+    } else {
+      this._listener.onIceCandidate(aCandidate);
+    }
+  },
+
+  // nsIPresentationControlChannel
+  get listener() {
+    return this._listener;
+  },
+
+  set listener(aListener) {
+    this._listener = aListener;
+
+    if (!this._listener) {
+      return;
+    }
+
+    if (this._pendingConnected) {
+      this.notifyConnected();
+      this._pendingConnected = false;
+    }
+
+    if (this._pendingOffer) {
+      this.onOffer(this._pendingOffer);
+      this._pendingOffer = null;
+    }
+
+    if (this._pendingCandidate) {
+      this.notifyIceCandidate(this._pendingCandidate);
+      this._pendingCandidate = null;
+    }
+
+    if (this._pendingDisconnect != null) {
+      this.disconnect(this._pendingDisconnect);
+      this._pendingDisconnect = null;
+    }
+  },
+
+  sendOffer: function LCC_sendOffer(aOffer) {
+    if (this._role == Ci.nsIPresentationService.ROLE_RECEIVER) {
+      log("LocalControlChannel - sendOffer of receiver should not be called.");
+      return;
+    }
+    log("LocalControlChannel - sendOffer aOffer=" + descriptionToString(aOffer));
+    this._correspondingControlChannel.onOffer(aOffer);
+  },
+
+  sendAnswer: function LCC_sendAnswer(aAnswer) {
+    if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) {
+      log("LocalControlChannel - sendAnswer of controller should not be called.");
+      return;
+    }
+    log("LocalControlChannel - sendAnswer aAnswer=" + descriptionToString(aAnswer));
+    this._correspondingControlChannel.onAnswer(aAnswer);
+  },
+
+  sendIceCandidate: function LCC_sendIceCandidate(aCandidate) {
+    log("LocalControlChannel - sendAnswer aCandidate=" + aCandidate);
+    this._correspondingControlChannel.notifyIceCandidate(aCandidate);
+  },
+
+  launch: function LCC_launch(aPresentationId, aUrl) {
+    log("LocalControlChannel - launch aPresentationId="
+        + aPresentationId + " aUrl=" + aUrl);
+    // Create control channel for receiver directly.
+    let controlChannel = new LocalControlChannel(this._provider,
+                                                 this._deviceId,
+                                                 Ci.nsIPresentationService.ROLE_RECEIVER);
+
+    // Set up the corresponding control channels for both controller and receiver.
+    this._correspondingControlChannel = controlChannel;
+    controlChannel._correspondingControlChannel = this;
+
+    this._provider.onSessionRequest(this._deviceId,
+                                    aUrl,
+                                    aPresentationId,
+                                    controlChannel);
+    controlChannel.notifyConnected();
+  },
+
+  terminate: function LCC_terminate(aPresentationId) {
+    log("LocalControlChannel - terminate aPresentationId="
+        + aPresentationId);
+
+    if (this._isOnTerminating) {
+      return;
+    }
+
+    // Create control channel for corresponding role directly.
+    let correspondingRole = this._role == Ci.nsIPresentationService.ROLE_CONTROLLER
+                          ? Ci.nsIPresentationService.ROLE_RECEIVER
+                          : Ci.nsIPresentationService.ROLE_CONTROLLER;
+    let controlChannel = new LocalControlChannel(this._provider,
+                                                 this._deviceId,
+                                                 correspondingRole);
+    // Prevent the termination recursion.
+    controlChannel._isOnTerminating = true;
+
+    // Set up the corresponding control channels for both controller and receiver.
+    this._correspondingControlChannel = controlChannel;
+    controlChannel._correspondingControlChannel = this;
+
+    this._provider.onTerminateRequest(this._deviceId,
+                                      aPresentationId,
+                                      controlChannel,
+                                      this._role == Ci.nsIPresentationService.ROLE_RECEIVER);
+    controlChannel.notifyConnected();
+  },
+
+  disconnect: function LCC_disconnect(aReason) {
+    log("LocalControlChannel - disconnect aReason=" + aReason);
+
+    if (this._isOnDisconnecting) {
+      return;
+    }
+
+    this._pendingOffer = null;
+    this._pendingCandidate = null;
+    this._pendingConnected = false;
+
+    // this._pendingDisconnect is a nsresult.
+    // If it is null, it means no pending disconnect.
+    // If it is NS_OK, it means this control channel is disconnected normally.
+    // If it is other nsresult value, it means this control channel is
+    // disconnected abnormally.
+
+    // Remote endpoint closes the control channel with abnormal reason.
+    if (aReason == Cr.NS_OK &&
+        this._pendingDisconnect != null &&
+        this._pendingDisconnect != Cr.NS_OK) {
+      aReason = this._pendingDisconnect;
+    }
+
+    if (!this._listener) {
+      this._pendingDisconnect = aReason;
+      return;
+    }
+
+    this._isOnDisconnecting = true;
+    this._correspondingControlChannel.disconnect(aReason);
+    this._listener.notifyDisconnected(aReason);
+  },
+
+  reconnect: function LCC_reconnect(aPresentationId, aUrl) {
+    log("1-UA on Android doesn't support reconnect.");
+    throw Cr.NS_ERROR_FAILURE;
+  },
+
+  classID: Components.ID("{c9be9450-e5c7-4294-a287-376971b017fd}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+};
+
+function ChromecastRemoteDisplayDevice(aProvider, aId, aName, aRole) {
+  this._provider = aProvider;
+  this._id       = aId;
+  this._name     = aName;
+  this._role     = aRole;
+}
+
+ChromecastRemoteDisplayDevice.prototype = {
+  _id:          null,
+  _name:        null,
+  _role:        null,
+  _provider:    null,
+  _ctrlChannel: null,
+
+  update: function CRDD_update(aName) {
+    this._name = aName || this._name;
+  },
+
+  // nsIPresentationDevice
+  get id() { return this._id; },
+
+  get name() { return this._name; },
+
+  get type() { return "chromecast"; },
+
+  establishControlChannel: function CRDD_establishControlChannel() {
+    this._ctrlChannel = new LocalControlChannel(this._provider,
+                                                this._id,
+                                                this._role);
+
+    if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) {
+      // Only connect to Chromecast for controller.
+      // Monitor the receiver being ready.
+      Services.obs.addObserver(this, TOPIC_PRESENTATION_VIEW_READY, true);
+
+      // Launch Chromecast service in Android.
+      Messaging.sendRequestForResult({
+        type: TOPIC_ANDROID_CAST_DEVICE_START,
+        id:   this.id
+      }).then(result => {
+        log("Chromecast is connected.");
+      }).catch(error => {
+        log("Can not connect to Chromecast.");
+        // If Chromecast can not be launched, remove the observer.
+        Services.obs.removeObserver(this, TOPIC_PRESENTATION_VIEW_READY);
+        this._ctrlChannel.disconnect(Cr.NS_ERROR_FAILURE);
+      });
+    } else {
+      // If establishControlChannel called from the receiver, we don't need to
+      // wait the 'presentation-view-ready' event.
+      this._ctrlChannel.notifyConnected();
+    }
+
+    return this._ctrlChannel;
+  },
+
+  disconnect: function CRDD_disconnect() {
+    // Disconnect from Chromecast.
+    Messaging.sendRequestForResult({
+      type: TOPIC_ANDROID_CAST_DEVICE_STOP,
+      id:   this.id
+    });
+  },
+
+  isRequestedUrlSupported: function CRDD_isRequestedUrlSupported(aUrl) {
+    let url = Cc["@mozilla.org/network/io-service;1"]
+                .getService(Ci.nsIIOService)
+                .newURI(aUrl, null, null);
+    return url.scheme == "http" || url.scheme == "https";
+  },
+
+  // nsIPresentationLocalDevice
+  get windowId() { return this._id; },
+
+  // nsIObserver
+  observe: function CRDD_observe(aSubject, aTopic, aData) {
+    if (aTopic == TOPIC_PRESENTATION_VIEW_READY) {
+      log("ChromecastRemoteDisplayDevice - observe: aTopic="
+          + aTopic + " data=" + aData);
+      if (this.windowId === aData) {
+        Services.obs.removeObserver(this, TOPIC_PRESENTATION_VIEW_READY);
+        this._ctrlChannel.notifyConnected();
+      }
+    }
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice,
+                                         Ci.nsIPresentationLocalDevice,
+                                         Ci.nsISupportsWeakReference,
+                                         Ci.nsIObserver]),
+};
+
+function AndroidCastDeviceProvider() {
+}
+
+AndroidCastDeviceProvider.prototype = {
+  _listener: null,
+  _deviceList: new Map(),
+
+  onSessionRequest: function APDP_onSessionRequest(aDeviceId,
+                                                   aUrl,
+                                                   aPresentationId,
+                                                   aControlChannel) {
+    log("AndroidCastDeviceProvider - onSessionRequest"
+        + " aDeviceId=" + aDeviceId);
+    let device = this._deviceList.get(aDeviceId);
+    let receiverDevice = new ChromecastRemoteDisplayDevice(this,
+                                                           device.id,
+                                                           device.name,
+                                                           Ci.nsIPresentationService.ROLE_RECEIVER);
+    this._listener.onSessionRequest(receiverDevice,
+                                    aUrl,
+                                    aPresentationId,
+                                    aControlChannel);
+  },
+
+  onTerminateRequest: function APDP_onTerminateRequest(aDeviceId,
+                                                       aPresentationId,
+                                                       aControlChannel,
+                                                       aIsFromReceiver) {
+    log("AndroidCastDeviceProvider - onTerminateRequest"
+        + " aDeviceId=" + aDeviceId
+        + " aPresentationId=" + aPresentationId
+        + " aIsFromReceiver=" + aIsFromReceiver);
+    let device = this._deviceList.get(aDeviceId);
+    this._listener.onTerminateRequest(device,
+                                      aPresentationId,
+                                      aControlChannel,
+                                      aIsFromReceiver);
+  },
+
+  // nsIPresentationDeviceProvider
+  set listener(aListener) {
+    this._listener = aListener;
+
+    // When unload this provider.
+    if (!this._listener) {
+      // remove observer
+      Services.obs.removeObserver(this, TOPIC_ANDROID_CAST_DEVICE_ADDED);
+      Services.obs.removeObserver(this, TOPIC_ANDROID_CAST_DEVICE_REMOVED);
+      return;
+    }
+
+    // Sync all device already found by Android.
+    Services.obs.notifyObservers(null, TOPIC_ANDROID_CAST_DEVICE_SYNCDEVICE, "");
+    // Observer registration
+    Services.obs.addObserver(this, TOPIC_ANDROID_CAST_DEVICE_ADDED, false);
+    Services.obs.addObserver(this, TOPIC_ANDROID_CAST_DEVICE_REMOVED, false);
+  },
+
+  get listener() {
+    return this._listener;
+  },
+
+  forceDiscovery: function APDP_forceDiscovery() {
+    // There is no API to do force discovery in Android SDK.
+  },
+
+  // nsIObserver
+  observe: function APDP_observe(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case TOPIC_ANDROID_CAST_DEVICE_ADDED: {
+        let deviceInfo = JSON.parse(aData);
+        let deviceId   = deviceInfo.uuid;
+
+        if (!this._deviceList.has(deviceId)) {
+          let device = new ChromecastRemoteDisplayDevice(this,
+                                                         deviceInfo.uuid,
+                                                         deviceInfo.friendlyName,
+                                                         Ci.nsIPresentationService.ROLE_CONTROLLER);
+          this._deviceList.set(device.id, device);
+          this._listener.addDevice(device);
+        } else {
+          let device = this._deviceList.get(deviceId);
+          device.update(deviceInfo.friendlyName);
+          this._listener.updateDevice(device);
+        }
+        break;
+      }
+      case TOPIC_ANDROID_CAST_DEVICE_REMOVED: {
+        let deviceId = aData;
+        let device   = this._deviceList.get(deviceId);
+        this._listener.removeDevice(device);
+        this._deviceList.delete(deviceId);
+        break;
+      }
+    }
+  },
+
+  classID: Components.ID("{7394f24c-dbc3-48c8-8a47-cd10169b7c6b}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsIPresentationDeviceProvider]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AndroidCastDeviceProvider]);
new file mode 100644
--- /dev/null
+++ b/dom/presentation/provider/AndroidCastDeviceProvider.manifest
@@ -0,0 +1,4 @@
+# AndroidCastDeviceProvider.js
+component {7394f24c-dbc3-48c8-8a47-cd10169b7c6b} AndroidCastDeviceProvider.js
+contract @mozilla.org/presentation-device/android-cast-device-provider;1 {7394f24c-dbc3-48c8-8a47-cd10169b7c6b}
+category presentation-device-provider AndroidCastDeviceProvider @mozilla.org/presentation-device/android-cast-device-provider;1
--- a/dom/presentation/provider/moz.build
+++ b/dom/presentation/provider/moz.build
@@ -17,19 +17,22 @@ UNIFIED_SOURCES += [
 ]
 
 EXTRA_JS_MODULES.presentation += [
     'ControllerStateMachine.jsm',
     'ReceiverStateMachine.jsm',
     'StateMachineHelper.jsm',
 ]
 
-# for TV 2.5 device backward capability
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
     EXTRA_COMPONENTS += [
+        # For android presentation device
+        'AndroidCastDeviceProvider.js',
+        'AndroidCastDeviceProvider.manifest',
+        # for TV 2.5 device backward capability
         'LegacyPresentationControlService.js',
         'LegacyProviders.manifest',
     ]
 
     UNIFIED_SOURCES += [
         'LegacyMDNSDeviceProvider.cpp',
     ]
 
--- a/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
@@ -19,16 +19,17 @@ import org.json.JSONObject;
 import org.mozilla.gecko.annotation.JNITarget;
 import org.mozilla.gecko.annotation.ReflectionTarget;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
 
 /**
  * Manages a list of GeckoMediaPlayers methods (i.e. Chromecast/Miracast). Routes messages
  * from Gecko to the correct caster based on the id of the display
  */
 public class MediaPlayerManager extends Fragment implements NativeEventListener {
     /**
@@ -83,34 +84,36 @@ public class MediaPlayerManager extends 
                                                                   "MediaPlayer:Start",
                                                                   "MediaPlayer:Stop",
                                                                   "MediaPlayer:Play",
                                                                   "MediaPlayer:Pause",
                                                                   "MediaPlayer:End",
                                                                   "MediaPlayer:Mirror",
                                                                   "MediaPlayer:Message",
                                                                   "AndroidCastDevice:Start",
-                                                                  "AndroidCastDevice:Stop");
+                                                                  "AndroidCastDevice:Stop",
+                                                                  "AndroidCastDevice:SyncDevice");
     }
 
     @Override
     @JNITarget
     public void onDestroy() {
         super.onDestroy();
         GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
                                                                     "MediaPlayer:Load",
                                                                     "MediaPlayer:Start",
                                                                     "MediaPlayer:Stop",
                                                                     "MediaPlayer:Play",
                                                                     "MediaPlayer:Pause",
                                                                     "MediaPlayer:End",
                                                                     "MediaPlayer:Mirror",
                                                                     "MediaPlayer:Message",
                                                                     "AndroidCastDevice:Start",
-                                                                    "AndroidCastDevice:Stop");
+                                                                    "AndroidCastDevice:Stop",
+                                                                    "AndroidCastDevice:SyncDevice");
     }
 
     // GeckoEventListener implementation
     @Override
     public void handleMessage(String event, final NativeJSObject message, final EventCallback callback) {
         debug(event);
         if (event.startsWith("MediaPlayer:")) {
             final GeckoMediaPlayer player = players.get(message.getString("id"));
@@ -140,26 +143,39 @@ public class MediaPlayerManager extends 
                 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 (event.startsWith("AndroidCastDevice:")) {
-            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 ("AndroidCastDevice:Start".equals(event)) {
+                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;
+                }
                 display.start(callback);
             } else if ("AndroidCastDevice:Stop".equals(event)) {
+                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;
+                }
                 display.stop(callback);
+            } else if ("AndroidCastDevice:SyncDevice".equals(event)) {
+                for (Map.Entry<String, GeckoPresentationDisplay> entry : displays.entrySet()) {
+                    GeckoPresentationDisplay display = entry.getValue();
+                    JSONObject json = display.toJSON();
+                    if (json == null) {
+                        break;
+                    }
+                    GeckoAppShell.notifyObservers("AndroidCastDevice:Added", json.toString());
+                }
             }
         }
     }
 
     private final MediaRouter.Callback callback =
         new MediaRouter.Callback() {
             @Override
             public void onRouteRemoved(MediaRouter router, RouteInfo route) {
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -1,9 +1,8 @@
-
 ; 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 file for the Fennec build.
 ;
 ; File format:
 ;
@@ -400,16 +399,18 @@
 @BINPATH@/components/BuiltinProviders.manifest
 @BINPATH@/components/PresentationControlService.js
 @BINPATH@/components/PresentationNetworkHelper.js
 @BINPATH@/components/PresentationNetworkHelper.manifest
 @BINPATH@/components/PresentationDataChannelSessionTransport.js
 @BINPATH@/components/PresentationDataChannelSessionTransport.manifest
 @BINPATH@/components/LegacyProviders.manifest
 @BINPATH@/components/LegacyPresentationControlService.js
+@BINPATH@/components/AndroidCastDeviceProvider.manifest
+@BINPATH@/components/AndroidCastDeviceProvider.js
 
 @BINPATH@/components/TVSimulatorService.js
 @BINPATH@/components/TVSimulatorService.manifest
 
 ; Modules
 @BINPATH@/modules/*
 
 ; Safe Browsing