Browser: add to support my navigation feature

When launch browser, display my navigation page.

We show a gridview in navigation page, user can
select to add website items to navigation page,
delete/edit existing website items, when tap a
item, browser will load the saved website.

CRs-Fixed: 530979

Change-Id: I9618b8df15f651ccee09b80de8626bacfb6fda12
diff --git a/src/com/android/browser/Controller.java b/src/com/android/browser/Controller.java
index 729e79e..b1df847 100644
--- a/src/com/android/browser/Controller.java
+++ b/src/com/android/browser/Controller.java
@@ -38,11 +38,13 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteException;
 import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
 import android.net.Uri;
 import android.net.http.SslError;
+import android.net.WebAddress;
 import android.net.wifi.WifiManager;
 import android.os.AsyncTask;
 import android.os.Bundle;
@@ -91,6 +93,8 @@
 import com.android.browser.UI.ComboViews;
 import com.android.browser.provider.BrowserProvider2.Thumbnails;
 import com.android.browser.provider.SnapshotProvider.Snapshots;
+import com.android.browser.mynavigation.AddMyNavigationPage;
+import com.android.browser.mynavigation.MyNavigationUtil;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -120,6 +124,8 @@
     private static final String PROP_NETSWITCH = "persist.env.browser.netswitch";
     private static final String INTENT_WIFI_SELECTION_DATA_CONNECTION =
             "android.net.wifi.cmcc.WIFI_SELECTION_DATA_CONNECTION";
+    private static final String OFFLINE_PAGE =
+            "content://com.android.browser.mynavigation/websites";
 
     // public message ids
     public final static int LOAD_URL = 1001;
@@ -142,6 +148,7 @@
     final static int FILE_SELECTED = 4;
     final static int AUTOFILL_SETUP = 5;
     final static int VOICE_RESULT = 6;
+    final static int MY_NAVIGATION = 7;
 
     private final static int WAKELOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes
 
@@ -231,6 +238,8 @@
     private boolean mBlockEvents;
 
     private String mVoiceResult;
+    private boolean mUpdateMyNavThumbnail;
+    private String mUpdateMyNavThumbnailUrl;
 
     public Controller(Activity browser) {
         mActivity = browser;
@@ -323,10 +332,23 @@
                 // If the intent is ACTION_VIEW and data is not null, the Browser is
                 // invoked to view the content by another application. In this case,
                 // the tab will be close when exit.
-                UrlData urlData = IntentHandler.getUrlDataFromIntent(intent);
+                UrlData urlData = null;
+                if (intent.getData() != null
+                        && Intent.ACTION_VIEW.equals(intent.getAction())
+                        && intent.getData().toString().startsWith("content://")) {
+                    urlData = new UrlData(intent.getData().toString());
+                } else {
+                    urlData = IntentHandler.getUrlDataFromIntent(intent);
+                }
+
                 Tab t = null;
                 if (urlData.isEmpty()) {
-                    t = openTabToHomePage();
+                    if (SystemProperties.get("persist.env.c.browser.resource", "default").equals(
+                            "cmcc")) {
+                        t = openTab(OFFLINE_PAGE, false, true, true);
+                    } else {
+                        t = openTabToHomePage();
+                    }
                 } else {
                     t = openTab(urlData);
                 }
@@ -1256,6 +1278,8 @@
                 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
                     Tab t = getCurrentTab();
                     Uri uri = intent.getData();
+                    mUpdateMyNavThumbnail = true;
+                    mUpdateMyNavThumbnailUrl = uri.toString();
                     loadUrl(t, uri.toString());
                 } else if (intent.hasExtra(ComboViewActivity.EXTRA_OPEN_ALL)) {
                     String[] urls = intent.getStringArrayExtra(
@@ -1289,6 +1313,18 @@
                     }
                 }
                 break;
+
+             case MY_NAVIGATION:
+                if (intent == null || resultCode != Activity.RESULT_OK) {
+                    break;
+                }
+
+                if (intent.getBooleanExtra("need_refresh", false) &&
+                        getCurrentTopWebView() != null) {
+                    getCurrentTopWebView().reload();
+                }
+                break;
+
             default:
                 break;
         }
@@ -1385,6 +1421,7 @@
 
         // Show the correct menu group
         final String extra = result.getExtra();
+        final String navigationUrl = MyNavigationUtil.getMyNavigationUrl(extra);
         if (extra == null) return;
         menu.setGroupVisible(R.id.PHONE_MENU,
                 type == WebView.HitTestResult.PHONE_TYPE);
@@ -1392,12 +1429,30 @@
                 type == WebView.HitTestResult.EMAIL_TYPE);
         menu.setGroupVisible(R.id.GEO_MENU,
                 type == WebView.HitTestResult.GEO_TYPE);
-        menu.setGroupVisible(R.id.IMAGE_MENU,
-                type == WebView.HitTestResult.IMAGE_TYPE
-                || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
-        menu.setGroupVisible(R.id.ANCHOR_MENU,
-                type == WebView.HitTestResult.SRC_ANCHOR_TYPE
-                || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
+
+        String itemUrl = null;
+        String url = webview.getOriginalUrl();
+        if (url != null && url.equalsIgnoreCase(MyNavigationUtil.MY_NAVIGATION)) {
+            itemUrl = Uri.decode(navigationUrl);
+            if (itemUrl != null && !MyNavigationUtil.isDefaultMyNavigation(itemUrl)) {
+                menu.setGroupVisible(R.id.MY_NAVIGATION_MENU,
+                        type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
+            } else {
+                menu.setGroupVisible(R.id.MY_NAVIGATION_MENU, false);
+            }
+            menu.setGroupVisible(R.id.IMAGE_MENU, false);
+            menu.setGroupVisible(R.id.ANCHOR_MENU, false);
+        } else {
+            menu.setGroupVisible(R.id.MY_NAVIGATION_MENU, false);
+
+            menu.setGroupVisible(R.id.IMAGE_MENU,
+                    type == WebView.HitTestResult.IMAGE_TYPE
+                            || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
+            menu.setGroupVisible(R.id.ANCHOR_MENU,
+                    type == WebView.HitTestResult.SRC_ANCHOR_TYPE
+                            || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
+        }
+
         boolean hitText = type == WebView.HitTestResult.SRC_ANCHOR_TYPE
                 || type == WebView.HitTestResult.PHONE_TYPE
                 || type == WebView.HitTestResult.EMAIL_TYPE
@@ -1487,6 +1542,43 @@
                                 });
                     }
                 }
+                if (url != null && url.equalsIgnoreCase(MyNavigationUtil.MY_NAVIGATION)) {
+                    menu.setHeaderTitle(navigationUrl);
+                    menu.findItem(R.id.open_newtab_context_menu_id).setVisible(false);
+
+                    if (itemUrl != null) {
+                        if (!MyNavigationUtil.isDefaultMyNavigation(itemUrl)) {
+                            menu.findItem(R.id.edit_my_navigation_context_menu_id)
+                                    .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+                                        @Override
+                                        public boolean onMenuItemClick(MenuItem item) {
+                                            final Intent intent = new Intent(Controller.this
+                                                    .getContext(),
+                                                    AddMyNavigationPage.class);
+                                            Bundle bundle = new Bundle();
+                                            String url = Uri.decode(navigationUrl);
+                                            bundle.putBoolean("isAdding", false);
+                                            bundle.putString("url", url);
+                                            bundle.putString("name", getNameFromUrl(url));
+                                            intent.putExtra("websites", bundle);
+                                            mActivity.startActivityForResult(intent, MY_NAVIGATION);
+                                            return false;
+                                        }
+                                    });
+                            menu.findItem(R.id.delete_my_navigation_context_menu_id)
+                                    .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+                                        @Override
+                                        public boolean onMenuItemClick(MenuItem item) {
+                                            showMyNavigationDeleteDialog(Uri.decode(navigationUrl));
+                                            return false;
+                                        }
+                                    });
+                        }
+                    } else {
+                        Log.e(LOGTAG, "mynavigation onCreateContextMenu itemUrl is null!");
+                    }
+                }
+
                 if (type == WebView.HitTestResult.SRC_ANCHOR_TYPE) {
                     break;
                 }
