diff --git a/mobile/android/base/AndroidManifest.xml.in b/mobile/android/base/AndroidManifest.xml.in
index 250bbb7ffed0..acf0e5c26367 100644
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -60,6 +60,11 @@
#endif
+#ifdef MOZ_ANDROID_TAB_QUEUE
+
+
+#endif
+
#ifdef MOZ_ANDROID_BEAM
@@ -165,6 +170,11 @@
+#ifndef MOZ_ANDROID_TAB_QUEUE
+
+
+
@@ -187,7 +197,7 @@
-
+#endif
@@ -239,6 +249,41 @@
+#ifdef MOZ_ANDROID_TAB_QUEUE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#endif
+
diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd
index 0e942507f166..b0c870fcbcfb 100644
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -184,6 +184,9 @@
+
+
+
diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build
index 836bd85d83d2..0049e806b2ea 100644
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -421,6 +421,8 @@ gbjar.sources += [
'SuggestClient.java',
'SurfaceBits.java',
'Tab.java',
+ 'tabqueue/TabQueueDispatcher.java',
+ 'tabqueue/TabQueueService.java',
'Tabs.java',
'tabs/PrivateTabsPanel.java',
'tabs/TabCurve.java',
diff --git a/mobile/android/base/resources/values/styles.xml b/mobile/android/base/resources/values/styles.xml
index afac27a2d0c5..8ce3a66d9653 100644
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -894,4 +894,5 @@
- 8dp
+
diff --git a/mobile/android/base/strings.xml.in b/mobile/android/base/strings.xml.in
index 8cf70e5f15df..74722e51f360 100644
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -239,6 +239,9 @@
&pref_tab_queue_title;
&pref_tab_queue_summary;
+ &tab_queue_toast_message;
+ &tab_queue_toast_action;
+
&pref_about_firefox;
&pref_vendor_faqs;
&pref_vendor_feedback;
diff --git a/mobile/android/base/tabqueue/TabQueueDispatcher.java b/mobile/android/base/tabqueue/TabQueueDispatcher.java
new file mode 100644
index 000000000000..865ca2915718
--- /dev/null
+++ b/mobile/android/base/tabqueue/TabQueueDispatcher.java
@@ -0,0 +1,81 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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.tabqueue;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.sync.setup.activities.WebURLFinder;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * This class takes over external url loads (Intent.VIEW) from the BrowserApp class. It determines if
+ * the tab queue functionality is enabled and forwards the intent to the TabQueueService to process if it is.
+ *
+ * If the tab queue functionality is not enabled then it forwards the intent to BrowserApp to handle as normal.
+ */
+public class TabQueueDispatcher extends Locales.LocaleAwareActivity {
+ private static final String LOGTAG = "Gecko" + TabQueueDispatcher.class.getSimpleName();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+
+ // For the moment lets exit early and start fennec as normal if we're not in nightly with
+ // the tab queue build flag.
+ if (!AppConstants.MOZ_ANDROID_TAB_QUEUE) {
+ loadNormally(intent);
+ finish();
+ }
+
+ // The URL is usually hiding somewhere in the extra text. Extract it.
+ final String dataString = intent.getDataString();
+ if (TextUtils.isEmpty(dataString)) {
+ abortDueToNoURL(dataString);
+ return;
+ }
+
+ // TODO: This code is shared with ShareDialog - we should extract this to a helper class.
+ final String pageUrl = new WebURLFinder(dataString).bestWebURL();
+ if (TextUtils.isEmpty(pageUrl)) {
+ abortDueToNoURL(dataString);
+ return;
+ }
+
+ showToast(intent);
+ }
+
+ private void showToast(Intent intent) {
+ intent.setClass(getApplicationContext(), TabQueueService.class);
+ startService(intent);
+ finish();
+ }
+
+ /**
+ * Start fennec with the supplied intent.
+ */
+ private void loadNormally(Intent intent) {
+ intent.setClass(getApplicationContext(), BrowserApp.class);
+ startActivity(intent);
+ finish();
+ }
+
+ /**
+ * Abort as we were started with no URL.
+ * @param dataString
+ */
+ private void abortDueToNoURL(String dataString) {
+ // TODO: Lets decide what to do here in bug 1134148
+ Log.w(LOGTAG, "Unable to process tab queue insertion. No URL found! - passed data string: " + dataString);
+ finish();
+ }
+}
diff --git a/mobile/android/base/tabqueue/TabQueueService.java b/mobile/android/base/tabqueue/TabQueueService.java
new file mode 100644
index 000000000000..cfa755b83c84
--- /dev/null
+++ b/mobile/android/base/tabqueue/TabQueueService.java
@@ -0,0 +1,189 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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.tabqueue;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.TextView;
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.mozglue.ContextUtils;
+
+
+/**
+ * On launch this Service displays a View over the currently running process with an action to open the url in Fennec
+ * immediately. If the user takes no action, allowing the runnable to be processed after the specified
+ * timeout (TOAST_TIMEOUT), the url is added to a file which is then read in Fennec on next launch, this allows the
+ * user to quickly queue urls to open without having to open Fennec each time. If the Service receives an Intent whilst
+ * the created View is still active, the old url is immediately processed and the View is re-purposed with the new
+ * Intent data.
+ *
+ * The SYSTEM_ALERT_WINDOW permission is used to allow us to insert a View from this Service which responds to user
+ * interaction, whilst still allowing whatever is in the background to be seen and interacted with.
+ *
+ * Using an Activity to do this doesn't seem to work as there's an issue to do with the native android intent resolver
+ * dialog not being hidden when the toast is shown. Using an IntentService instead of a Service doesn't work as
+ * each new Intent received kicks off the IntentService lifecycle anew which means that a new View is created each time,
+ * meaning that we can't quickly queue the current data and re-purpose the View. The asynchronous nature of the
+ * IntentService is another prohibitive factor.
+ *
+ * General approach taken is similar to the FB chat heads functionality:
+ * http://stackoverflow.com/questions/15975988/what-apis-in-android-is-facebook-using-to-create-chat-heads
+ */
+public class TabQueueService extends Service {
+ private static final String LOGTAG = "Gecko" + TabQueueService.class.getSimpleName();
+ private static final long TOAST_TIMEOUT = 3000;
+ private WindowManager windowManager;
+ private View toastLayout;
+ private Button openNowButton;
+ private Handler tabQueueHandler;
+ private WindowManager.LayoutParams toastLayoutParams;
+ private volatile StopServiceRunnable stopServiceRunnable;
+ private HandlerThread handlerThread;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ // Not used
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ handlerThread = new HandlerThread("TabQueueHandlerThread");
+ handlerThread.start();
+ tabQueueHandler = new Handler(handlerThread.getLooper());
+
+ windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
+
+ LayoutInflater layoutInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
+ toastLayout = layoutInflater.inflate(R.layout.button_toast, null);
+
+ final Resources resources = getResources();
+
+ TextView messageView = (TextView) toastLayout.findViewById(R.id.toast_message);
+ messageView.setText(resources.getText(R.string.tab_queue_toast_message));
+
+ openNowButton = (Button) toastLayout.findViewById(R.id.toast_button);
+ openNowButton.setText(resources.getText(R.string.tab_queue_toast_action));
+
+ toastLayoutParams = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.WRAP_CONTENT,
+ WindowManager.LayoutParams.WRAP_CONTENT,
+ WindowManager.LayoutParams.TYPE_PHONE,
+ WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
+ WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSLUCENT);
+
+ toastLayoutParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
+ }
+
+ @Override
+ public int onStartCommand(final Intent intent, final int flags, final int startId) {
+ if (stopServiceRunnable != null) {
+ // If we're already displaying a toast, keep displaying it but store the previous url.
+ // The open button will refer to the most recently opened link.
+ tabQueueHandler.removeCallbacks(stopServiceRunnable);
+ stopServiceRunnable.run(false);
+ } else {
+ windowManager.addView(toastLayout, toastLayoutParams);
+ }
+
+ stopServiceRunnable = new StopServiceRunnable(startId) {
+ @Override
+ public void onRun() {
+ addUrlToTabQueue(intent);
+ stopServiceRunnable = null;
+ }
+ };
+
+ openNowButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ tabQueueHandler.removeCallbacks(stopServiceRunnable);
+ stopServiceRunnable = null;
+
+
+ Intent forwardIntent = new Intent(intent);
+ forwardIntent.setClass(getApplicationContext(), BrowserApp.class);
+ forwardIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(forwardIntent);
+
+ removeView();
+ }
+ });
+
+ tabQueueHandler.postDelayed(stopServiceRunnable, TOAST_TIMEOUT);
+
+ return START_FLAG_REDELIVERY;
+ }
+
+ private void removeView() {
+ windowManager.removeView(toastLayout);
+ }
+
+ private void addUrlToTabQueue(Intent intentParam) {
+ if (intentParam == null) {
+ // This should never happen, but let's return silently instead of crash if it does.
+ Log.w(LOGTAG, "Error adding URL to tab queue - invalid intent passed in.");
+ return;
+ }
+ final ContextUtils.SafeIntent intent = new ContextUtils.SafeIntent(intentParam);
+ final String intentData = intent.getDataString();
+
+ // TODO Add url to tab queue here - bug 1134235
+ Log.d(LOGTAG, "Adding URL to tab queue: " + intentData);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ tabQueueHandler = null;
+ handlerThread.quit();
+ }
+
+ /**
+ * A modified Runnable which additionally removes the view from the window view hierarchy and stops the service
+ * when run, unless explicitly instructed not to.
+ */
+ private abstract class StopServiceRunnable implements Runnable {
+
+ private final int startId;
+
+ public StopServiceRunnable(int startId) {
+ this.startId = startId;
+ }
+
+ public void run(boolean shouldStopService) {
+ onRun();
+
+ if (shouldStopService) {
+ removeView();
+ }
+
+ stopSelfResult(startId);
+ }
+
+ public void run() {
+ run(true);
+ }
+
+ public abstract void onRun();
+ }
+}
\ No newline at end of file