Merge "Fix omnibox ordering"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 123e5c8..77e25a3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -646,8 +646,25 @@
     <string name="pref_data_title">Bandwidth Management</string>
     <!-- Title for search preloading [CHAR LIMIT=40] -->
     <string name="pref_data_preload_title">Search result preloading</string>
+    <!-- Search preloading options [CHAR LIMIT=20] -->
+    <string-array name="pref_data_preload_choices">
+        <item>Never</item>
+        <item>Only on Wi-Fi</item>
+        <item>Always</item>
+    </string-array>
+    <!-- Do not translate -->
+    <string name="pref_data_preload_value_never" translatable="false">NEVER</string>
+    <string name="pref_data_preload_value_wifi_only" translatable="false">WIFI_ONLY</string>
+    <string name="pref_data_preload_value_always" translatable="false">ALWAYS</string>
+    <string-array name="pref_data_preload_values" translatable="false">
+        <item><xliff:g>@string/pref_data_preload_value_never</xliff:g></item>
+        <item><xliff:g>@string/pref_data_preload_value_wifi_only</xliff:g></item>
+        <item><xliff:g>@string/pref_data_preload_value_always</xliff:g></item>
+    </string-array>
     <!-- Summary for search preloading [CHAR LIMIT=80] -->
     <string name="pref_data_preload_summary">Allow the browser to preload high confidence search results in the background</string>
+    <!-- Title of dialog for search preloading [CHAR LIMIT=40] -->
+    <string name="pref_data_preload_dialogtitle">Search result preloading</string>
     <!-- Title for a dialog displayed when the browser has a data connectivity
             problem -->
     <string name="browserFrameNetworkErrorLabel">Data connectivity problem</string>
diff --git a/res/xml/bandwidth_preferences.xml b/res/xml/bandwidth_preferences.xml
index 0767ac5..8a31304 100644
--- a/res/xml/bandwidth_preferences.xml
+++ b/res/xml/bandwidth_preferences.xml
@@ -16,11 +16,14 @@
 
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <CheckBoxPreference
-        android:key="preload_enabled"
+    <ListPreference
+        android:key="preload_when"
         android:title="@string/pref_data_preload_title"
         android:summary="@string/pref_data_preload_summary"
-        android:defaultValue="false" />
+        android:defaultValue="@string/pref_data_preload_value_wifi_only"
+        android:entries="@array/pref_data_preload_choices"
+        android:entryValues="@array/pref_data_preload_values"
+        android:dialogTitle="@string/pref_data_preload_dialogtitle" />
 
     <CheckBoxPreference
         android:key="load_images"
diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java
index 11316ff..ce8a9c1 100644
--- a/src/com/android/browser/BrowserSettings.java
+++ b/src/com/android/browser/BrowserSettings.java
@@ -780,7 +780,20 @@
     // getter/setters for bandwidth_preferences.xml
     // -----------------------------
 
-    public boolean isPreloadEnabled() {
-        return mPrefs.getBoolean(PREF_DATA_PRELOAD, false);
+    public static String getPreloadOnWifiOnlyPreferenceString(Context context) {
+        return context.getResources().getString(R.string.pref_data_preload_value_wifi_only);
     }
+
+    public static String getPreloadAlwaysPreferenceString(Context context) {
+        return context.getResources().getString(R.string.pref_data_preload_value_always);
+    }
+
+    private String getDefaultPreloadSetting() {
+        return getPreloadOnWifiOnlyPreferenceString(mContext);
+    }
+
+    public String getPreloadEnabled() {
+        return mPrefs.getString(PREF_DATA_PRELOAD, getDefaultPreloadSetting());
+    }
+
 }
diff --git a/src/com/android/browser/Controller.java b/src/com/android/browser/Controller.java
index b7cdcd4..a3887f5 100644
--- a/src/com/android/browser/Controller.java
+++ b/src/com/android/browser/Controller.java
@@ -18,6 +18,7 @@
 
 import android.app.Activity;
 import android.app.DownloadManager;
