Bug 836450 - Add default bookmark support for distributions. r=mfinkle,wesj
authorMargaret Leibovic <margaret.leibovic@gmail.com>
Thu, 14 Feb 2013 15:35:39 -0800
changeset 121983 458969f6b3690ad647f3349b6ff9aaf50e773807
parent 121982 3be58b112a33bb9c94725378cf880fc7b101a76f
child 121984 df6fbdf62e13933bde8593f623306846f0306de5
push id24314
push userryanvm@gmail.com
push dateFri, 15 Feb 2013 14:39:46 +0000
treeherdermozilla-central@326c5e4868fe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle, wesj
bugs836450
milestone21.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 836450 - Add default bookmark support for distributions. r=mfinkle,wesj
mobile/android/base/Distribution.java
mobile/android/base/db/BrowserProvider.java.in
--- a/mobile/android/base/Distribution.java
+++ b/mobile/android/base/Distribution.java
@@ -7,59 +7,66 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.util.GeckoBackgroundThread;
 
 import android.app.Activity;
+import android.content.Context;
 import android.content.SharedPreferences;
 import android.util.Log;
 
+import java.io.BufferedReader;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.OutputStream;
-
 import java.util.Enumeration;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
 public final class Distribution {
     private static final String LOGTAG = "GeckoDistribution";
 
     private static final int STATE_UNKNOWN = 0;
     private static final int STATE_NONE = 1;
     private static final int STATE_SET = 2;
 
     /**
      * Initializes distribution if it hasn't already been initalized.
      */
-    public static void init(final Activity activity) {
+    public static void init(final Context context) {
         // Read/write preferences and files on the background thread.
         GeckoBackgroundThread.getHandler().post(new Runnable() {
             public void run() {
                 // Bail if we've already initialized the distribution.
-                SharedPreferences settings = activity.getPreferences(Activity.MODE_PRIVATE);
-                String keyName = activity.getPackageName() + ".distribution_state";
+                SharedPreferences settings = context.getSharedPreferences(GeckoApp.PREFS_NAME, Activity.MODE_PRIVATE);
+                String keyName = context.getPackageName() + ".distribution_state";
                 int state = settings.getInt(keyName, STATE_UNKNOWN);
                 if (state == STATE_NONE)
                     return;
 
                 // Send a message to Gecko if we've set a distribution.
                 if (state == STATE_SET) {
                     GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", null));
                     return;
                 }
 
                 boolean distributionSet = false;
                 try {
-                    distributionSet = copyFiles(activity);
+                    distributionSet = copyFiles(context);
                 } catch (IOException e) {
                     Log.e(LOGTAG, "Error copying distribution files", e);
                 }
 
                 if (distributionSet) {
                     GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", null));
                     settings.edit().putInt(keyName, STATE_SET).commit();
                 } else {
@@ -68,32 +75,32 @@ public final class Distribution {
             }
         });
     }
 
     /**
      * Copies the /distribution folder out of the APK and into the app's data directory.
      * Returns true if distribution files were found and copied.
      */
-    private static boolean copyFiles(Activity activity) throws IOException {
-        File applicationPackage = new File(activity.getPackageResourcePath());
+    private static boolean copyFiles(Context context) throws IOException {
+        File applicationPackage = new File(context.getPackageResourcePath());
         ZipFile zip = new ZipFile(applicationPackage);
 
         boolean distributionSet = false;
         Enumeration<? extends ZipEntry> zipEntries = zip.entries();
         while (zipEntries.hasMoreElements()) {
             ZipEntry fileEntry = zipEntries.nextElement();
             String name = fileEntry.getName();
 
             if (!name.startsWith("distribution/"))
                 continue;
 
             distributionSet = true;
 
-            File dataDir = new File(activity.getApplicationInfo().dataDir);
+            File dataDir = new File(context.getApplicationInfo().dataDir);
             File outFile = new File(dataDir, name);
 
             File dir = outFile.getParentFile();
             if (!dir.exists())
                 dir.mkdirs();
 
             InputStream fileStream = zip.getInputStream(fileEntry);
             OutputStream outStream = new FileOutputStream(outFile);
@@ -106,9 +113,67 @@ public final class Distribution {
             outStream.close();
             outFile.setLastModified(fileEntry.getTime());
         }
 
         zip.close();
 
         return distributionSet;
     }
+
+    /**
+     * Returns parsed contents of bookmarks.json.
+     * This method should only be called from a background thread.
+     */
+    public static JSONArray getBookmarks(Context context) {
+        SharedPreferences settings = context.getSharedPreferences(GeckoApp.PREFS_NAME, Activity.MODE_PRIVATE);
+        String keyName = context.getPackageName() + ".distribution_state";
+        int state = settings.getInt(keyName, STATE_UNKNOWN);
+        if (state == STATE_NONE) {
+            return null;
+        }
+
+        ZipFile zip = null;
+        InputStream inputStream = null;
+        try {
+            if (state == STATE_UNKNOWN) {
+                // If the distribution hasn't been set yet, get bookmarks.json out of the APK
+                File applicationPackage = new File(context.getPackageResourcePath());
+                zip = new ZipFile(applicationPackage);
+                ZipEntry zipEntry = zip.getEntry("distribution/bookmarks.json");
+                if (zipEntry == null) {
+                    return null;
+                }
+                inputStream = zip.getInputStream(zipEntry);
+            } else {
+                // Otherwise, get bookmarks.json out of the data directory
+                File dataDir = new File(context.getApplicationInfo().dataDir);
+                File file = new File(dataDir, "distribution/bookmarks.json");
+                inputStream = new FileInputStream(file);
+            }
+
+            // Convert input stream to JSONArray
+            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+            StringBuilder stringBuilder = new StringBuilder();
+            String s;
+            while ((s = reader.readLine()) != null) {
+                stringBuilder.append(s);
+            }
+            return new JSONArray(stringBuilder.toString());
+        } catch (IOException e) {
+            Log.e(LOGTAG, "Error getting bookmarks", e);
+        } catch (JSONException e) {
+            Log.e(LOGTAG, "Error parsing bookmarks.json", e);
+        } finally {
+            try {
+                if (zip != null) {
+                    zip.close();
+                }
+                if (inputStream != null) {
+                    inputStream.close();
+                }
+            } catch (IOException e) {
+                Log.e(LOGTAG, "Error closing streams", e);
+            } 
+        }
+        return null;
+    }
 }
--- a/mobile/android/base/db/BrowserProvider.java.in
+++ b/mobile/android/base/db/BrowserProvider.java.in
@@ -3,75 +3,79 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #filter substitution
 package @ANDROID_PACKAGE_NAME@.db;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.IOException;
 import java.lang.Class;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Iterator;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Random;
 import java.util.regex.Pattern;
 import java.util.regex.Matcher;
 
-import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.Distribution;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.CommonColumns;
 import org.mozilla.gecko.db.BrowserContract.Control;
 import org.mozilla.gecko.db.BrowserContract.Favicons;
 import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.Schema;
 import org.mozilla.gecko.db.BrowserContract.SyncColumns;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.DBUtils;
+import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.ProfileMigrator;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.util.GeckoBackgroundThread;
 import org.mozilla.gecko.util.GeckoJarReader;
 
 import android.app.SearchManager;
 import android.content.ContentProvider;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.ContentProviderResult;
 import android.content.ContentProviderOperation;
+import android.content.Context;
 import android.content.OperationApplicationException;
-import android.content.Context;
 import android.content.UriMatcher;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.MatrixCursor;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.graphics.BitmapFactory;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
 import android.net.Uri;
 import android.os.Build;
 import android.text.TextUtils;
 import android.util.Log;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
 public class BrowserProvider extends ContentProvider {
     private static final String LOGTAG = "GeckoBrowserProvider";
     private Context mContext;
 
     static final String DATABASE_NAME = "browser.db";
 
     static final int DATABASE_VERSION = 14;
 
@@ -968,129 +972,180 @@ public class BrowserProvider extends Con
             createHistoryWithFaviconsView(db);
             createCombinedViewOn13(db);
 
             createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
                 R.string.bookmarks_folder_places, 0);
 
             createOrUpdateAllSpecialFolders(db);
 
-            createDefaultBookmarks(db, "^bookmarkdefaults_title_");
+            // Create distribution bookmarks before our own default bookmarks
+            int pos = createDistributionBookmarks(db);
+            createDefaultBookmarks(db, pos);
+        }
+
+        private String getLocalizedProperty(JSONObject bookmark, String property, Locale locale) throws JSONException {
+            // Try the full locale
+            String fullLocale = property + "." + locale.toString();
+            if (bookmark.has(fullLocale)) {
+                return bookmark.getString(fullLocale);
+            }
+            // Try without a variant
+            if (!TextUtils.isEmpty(locale.getVariant())) {
+                String noVariant = fullLocale.substring(0, fullLocale.lastIndexOf("_"));
+                if (bookmark.has(noVariant)) {
+                    return bookmark.getString(noVariant);
+                }
+            }
+            // Try just the language
+            String lang = property + "." + locale.getLanguage();
+            if (bookmark.has(lang)) {
+                return bookmark.getString(lang);
+            }
+            // Default to the non-localized property name
+            return bookmark.getString(property);
         }
 
-        private void createDefaultBookmarks(SQLiteDatabase db, String pattern) {
+        // Returns the number of bookmarks inserted in the db
+        private int createDistributionBookmarks(SQLiteDatabase db) {
+            JSONArray bookmarks = Distribution.getBookmarks(mContext);
+            if (bookmarks == null) {
+                return 0;
+            }
+
+            Locale locale = Locale.getDefault();
+            int pos = 0;
+            for (int i = 0; i < bookmarks.length(); i++) {
+                try {
+                    JSONObject bookmark = bookmarks.getJSONObject(i);
+
+                    String title = getLocalizedProperty(bookmark, "title", locale);
+                    String url = getLocalizedProperty(bookmark, "url", locale);
+
+                    // Look for an optional icon data URI
+                    Bitmap icon = null;
+                    if (bookmark.has("icon")) {
+                        String iconData = bookmark.getString("icon");
+                        icon = BitmapUtils.getBitmapFromDataURI(iconData);
+                    }
+
+                    createBookmark(db, title, url, pos, icon);
+                    pos++;
+                } catch (JSONException e) {
+                    Log.e(LOGTAG, "Error creating distribution bookmark", e);
+                }
+            }
+            return pos;
+        }
+
+        // Inserts default bookmarks, starting at a specified position
+        private void createDefaultBookmarks(SQLiteDatabase db, int pos) {
             Class<?> stringsClass = R.string.class;
-
             Field[] fields = stringsClass.getFields();
-            Pattern p = Pattern.compile(pattern);
-
-            ContentValues bookmarksValues = new ContentValues();
-            bookmarksValues.put(Bookmarks.PARENT, guidToID(db, Bookmarks.MOBILE_FOLDER_GUID));
-            long now = System.currentTimeMillis();
-            bookmarksValues.put(Bookmarks.DATE_CREATED, now);
-            bookmarksValues.put(Bookmarks.DATE_MODIFIED, now);
-
-            int pos = 0;
+            Pattern p = Pattern.compile("^bookmarkdefaults_title_");
+
             for (int i = 0; i < fields.length; i++) {
                 String name = fields[i].getName();
                 Matcher m = p.matcher(name);
-                if (!m.find())
+                if (!m.find()) {
                     continue;
-
+                }
                 try {
                     int titleid = fields[i].getInt(null);
                     String title = mContext.getString(titleid);
 
                     Field urlField = stringsClass.getField(name.replace("_title_", "_url_"));
                     int urlId = urlField.getInt(null);
                     String url = mContext.getString(urlId);
 
-                    bookmarksValues.put(Bookmarks.TITLE, title);
-                    bookmarksValues.put(Bookmarks.URL, url);
-                    bookmarksValues.put(Bookmarks.GUID, Utils.generateGuid());
-                    bookmarksValues.put(Bookmarks.POSITION, pos);
-                    db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, bookmarksValues);
-
-                    setDefaultFavicon(db, name, url);
+                    Bitmap icon = getDefaultFaviconFromPath(name);
+                    if (icon == null) {
+                        icon = getDefaultFaviconFromDrawable(name);
+                    }
+                    createBookmark(db, title, url, pos, icon);
                     pos++;
                 } catch (java.lang.IllegalAccessException ex) {
                     Log.e(LOGTAG, "Can't create bookmark " + name, ex);
                 } catch (java.lang.NoSuchFieldException ex) {
                     Log.e(LOGTAG, "Can't create bookmark " + name, ex);
                 }
             }
         }
 
-        private void setDefaultFavicon(SQLiteDatabase db, String name, String url) {
-            ByteArrayOutputStream stream = getDefaultFaviconFromPath(db, name, url);
-            if (stream == null) {
-              stream = getDefaultFaviconFromDrawable(db, name, url);
+        private void createBookmark(SQLiteDatabase db, String title, String url, int pos, Bitmap icon) {
+            ContentValues bookmarkValues = new ContentValues();
+            bookmarkValues.put(Bookmarks.PARENT, guidToID(db, Bookmarks.MOBILE_FOLDER_GUID));
+
+            long now = System.currentTimeMillis();
+            bookmarkValues.put(Bookmarks.DATE_CREATED, now);
+            bookmarkValues.put(Bookmarks.DATE_MODIFIED, now);
+
+            bookmarkValues.put(Bookmarks.TITLE, title);
+            bookmarkValues.put(Bookmarks.URL, url);
+            bookmarkValues.put(Bookmarks.GUID, Utils.generateGuid());
+            bookmarkValues.put(Bookmarks.POSITION, pos);
+            db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, bookmarkValues);
+
+            // Return early if there's no icon to set
+            if (icon == null) {
+                return;
             }
-            if (stream != null) {
-                ContentValues values = new ContentValues();
-                values.put(Favicons.DATA, stream.toByteArray());
-                values.put(Favicons.PAGE_URL, url);
-                insertFavicon(db, values);
-            }
+
+            ByteArrayOutputStream stream = new ByteArrayOutputStream();
+            icon.compress(Bitmap.CompressFormat.PNG, 100, stream);
+
+            ContentValues iconValues = new ContentValues();
+            iconValues.put(Favicons.DATA, stream.toByteArray());
+            iconValues.put(Favicons.PAGE_URL, url);
+            insertFavicon(db, iconValues);
         }
 
-        private ByteArrayOutputStream getDefaultFaviconFromPath(SQLiteDatabase db, String name, String url) {
-            ByteArrayOutputStream stream = null;
+        private Bitmap getDefaultFaviconFromPath(String name) {
             Class<?> stringClass = R.string.class;
             try {
                 // Look for a drawable with the id R.drawable.bookmarkdefaults_favicon_*
                 Field faviconField = stringClass.getField(name.replace("_title_", "_favicon_"));
-                if (faviconField == null)
-                  return null;
+                if (faviconField == null) {
+                    return null;
+                }
                 int faviconId = faviconField.getInt(null);
                 String path = mContext.getString(faviconId);
 
                 String apkPath = mContext.getPackageResourcePath();
                 File apkFile = new File(apkPath);
                 BitmapDrawable bitmapDrawable = GeckoJarReader.getBitmapDrawable(mContext.getResources(),
                                                                                  "jar:jar:" + apkFile.toURI() + "!/omni.ja!/" + path);
                 if (bitmapDrawable == null) {
-                  return null;
+                    return null;
                 }
-                Bitmap bitmap = bitmapDrawable.getBitmap();
-                if (bitmap == null) {
-                  return null;
-                }
-                stream = new ByteArrayOutputStream();
-                bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
+                return bitmapDrawable.getBitmap();
             } catch (java.lang.IllegalAccessException ex) {
                 Log.e(LOGTAG, "[Path] Can't create favicon " + name, ex);
             } catch (java.lang.NoSuchFieldException ex) {
-                // if there is no such field, create the bookmark without a favicon
-                Log.d(LOGTAG, "[Path] Can't create favicon " + name);
+                Log.e(LOGTAG, "[Path] Can't create favicon " + name, ex);
             }
-            return stream;
+            return null;
         }
 
-        private ByteArrayOutputStream getDefaultFaviconFromDrawable(SQLiteDatabase db, String name, String url) {
+        private Bitmap getDefaultFaviconFromDrawable(String name) {
             Class<?> drawablesClass = R.drawable.class;
-            ByteArrayOutputStream stream = null;
             try {
                 // Look for a drawable with the id R.drawable.bookmarkdefaults_favicon_*
                 Field faviconField = drawablesClass.getField(name.replace("_title_", "_favicon_"));
-                if (faviconField == null)
-                  return null;
-
+                if (faviconField == null) {
+                    return null;
+                }
                 int faviconId = faviconField.getInt(null);
-                Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), faviconId);
-                stream = new ByteArrayOutputStream();
-                bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
+                return BitmapFactory.decodeResource(mContext.getResources(), faviconId);
             } catch (java.lang.IllegalAccessException ex) {
                 Log.e(LOGTAG, "[Drawable] Can't create favicon " + name, ex);
             } catch (java.lang.NoSuchFieldException ex) {
-                // if there is no such field, create the bookmark without a favicon
-                Log.d(LOGTAG, "[Drawable] Can't create favicon " + name);
+                Log.e(LOGTAG, "[Drawable] Can't create favicon " + name, ex);
             }
-
-            return stream;
+            return null;
         }
 
         private void createOrUpdateAllSpecialFolders(SQLiteDatabase db) {
             createOrUpdateSpecialFolder(db, Bookmarks.MOBILE_FOLDER_GUID,
                 R.string.bookmarks_folder_mobile, 0);
             createOrUpdateSpecialFolder(db, Bookmarks.TOOLBAR_FOLDER_GUID,
                 R.string.bookmarks_folder_toolbar, 1);
             createOrUpdateSpecialFolder(db, Bookmarks.MENU_FOLDER_GUID,