Most Visited custom homepage

Change-Id: Ic57762855e5d187aa0fe3a8eab2757b5a76ff08d
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 5b844e0..9ceaf82 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -223,6 +223,13 @@
                 <action android:name="android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED"/>
             </intent-filter>
         </receiver>
+
+        <!-- For custom home pages (like most visited) -->
+        <provider
+            android:name=".homepages.HomeProvider"
+            android:authorities="com.android.browser.home"
+            android:readPermission="com.android.browser.permission.READ_HISTORY_BOOKMARKS"
+            android:exported="false" />
     </application>
 
 </manifest>
diff --git a/res/raw/most_visited.ktpl b/res/raw/most_visited.ktpl
new file mode 100644
index 0000000..04b9eee
--- /dev/null
+++ b/res/raw/most_visited.ktpl
@@ -0,0 +1,85 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

+

+<html>

+<head>

+<title><%@ string/new_tab %></title>

+<meta name="viewport" content="width=device-width; initial-scale=1.0;" />

+

+<style type="text/css">

+

+* {

+    padding: 0;

+    margin: 0;

+}

+

+body {

+    text-align: center;

+    margin: 16px auto;

+    padding: 0 8px 0 8px;

+    max-width: <%@ dimen/mv_max_width %>px;

+}

+

+#most_visited h3 {

+    text-align: center;

+    padding: 0;

+    margin: 5px 0 5px 0px;

+}

+

+.thumbwrap li {

+    display: inline-block;

+    margin: 0 7px 15px 7px;

+    padding: 0;

+}

+

+@media all and (orientation:portrait) {

+.thumbwrap li {

+    width: <%@ dimen/mv_item_width_portrait %>px;

+}

+}

+

+@media all and (orientation:landscape) {

+.thumbwrap li {

+    width: <%@ dimen/mv_item_width %>px;

+}

+}

+

+.thumbwrap a {

+    display: block;

+    text-decoration: none;

+    color: #000;

+}

+

+.thumbwrap img {

+    border: <%@ dimen/mv_border_width %>px solid #e0e0e0;

+    border-radius: 5px;

+    width: 100%;

+}

+

+.thumbwrap .caption {

+    margin-top: 2px;

+    margin-left: 4px;

+    white-space: nowrap;

+    overflow: hidden;

+    text-overflow: ellipsis;

+    display: block;

+    font-size: .8em;

+    text-align: left;

+}

+

+</style>

+

+</head>

+<body>

+    <h3><%@ string/tab_most_visited %></h3>

+    <ul class="thumbwrap">

+        <%{ most_visited %>

+            <li>

+                <a href="<%= url %>">

+                    <img class="wrimg" src="<%= thumbnail %>" />

+                    <span class="caption"><%= title %></span>

+                </a>

+            </li>

+        <%} most_visited %>

+    </ul>

+</body>

+</html>

diff --git a/res/values-xlarge/dimensions.xml b/res/values-xlarge/dimensions.xml
index 5b86c86..9f5a602 100644
--- a/res/values-xlarge/dimensions.xml
+++ b/res/values-xlarge/dimensions.xml
@@ -14,4 +14,9 @@
     <dimen name="bookmarkThumbnailWidth">180dip</dimen>
     <dimen name="bookmarkThumbnailHeight">120dip</dimen>
     <dimen name="favicon_padded_size">24dip</dimen>
+    <!-- For the most visited page -->
+    <dimen name="mv_max_width">1010dp</dimen>
+    <dimen name="mv_item_width">231dp</dimen>
+    <dimen name="mv_item_width_portrait">213dp</dimen>
+    <dimen name="mv_border_width">3dp</dimen>
 </resources>
diff --git a/res/values/dimensions.xml b/res/values/dimensions.xml
index 03127dd..d50ce13 100644
--- a/res/values/dimensions.xml
+++ b/res/values/dimensions.xml
@@ -33,4 +33,9 @@
     <dimen name="qc_slop">15dip</dimen>
     <dimen name="bookmark_widget_thumb_size">32dip</dimen>
     <dimen name="bookmark_widget_favicon_size">26dip</dimen>
