bug 713503 - prefetch urls from known url shortening sites before gecko is running r=mfinkle
authorBrad Lassey <blassey@mozilla.com>
Mon, 09 Jan 2012 23:50:56 -0800
changeset 84775 952ff8dadb81d36ddb12b20d74061890d3e4c66d
parent 84774 b7ca6c0493dd25b78aefaf9582ce4d69546e0155
child 84776 6eec3b4644bd983359ce9f0e92ca5de42278d9bc
push id21873
push usermlamouri@mozilla.com
push dateWed, 18 Jan 2012 10:29:07 +0000
treeherdermozilla-central@7538f4d4697c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs713503
milestone12.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 713503 - prefetch urls from known url shortening sites before gecko is running r=mfinkle
mobile/android/base/App.java.in
mobile/android/base/GeckoApp.java
mobile/android/base/Makefile.in
--- a/mobile/android/base/App.java.in
+++ b/mobile/android/base/App.java.in
@@ -43,10 +43,24 @@ import org.mozilla.gecko.GeckoApp;
 public class App extends GeckoApp {
     public String getPackageName() {
 	return "@ANDROID_PACKAGE_NAME@";
     }
 
     public String getContentProcessName() {
         return "@MOZ_CHILD_PROCESS_NAME@";
     }
+
+    public String getDefaultUAString() {
+        return "Mozilla/5.0 (Android; Linux armv7l; rv:@MOZ_APP_VERSION@) Gecko/@UA_BUILDID@ Firefox/@MOZ_APP_VERSION@ Fennec/@MOZ_APP_VERSION@";
+    }
+
+    public String getUAStringForHost(String host) {
+        // With our standard UA String, we get a 200 response code and 
+        // client-side redirect from t.co. This slight tweak gives us a 
+        // 302 response code
+        if ("t.co".equals(host))
+            return "Mozilla/5.0 (Android; Linux armv7l; rv:@MOZ_APP_VERSION@) Gecko/@UA_BUILDID@ Firefox Mobile/@MOZ_APP_VERSION@";
+        return getDefaultUAString();
+    }
+
 };
 
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -93,16 +93,17 @@ abstract public class GeckoApp
 {
     private static final String LOGTAG = "GeckoApp";
 
     public static final String ACTION_ALERT_CLICK   = "org.mozilla.gecko.ACTION_ALERT_CLICK";
     public static final String ACTION_ALERT_CLEAR   = "org.mozilla.gecko.ACTION_ALERT_CLEAR";
     public static final String ACTION_WEBAPP        = "org.mozilla.gecko.WEBAPP";
     public static final String ACTION_DEBUG         = "org.mozilla.gecko.DEBUG";
     public static final String ACTION_BOOKMARK      = "org.mozilla.gecko.BOOKMARK";
+    public static final String ACTION_LOAD          = "org.mozilla.gecko.LOAD";
     public static final String SAVED_STATE_URI      = "uri";
     public static final String SAVED_STATE_TITLE    = "title";
     public static final String SAVED_STATE_VIEWPORT = "viewport";
     public static final String SAVED_STATE_SCREEN   = "screen";
     public static final String SAVED_STATE_SESSION  = "session";
 
     private LinearLayout mMainLayout;
     private RelativeLayout mGeckoLayout;
@@ -1441,19 +1442,33 @@ abstract public class GeckoApp
             showAboutHome();
         }
 
         mAppContext = this;
 
         if (sGREDir == null)
             sGREDir = new File(this.getApplicationInfo().dataDir);
 
-        prefetchDNS(intent.getData());
+        String passedUri = mLastUri;
 
-        sGeckoThread = new GeckoThread(intent, mLastUri, mRestoreSession);
+        Uri data = intent.getData();
+        if (data != null && "http".equals(data.getScheme()) &&
+            isHostOnPrefetchWhitelist(data.getHost())) {
+            Intent copy = new Intent(intent);
+            copy.setAction(ACTION_LOAD);
+            GeckoAppShell.getHandler().post(new RedirectorRunnable(copy));
+            // We're going to handle this uri with the redirector, so setting
+            // the action to MAIN and clearing the uri data prevents us from
+            // loading it twice
+            intent.setAction(Intent.ACTION_MAIN);
+            intent.setData(null);
+            passedUri = null;
+        }
+
+        sGeckoThread = new GeckoThread(intent, passedUri, mRestoreSession);
         if (!ACTION_DEBUG.equals(intent.getAction()) &&
             checkAndSetLaunchState(LaunchState.Launching, LaunchState.Launched))
             sGeckoThread.start();
 
         super.onCreate(savedInstanceState);
 
         setContentView(R.layout.gecko_app);
 