@@ -1531,6 +1623,152 @@
         mUi.onContextMenuCreated(menu);
     }
 
+    public void startAddMyNavigation(String url) {
+        final Intent intent = new Intent(Controller.this.getContext(), AddMyNavigationPage.class);
+        Bundle bundle = new Bundle();
+        bundle.putBoolean("isAdding", true);
+        bundle.putString("url", url);
+        bundle.putString("name", getNameFromUrl(url));
+        intent.putExtra("websites", bundle);
+        mActivity.startActivityForResult(intent, MY_NAVIGATION);
+    }
+
+    private void showMyNavigationDeleteDialog(final String itemUrl) {
+        new AlertDialog.Builder(this.getContext())
+                .setTitle(R.string.my_navigation_delete_label)
+                .setIcon(android.R.drawable.ic_dialog_alert)
+                .setMessage(R.string.my_navigation_delete_msg)
+                .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                        deleteMyNavigationItem(itemUrl);
+                    }
+                })
+                .setNegativeButton(R.string.cancel, null)
+                .show();
+    }
+
+    private void deleteMyNavigationItem(final String itemUrl) {
+        ContentResolver cr = this.getContext().getContentResolver();
+        Cursor cursor = null;
+
+        try {
+            cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+                    new String[] {
+                        MyNavigationUtil.ID
+                    }, "url = ?", new String[] {
+                        itemUrl
+                    }, null);
+            if (null != cursor && cursor.moveToFirst()) {
+                Uri uri = ContentUris.withAppendedId(MyNavigationUtil.MY_NAVIGATION_URI,
+                        cursor.getLong(0));
+
+                ContentValues values = new ContentValues();
+                values.put(MyNavigationUtil.TITLE, "");
+                values.put(MyNavigationUtil.URL, "ae://" + cursor.getLong(0) + "add-fav");
+                values.put(MyNavigationUtil.WEBSITE, 0 + "");
+                ByteArrayOutputStream os = new ByteArrayOutputStream();
+                Bitmap bm = BitmapFactory.decodeResource(this.getContext().getResources(),
+                        R.raw.my_navigation_add);
+                bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+                values.put(MyNavigationUtil.THUMBNAIL, os.toByteArray());
+                Log.d(LOGTAG, "deleteMyNavigationItem uri is : " + uri);
+                cr.update(uri, values, null, null);
+            } else {
+                Log.e(LOGTAG, "deleteMyNavigationItem the item does not exist!");
+            }
+        } catch (IllegalStateException e) {
+            Log.e(LOGTAG, "deleteMyNavigationItem", e);
+        } finally {
+            if (null != cursor) {
+                cursor.close();
+            }
+        }
+
+        if (getCurrentTopWebView() != null) {
+            getCurrentTopWebView().reload();
+        }
+    }
+
+    private String getNameFromUrl(String itemUrl) {
+        ContentResolver cr = this.getContext().getContentResolver();
+        Cursor cursor = null;
+        String name = null;
+
+        try {
+            cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+                    new String[] {
+                        MyNavigationUtil.TITLE
+                    }, "url = ?", new String[] {
+                        itemUrl
+                    }, null);
+            if (null != cursor && cursor.moveToFirst()) {
+                name = cursor.getString(0);
+            } else {
+                Log.e(LOGTAG, "this item does not exist!");
+            }
+        } catch (IllegalStateException e) {
+            Log.e(LOGTAG, "getNameFromUrl", e);
+        } finally {
+            if (null != cursor) {
+                cursor.close();
+            }
+        }
+        return name;
+    }
+
+    private void updateMyNavigationThumbnail(final String itemUrl, WebView webView) {
+        int width = mActivity.getResources().getDimensionPixelOffset(
+                R.dimen.myNavigationThumbnailWidth);
+        int height = mActivity.getResources().getDimensionPixelOffset(
+                R.dimen.myNavigationThumbnailHeight);
+
+        final Bitmap bm = createScreenshot(webView, width, height);
+
+        if (bm == null) {
+            Log.e(LOGTAG, "updateMyNavigationThumbnail bm is null!");
+            return;
+        }
+
+        final ContentResolver cr = mActivity.getContentResolver();
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... unused) {
+                ContentResolver cr = mActivity.getContentResolver();
+                Cursor cursor = null;
+                try {
+                    cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+                            new String[] {
+                                MyNavigationUtil.ID
+                            }, "url = ?", new String[] {
+                                itemUrl
+                            }, null);
+                    if (null != cursor && cursor.moveToFirst()) {
+                        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+                        bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+
+                        ContentValues values = new ContentValues();
+                        values.put(MyNavigationUtil.THUMBNAIL, os.toByteArray());
+                        Uri uri = ContentUris.withAppendedId(MyNavigationUtil.MY_NAVIGATION_URI,
+                                cursor.getLong(0));
+                        Log.d(LOGTAG, "updateMyNavigationThumbnail uri is " + uri);
+                        cr.update(uri, values, null, null);
+                        os.close();
+                    }
+                } catch (IllegalStateException e) {
+                    Log.e(LOGTAG, "updateMyNavigationThumbnail", e);
+                } catch (IOException e) {
+                    Log.e(LOGTAG, "updateMyNavigationThumbnail", e);
+                } finally {
+                    if (null != cursor) {
+                        cursor.close();
+                    }
+                }
+                return null;
+            }
+        }.execute();
+    }
+
     /**
      * As the menu can be open when loading state changes
      * we must manually update the state of the stop/reload menu
@@ -2217,10 +2455,16 @@
         }
         final String url = tab.getUrl();
         final String originalUrl = view.getOriginalUrl();
+        final String thumbnailUrl = mUpdateMyNavThumbnailUrl;
         if (TextUtils.isEmpty(url)) {
             return;
         }
 
+        //update My Navigation Thumbnails
+        boolean isMyNavigationUrl = MyNavigationUtil.isMyNavigationUrl(mActivity, url);
+        if (isMyNavigationUrl) {
+            updateMyNavigationThumbnail(url, view);
+        }
         // Only update thumbnails for web urls (http(s)://), not for
         // about:, javascript:, data:, etc...
         // Unless it is a bookmarked site, then always update
@@ -2228,6 +2472,24 @@
             return;
         }
 
+        if (url != null && mUpdateMyNavThumbnailUrl != null
+                && Patterns.WEB_URL.matcher(url).matches()
+                && Patterns.WEB_URL.matcher(mUpdateMyNavThumbnailUrl).matches()) {
+            String urlHost = (new WebAddress(url)).getHost();
+            String bookmarkHost = (new WebAddress(mUpdateMyNavThumbnailUrl)).getHost();
+            if (urlHost == null || urlHost.length() == 0 || bookmarkHost == null
+                    || bookmarkHost.length() == 0) {
+                return;
+            }
+            String urlDomain = urlHost.substring(urlHost.indexOf('.'), urlHost.length());
+            String bookmarkDomain = bookmarkHost.substring(bookmarkHost.indexOf('.'),
+                    bookmarkHost.length());
+            Log.d(LOGTAG, "addressUrl domain is  " + urlDomain);
+            Log.d(LOGTAG, "bookmarkUrl domain is " + bookmarkDomain);
+            if (!bookmarkDomain.equals(urlDomain)) {
+                return;
+            }
+        }
         final Bitmap bm = createScreenshot(view, getDesiredThumbnailWidth(mActivity),
                 getDesiredThumbnailHeight(mActivity));
         if (bm == null) {
@@ -2241,7 +2503,13 @@
                 Cursor cursor = null;
                 try {
                     // TODO: Clean this up
-                    cursor = Bookmarks.queryCombinedForUrl(cr, originalUrl, url);
+                    cursor = Bookmarks.queryCombinedForUrl(cr, originalUrl,
+                            mUpdateMyNavThumbnail ? ((thumbnailUrl != null) ? thumbnailUrl : url)
+                                    : url);
+                    if (mUpdateMyNavThumbnail) {
+                        mUpdateMyNavThumbnail = false;
+                        mUpdateMyNavThumbnailUrl = null;
+                    }
                     if (cursor != null && cursor.moveToFirst()) {
                         final ByteArrayOutputStream os =
                                 new ByteArrayOutputStream();
diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java
index e3a0333..852aa25 100644
--- a/src/com/android/browser/Tab.java
+++ b/src/com/android/browser/Tab.java
@@ -70,6 +70,8 @@
 
 import com.android.browser.TabControl.OnThumbnailUpdatedListener;
 import com.android.browser.homepages.HomeProvider;
+import com.android.browser.mynavigation.MyNavigationUtil;
+import com.android.browser.provider.MyNavigationProvider;
 import com.android.browser.provider.SnapshotProvider.Snapshots;
 
 import java.io.ByteArrayOutputStream;
@@ -626,8 +628,12 @@
         @Override
         public WebResourceResponse shouldInterceptRequest(WebView view,
                 String url) {
-            WebResourceResponse res = HomeProvider.shouldInterceptRequest(
-                    mContext, url);
+            WebResourceResponse res;
+            if (MyNavigationUtil.MY_NAVIGATION.equals(url)) {
+                res = MyNavigationProvider.shouldInterceptRequest(mContext, url);
+            } else {
+                res = HomeProvider.shouldInterceptRequest(mContext, url);
+            }
             return res;
         }
 
diff --git a/src/com/android/browser/UrlHandler.java b/src/com/android/browser/UrlHandler.java
old mode 100644
new mode 100755
index 37e70f8..ac8442a
--- a/src/com/android/browser/UrlHandler.java
+++ b/src/com/android/browser/UrlHandler.java
@@ -105,6 +105,10 @@
             return false;
         }
 
+        if (url.startsWith("ae://") && url.endsWith("add-fav")) {
+            mController.startAddMyNavigation(url);
+            return true;
+        }
         // If this is a Google search, attempt to add an RLZ string
         // (if one isn't already present).
         if (rlzProviderPresent()) {
diff --git a/src/com/android/browser/UrlUtils.java b/src/com/android/browser/UrlUtils.java
old mode 100644
new mode 100755
index e24000c..ff78647
--- a/src/com/android/browser/UrlUtils.java
+++ b/src/com/android/browser/UrlUtils.java
@@ -130,7 +130,7 @@
         return null;
     }
 
-    /* package */ static String fixUrl(String inUrl) {
+    public static String fixUrl(String inUrl) {
         // FIXME: Converting the url to lower case
         // duplicates functionality in smartUrlFilter().
         // However, changing all current callers of fixUrl to
diff --git a/src/com/android/browser/mynavigation/AddMyNavigationPage.java b/src/com/android/browser/mynavigation/AddMyNavigationPage.java
new file mode 100755
index 0000000..1db9fa5
--- /dev/null
+++ b/src/com/android/browser/mynavigation/AddMyNavigationPage.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *       * Redistributions in binary form must reproduce the above
+ *         copyright notice, this list of conditions and the following
+ *         disclaimer in the documentation and/or other materials provided
+ *         with the distribution.
+ *       * Neither the name of The Linux Foundation nor the names of its
+ *         contributors may be used to endorse or promote products derived
+ *         from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.mynavigation;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.ParseException;
+import android.net.Uri;
+import android.net.WebAddress;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.View;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.util.Log;
+
+import com.android.browser.BrowserUtils;
+import com.android.browser.R;
+import com.android.browser.UrlUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class AddMyNavigationPage extends Activity {
+
+    private static final String LOGTAG = "AddMyNavigationPage";
+    private static final int SAVE_SITE_NAVIGATION = 100;
+
+    private EditText mName;
+    private EditText mAddress;
+    private Button mButtonOK;
+    private Button mButtonCancel;
+    private Bundle mMap;
+    private String mItemUrl;
+    private boolean mIsAdding;
+    private TextView mDialogText;
+    private Handler mHandler;
+
+    private View.OnClickListener mOKListener = new View.OnClickListener() {
+        public void onClick(View v) {
+            if (save()) {
+                AddMyNavigationPage.this.setResult(Activity.RESULT_OK,
+                        (new Intent()).putExtra("need_refresh", true));
+                finish();
+            }
+        }
+    };
+
+    private View.OnClickListener mCancelListener = new View.OnClickListener() {
+        public void onClick(View v) {
+            finish();
+        }
+    };
+
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.my_navigation_add_page);
+        String name = null;
+        String url = null;
+        mMap = getIntent().getExtras();
+        if (mMap != null) {
+            Bundle b = mMap.getBundle("websites");
+            if (b != null) {
+                mMap = b;
+            }
+            name = mMap.getString("name");
+            url = mMap.getString("url");
+            mIsAdding = mMap.getBoolean("isAdding");
+        }
+
+        // The original url
+        mItemUrl = url;
+        mName = (EditText) findViewById(R.id.title);
+        mAddress = (EditText) findViewById(R.id.address);
+
+        BrowserUtils.maxLengthFilter(AddMyNavigationPage.this, mName,
+                BrowserUtils.FILENAME_MAX_LENGTH);
+        BrowserUtils.maxLengthFilter(AddMyNavigationPage.this, mAddress,
+                BrowserUtils.ADDRESS_MAX_LENGTH);
+
+        if (url.startsWith("ae://") && url.endsWith("add-fav")) {
+            mName.setText("");
+            mAddress.setText("");
+        } else {
+            mName.setText(name);
+            mAddress.setText(url);
+        }
+        mDialogText = (TextView) findViewById(R.id.dialog_title);
+        if (mIsAdding) {
+            mDialogText.setText(R.string.my_navigation_add_label);
+        }
+
+        mButtonOK = (Button) findViewById(R.id.OK);
+        mButtonOK.setOnClickListener(mOKListener);
+        mButtonCancel = (Button) findViewById(R.id.cancel);
+        mButtonCancel.setOnClickListener(mCancelListener);
+
+        if (!getWindow().getDecorView().isInTouchMode()) {
+            mButtonOK.requestFocus();
+        }
+    }
+
+    /**
+     * Runnable to save a website
+     */
+    private class SaveMyNavigationRunnable implements Runnable {
+        private Message mMessage;
+
+        public SaveMyNavigationRunnable(Message msg) {
+            mMessage = msg;
+        }
+
+        public void run() {
+            Bundle bundle = mMessage.getData();
+            String title = bundle.getString("title");
+            String url = bundle.getString("url");
+            String itemUrl = bundle.getString("itemUrl");
+            Boolean toDefaultThumbnail = bundle.getBoolean("toDefaultThumbnail");
+            ContentResolver cr = AddMyNavigationPage.this.getContentResolver();
+            Cursor cursor = null;
+            try {
+                cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+                        new String[] {
+                            MyNavigationUtil.ID
+                        }, "url = ?", new String[] {
+                            itemUrl
+                        }, null);
+                if (cursor != null && cursor.moveToFirst()) {
+                    ContentValues values = new ContentValues();
+                    values.put(MyNavigationUtil.TITLE, title);
+                    values.put(MyNavigationUtil.URL, url);
+                    values.put(MyNavigationUtil.WEBSITE, 1 + "");
+                    if (toDefaultThumbnail) {
+                        ByteArrayOutputStream os = new ByteArrayOutputStream();
+                        Bitmap bm = BitmapFactory.decodeResource(
+                                AddMyNavigationPage.this.getResources(),
+                                R.raw.my_navigation_thumbnail_default);
+                        bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+                        values.put(MyNavigationUtil.THUMBNAIL, os.toByteArray());
+                    }
+                    Uri uri = ContentUris.withAppendedId(MyNavigationUtil.MY_NAVIGATION_URI,
+                            cursor.getLong(0));
+                    cr.update(uri, values, null, null);
+                } else {
+                    Log.e(LOGTAG, "this item does not exist!");
+                }
+            } catch (IllegalStateException e) {
+                Log.e(LOGTAG, "SaveMyNavigationRunnable", e);
+            } finally {
+                if (null != cursor) {
+                    cursor.close();
+                }
+            }
+        }
+    }
+
+    boolean save() {
+        String name = mName.getText().toString().trim();
+        String unfilteredUrl = UrlUtils.fixUrl(mAddress.getText().toString());
+        boolean emptyTitle = name.length() == 0;
+        boolean emptyUrl = unfilteredUrl.trim().length() == 0;
+        Resources r = getResources();
+        if (emptyTitle || emptyUrl) {
+            if (emptyTitle) {
+                mName.setError(r.getText(R.string.website_needs_title));
+            }
+            if (emptyUrl) {
+                mAddress.setError(r.getText(R.string.website_needs_url));
+            }
+            return false;
+        }
+        String url = unfilteredUrl.trim();
+        try {
+            if (!url.toLowerCase().startsWith("javascript:")) {
+                URI uriObj = new URI(url);
+                String scheme = uriObj.getScheme();
+                if (!MyNavigationUtil.urlHasAcceptableScheme(url)) {
+                    if (scheme != null) {
+                        mAddress.setError(r.getText(R.string.my_navigation_cannot_save_url));
+                        return false;
+                    }
+                    WebAddress address;
+                    try {
+                        address = new WebAddress(unfilteredUrl);
+                    } catch (ParseException e) {
+                        throw new URISyntaxException("", "");
+                    }
+                    if (address.getHost().length() == 0) {
+                        throw new URISyntaxException("", "");
+                    }
+                    url = address.toString();
+                } else {
+                    String mark = "://";
+                    int iRet = -1;
+                    if (null != url) {
+                        iRet = url.indexOf(mark);
+                    }
+                    if (iRet > 0 && url.indexOf("/", iRet + mark.length()) < 0) {
+                        url = url + "/";
+                        Log.d(LOGTAG, "URL=" + url);
+                    }
+                }
+            }
+        } catch (URISyntaxException e) {
+            mAddress.setError(r.getText(R.string.bookmark_url_not_valid));
+            return false;
+        }
+
+        // When it is adding, avoid duplicate url that already existing in the
+        // database
+        if (!mItemUrl.equals(url)) {
+            boolean exist = MyNavigationUtil.isMyNavigationUrl(this, url);
+            if (exist) {
+                mAddress.setError(r.getText(R.string.my_navigation_duplicate_url));
+                return false;
+            }
+        }
+        Bundle bundle = new Bundle();
+        bundle.putString("title", name);
+        bundle.putString("url", url);
+        bundle.putString("itemUrl", mItemUrl);
+        if (!mItemUrl.equals(url)) {
+            bundle.putBoolean("toDefaultThumbnail", true);
+        } else {
+            bundle.putBoolean("toDefaultThumbnail", false);
+        }
+        Message msg = Message.obtain(mHandler, SAVE_SITE_NAVIGATION);
+        msg.setData(bundle);
+        Thread t = new Thread(new SaveMyNavigationRunnable(msg));
+        t.start();
+        return true;
+    }
+}
diff --git a/src/com/android/browser/mynavigation/MyNavigationRequestHandler.java b/src/com/android/browser/mynavigation/MyNavigationRequestHandler.java
new file mode 100755
index 0000000..e1f9ebe
--- /dev/null
+++ b/src/com/android/browser/mynavigation/MyNavigationRequestHandler.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *       * Redistributions in binary form must reproduce the above
+ *         copyright notice, this list of conditions and the following
+ *         disclaimer in the documentation and/or other materials provided
+ *         with the distribution.
+ *       * Neither the name of The Linux Foundation nor the names of its
+ *         contributors may be used to endorse or promote products derived
+ *         from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.mynavigation;
+
+import android.content.Context;
+import android.content.UriMatcher;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import com.android.browser.R;
+
+public class MyNavigationRequestHandler extends Thread {
+
+    private static final String LOGTAG = "MyNavigationRequestHandler";
+    private static final int MY_NAVIGATION = 1;
+    private static final int RESOURCE = 2;
+    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+
+    Uri mUri;
+    Context mContext;
+    OutputStream mOutput;
+
+    static {
+        URI_MATCHER.addURI(MyNavigationUtil.AUTHORITY, "websites/res/*/*", RESOURCE);
+        URI_MATCHER.addURI(MyNavigationUtil.AUTHORITY, "websites", MY_NAVIGATION);
+    }
+
+    public MyNavigationRequestHandler(Context context, Uri uri, OutputStream out) {
+        mUri = uri;
+        mContext = context.getApplicationContext();
+        mOutput = out;
+    }
+
+    @Override
+    public void run() {
+        super.run();
+        try {
+            doHandleRequest();
+        } catch (IOException e) {
+            Log.e(LOGTAG, "Failed to handle request: " + mUri, e);
+        } finally {
+            cleanup();
+        }
+    }
+
+    void doHandleRequest() throws IOException {
+        int match = URI_MATCHER.match(mUri);
+        switch (match) {
+            case MY_NAVIGATION:
+                writeTemplatedIndex();
+                break;
+            case RESOURCE:
+                writeResource(getUriResourcePath());
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void writeTemplatedIndex() throws IOException {
+        MyNavigationTemplate t = MyNavigationTemplate.getCachedTemplate(mContext,
+                R.raw.my_navigation);
+        Cursor cursor = mContext.getContentResolver().query(
+                Uri.parse("content://com.android.browser.mynavigation/websites"),
+                new String[] {
+                        "url", "title", "thumbnail"
+                },
+                null, null, null);
+
+        t.assignLoop("my_navigation", new MyNavigationTemplate.CursorListEntityWrapper(cursor) {
+            @Override
+            public void writeValue(OutputStream stream, String key) throws IOException {
+                Cursor cursor = getCursor();
+                if (key.equals("url")) {
+                    stream.write(htmlEncode(cursor.getString(0)));
+                } else if (key.equals("title")) {
+                    String title = cursor.getString(1);
+                    if (title == null || title.length() == 0) {
+                        title = mContext.getString(R.string.my_navigation_add);
+                    }
+                    stream.write(htmlEncode(title));
+                } else if (key.equals("thumbnail")) {
+                    stream.write("data:image/png".getBytes());
+                    stream.write(htmlEncode(cursor.getString(0)));
+                    stream.write(";base64,".getBytes());
+                    byte[] thumb = cursor.getBlob(2);
+                    stream.write(Base64.encode(thumb, Base64.DEFAULT));
+                }
+            }
+        });
+        t.write(mOutput);
+        cursor.close();
+    }
+
+    byte[] htmlEncode(String s) {
+        return TextUtils.htmlEncode(s).getBytes();
+    }
+
+    String getUriResourcePath() {
+        final Pattern pattern = Pattern.compile("/?res/([\\w/]+)");
+        Matcher m = pattern.matcher(mUri.getPath());
+        if (m.matches()) {
+            return m.group(1);
+        } else {
+            return mUri.getPath();
+        }
+    }
+
+    void writeResource(String fileName) throws IOException {
+        Resources res = mContext.getResources();
+        String packageName = R.class.getPackage().getName();
+        int id = res.getIdentifier(fileName, null, packageName);
+        if (id != 0) {
+            InputStream in = res.openRawResource(id);
+            byte[] buf = new byte[4096];
+            int read;
+            while ((read = in.read(buf)) > 0) {
+                mOutput.write(buf, 0, read);
+            }
+        }
+    }
+
+    void writeString(String str) throws IOException {
+        mOutput.write(str.getBytes());
+    }
+
+    void writeString(String str, int offset, int count) throws IOException {
+        mOutput.write(str.getBytes(), offset, count);
+    }
+
+    void cleanup() {
+        try {
+            mOutput.close();
+        } catch (IOException e) {
+            Log.e(LOGTAG, "Failed to close pipe!", e);
+        }
+    }
+}
diff --git a/src/com/android/browser/mynavigation/MyNavigationTemplate.java b/src/com/android/browser/mynavigation/MyNavigationTemplate.java
new file mode 100755
index 0000000..85d1baf
--- /dev/null
+++ b/src/com/android/browser/mynavigation/MyNavigationTemplate.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *       * Redistributions in binary form must reproduce the above
+ *         copyright notice, this list of conditions and the following
+ *         disclaimer in the documentation and/or other materials provided
+ *         with the distribution.
+ *       * Neither the name of The Linux Foundation nor the names of its
+ *         contributors may be used to endorse or promote products derived
+ *         from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.mynavigation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.util.TypedValue;
+import android.util.Log;
+
+import com.android.browser.R;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MyNavigationTemplate {
+
+    private static final String LOGTAG = "MyNavigationTemplate";
+    private static HashMap<Integer, MyNavigationTemplate> sCachedTemplates =
+            new HashMap<Integer, MyNavigationTemplate>();
+    private static boolean sCountryChanged = false;
+    private static String sCurrentCountry = "US";
+
+    private List<Entity> mTemplate;
+    private HashMap<String, Object> mData = new HashMap<String, Object>();
+
+    public static MyNavigationTemplate getCachedTemplate(Context context, int id) {
+
+        String changeToCountry = context.getResources().getConfiguration().locale
+                .getDisplayCountry();
+        Log.d(LOGTAG, "MyNavigationTemplate.getCachedTemplate() display country :"
+                + changeToCountry + ", before country :" + sCurrentCountry);
+        if (changeToCountry != null && !changeToCountry.equals(sCurrentCountry)) {
+            sCountryChanged = true;
+            sCurrentCountry = changeToCountry;
+        }
+        synchronized (sCachedTemplates) {
+            MyNavigationTemplate template = sCachedTemplates.get(id);
+            if (template == null || sCountryChanged) {
+                sCountryChanged = false;
+                template = new MyNavigationTemplate(context, id);
+                sCachedTemplates.put(id, template);
+            }
+            return template.copy();
+        }
+    }
+
+    interface Entity {
+        void write(OutputStream stream, EntityData params) throws IOException;
+    }
+
+    interface EntityData {
+        void writeValue(OutputStream stream, String key) throws IOException;
+
+        ListEntityIterator getListIterator(String key);
+    }
+
+    interface ListEntityIterator extends EntityData {
+        void reset();
+
+        boolean moveToNext();
+    }
+
+    static class StringEntity implements Entity {
+        byte[] mValue;
+
+        public StringEntity(String value) {
+            mValue = value.getBytes();
+        }
+
+        @Override
+        public void write(OutputStream stream, EntityData params) throws IOException {
+            stream.write(mValue);
+        }
+    }
+
+    static class SimpleEntity implements Entity {
+        String mKey;
+
+        public SimpleEntity(String key) {
+            mKey = key;
+        }
+
+        @Override
+        public void write(OutputStream stream, EntityData params) throws IOException {
+            params.writeValue(stream, mKey);
+        }
+    }
+
+    static class ListEntity implements Entity {
+        String mKey;
+        MyNavigationTemplate mSubTemplate;
+
+        public ListEntity(Context context, String key, String subTemplate) {
+            mKey = key;
+            mSubTemplate = new MyNavigationTemplate(context, subTemplate);
+        }
+
+        @Override
+        public void write(OutputStream stream, EntityData params) throws IOException {
+            ListEntityIterator iter = params.getListIterator(mKey);
+            if (null == iter) {
+                return;
+            }
+            iter.reset();
+            while (iter.moveToNext()) {
+                mSubTemplate.write(stream, iter);
+            }
+        }
+    }
+
+    public abstract static class CursorListEntityWrapper implements ListEntityIterator {
+        private Cursor mCursor;
+
+        public CursorListEntityWrapper(Cursor cursor) {
+            mCursor = cursor;
+        }
+
+        @Override
+        public boolean moveToNext() {
+            return mCursor.moveToNext();
+        }
+
+        @Override
+        public void reset() {
+            mCursor.moveToPosition(-1);
+        }
+
+        @Override
+        public ListEntityIterator getListIterator(String key) {
+            return null;
+        }
+
+        public Cursor getCursor() {
+            return mCursor;
+        }
+    }
+
+    static class HashMapEntityData implements EntityData {
+        HashMap<String, Object> mData;
+
+        public HashMapEntityData(HashMap<String, Object> map) {
+            mData = map;
+        }
+
+        @Override
+        public ListEntityIterator getListIterator(String key) {
+            return (ListEntityIterator) mData.get(key);
+        }
+
+        @Override
+        public void writeValue(OutputStream stream, String key) throws IOException {
+            stream.write((byte[]) mData.get(key));
+        }
+    }
+
+    private MyNavigationTemplate(Context context, int tid) {
+        this(context, readRaw(context, tid));
+    }
+
+    private MyNavigationTemplate(Context context, String template) {
+        mTemplate = new ArrayList<Entity>();
+        template = replaceConsts(context, template);
+        parseTemplate(context, template);
+    }
+
+    private MyNavigationTemplate(MyNavigationTemplate copy) {
+        mTemplate = copy.mTemplate;
+    }
+
+    MyNavigationTemplate copy() {
+        return new MyNavigationTemplate(this);
+    }
+
+    void parseTemplate(Context context, String template) {
+        final Pattern pattern = Pattern.compile("<%([=\\{])\\s*(\\w+)\\s*%>");
+        Matcher m = pattern.matcher(template);
+        int start = 0;
+        while (m.find()) {
+            String staticPart = template.substring(start, m.start());
+            if (staticPart.length() > 0) {
+                mTemplate.add(new StringEntity(staticPart));
+            }
+            String type = m.group(1);
+            String name = m.group(2);
+            if (type.equals("=")) {
+                mTemplate.add(new SimpleEntity(name));
+            } else if (type.equals("{")) {
+                Pattern p = Pattern.compile("<%\\}\\s*" + Pattern.quote(name) + "\\s*%>");
+                Matcher end = p.matcher(template);
+                if (end.find(m.end())) {
+                    start = m.end();
+                    m.region(end.end(), template.length());
+                    String subTemplate = template.substring(start, end.start());
+                    mTemplate.add(new ListEntity(context, name, subTemplate));
+                    start = end.end();
+                    continue;
+                }
+            }
+            start = m.end();
+        }
+        String staticPart = template.substring(start, template.length());
+        if (staticPart.length() > 0) {
+            mTemplate.add(new StringEntity(staticPart));
+        }
+    }
+
+    public void assign(String name, String value) {
+        mData.put(name, value.getBytes());
+    }
+
+    public void assignLoop(String name, ListEntityIterator iter) {
+        mData.put(name, iter);
+    }
+
+    public void write(OutputStream stream) throws IOException {
+        write(stream, new HashMapEntityData(mData));
+    }
+
+    public void write(OutputStream stream, EntityData data) throws IOException {
+        for (Entity ent : mTemplate) {
+            ent.write(stream, data);
+        }
+    }
+
+    private static String replaceConsts(Context context, String template) {
+        final Pattern pattern = Pattern.compile("<%@\\s*(\\w+/\\w+)\\s*%>");
+        final Resources res = context.getResources();
+        final String packageName = R.class.getPackage().getName();
+        Matcher m = pattern.matcher(template);
+        StringBuffer sb = new StringBuffer();
+        while (m.find()) {
+            String name = m.group(1);
+            if (name.startsWith("drawable/")) {
+                m.appendReplacement(sb, "res/" + name);
+            } else {
+                int id = res.getIdentifier(name, null, packageName);
+                if (id != 0) {
+                    TypedValue value = new TypedValue();
+                    res.getValue(id, value, true);
+                    String replacement;
+                    if (value.type == TypedValue.TYPE_DIMENSION) {
+                        float dimen = res.getDimension(id);
+                        int dimeni = (int) dimen;
+                        if (dimeni == dimen) {
+                            replacement = Integer.toString(dimeni);
+                        } else {
+                            replacement = Float.toString(dimen);
+                        }
+                    } else {
+                        replacement = value.coerceToString().toString();
+                    }
+                    m.appendReplacement(sb, replacement);
+                }
+            }
+        }
+        m.appendTail(sb);
+        return sb.toString();
+    }
+
+    private static String readRaw(Context context, int id) {
+        InputStream ins = context.getResources().openRawResource(id);
+        try {
+            byte[] buf = new byte[ins.available()];
+            ins.read(buf);
+            return new String(buf, "utf-8");
+        } catch (IOException ex) {
+            return "<html><body>Error</body></html>";
+        }
+    }
+}
diff --git a/src/com/android/browser/mynavigation/MyNavigationUtil.java b/src/com/android/browser/mynavigation/MyNavigationUtil.java
new file mode 100755
index 0000000..3b1836d
--- /dev/null
+++ b/src/com/android/browser/mynavigation/MyNavigationUtil.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *       * Redistributions in binary form must reproduce the above
+ *         copyright notice, this list of conditions and the following
+ *         disclaimer in the documentation and/or other materials provided
+ *         with the distribution.
+ *       * Neither the name of The Linux Foundation nor the names of its
+ *         contributors may be used to endorse or promote products derived
+ *         from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.mynavigation;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.ContentResolver;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.Log;
+
+public class MyNavigationUtil {
+
+    public static final String ID = "_id";
+    public static final String URL = "url";
+    public static final String TITLE = "title";
+    public static final String DATE_CREATED = "created";
+    public static final String WEBSITE = "website";
+    public static final String FAVICON = "favicon";
+    public static final String THUMBNAIL = "thumbnail";
+    public static final int WEBSITE_NUMBER = 12;
+
+    public static final String AUTHORITY = "com.android.browser.mynavigation";
+    public static final String MY_NAVIGATION = "content://" + AUTHORITY + "/" + "websites";
+    public static final Uri MY_NAVIGATION_URI = Uri
+            .parse("content://com.android.browser.mynavigation/websites");
+    public static final String DEFAULT_THUMB = "default_thumb";
+    public static final String LOGTAG = "MyNavigationUtil";
+
+    public static boolean isDefaultMyNavigation(String url) {
+        if (url != null && url.startsWith("ae://") && url.endsWith("add-fav")) {
+            Log.d(LOGTAG, "isDefaultMyNavigation will return true.");
+            return true;
+        }
+        return false;
+    }
+
+    public static String getMyNavigationUrl(String srcUrl) {
+        String srcPrefix = "data:image/png";
+        String srcSuffix = ";base64,";
+        if (srcUrl != null && srcUrl.startsWith(srcPrefix)) {
+            int indexPrefix = srcPrefix.length();
+            int indexSuffix = srcUrl.indexOf(srcSuffix);
+            return srcUrl.substring(indexPrefix, indexSuffix);
+        }
+        return "";
+    }
+
+    public static boolean isMyNavigationUrl(Context context, String itemUrl) {
+        ContentResolver cr = context.getContentResolver();
+        Cursor cursor = null;
+        try {
+            cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+                    new String[] {
+                        MyNavigationUtil.TITLE
+                    }, "url = ?", new String[] {
+                        itemUrl
+                    }, null);
+            if (null != cursor && cursor.moveToFirst()) {
+                Log.d(LOGTAG, "isMyNavigationUrl will return true.");
+                return true;
+            }
+        } catch (IllegalStateException e) {
+            Log.e(LOGTAG, "isMyNavigationUrl", e);
+        } finally {
+            if (null != cursor) {
+                cursor.close();
+            }
+        }
+        return false;
+    }
+
+    private static final String ACCEPTABLE_WEBSITE_SCHEMES[] = {
+            "http:",
+            "https:",
+            "about:",
+            "data:",
+            "javascript:",
+            "file:",
+            "content:"
+    };
+
+    public static boolean urlHasAcceptableScheme(String url) {
+        if (url == null) {
+            return false;
+        }
+
+        for (int i = 0; i < ACCEPTABLE_WEBSITE_SCHEMES.length; i++) {
+            if (url.startsWith(ACCEPTABLE_WEBSITE_SCHEMES[i])) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/browser/provider/MyNavigationProvider.java b/src/com/android/browser/provider/MyNavigationProvider.java
new file mode 100755
index 0000000..3dc2954
--- /dev/null
+++ b/src/com/android/browser/provider/MyNavigationProvider.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *       * Redistributions in binary form must reproduce the above
+ *         copyright notice, this list of conditions and the following
+ *         disclaimer in the documentation and/or other materials provided
+ *         with the distribution.
+ *       * Neither the name of The Linux Foundation nor the names of its
+ *         contributors may be used to endorse or promote products derived
+ *         from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.provider;
+
+import android.content.Context;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.WebResourceResponse;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.homepages.RequestHandler;
+import com.android.browser.mynavigation.MyNavigationUtil;
+import com.android.browser.mynavigation.MyNavigationRequestHandler;
+import com.android.browser.provider.BrowserProvider2;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import com.android.browser.R;
+
+public class MyNavigationProvider extends ContentProvider {
+
+    private static final String LOGTAG = "MyNavigationProvider";
+    private static final String TABLE_WEB_SITES = "websites";
+    private static final int WEB_SITES_ALL = 0;
+    private static final int WEB_SITES_ID = 1;
+    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+    static {
+        URI_MATCHER.addURI(MyNavigationUtil.AUTHORITY, "websites", WEB_SITES_ALL);
+        URI_MATCHER.addURI(MyNavigationUtil.AUTHORITY, "websites/#", WEB_SITES_ID);
+    }
+    private static final Uri NOTIFICATION_URI = MyNavigationUtil.MY_NAVIGATION_URI;
+
+    private SiteNavigationDatabaseHelper mOpenHelper;
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        // Current not used, just return 0
+        return 0;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        // Current not used, just return null
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        // Current not used, just return null
+        return null;
+    }
+
+    @Override
+    public boolean onCreate() {
+        mOpenHelper = new SiteNavigationDatabaseHelper(this.getContext());
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        qb.setTables(TABLE_WEB_SITES);
+        switch (URI_MATCHER.match(uri)) {
+            case WEB_SITES_ALL:
+                break;
+            case WEB_SITES_ID:
+                qb.appendWhere(MyNavigationUtil.ID + "=" + uri.getPathSegments().get(0));
+                break;
+            default:
+                Log.e(LOGTAG, "query Unknown URI: " + uri);
+                return null;
+        }
+
+        String orderBy;
+        if (TextUtils.isEmpty(sortOrder)) {
+            orderBy = null;
+        } else {
+            orderBy = sortOrder;
+        }
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
+        if (c != null) {
+            c.setNotificationUri(getContext().getContentResolver(), NOTIFICATION_URI);
+        }
+        return c;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int count = 0;
+
+        switch (URI_MATCHER.match(uri)) {
+            case WEB_SITES_ALL:
+                count = db.update(TABLE_WEB_SITES, values, selection, selectionArgs);
+                break;
+            case WEB_SITES_ID:
+                String newIdSelection = MyNavigationUtil.ID + "=" + uri.getLastPathSegment()
+                        + (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
+                count = db.update(TABLE_WEB_SITES, values, newIdSelection, selectionArgs);
+                break;
+            default:
+                Log.e(LOGTAG, "update Unknown URI: " + uri);
+                return count;
+        }
+
+        if (count > 0) {
+            ContentResolver cr = getContext().getContentResolver();
+            cr.notifyChange(uri, null);
+        }
+        return count;
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode) {
+        try {
+            ParcelFileDescriptor[] pipes = ParcelFileDescriptor.createPipe();
+            final ParcelFileDescriptor write = pipes[1];
+            AssetFileDescriptor afd = new AssetFileDescriptor(write, 0, -1);
+            new MyNavigationRequestHandler(getContext(), uri, afd.createOutputStream())
+                    .start();
+            return pipes[0];
+        } catch (IOException e) {
+            Log.e(LOGTAG, "Failed to handle request: " + uri, e);
+            return null;
+        }
+    }
+
+    public static WebResourceResponse shouldInterceptRequest(Context context,
+            String url) {
+        try {
+            if (MyNavigationUtil.MY_NAVIGATION.equals(url)) {
+                Uri uri = Uri.parse(url);
+                if (MyNavigationUtil.AUTHORITY.equals(uri.getAuthority())) {
+                    InputStream ins = context.getContentResolver()
+                            .openInputStream(uri);
+                    return new WebResourceResponse("text/html", "utf-8", ins);
+                }
+            }
+            boolean listFiles = BrowserSettings.getInstance().isDebugEnabled();
+            if (listFiles && interceptFile(url)) {
+                PipedInputStream ins = new PipedInputStream();
+                PipedOutputStream outs = new PipedOutputStream(ins);
+                new RequestHandler(context, Uri.parse(url), outs).start();
+                return new WebResourceResponse("text/html", "utf-8", ins);
+            }
+        } catch (Exception e) {}
+        return null;
+    }
+
+    private static boolean interceptFile(String url) {
+        if (!url.startsWith("file:///")) {
+            return false;
+        }
+        String fpath = url.substring(7);
+        File f = new File(fpath);
+        if (!f.isDirectory()) {
+            return false;
+        }
+        return true;
+    }
+
+    private class SiteNavigationDatabaseHelper extends SQLiteOpenHelper {
+        private Context mContext;
+        static final String DATABASE_NAME = "mynavigation.db";
+
+        public SiteNavigationDatabaseHelper(Context context) {
+            super(context, DATABASE_NAME, null, 1); // "1" is the db version here
+            // TODO Auto-generated constructor stub
+            mContext = context;
+        }
+
+        public SiteNavigationDatabaseHelper(Context context, String name,
+                CursorFactory factory, int version) {
+            super(context, name, factory, version);
+            // TODO Auto-generated constructor stub
+            mContext = context;
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            // TODO Auto-generated method stub
+            createWebsitesTable(db);
+            initWebsitesTable(db);
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase arg0, int arg1, int arg2) {
+            // TODO Auto-generated method stub
+        }
+
+        private void createWebsitesTable(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE websites (" +
+                    MyNavigationUtil.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                    MyNavigationUtil.URL + " TEXT," +
+                    MyNavigationUtil.TITLE + " TEXT," +
+                    MyNavigationUtil.DATE_CREATED + " LONG," +
+                    MyNavigationUtil.WEBSITE + " INTEGER," +
+                    MyNavigationUtil.THUMBNAIL + " BLOB DEFAULT NULL," +
+                    MyNavigationUtil.FAVICON + " BLOB DEFAULT NULL," +
+                    MyNavigationUtil.DEFAULT_THUMB + " TEXT" +
+                    ");");
+        }
+
+        // initial table , insert websites to table websites
+        private void initWebsitesTable(SQLiteDatabase db) {
+            int WebsiteNumber = MyNavigationUtil.WEBSITE_NUMBER;
+            for (int i = 0; i < WebsiteNumber; i++) {
+                ByteArrayOutputStream os = new ByteArrayOutputStream();
+                Bitmap bm = BitmapFactory.decodeResource(mContext.getResources(),
+                        R.raw.my_navigation_add);
+                bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+                ContentValues values = new ContentValues();
+                values.put(MyNavigationUtil.URL, "ae://" + (i + 1) + "add-fav");
+                values.put(MyNavigationUtil.TITLE, mContext.getString(R.string.my_navigation_add));
+                values.put(MyNavigationUtil.DATE_CREATED, 0 + "");
+                values.put(MyNavigationUtil.WEBSITE, 1 + "");
+                values.put(MyNavigationUtil.THUMBNAIL, os.toByteArray());
+                db.insertOrThrow(TABLE_WEB_SITES, MyNavigationUtil.URL, values);
+            }
+        }
+    }
+}