+    <!-- For the most visited page -->
+    <dimen name="mv_max_width">830dp</dimen>
+    <dimen name="mv_item_width">96dp</dimen>
+    <dimen name="mv_item_width_portrait">96dp</dimen>
+    <dimen name="mv_border_width">3dp</dimen>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index b8a45bf..9c12dc3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -609,6 +609,11 @@
     <!-- Summary for lab quick controls feature [CHAR LIMIT=80] -->
     <string name="pref_lab_quick_controls_summary">
         Swipe thumb from left or right edge to access quick controls</string>
+    <!-- Title for lab "Most Visited" homepage feature [CHAR LIMIT=40] -->
+    <string name="pref_lab_most_visited_homepage">Most Visited Homepage</string>
+    <!-- Summary for lab "Most Visited" homepage feature [CHAR LIMIT=80] -->
+    <string name="pref_lab_most_visited_homepage_summary">
+        Sets your homepage to show the most visited pages.</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/lab_preferences.xml b/res/xml/lab_preferences.xml
index 2168471..16a5169 100644
--- a/res/xml/lab_preferences.xml
+++ b/res/xml/lab_preferences.xml
@@ -23,4 +23,10 @@
         android:title="@string/pref_lab_quick_controls"
         android:summary="@string/pref_lab_quick_controls_summary" />
 
+    <CheckBoxPreference
+        android:key="use_most_visited_homepage"
+        android:defaultValue="false"
+        android:title="@string/pref_lab_most_visited_homepage"
+        android:summary="@string/pref_lab_most_visited_homepage_summary" />
+
 </PreferenceScreen>
diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java
index 267056e..ca8091f 100644
--- a/src/com/android/browser/BrowserSettings.java
+++ b/src/com/android/browser/BrowserSettings.java
@@ -17,6 +17,7 @@
 
 package com.android.browser;
 
+import com.android.browser.homepages.HomeProvider;
 import com.android.browser.search.SearchEngine;
 import com.android.browser.search.SearchEngines;
 
@@ -119,6 +120,7 @@
 
     // Lab settings
     private boolean quickControls = false;
+    private boolean useMostVisitedHomepage = false;
 
     // By default the error console is shown once the user navigates to about:debug.
     // The setting can be then toggled from the settings menu.
@@ -171,6 +173,7 @@
     public final static String PREF_USER_AGENT = "user_agent";
 
     public final static String PREF_QUICK_CONTROLS = "enable_quick_controls";
+    public final static String PREF_MOST_VISITED_HOMEPAGE = "use_most_visited_homepage";
 
     private static final String DESKTOP_USERAGENT = "Mozilla/5.0 (Macintosh; " +
             "U; Intel Mac OS X 10_6_3; en-us) AppleWebKit/533.16 (KHTML, " +
@@ -496,6 +499,7 @@
         }
 
         quickControls = p.getBoolean(PREF_QUICK_CONTROLS, quickControls);
+        useMostVisitedHomepage = p.getBoolean(PREF_MOST_VISITED_HOMEPAGE, useMostVisitedHomepage);
 
         // Only set these on startup if it is a dev build
         if (DEV_BUILD) {
@@ -525,6 +529,9 @@
     }
 
     public String getHomePage() {
+        if (useMostVisitedHomepage) {
+            return HomeProvider.MOST_VISITED;
+        }
         return homeUrl;
     }
 
@@ -584,6 +591,10 @@
         return quickControls;
     }
 
+    public boolean useMostVisitedHomepage() {
+        return useMostVisitedHomepage;
+    }
+
     public boolean showDebugSettings() {
         return showDebugSettings;
     }
@@ -847,6 +858,8 @@
             update();
         } else if (PREF_QUICK_CONTROLS.equals(key)) {
             quickControls = p.getBoolean(PREF_QUICK_CONTROLS, quickControls);
+        } else if (PREF_MOST_VISITED_HOMEPAGE.equals(key)) {
+            useMostVisitedHomepage = p.getBoolean(PREF_MOST_VISITED_HOMEPAGE, useMostVisitedHomepage);
         }
     }
 }
diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java
index 320d3b3..5ef7564 100644
--- a/src/com/android/browser/Tab.java
+++ b/src/com/android/browser/Tab.java
@@ -16,6 +16,7 @@
 
 package com.android.browser;
 
