Bug 715550 - Migration of Sync settings from XUL Fennec. r=mfinkle a=blocking-fennec a=mfinkle
authorGian-Carlo Pascutto <gpascutto@mozilla.com>
Tue, 17 Apr 2012 22:01:28 +0200
changeset 91893 36c22a5ddbe5d92cc55d0fbbcbf827cc4ad02067
parent 91892 c80635a1c62f6bb7c6f555f5edb2282aa4a616a9
child 91894 656d5df8c8de91e53ef12b6ffa09929723bd46bc
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmfinkle, blocking-fennec, mfinkle
bugs715550
milestone14.0a1
Bug 715550 - Migration of Sync settings from XUL Fennec. r=mfinkle a=blocking-fennec a=mfinkle
mobile/android/base/GeckoApp.java
mobile/android/base/ProfileMigrator.java
mobile/android/base/db/BrowserProvider.java.in
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1644,16 +1644,20 @@ abstract public class GeckoApp
             if (!mRestoreSession) {
                 mBrowserToolbar.updateTabCount(1);
                 showAboutHome();
             }
         } else {
             mBrowserToolbar.updateTabCount(1);
         }
 
+        // Start migrating as early as possible, can do this in
+        // parallel with Gecko load.
+        checkMigrateProfile();
+
         Uri data = intent.getData();
         if (data != null && "http".equals(data.getScheme()) &&
             isHostOnPrefetchWhitelist(data.getHost())) {
             Intent copy = new Intent(intent);
             copy.setAction(ACTION_LOAD);
             GeckoAppShell.getHandler().post(new RedirectorRunnable(copy));
             // We're going to handle this uri with the redirector, so setting
             // the action to MAIN and clearing the uri data prevents us from
@@ -1766,31 +1770,32 @@ abstract public class GeckoApp
 
         GeckoScreenOrientationListener.getInstance().start();
 
         final GeckoApp self = this;
 
         GeckoAppShell.getHandler().postDelayed(new Runnable() {
             public void run() {
                 Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - pre checkLaunchState");
+                // Sync settings need Gecko to be loaded, so
+                // no hurry in starting this.
+                checkMigrateSync();
 
                 /*
                   XXXX see bug 635342
                    We want to disable this code if possible.  It is about 145ms in runtime
                 SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
                 String localeCode = settings.getString(getPackageName() + ".locale", "");
                 if (localeCode != null && localeCode.length() > 0)
                     GeckoAppShell.setSelectedLocale(localeCode);
                 */
 
                 if (!checkLaunchState(LaunchState.Launched)) {
                     return;
                 }
-
-                checkMigrateProfile();
             }
         }, 50);
     }
 
     public GeckoProfile getProfile() {
         // fall back to default profile if we didn't load a specific one
         if (mProfile == null) {
             mProfile = GeckoProfile.get(this);
@@ -2273,40 +2278,59 @@ abstract public class GeckoApp
             reader.close();
         } catch (Exception e) {
             Log.i(LOGTAG, "error reading update status", e);
         }
         return status;
     }
 
     private void checkMigrateProfile() {
-        File profileDir = getProfile().getDir();
-        long currentTime = SystemClock.uptimeMillis();
+        final File profileDir = getProfile().getDir();
+        final long currentTime = SystemClock.uptimeMillis();
 
         if (profileDir != null) {
-            Log.i(LOGTAG, "checking profile migration in: " + profileDir.getAbsolutePath());
+            Log.i(LOGTAG, "Checking profile migration in: " + profileDir.getAbsolutePath());
             final GeckoApp app = GeckoApp.mAppContext;
-            ProfileMigrator profileMigrator =
+            final ProfileMigrator profileMigrator =
                 new ProfileMigrator(app, profileDir);
 
             // Do a migration run on the first start after an upgrade.
             if (!profileMigrator.hasMigrationRun()) {
                 final SetupScreen setupScreen = new SetupScreen(app);
 
-                // don't show unless this take a while
+                // Don't show unless this take a while.
                 setupScreen.showDelayed(mMainHandler);
-                profileMigrator.launch();
-                setupScreen.dismiss();
-
-                // Update about:home with the new information.
-                updateAboutHomeTopSites();
+
+                GeckoAppShell.getHandler().post(new Runnable() {
+                    public void run() {
+                        profileMigrator.launchPlaces();
+                        setupScreen.dismiss();
+
+                        long timeDiff = SystemClock.uptimeMillis() - currentTime;
+                        Log.i(LOGTAG, "Profile migration took " + timeDiff + " ms");
+
+                        // Update about:home with the new information.
+                        updateAboutHomeTopSites();
+                    }
+                });
             }
         }
-        long timeDiff = SystemClock.uptimeMillis() - currentTime;
-        Log.i(LOGTAG, "Profile migration took " + timeDiff + " ms");
+    }
+
+    private void checkMigrateSync() {
+        final File profileDir = getProfile().getDir();
+        if (profileDir != null) {
+            final GeckoApp app = GeckoApp.mAppContext;
+            ProfileMigrator profileMigrator =
+                new ProfileMigrator(app, profileDir);
+            if (!profileMigrator.hasSyncMigrated()) {
+                Log.i(LOGTAG, "Checking Sync settings in: " + profileDir.getAbsolutePath());
+                profileMigrator.launchSyncPrefs();
+            }
+        }
     }
 
     /**
      * The FilePickerPromptRunnable has to be called to show an intent-like
      * context menu UI using the PromptService.
      */
     private class FilePickerPromptRunnable implements Runnable {
         public FilePickerPromptRunnable(String aTitle, PromptService.PromptListItem[] aItems) {
--- a/mobile/android/base/ProfileMigrator.java
+++ b/mobile/android/base/ProfileMigrator.java
@@ -37,40 +37,45 @@
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.ImageColumns;
 import org.mozilla.gecko.db.BrowserContract.Images;
+import org.mozilla.gecko.db.BrowserContract.Passwords;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.db.BrowserContract.SyncColumns;
 import org.mozilla.gecko.sqlite.SQLiteBridge;
 import org.mozilla.gecko.sqlite.SQLiteBridgeException;
+import org.mozilla.gecko.sync.setup.SyncAccounts;
+import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
 
+import android.accounts.Account;
 import android.content.ContentResolver;
 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.SharedPreferences;
 import android.database.Cursor;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteConstraintException;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
+import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.RemoteException;
 import android.provider.Browser;
+import android.text.TextUtils;
 import android.util.Log;
-import android.net.Uri;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.File;
 import java.io.InputStream;
 import java.util.Arrays;
 import java.util.ArrayList;
@@ -78,16 +83,20 @@ import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.json.JSONException;
+
 public class ProfileMigrator {
     private static final String LOGTAG = "ProfileMigrator";
     private static final String PREFS_NAME = "ProfileMigrator";
     private File mProfileDir;
     private ContentResolver mCr;
     private Context mContext;
 
     // Default number of history entries to migrate in one run.
@@ -96,16 +105,17 @@ public class ProfileMigrator {
     // Maximum number of history entries to fetch at once.
     // This limits the max memory use to about 10M (empirically), so we don't OOM.
     private static final int HISTORY_MAX_BATCH = 5000;
 
     private static final String PREFS_MIGRATE_BOOKMARKS_DONE = "bookmarks_done";
     private static final String PREFS_MIGRATE_HISTORY_DONE = "history_done";
     // Number of history entries already migrated.
     private static final String PREFS_MIGRATE_HISTORY_COUNT = "history_count";
+    private static final String PREFS_MIGRATE_SYNC_DONE = "sync_done";
 
     /*
        These queries are derived from the low-level Places schema
        https://developer.mozilla.org/en/The_Places_database
     */
     private final String kRootQuery =
         "SELECT root_name, folder_id FROM moz_bookmarks_roots";
     private final String kRootName     = "root_name";
@@ -194,47 +204,80 @@ public class ProfileMigrator {
         "DESC LIMIT ? OFFSET ?";
 
     private final String kHistoryUrl    = "p_url";
     private final String kHistoryTitle  = "p_title";
     private final String kHistoryGuid   = "p_guid";
     private final String kHistoryDate   = "h_date";
     private final String kHistoryVisits = "h_visits";
 
+    /*
+      Sync settings to get from prefs.js.
+    */
+    private final String[] kSyncSettingsList = new String[] {
+        "services.sync.account",
+        "services.sync.client.name",
+        "services.sync.client.GUID",
+        "services.sync.serverURL",
+        "services.sync.clusterURL"
+    };
+
+    /*
+      Sync settings to get from password manager.
+    */
+    private final String kSyncHostName = "chrome://weave";
+    private final String[] kSyncRealmList = new String[] {
+        "Mozilla Services Password",
+        "Mozilla Services Encryption Passphrase"
+    };
+
+
     public ProfileMigrator(Context context, File profileDir) {
         mProfileDir = profileDir;
         mContext = context;
         mCr = mContext.getContentResolver();
     }
 
-    public void launch() {
+    public void launchPlaces() {
         boolean timeThisRun = false;
         Telemetry.Timer timer = null;
         // First run, time things
         if (!hasMigrationRun()) {
             timeThisRun = true;
             timer = new Telemetry.Timer("BROWSERPROVIDER_XUL_IMPORT_TIME");
         }
-        launch(DEFAULT_HISTORY_MIGRATE_COUNT);
+        launchPlaces(DEFAULT_HISTORY_MIGRATE_COUNT);
         if (timeThisRun)
             timer.stop();
     }
 
-    public void launch(int maxEntries) {
+    public void launchPlaces(int maxEntries) {
+        // Places migration is heavy on the phone, allow it to block
+        // other processing.
         new PlacesRunnable(maxEntries).run();
     }
 
+    public void launchSyncPrefs() {
+        // Sync settings will post a runnable, no need for a seperate thread.
+        new SyncTask().run();
+    }
+
     public boolean areBookmarksMigrated() {
         return getPreferences().getBoolean(PREFS_MIGRATE_BOOKMARKS_DONE, false);
     }
 
     public boolean isHistoryMigrated() {
         return getPreferences().getBoolean(PREFS_MIGRATE_HISTORY_DONE, false);
     }
 
+    // Have Sync settings been transferred?
+    public boolean hasSyncMigrated() {
+        return getPreferences().getBoolean(PREFS_MIGRATE_SYNC_DONE, false);
+    }
+
     // Has migration run before?
     protected boolean hasMigrationRun() {
         return areBookmarksMigrated() && (getMigratedHistoryEntries() > 0);
     }
 
     // Has migration entirely finished?
     protected boolean hasMigrationFinished() {
         return areBookmarksMigrated() && isHistoryMigrated();
@@ -261,16 +304,216 @@ public class ProfileMigrator {
     }
 
     protected void setMigratedBookmarks() {
         SharedPreferences.Editor editor = getPreferences().edit();
         editor.putBoolean(PREFS_MIGRATE_BOOKMARKS_DONE, true);
         editor.commit();
     }
 
+    protected void setMigratedSync() {
+        SharedPreferences.Editor editor = getPreferences().edit();
+        editor.putBoolean(PREFS_MIGRATE_SYNC_DONE, true);
+        editor.commit();
+    }
+
+    private class SyncTask implements Runnable, GeckoEventListener {
+        private List<String> mSyncSettingsList;
+        private Map<String, String> mSyncSettingsMap;
+
+        // Initialize preferences by sending the "Preferences:Get" command to Gecko
+        protected void requestValues() {
+            mSyncSettingsList = Arrays.asList(kSyncSettingsList);
+            mSyncSettingsMap = new HashMap<String, String>();
+            JSONArray jsonPrefs = new JSONArray(mSyncSettingsList);
+            Log.d(LOGTAG, "Sending: " + jsonPrefs.toString());
+            GeckoEvent event =
+                GeckoEvent.createBroadcastEvent("Preferences:Get",
+                                                jsonPrefs.toString());
+            GeckoAppShell.sendEventToGecko(event);
+        }
+
+        // Receive settings reply from Gecko, do the rest of the setup
+        public void handleMessage(String event, JSONObject message) {
+            Log.d(LOGTAG, "Received event: " + event);
+            try {
+                if (event.equals("Preferences:Data")) {
+                    // Receive most settings from Gecko's service.
+                    // This includes personal info, so don't log.
+                    // Log.d(LOGTAG, "Message: " + message.toString());
+                    JSONArray jsonPrefs = message.getJSONArray("preferences");
+                    parsePrefs(jsonPrefs);
+                    GeckoAppShell.unregisterGeckoEventListener("Preferences:Data",
+                                                               (GeckoEventListener)this);
+
+                    // Now call the password provider to fill in the rest.
+                    for (String location: kSyncRealmList) {
+                        Log.d(LOGTAG, "Checking: " + location);
+                        String passwd = getPassword(location);
+                        if (!TextUtils.isEmpty(passwd)) {
+                            Log.d(LOGTAG, "Got password");
+                            mSyncSettingsMap.put(location, passwd);
+                        } else {
+                            Log.d(LOGTAG, "No password found");
+                            mSyncSettingsMap.put(location, null);
+                        }
+                    }
+
+                    // Call Sync and transfer settings.
+                    configureSync();
+                }
+            } catch (Exception e) {
+                Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
+            }
+        }
+
+        protected String getPassword(String realm) {
+            Cursor cursor = null;
+            String result = null;
+            try {
+                cursor = mCr.query(Passwords.CONTENT_URI,
+                                   null,
+                                   Passwords.HOSTNAME + " = ? AND "
+                                   + Passwords.HTTP_REALM + " = ?",
+                                   new String[] { kSyncHostName, realm },
+                                   null);
+
+                if (cursor != null) {
+                    final int userCol =
+                        cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_USERNAME);
+                    final int passCol =
+                        cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_PASSWORD);
+
+                    if (cursor.moveToFirst()) {
+                        String user = cursor.getString(userCol);
+                        String pass = cursor.getString(passCol);
+                        result = pass;
+                    } else {
+                        Log.i(LOGTAG, "No password found for realm = " + realm);
+                    }
+                }
+            } finally {
+                if (cursor != null)
+                    cursor.close();
+            }
+
+            return result;
+        }
+
+        protected void parsePrefs(JSONArray jsonPrefs) {
+            try {
+                final int length = jsonPrefs.length();
+                for (int i = 0; i < length; i++) {
+                    JSONObject jPref = jsonPrefs.getJSONObject(i);
+                    final String prefName = jPref.getString("name");
+                    final String prefType = jPref.getString("type");
+                    if ("bool".equals(prefType)) {
+                        final boolean value = jPref.getBoolean("value");
+                        mSyncSettingsMap.put(prefName, value ? "1" : "0");
+                    } else {
+                        final String value = jPref.getString("value");
+                        if (!TextUtils.isEmpty(value)) {
+                            mSyncSettingsMap.put(prefName, value);
+                        } else {
+                            Log.w(LOGTAG, "Could not recover setting for = " + prefName);
+                            mSyncSettingsMap.put(prefName, null);
+                        }
+                    }
+                }
+            } catch (JSONException e) {
+                Log.e(LOGTAG, "Exception handling preferences answer: "
+                      + e.getMessage());
+            }
+        }
+
+        protected void configureSync() {
+            final String userName = mSyncSettingsMap.get("services.sync.account");
+            final String syncKey = mSyncSettingsMap.get("Mozilla Services Password");
+            final String syncPass = mSyncSettingsMap.get("Mozilla Services Encryption Passphrase");
+            final String serverURL = mSyncSettingsMap.get("services.sync.serverURL");
+            final String clusterURL = mSyncSettingsMap.get("services.sync.clusterURL");
+            final String clientName = mSyncSettingsMap.get("services.sync.client.name");
+            final String clientGuid = mSyncSettingsMap.get("services.sync.client.GUID");
+
+            if (userName == null || syncKey == null || syncPass == null) {
+                // This isn't going to work. Give up.
+                Log.e(LOGTAG, "Profile has incomplete Sync config. Not migrating.");
+                setMigratedSync();
+                return;
+            }
+
+            final SyncAccountParameters params =
+                new SyncAccountParameters(mContext, null,
+                                          userName, syncKey,
+                                          syncPass, serverURL, clusterURL,
+                                          clientName, clientGuid);
+
+            new SyncAccounts.CreateSyncAccountTask() {
+                @Override
+                protected void onPostExecute(Account account) {
+                    if (account == null) {
+                        Log.e(LOGTAG, "Failed to migrate Sync account.");
+                    } else {
+                        Log.i(LOGTAG, "Migrating Sync account succeeded.");
+                    }
+                    setMigratedSync();
+                }
+            }.execute(params);
+        }
+
+        protected void registerAndRequest() {
+            GeckoAppShell.getHandler().post(new Runnable() {
+                public void run() {
+                    GeckoAppShell.registerGeckoEventListener("Preferences:Data",
+                                                             SyncTask.this);
+                    requestValues();
+                }
+            });
+        }
+
+        @Override
+        public void run() {
+            // Run only if no Sync accounts exist.
+            new SyncAccounts.AccountsExistTask() {
+                @Override
+                protected void onPostExecute(Boolean result) {
+                    if (result.booleanValue()) {
+                        Log.i(LOGTAG, "Sync account already configured, skipping.");
+                        setMigratedSync();
+                    } else {
+                        // No account configured, fire up.
+                        registerAndRequest();
+                    }
+                }
+            }.execute(mContext);
+        }
+    }
+
+    private class MiscTask implements Runnable {
+        protected void cleanupXULLibCache() {
+            File cacheFile = GeckoAppShell.getCacheDir(mContext);
+            File[] files = cacheFile.listFiles();
+            if (files != null) {
+                Iterator cacheFiles = Arrays.asList(files).iterator();
+                while (cacheFiles.hasNext()) {
+                    File libFile = (File)cacheFiles.next();
+                    if (libFile.getName().endsWith(".so")) {
+                        libFile.delete();
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void run() {
+            // XXX: Land dependent bugs (732069) first
+            // cleanupXULLibCache();
+        }
+    }
+
     private class PlacesRunnable implements Runnable {
         private Map<Long, Long> mRerootMap;
         private ArrayList<ContentProviderOperation> mOperations;
         private int mMaxEntries;
 
         public PlacesRunnable(int limit) {
             mMaxEntries = limit;
         }
@@ -873,30 +1116,14 @@ public class ProfileMigrator {
                 if (db != null) {
                     db.close();
                 }
                 Log.e(LOGTAG, "Error on places database:", e);
                 return;
             }
         }
 
-        protected void cleanupXULLibCache() {
-            File cacheFile = GeckoAppShell.getCacheDir(mContext);
-            File[] files = cacheFile.listFiles();
-            if (files != null) {
-                Iterator<File> cacheFiles = Arrays.asList(files).iterator();
-                while (cacheFiles.hasNext()) {
-                    File libFile = cacheFiles.next();
-                    if (libFile.getName().endsWith(".so")) {
-                        libFile.delete();
-                    }
-                }
-            }
-        }
-
         @Override
         public void run() {
             migratePlaces(mProfileDir);
-            // XXX: Land dependent bugs first
-            // cleanupXULLibCache();
         }
     }
 }
--- a/mobile/android/base/db/BrowserProvider.java.in
+++ b/mobile/android/base/db/BrowserProvider.java.in
@@ -1492,17 +1492,17 @@ public class BrowserProvider extends Con
             if (wantHistory || wantBookmarks) {
                 ProfileMigrator migrator =
                     new ProfileMigrator(mContext, profileDir);
 
                 boolean needBookmarks = wantBookmarks && !migrator.areBookmarksMigrated();
                 boolean needHistory = wantHistory && !migrator.isHistoryMigrated();
 
                 if (needBookmarks || needHistory) {
-                    migrator.launch();
+                    migrator.launchPlaces();
 
                     needBookmarks = wantBookmarks && !migrator.areBookmarksMigrated();
                     needHistory = wantHistory && !migrator.isHistoryMigrated();
                     // Bookmarks are expected to finish at the first run.
                     if (needBookmarks) {
                         Log.w(LOGTAG, "Bookmarks migration did not finish.");
                     }
                 }