From bafe58a83ade9cfa5c91e7033fae2d08a64603e8 Mon Sep 17 00:00:00 2001 From: John Reck Date: Tue, 11 Jan 2011 10:26:02 -0800 Subject: Most Visited custom homepage Change-Id: Ic57762855e5d187aa0fe3a8eab2757b5a76ff08d --- AndroidManifest.xml | 7 + res/raw/most_visited.ktpl | 85 +++++++ res/values-xlarge/dimensions.xml | 5 + res/values/dimensions.xml | 5 + res/values/strings.xml | 5 + res/xml/lab_preferences.xml | 6 + src/com/android/browser/BrowserSettings.java | 13 + src/com/android/browser/Tab.java | 4 + .../android/browser/homepages/HomeProvider.java | 81 ++++++ .../android/browser/homepages/RequestHandler.java | 145 +++++++++++ src/com/android/browser/homepages/Template.java | 279 +++++++++++++++++++++ 11 files changed, 635 insertions(+) create mode 100644 res/raw/most_visited.ktpl create mode 100644 src/com/android/browser/homepages/HomeProvider.java create mode 100644 src/com/android/browser/homepages/RequestHandler.java create mode 100644 src/com/android/browser/homepages/Template.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 5b844e0..9ceaf82 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -223,6 +223,13 @@ + + + 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 @@ + + + + +<%@ string/new_tab %> + + + + + + +

<%@ string/tab_most_visited %>

+ + + 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 @@ 180dip 120dip 24dip + + 1010dp + 231dp + 213dp + 3dp 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 @@ 15dip 32dip 26dip + + 830dp + 96dp + 96dp + 3dp 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 @@ Swipe thumb from left or right edge to access quick controls + + Most Visited Homepage + + + Sets your homepage to show the most visited pages. Data connectivity problem 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" /> + + 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 @@ public class BrowserSettings extends Observable implements OnSharedPreferenceCha // 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 class BrowserSettings extends Observable implements OnSharedPreferenceCha 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 @@ public class BrowserSettings extends Observable implements OnSharedPreferenceCha } 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 class BrowserSettings extends Observable implements OnSharedPreferenceCha } public String getHomePage() { + if (useMostVisitedHomepage) { + return HomeProvider.MOST_VISITED; + } return homeUrl; } @@ -584,6 +591,10 @@ public class BrowserSettings extends Observable implements OnSharedPreferenceCha return quickControls; } + public boolean useMostVisitedHomepage() { + return useMostVisitedHomepage; + } + public boolean showDebugSettings() { return showDebugSettings; } @@ -847,6 +858,8 @@ public class BrowserSettings extends Observable implements OnSharedPreferenceCha 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 @@ class Tab { } 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 sCachedTemplates = new HashMap(); + + 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 mData; + + public HashMapEntityData(HashMap 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 mTemplate; + private HashMap mData = new HashMap(); + private Template(Context context, int tid) { + this(context, readRaw(context, tid)); + } + + private Template(Context context, String template) { + mTemplate = new ArrayList(); + 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 "Error"; + } + } + +} -- cgit v1.1