+import com.android.browser.homepages.HomeProvider;
 import com.android.common.speech.LoggingEvents;
 
 import android.app.Activity;
@@ -1568,6 +1569,9 @@
     }
 
     String getUrl() {
+        if (HomeProvider.MOST_VISITED.equals(mCurrentState.mUrl)) {
+            return "";
+        }
         return mCurrentState.mUrl;
     }
 
diff --git a/src/com/android/browser/homepages/HomeProvider.java b/src/com/android/browser/homepages/HomeProvider.java
new file mode 100644
index 0000000..5c368eb
--- /dev/null
+++ b/src/com/android/browser/homepages/HomeProvider.java
@@ -0,0 +1,81 @@
+

+/*

+ * Copyright (C) 2011 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+package com.android.browser.homepages;

+

+import android.content.ContentProvider;

+import android.content.ContentValues;

+import android.content.res.AssetFileDescriptor;

+import android.database.Cursor;

+import android.net.Uri;

+import android.os.ParcelFileDescriptor;

+import android.util.Log;

+

+import java.io.IOException;

+

+public class HomeProvider extends ContentProvider {

+

+    private static final String TAG = "HomeProvider";

+    public static final String AUTHORITY = "com.android.browser.home";

+    public static final String MOST_VISITED = "content://" + AUTHORITY + "/";

+

+    @Override

+    public int delete(Uri uri, String selection, String[] selectionArgs) {

+        return 0;

+    }

+

+    @Override

+    public String getType(Uri uri) {

+        return null;

+    }

+

+    @Override

+    public Uri insert(Uri uri, ContentValues values) {

+        return null;

+    }

+

+    @Override

+    public boolean onCreate() {

+        return false;

+    }

+

+    @Override

+    public Cursor query(Uri uri, String[] projection, String selection,

+            String[] selectionArgs, String sortOrder) {

+        return null;

+    }

+

+    @Override

+    public int update(Uri uri, ContentValues values, String selection,

+            String[] selectionArgs) {

+        return 0;

+    }

+

+    @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 RequestHandler(getContext(), uri, afd.createOutputStream()).start();

+            return pipes[0];

+        } catch (IOException e) {

+            Log.e(TAG, "Failed to handle request: " + uri, e);

+            return null;

+        }

+    }

+

+}

diff --git a/src/com/android/browser/homepages/RequestHandler.java b/src/com/android/browser/homepages/RequestHandler.java
new file mode 100644
index 0000000..a53fb52
--- /dev/null
+++ b/src/com/android/browser/homepages/RequestHandler.java
@@ -0,0 +1,145 @@
+

+/*

+ * Copyright (C) 2011 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+package com.android.browser.homepages;

+

+import com.android.browser.R;

+

+import java.io.IOException;

+import java.io.InputStream;

+import java.io.OutputStream;

+import java.util.regex.Matcher;

+import java.util.regex.Pattern;

+

+import android.content.Context;

+import android.content.UriMatcher;

+import android.content.res.Resources;

+import android.database.Cursor;

+import android.net.Uri;

+import android.provider.Browser;

+import android.util.Base64;

+import android.util.Log;

+

+public class RequestHandler extends Thread {

+

+    private static final String TAG = "RequestHandler";

+    private static final int INDEX = 1;

+    private static final int RESOURCE = 2;

+    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

+

+    Uri mUri;

+    Context mContext;

+    OutputStream mOutput;

+

+    static {

+        sUriMatcher.addURI(HomeProvider.AUTHORITY, "/", INDEX);

+        sUriMatcher.addURI(HomeProvider.AUTHORITY, "res/*/*", RESOURCE);

+    }

+

+    public RequestHandler(Context context, Uri uri, OutputStream out) {

+        mUri = uri;

+        mContext = context;

+        mOutput = out;

+    }

+

+    @Override

+    public void run() {

+        super.run();

+        try {

+            doHandleRequest();

+        } catch (Exception e) {

+            Log.e(TAG, "Failed to handle request: " + mUri, e);

+        } finally {

+            cleanup();

+        }

+    }

+

+    void doHandleRequest() throws IOException {

+        int match = sUriMatcher.match(mUri);

+        switch (match) {

+        case INDEX:

+            writeTemplatedIndex();

+            break;

+        case RESOURCE:

+            writeResource(getUriResourcePath());

+            break;

+        }

+    }