+import android.app.PendingIntent;
 import android.app.SearchManager;
 import android.content.ClipboardManager;
 import android.content.ContentProvider;
@@ -93,6 +94,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Pattern;
 
 /**
  * Controller for browser
@@ -761,6 +763,7 @@
     public void stopLoading() {
         mLoadStopped = true;
         Tab tab = mTabControl.getCurrentTab();
+        tab.clearPageLoadCompleteListener();
         WebView w = getCurrentTopWebView();
         w.stopLoading();
         mUi.onPageStopped(tab);
@@ -2160,7 +2163,8 @@
         final PreloadedTabControl tabControl = urlData.getPreloadedTab();
         final String sbQuery = urlData.getSearchBoxQueryToSubmit();
         if (sbQuery != null) {
-            if (!tabControl.searchBoxSubmit(sbQuery, urlData.mUrl, urlData.mHeaders)) {
+            if (!tabControl.searchBoxSubmit(sbQuery, urlData.mUrl, urlData.mHeaders,
+                    urlData.getOnLoadCompletePendingIntent())) {
                 // Could not submit query. Fallback to regular tab creation
                 tabControl.destroy();
                 return null;
@@ -2178,6 +2182,18 @@
         mTabControl.addPreloadedTab(t);
         addTab(t);
         setActiveTab(t);
+        if (sbQuery == null) {
+            // if the searchbox query is set, the load complete notification is handled within
+            // the preloaded tab controller.
+            if (t.inPageLoad()) {
+                requestLoadCompleteNotification(urlData.mOnLoadCompletePendingIntent, t,
+                        urlData.mUrl, true, true);
+            } else {
+                // the page is already fully loaded
+                IntentHandler.sendPageLoadCompletePendingIntent(mActivity,
+                        urlData.mOnLoadCompletePendingIntent, true, true);
+            }
+        }
         return t;
     }
 
@@ -2371,10 +2387,26 @@
                 // this isn't called for preloaded tabs
             } else {
                 loadUrl(t, data.mUrl, data.mHeaders);
+                requestLoadCompleteNotification(data.mOnLoadCompletePendingIntent, t, data.mUrl,
+                        null, null);
             }
         }
     }
 
+    private void requestLoadCompleteNotification(final PendingIntent loadCompletePendingIntent,
+            Tab t, String forUrl, final Boolean preloaded, final Boolean preloadSuccess) {
+        if (loadCompletePendingIntent != null) {
+            Pattern urlMatch = Pattern.compile(Pattern.quote(forUrl));
+            t.setOnPageLoadCompleteListener(urlMatch, new Tab.OnPageLoadCompleteListener() {
+                @Override
+                public void onPageLoadComplete() {
+                    IntentHandler.sendPageLoadCompletePendingIntent(mActivity,
+                            loadCompletePendingIntent, preloaded, preloadSuccess);
+                }
+            });
+        }
+    }
+
     @Override
     public void onUserCanceledSsl(Tab tab) {
         // TODO: Figure out the "right" behavior
diff --git a/src/com/android/browser/IntentHandler.java b/src/com/android/browser/IntentHandler.java
index cc6b57c..178ba62 100644
--- a/src/com/android/browser/IntentHandler.java
+++ b/src/com/android/browser/IntentHandler.java
@@ -18,6 +18,8 @@
 package com.android.browser;
 
 import android.app.Activity;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
 import android.app.SearchManager;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -30,6 +32,7 @@
 import android.provider.MediaStore;
 import android.speech.RecognizerResultsIntent;
 import android.text.TextUtils;
+import android.util.Log;
 import android.util.Patterns;
 
 import com.android.browser.search.SearchEngine;
@@ -50,6 +53,22 @@
     // "source" parameter for Google search from unknown source
     final static String GOOGLE_SEARCH_SOURCE_UNKNOWN = "unknown";
 
+    // Pending intent extra attached to browser intents that is broadcast when the page load
+    // completes.
+    // TODO move to android.provider.Browser & make public?
+    private static final String EXTRA_LOAD_COMPLETE_PENDINGINTENT = "load_complete_intent";
+    // extra attached to intent received via EXTRA_LOAD_COMPLETE_PENDINGINTENT indicating the
+    // time at which the load completed.
+    public static final String EXTRA_LOAD_COMPLETION_TIME = "completets";
+    // extra attached to intent received via EXTRA_LOAD_COMPLETE_PENDINGINTENT indicating if
+    // preloading was attempted.
+    public static final String EXTRA_PREFETCH_ATTEMPTED = "prefattempt";
+    // extra attached to intent received via EXTRA_LOAD_COMPLETE_PENDINGINTENT indicating if
+    // preloading succeeded.
+    public static final String EXTRA_PREFETCH_SUCCESS = "prefsuccess";
+
+
+
     /* package */ static final UrlData EMPTY_URL_DATA = new UrlData(null);
 
     private Activity mActivity;