@@ -1641,30 +1656,115 @@ abstract public class GeckoApp
         // Some phones (eg. nexus S) need at least a 8x16 preview size
         mMainLayout.addView(cameraView, new AbsoluteLayout.LayoutParams(8, 16, 0, 0));
     }
 
     public void disableCameraView() {
         mMainLayout.removeView(cameraView);
     }
 
+    abstract public String getDefaultUAString();
+    abstract public String getUAStringForHost(String host);
+
+    class RedirectorRunnable implements Runnable {
+        Intent mIntent;
+        RedirectorRunnable(Intent intent) {
+            mIntent = intent;
+        }
+        public void run() {
+            HttpURLConnection connection = null;
+            try {
+                // this class should only be initialized with an intent with non-null data
+                URL url = new URL(mIntent.getData().toString());
+                // data url should have an http scheme
+                connection = (HttpURLConnection) url.openConnection();
+                connection.setRequestProperty("User-Agent", getUAStringForHost(url.getHost()));
+                connection.setInstanceFollowRedirects(false);
+                connection.setRequestMethod("GET");
+                connection.connect();
+                int code = connection.getResponseCode();
+                if (code >= 300 && code < 400) {
+                    String location = connection.getHeaderField("Location");
+                    Uri data;
+                    if (location != null &&
+                        (data = Uri.parse(location)) != null &&
+                        !"about".equals(data.getScheme()) && 
+                        !"chrome".equals(data.getScheme())) {
+                        mIntent.setData(data);
+                        mLastUri = mLastTitle = location;
+                    } else {
+                        mIntent.putExtra("prefetched", 1);
+                    }
+                } else {
+                    mIntent.putExtra("prefetched", 1);
+                }
+            } catch (IOException ioe) {
+                Log.i(LOGTAG, "exception trying to pre-fetch redirected url", ioe);
+                mIntent.putExtra("prefetched", 1);
+            } catch (Exception e) {
+                Log.w(LOGTAG, "unexpected exception, passing url directly to Gecko but we should explicitly catch this", e);
+                mIntent.putExtra("prefetched", 1);
+            } finally {
+                if (connection != null)
+                    connection.disconnect();
+            }
+            mMainHandler.postAtFrontOfQueue(new Runnable() {
+                public void run() {
+                    onNewIntent(mIntent);
+                }
+            });
+        }
+    }
+
+    private final String kPrefetchWhiteListArray[] = new String[] { 
+        "t.co",
+        "bit.ly",
+        "moz.la",
+        "aje.me",
+        "facebook.com",
+        "goo.gl",
+        "tinyurl.com"
+    };
+    
+    private final CopyOnWriteArrayList<String> kPrefetchWhiteList =
+        new CopyOnWriteArrayList<String>(kPrefetchWhiteListArray);
+
+    private boolean isHostOnPrefetchWhitelist(String host) {
+        return kPrefetchWhiteList.contains(host);
+    }
+
     @Override
     protected void onNewIntent(Intent intent) {
         Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - onNewIntent");
 
         if (checkLaunchState(LaunchState.GeckoExiting)) {
             // We're exiting and shouldn't try to do anything else just incase
             // we're hung for some reason we'll force the process to exit
             System.exit(0);
             return;
         }
+
+        if (checkLaunchState(LaunchState.Launched)) {
+            Uri data = intent.getData();
+            Bundle bundle = intent.getExtras();
+            // if the intent has data (i.e. a URI to be opened) and the scheme
+            // is either http, we'll prefetch it, which means warming
+            // up the radio and DNS cache by connecting and parsing the redirect
+            // if the return code is between 300 and 400
+            if (data != null && 
+                "http".equals(data.getScheme()) &&
+                (bundle == null || bundle.getInt("prefetched", 0) != 1) &&
+                isHostOnPrefetchWhitelist(data.getHost())) {
+                GeckoAppShell.getHandler().post(new RedirectorRunnable(intent));
+                return;
+            }
+        }
         final String action = intent.getAction();
         if (ACTION_DEBUG.equals(action) &&
             checkAndSetLaunchState(LaunchState.Launching, LaunchState.WaitForDebugger)) {
-
             mMainHandler.postDelayed(new Runnable() {
                 public void run() {
                     Log.i(LOGTAG, "Launching from debug intent after 5s wait");
                     setLaunchState(LaunchState.Launching);
                     sGeckoThread.start();
                 }
             }, 1000 * 5 /* 5 seconds */);
             Log.i(LOGTAG, "Intent : ACTION_DEBUG - waiting 5s before launching");
@@ -1672,18 +1772,22 @@ abstract public class GeckoApp
         }
         if (checkLaunchState(LaunchState.WaitForDebugger) || intent == getIntent())
             return;
 
         if (Intent.ACTION_MAIN.equals(action)) {
             Log.i(LOGTAG, "Intent : ACTION_MAIN");
             GeckoAppShell.sendEventToGecko(new GeckoEvent(""));
         }
+        else if (ACTION_LOAD.equals(action)) {
+            String uri = intent.getDataString();
+            loadUrl(uri, AwesomeBar.Type.EDIT);
+            Log.i(LOGTAG,"onNewIntent: " + uri);
+        }
         else if (Intent.ACTION_VIEW.equals(action)) {
-            prefetchDNS(intent.getData());
             String uri = intent.getDataString();
             GeckoAppShell.sendEventToGecko(new GeckoEvent(uri));
             Log.i(LOGTAG,"onNewIntent: " + uri);
         }
         else if (ACTION_WEBAPP.equals(action)) {
             String uri = intent.getStringExtra("args");
             GeckoAppShell.sendEventToGecko(new GeckoEvent(uri));
             Log.i(LOGTAG,"Intent : WEBAPP - " + uri);
@@ -2329,32 +2433,16 @@ abstract public class GeckoApp
     private void connectGeckoLayerClient() {
         if (mPlaceholderLayerClient != null)
             mPlaceholderLayerClient.destroy();
 
         LayerController layerController = getLayerController();
         layerController.setLayerClient(mSoftwareLayerClient);
     }
 
-    private void prefetchDNS(final Uri u) {
-        // resolving the host here starts up the radio
-        // and may prime the dns cache.  See
-        // http://www.stevesouders.com/blog/2011/09/21/making-a-mobile-connection/
-        // for more information.
-        new Thread(new Runnable() {
-                public void run() {
-                    try {
-                        Log.i(LOGTAG,"resolving: " + u.getHost());
-                        InetAddress.getByName(u.getHost());
-                    } catch (Exception e) {
-                        // we really don't care.
-                    }
-                }
-            }, "DNSPrefetcher Thread").start();
-    }
 }
 
 class PluginLayoutParams extends AbsoluteLayout.LayoutParams
 {
     private static final int MAX_DIMENSION = 2048;
     private static final String LOGTAG = "GeckoApp.PluginLayoutParams";
 
     private int mOriginalX;
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -146,26 +146,29 @@ MIN_CPU_VERSION=7
 else
 MIN_CPU_VERSION=5
 endif
 
 ifeq (,$(ANDROID_VERSION_CODE))
 ANDROID_VERSION_CODE=$(shell $(PYTHON) $(topsrcdir)/toolkit/xre/make-platformini.py --print-buildid | cut -c1-10)
 endif
 
+UA_BUILDID=$(shell $(PYTHON) $(topsrcdir)/toolkit/xre/make-platformini.py --print-buildid | cut -c1-8)
+
 DEFINES += \
   -DANDROID_PACKAGE_NAME=$(ANDROID_PACKAGE_NAME) \
   -DMOZ_APP_DISPLAYNAME="$(MOZ_APP_DISPLAYNAME)" \
   -DMOZ_APP_NAME=$(MOZ_APP_NAME) \
   -DMOZ_APP_VERSION=$(MOZ_APP_VERSION) \
   -DMOZ_CHILD_PROCESS_NAME=$(MOZ_CHILD_PROCESS_NAME) \
   -DMOZ_MIN_CPU_VERSION=$(MIN_CPU_VERSION) \
   -DMOZ_CRASHREPORTER=$(MOZ_CRASHREPORTER) \
   -DANDROID_VERSION_CODE=$(ANDROID_VERSION_CODE) \
   -DMOZILLA_OFFICIAL=$(MOZILLA_OFFICIAL) \
+  -DUA_BUILDID=$(UA_BUILDID) \
   $(NULL)
 
 GARBAGE += \
   AndroidManifest.xml  \
   classes.dex  \
   $(FENNEC_PP_JAVA_FILES) \
   $(SYNC_PP_JAVA_FILES) \
   gecko.ap_  \
@@ -549,17 +552,17 @@ classes.dex: $(FENNEC_JAVA_FILES) $(FENN
 	$(DX) --dex --output=$@ classes
 
 PP_RES_XML=$(SYNC_PP_RES_XML)
 
 $(PP_RES_XML): $(subst res/,$(srcdir)/resources/, $(PP_RES_XML).in)
 	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
              $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $< > $@
 
-AndroidManifest.xml $(FENNEC_PP_JAVA_FILES) $(SYNC_PP_JAVA_FILES) package-name.txt: % : %.in
+AndroidManifest.xml $(FENNEC_PP_JAVA_FILES) $(SYNC_PP_JAVA_FILES) package-name.txt: % : %.in Makefile.in
 	mkdir -p db sync/repositories/android
 	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
              $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $< > $@
 
 res/drawable/icon.png: $(MOZ_APP_ICON)
 	$(NSINSTALL) -D res/drawable
 	cp $(ICON_PATH) $@