+

+    void writeTemplatedIndex() throws IOException {

+        Template t = Template.getCachedTemplate(mContext, R.raw.most_visited);

+        Cursor cursor = mContext.getContentResolver().query(Browser.BOOKMARKS_URI,

+                new String[] { "DISTINCT url", "title", "thumbnail" },

+                "(visits > 0 OR bookmark = 1) AND url NOT LIKE 'content:%' AND thumbnail IS NOT NULL", null, "visits DESC LIMIT 12");

+

+        t.assignLoop("most_visited", new Template.CursorListEntityWrapper(cursor) {

+            @Override

+            public void writeValue(OutputStream stream, String key) throws IOException {

+                Cursor cursor = getCursor();

+                if (key.equals("url")) {

+                    stream.write(cursor.getString(0).getBytes());

+                } else if (key.equals("title")) {

+                    stream.write(cursor.getString(1).getBytes());

+                } else if (key.equals("thumbnail")) {

+                    stream.write("data:image/png;base64,".getBytes());

+                    byte[] thumb = cursor.getBlob(2);

+                    stream.write(Base64.encode(thumb, Base64.DEFAULT));

+                }

+            }

+        });

+        t.write(mOutput);

+    }

+

+    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();

+        int id = res.getIdentifier(fileName, null, mContext.getPackageName());

+        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 (Exception e) {

+            Log.e(TAG, "Failed to close pipe!", e);

+        }

+    }

+

+}

diff --git a/src/com/android/browser/homepages/Template.java b/src/com/android/browser/homepages/Template.java
new file mode 100644
index 0000000..c1a6b0e
--- /dev/null
+++ b/src/com/android/browser/homepages/Template.java
@@ -0,0 +1,279 @@
+

+/*

+ * Copyright (C) 2011 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+package com.android.browser.homepages;

+

+import java.io.IOException;

+import java.io.InputStream;

+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;

+

+import android.content.Context;

+import android.content.res.Resources;

+import android.database.Cursor;

+import android.util.TypedValue;

+

+public class Template {

+

+    private static HashMap<Integer, Template> sCachedTemplates = new HashMap<Integer, Template>();

+

+    public static Template getCachedTemplate(Context context, int id) {

+        synchronized (sCachedTemplates) {

+            Template template = sCachedTemplates.get(id);

+            if (template == null) {

+                template = new Template(context, id);

+                sCachedTemplates.put(id, template);

+            }

+            // Return a copy so that we don't share data

+            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;

+        Template mSubTemplate;

+

+        public ListEntity(Context context, String key, String subTemplate) {

+            mKey = key;

+            mSubTemplate = new Template(context, subTemplate);

+        }

+

+        @Override

+        public void write(OutputStream stream, EntityData params) throws IOException {

+            ListEntityIterator iter = params.getListIterator(mKey);

+            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 List<Entity> mTemplate;

+    private HashMap<String, Object> mData = new HashMap<String, Object>();

+    private Template(Context context, int tid) {

+        this(context, readRaw(context, tid));

+    }

+

+    private Template(Context context, String template) {

+        mTemplate = new ArrayList<Entity>();

+        template = replaceConsts(context, template);

+        parseTemplate(context, template);

+    }

+

+    private Template(Template copy) {

+        mTemplate = copy.mTemplate;

+    }

+

+    Template copy() {

+        return new Template(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 static_part = template.substring(start, m.start());

+            if (static_part.length() > 0) {

+                mTemplate.add(new StringEntity(static_part));

+            }

+            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_m = p.matcher(template);

+                if (end_m.find(m.end())) {

+                    start = m.end();

+                    m.region(end_m.end(), template.length());

+                    String subTemplate = template.substring(start, end_m.start());

+                    mTemplate.add(new ListEntity(context, name, subTemplate));

+                    start = end_m.end();

+                    continue;

+                }

+            }

+            start = m.end();

+        }

+        String static_part = template.substring(start, template.length());

+        if (static_part.length() > 0) {

+            mTemplate.add(new StringEntity(static_part));

+        }

+    }

+

+    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 = context.getPackageName();

+        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>";

+        }

+    }

+

+}