@@ -224,11 +243,39 @@
         }
     }
 
+    /**
+     * Send a pending intent received in a page view intent. This should be called when the page
+     * has finished loading.
+     *
+     * @param prefetchAttempted Indicates if prefetching was attempted, {@code null} if prefetching
+     *      was not requested or is disabled.
+     * @param prefetchSucceeded Indicates if prefetching succeeded, {@code null} if prefetching
+     *      was not requested or is disabled.
+     */
+    public static void sendPageLoadCompletePendingIntent(Context context, PendingIntent pi,
+            Boolean prefetchAttempted, Boolean prefetchSucceeded) {
+        if (pi == null) return;
+        Intent fillIn = new Intent();
+        fillIn.putExtra(EXTRA_LOAD_COMPLETION_TIME, System.currentTimeMillis());
+        if (prefetchAttempted != null) {
+            fillIn.putExtra(EXTRA_PREFETCH_ATTEMPTED, prefetchAttempted.booleanValue());
+        }
+        if (prefetchSucceeded != null) {
+            fillIn.putExtra(EXTRA_PREFETCH_SUCCESS, prefetchSucceeded.booleanValue());
+        }
+        try {
+            pi.send(context, Activity.RESULT_OK, fillIn);
+        } catch (CanceledException e) {
+            // ignore
+        }
+    }
+
     protected static UrlData getUrlDataFromIntent(Intent intent) {
         String url = "";
         Map<String, String> headers = null;
         PreloadedTabControl preloaded = null;
         String preloadedSearchBoxQuery = null;
+        PendingIntent loadCompletePendingIntent = null;
         if (intent != null
                 && (intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
             final String action = intent.getAction();
@@ -253,6 +300,10 @@
                             PreloadRequestReceiver.EXTRA_SEARCHBOX_SETQUERY);
                     preloaded = Preloader.getInstance().getPreloadedTab(id);
                 }
+                if (intent.hasExtra(EXTRA_LOAD_COMPLETE_PENDINGINTENT)) {
+                    loadCompletePendingIntent =
+                        intent.getParcelableExtra(EXTRA_LOAD_COMPLETE_PENDINGINTENT);
+                }
             } else if (Intent.ACTION_SEARCH.equals(action)
                     || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
                     || Intent.ACTION_WEB_SEARCH.equals(action)) {
@@ -277,7 +328,8 @@
                 }
             }
         }
-        return new UrlData(url, headers, intent, preloaded, preloadedSearchBoxQuery);
+        return new UrlData(url, headers, intent, preloaded, preloadedSearchBoxQuery,
+                loadCompletePendingIntent);
     }
 
     /**
@@ -362,6 +414,7 @@
         final Intent mVoiceIntent;
         final PreloadedTabControl mPreloadedTab;
         final String mSearchBoxQueryToSubmit;
+        final PendingIntent mOnLoadCompletePendingIntent;
 
         UrlData(String url) {
             this.mUrl = url;
@@ -369,14 +422,16 @@
             this.mVoiceIntent = null;
             this.mPreloadedTab = null;
             this.mSearchBoxQueryToSubmit = null;
+            this.mOnLoadCompletePendingIntent = null;
         }
 
         UrlData(String url, Map<String, String> headers, Intent intent) {
-            this(url, headers, intent, null, null);
+            this(url, headers, intent, null, null, null);
         }
 
         UrlData(String url, Map<String, String> headers, Intent intent,
-                PreloadedTabControl preloaded, String searchBoxQueryToSubmit) {
+                PreloadedTabControl preloaded, String searchBoxQueryToSubmit,
+                PendingIntent onLoadCompletePendingIntent) {
             this.mUrl = url;
             this.mHeaders = headers;
             if (RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS
@@ -387,6 +442,7 @@
             }
             this.mPreloadedTab = preloaded;
             this.mSearchBoxQueryToSubmit = searchBoxQueryToSubmit;
+            this.mOnLoadCompletePendingIntent = onLoadCompletePendingIntent;
         }
 
         boolean isEmpty() {
@@ -404,6 +460,10 @@
         String getSearchBoxQueryToSubmit() {
             return mSearchBoxQueryToSubmit;
         }
+
+        PendingIntent getOnLoadCompletePendingIntent() {
+            return mOnLoadCompletePendingIntent;
+        }
     }
 
 }
diff --git a/src/com/android/browser/PreferenceKeys.java b/src/com/android/browser/PreferenceKeys.java
index 1bfe389..f3da937 100644
--- a/src/com/android/browser/PreferenceKeys.java
+++ b/src/com/android/browser/PreferenceKeys.java
@@ -100,7 +100,7 @@
     // ----------------------
     // Keys for bandwidth_preferences.xml
     // ----------------------
-    static final String PREF_DATA_PRELOAD = "preload_enabled";
+    static final String PREF_DATA_PRELOAD = "preload_when";
     static final String PREF_LOAD_IMAGES = "load_images";
 
 }
diff --git a/src/com/android/browser/PreloadRequestReceiver.java b/src/com/android/browser/PreloadRequestReceiver.java
index ff3c8e3..c654037 100644
--- a/src/com/android/browser/PreloadRequestReceiver.java
+++ b/src/com/android/browser/PreloadRequestReceiver.java
@@ -18,9 +18,9 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
 import android.os.Bundle;
-import android.preference.PreferenceManager;
 import android.provider.Browser;
 import android.util.Log;
 
@@ -42,15 +42,57 @@
     static final String EXTRA_SEARCHBOX_CANCEL = "searchbox_cancel";
     static final String EXTRA_SEARCHBOX_SETQUERY = "searchbox_query";
 
+    private ConnectivityManager mConnectivityManager;
+
     @Override
     public void onReceive(Context context, Intent intent) {
         if (LOGD_ENABLED) Log.d(LOGTAG, "received intent " + intent);
-        if (BrowserSettings.getInstance().isPreloadEnabled()
-                && intent.getAction().equals(ACTION_PRELOAD)) {
+        if (isPreloadEnabledOnCurrentNetwork(context) &&
+                intent.getAction().equals(ACTION_PRELOAD)) {
             handlePreload(context, intent);
         }
     }
 
+    private boolean isPreloadEnabledOnCurrentNetwork(Context context) {
+        String preload = BrowserSettings.getInstance().getPreloadEnabled();
+        if (LOGD_ENABLED) Log.d(LOGTAG, "Preload setting: " + preload);
+        if (BrowserSettings.getPreloadAlwaysPreferenceString(context).equals(preload)) {
+            return true;
+        } else if (BrowserSettings.getPreloadOnWifiOnlyPreferenceString(context).equals(preload)) {
+            boolean onWifi = isOnWifi(context);
+            if (LOGD_ENABLED) Log.d(LOGTAG, "on wifi:" + onWifi);
+            return onWifi;
+        } else {
+            return false;
+        }
+    }
+
+    private boolean isOnWifi(Context context) {
+        if (mConnectivityManager == null) {
+            mConnectivityManager = (ConnectivityManager)
+                    context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        }
+        NetworkInfo ni = mConnectivityManager.getActiveNetworkInfo();
+        if (ni == null) {
+            return false;
+        }
+        switch (ni.getType()) {
+            case ConnectivityManager.TYPE_MOBILE:
+            case ConnectivityManager.TYPE_MOBILE_DUN:
+            case ConnectivityManager.TYPE_MOBILE_MMS:
+            case ConnectivityManager.TYPE_MOBILE_SUPL:
+            case ConnectivityManager.TYPE_MOBILE_HIPRI:
+            case ConnectivityManager.TYPE_WIMAX: // separate case for this?
+                return false;
+            case ConnectivityManager.TYPE_WIFI:
+            case ConnectivityManager.TYPE_ETHERNET:
+            case ConnectivityManager.TYPE_BLUETOOTH:
+                return true;
+            default:
+                return false;
+        }
+    }
+
     private void handlePreload(Context context, Intent i) {
         String url = UrlUtils.smartUrlFilter(i.getData());
         String id = i.getStringExtra(EXTRA_PRELOAD_ID);
diff --git a/src/com/android/browser/PreloadedTabControl.java b/src/com/android/browser/PreloadedTabControl.java
index 4ffe6b4..b0eff63 100644
--- a/src/com/android/browser/PreloadedTabControl.java
+++ b/src/com/android/browser/PreloadedTabControl.java
@@ -15,6 +15,7 @@
  */
 package com.android.browser;
 
+import android.app.PendingIntent;
 import android.net.Uri;
 import android.text.TextUtils;
 import android.util.Log;
@@ -39,7 +40,7 @@
         mTab = t;
     }
 
-    private void maybeSetQuery(final String query, SearchBox sb) {
+    private boolean maybeSetQuery(final String query, SearchBox sb) {
         if (!TextUtils.equals(mLastQuery, query)) {
             if (sb != null) {
                 if (LOGD_ENABLED) Log.d(LOGTAG, "Changing searchbox query to " + query);
@@ -55,25 +56,27 @@
                         }
                     }
                 });
+                return true;
             } else {
                 if (LOGD_ENABLED) Log.d(LOGTAG, "Cannot set query: no searchbox interface");
             }
         }
+        return false;
     }
 
     public void setQuery(String query) {
         maybeSetQuery(query, mTab.getWebView().getSearchBox());
     }
 
-    public boolean searchBoxSubmit(final String query,
-            final String fallbackUrl, final Map<String, String> fallbackHeaders) {
+    public boolean searchBoxSubmit(final String query, final String fallbackUrl,
+            final Map<String, String> fallbackHeaders, final PendingIntent onLoadCompleteIntent) {
         final SearchBox sb = mTab.getWebView().getSearchBox();
         if (sb == null) {
             // no searchbox, cannot submit. Fallback to regular tab creation
             if (LOGD_ENABLED) Log.d(LOGTAG, "No searchbox, cannot submit query");
             return false;
         }
-        maybeSetQuery(query, sb);
+        final boolean newQuery = maybeSetQuery(query, sb);
         if (LOGD_ENABLED) Log.d(LOGTAG, "Submitting query " + query);
         final String currentUrl = mTab.getUrl();
         sb.onsubmit(new SearchBox.SearchBoxListener() {
@@ -84,9 +87,14 @@
                 if (!called) {
                     if (LOGD_ENABLED) Log.d(LOGTAG, "Query not submitted; falling back");
                     loadUrl(fallbackUrl, fallbackHeaders);
+
                     // make sure that the failed, preloaded URL is cleared from the back stack
                     mTab.clearBackStackWhenItemAdded(Pattern.compile(
                             "^" + Pattern.quote(fallbackUrl) + "$"));
+                    // When setting the search box query, preloadAttempted=true implies that the
+                    // the query was prefetched using the searchbox API. This is the case if we
+                    // the query is not new.
+                    registerLoadCompleteListener(!newQuery, false, onLoadCompleteIntent);
                 } else {
                     // ignore the next fragment change, to avoid leaving a blank page in the browser
                     // after the query has been submitted.
@@ -100,11 +108,27 @@
                                     Pattern.quote(currentWithoutFragment) +
                                     "(\\#.*)?" +
                                     "$"));
+                    registerLoadCompleteListener(!newQuery, true, onLoadCompleteIntent);
                 }
             }});
         return true;
     }
 
+    private void registerLoadCompleteListener(
+            final boolean queryPreloaded,
+            final boolean preloadSucceeded,
+            final PendingIntent pendingIntent) {
+        if (pendingIntent == null) {
+            return;
+        }
+        mTab.setOnPageLoadCompleteListener(null, new Tab.OnPageLoadCompleteListener(){
+            @Override
+            public void onPageLoadComplete() {
+                IntentHandler.sendPageLoadCompletePendingIntent(mTab.mContext, pendingIntent,
+                        queryPreloaded, preloadSucceeded);
+            }});
+    }
+
     public void searchBoxCancel() {
         SearchBox sb = mTab.getWebView().getSearchBox();
         if (sb != null) {
diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java
index 8c9dc02..8dcc54b 100644
--- a/src/com/android/browser/Tab.java
+++ b/src/com/android/browser/Tab.java
@@ -66,8 +66,6 @@
 import android.webkit.WebView.PictureListener;
 import android.webkit.WebViewClient;
 import android.widget.CheckBox;
-import android.widget.LinearLayout;
-import android.widget.TextView;
 import android.widget.Toast;
 
 import com.android.browser.TabControl.OnThumbnailUpdatedListener;
@@ -174,10 +172,13 @@
     private Handler mHandler;
 
     /**
-     * See {@link #clearBackStackWhenItemAdded(String)}.
+     * See {@link #clearBackStackWhenItemAdded(Pattern)}.
      */
     private Pattern mClearHistoryUrlPattern;
 
+    private OnPageLoadCompleteListener mOnPageLoadCompleteListener;
+    private Pattern mOnPageLoadCompleteUrlMatch;
+
     private static synchronized Bitmap getDefaultFavicon(Context context) {
         if (sDefaultFavicon == null) {
             sDefaultFavicon = BitmapFactory.decodeResource(
@@ -532,6 +533,26 @@
         }
     }
 
+    public interface OnPageLoadCompleteListener {
+        void onPageLoadComplete();
+    }
+
+    /**
+     * Requests a notification when the next page load completes. This is a one shot notification,
+     * the listener will be discarded after the first callback, or if the page load is cancelled.
+     * @param listener
+     */
+    public void setOnPageLoadCompleteListener(Pattern urlMatch,
+            OnPageLoadCompleteListener listener) {
+        mOnPageLoadCompleteListener = listener;
+        mOnPageLoadCompleteUrlMatch = urlMatch;
+    }
+
+    public void clearPageLoadCompleteListener() {
+        mOnPageLoadCompleteListener = null;
+        mOnPageLoadCompleteUrlMatch = null;
+    }
+
     // -------------------------------------------------------------------------
     // WebViewClient implementation for the main WebView
     // -------------------------------------------------------------------------
@@ -595,6 +616,13 @@
 
         @Override
         public void onPageFinished(WebView view, String url) {
+            if (mOnPageLoadCompleteListener != null) {
+                if (mOnPageLoadCompleteUrlMatch == null
+                        || mOnPageLoadCompleteUrlMatch.matcher(url).matches())
+                mOnPageLoadCompleteListener.onPageLoadComplete();
+                mOnPageLoadCompleteListener = null;
+                mOnPageLoadCompleteUrlMatch = null;
+            }
             if (!mInPageLoad) {
                 // In page navigation links (www.something.com#footer) will
                 // trigger an onPageFinished which we don't care about.
@@ -2042,6 +2070,7 @@
             mCurrentState = new PageState(mContext, false, url, null);
             mWebViewController.onPageStarted(this, mMainView, null);
             mMainView.loadUrl(url, headers);
+            clearPageLoadCompleteListener();
         }
     }
 
@@ -2093,12 +2122,14 @@
 
     public void goBack() {
         if (mMainView != null) {
+            clearPageLoadCompleteListener();
             mMainView.goBack();
         }
     }
 
     public void goForward() {
         if (mMainView != null) {
+            clearPageLoadCompleteListener();
             mMainView.goForward();
         }
     }