Bug 1207417 - Settings mapper to sync b2g and android configurations r=snorp
authorFabrice Desré <fabrice@mozilla.com>
Thu, 24 Sep 2015 09:55:52 -0700
changeset 264281 1b161b73d4b94ab56d09b67f0d37a10591bf2e63
parent 264280 3c90be122d3e352416e25a088ed7f282f3e0433e
child 264282 e014082c421e633a87e1185dfb6e062af508eb2c
push id65590
push userkwierso@gmail.com
push dateFri, 25 Sep 2015 00:14:23 +0000
treeherdermozilla-inbound@0ab67cace54f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp
bugs1207417
milestone44.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 1207417 - Settings mapper to sync b2g and android configurations r=snorp
mobile/android/b2gdroid/app/Makefile.in
mobile/android/b2gdroid/app/src/main/AndroidManifest.xml
mobile/android/b2gdroid/app/src/main/java/org/mozilla/b2gdroid/Launcher.java
mobile/android/b2gdroid/app/src/main/java/org/mozilla/b2gdroid/SettingsMapper.java
mobile/android/b2gdroid/components/MessagesBridge.jsm
--- a/mobile/android/b2gdroid/app/Makefile.in
+++ b/mobile/android/b2gdroid/app/Makefile.in
@@ -3,16 +3,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 ANDROID_MANIFEST_FILE := src/main/AndroidManifest.xml
 
 JAVAFILES := \
   src/main/java/org/mozilla/b2gdroid/Apps.java \
   src/main/java/org/mozilla/b2gdroid/Launcher.java \
   src/main/java/org/mozilla/b2gdroid/ScreenStateObserver.java \
+  src/main/java/org/mozilla/b2gdroid/SettingsMapper.java \
   $(NULL)
 
 # The GeckoView consuming APK depends on the GeckoView JAR files.  There are two
 # issues: first, the GeckoView JAR files need to be built before they are
 # consumed here.  This happens for delicate reasons.  In the (serial) libs tier,
 # base/ is traversed before b2gdroid/app.  Since base/libs builds classes.dex,
 # the underlying JAR files are built before the libs tier of b2gdroid/app is
 # processed.  Second, there is a correctness issue: the GeckoView JAR providing
--- a/mobile/android/b2gdroid/app/src/main/AndroidManifest.xml
+++ b/mobile/android/b2gdroid/app/src/main/AndroidManifest.xml
@@ -44,16 +44,19 @@
     <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
 
     <!-- App requires OpenGL ES 2.0 -->
     <uses-feature android:glEsVersion="0x00020000" android:required="true" />
 
     <!-- Needed to disable the default lockscreen -->
     <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
 
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.SET_WALLPAPER" />
+
     <application android:label="@string/b2g"
                  android:icon="@drawable/b2g"
                  android:logo="@drawable/b2g"
                  android:hardwareAccelerated="true"
                  android:debuggable="true">
 
         <meta-data android:name="com.sec.android.support.multiwindow" android:value="true"/>
 
--- a/mobile/android/b2gdroid/app/src/main/java/org/mozilla/b2gdroid/Launcher.java
+++ b/mobile/android/b2gdroid/app/src/main/java/org/mozilla/b2gdroid/Launcher.java
@@ -28,24 +28,26 @@ import org.mozilla.gecko.GeckoBatteryMan
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoThread;
 import org.mozilla.gecko.IntentHelper;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 import org.mozilla.gecko.util.GeckoEventListener;
 
 import org.mozilla.b2gdroid.ScreenStateObserver;
 import org.mozilla.b2gdroid.Apps;
+import org.mozilla.b2gdroid.SettingsMapper;
 
 public class Launcher extends Activity
                       implements GeckoEventListener, ContextGetter {
     private static final String LOGTAG = "B2G";
 
     private ContactService      mContactService;
     private ScreenStateObserver mScreenStateObserver;
     private Apps                mApps;
+    private SettingsMapper      mSettings;
 
     /** ContextGetter */
     public Context getContext() {
         return this;
     }
 
     public SharedPreferences getSharedPreferences() {
         return null;
@@ -53,16 +55,17 @@ public class Launcher extends Activity
 
     /** Initializes Gecko APIs */
     private void initGecko() {
         GeckoAppShell.setContextGetter(this);
 
         GeckoBatteryManager.getInstance().start(this);
         mContactService = new ContactService(EventDispatcher.getInstance(), this);
         mApps = new Apps(this);
+        mSettings = new SettingsMapper(this, null);
     }
 
     private void hideSplashScreen() {
         final View splash = findViewById(R.id.splashscreen);
         runOnUiThread(new Runnable() {
             @Override public void run() {
                 splash.setVisibility(View.GONE);
             }
@@ -111,16 +114,17 @@ public class Launcher extends Activity
         mScreenStateObserver.destroy(this);
         mScreenStateObserver = null;
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
             "Launcher:Ready");
 
         mContactService.destroy();
         mApps.destroy();
+        mSettings.destroy();
     }
 
     @Override
     protected void onNewIntent(Intent intent) {
         final String action = intent.getAction();
         Log.w(LOGTAG, "onNewIntent " + action);
         if (Intent.ACTION_VIEW.equals(action)) {
             Log.w(LOGTAG, "Asking gecko to view " + intent.getDataString());
new file mode 100644
--- /dev/null
+++ b/mobile/android/b2gdroid/app/src/main/java/org/mozilla/b2gdroid/SettingsMapper.java
@@ -0,0 +1,247 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+package org.mozilla.b2gdroid;
+
+import java.util.Hashtable;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings.System;
+import android.util.Base64;
+import android.util.Log;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+// This class communicates back and forth with MessagesBridge.jsm to
+// map Android configuration settings and gaia settings.
+// Each setting extends the base BaseMapping class to normalize values
+// when needed.
+
+class SettingsMapper extends ContentObserver implements GeckoEventListener {
+    private static final String LOGTAG = "SettingsMapper";
+
+    private Context mContext;
+    private Hashtable<String, BaseMapping> mGeckoSettings;
+    private Hashtable<String, BaseMapping> mAndroidSettings;
+
+    abstract class BaseMapping {
+        // Returns the list of gaia settings that are managed this class.
+        abstract String[] getGeckoSettings();
+
+        // Returns the list of android settings that are managed this class.
+        abstract String[] getAndroidSettings();
+
+        // Called when we a registered gecko setting changes.
+        abstract void onGeckoChange(String setting, JSONObject message);
+
+        // Called when we a registered android setting changes.
+        abstract void onAndroidChange(Uri uri);
+
+        void sendGeckoSetting(String name, String value) {
+             JSONObject obj = new JSONObject();
+             try {
+                 obj.put(name, value);
+                 sendGeckoSetting(obj);
+             } catch(JSONException e) {
+                Log.d(LOGTAG, e.toString());
+             }
+        }
+
+        void sendGeckoSetting(String name, long value) {
+             JSONObject obj = new JSONObject();
+             try {
+                 obj.put(name, value);
+                 sendGeckoSetting(obj);
+             } catch(JSONException e) {
+                Log.d(LOGTAG, e.toString());
+             }
+        }
+
+        void sendGeckoSetting(JSONObject obj) {
+            GeckoEvent e = GeckoEvent.createBroadcastEvent("Android:Setting", obj.toString());
+            GeckoAppShell.sendEventToGecko(e);
+        }
+    }
+
+    class ScreenTimeoutMapping extends BaseMapping {
+        ScreenTimeoutMapping() {}
+
+        String[] getGeckoSettings() {
+            String props[] = {"screen.timeout"};
+            return props;
+        }
+
+        String[] getAndroidSettings() {
+            String props[] = {"content://settings/system/screen_off_timeout"};
+            return props;
+        }
+
+        void onGeckoChange(String setting, JSONObject message) {
+            try {
+                int timeout = message.getInt("value");
+                // b2g uses seconds for the timeout while Android expects ms.
+                // "never" is 0 in b2g, -1 in Android.
+                if (timeout == 0) {
+                    timeout = -1;
+                } else {
+                    timeout *= 1000;
+                }
+                System.putInt(mContext.getContentResolver(),
+                              System.SCREEN_OFF_TIMEOUT,
+                              timeout);
+            } catch(Exception ex) {
+                Log.d(LOGTAG, "Error setting screen.timeout value", ex);
+            }
+        }
+
+        void onAndroidChange(Uri uri) {
+            try {
+                int timeout = System.getInt(mContext.getContentResolver(),
+                                            System.SCREEN_OFF_TIMEOUT);
+                Log.d(LOGTAG, "Android set timeout to " + timeout);
+
+                // Convert to a gaia timeout.
+                timeout /= 1000;
+                sendGeckoSetting("screen.timeout", timeout);
+            } catch(Exception e) {}
+        }
+    }
+
+    class WallpaperMapping extends BaseMapping {
+        private Context mContext;
+
+        WallpaperMapping(Context context) {
+            mContext = context;
+        }
+
+        String[] getGeckoSettings() {
+            String props[] = {"wallpaper.image"};
+            return props;
+        }
+
+        String[] getAndroidSettings() {
+            String props[] = {};
+            return props;
+        }
+
+        void onGeckoChange(String setting, JSONObject message) {
+            try {
+                final String url = message.getString("value");
+                Log.d(LOGTAG, "wallpaper.image is now " + url);
+                WallpaperManager manager = WallpaperManager.getInstance(mContext);
+                // Remove the data:image/png;base64, prefix from the url.
+                byte[] raw = Base64.decode(url.substring(22), Base64.NO_WRAP);
+                Bitmap bitmap = BitmapFactory.decodeByteArray(raw, 0, raw.length);
+                if (bitmap == null) {
+                    Log.d(LOGTAG, "Unable to create a bitmap!");
+                }
+                manager.setBitmap(bitmap);
+            } catch(Exception ex) {
+                Log.d(LOGTAG, "Error setting wallpaper", ex);
+            }
+        }
+
+        // Android doesn't notify on wallpaper changes.
+        void onAndroidChange(Uri uri) { }
+    }
+
+    SettingsMapper(Context context, Handler handler) {
+        super(handler);
+        mContext = context;
+        EventDispatcher.getInstance()
+                       .registerGeckoThreadListener(this,
+                                                    "Settings:Change");
+
+        mContext.getContentResolver()
+                .registerContentObserver(System.CONTENT_URI,
+                                         true,
+                                         this);
+
+        mGeckoSettings = new Hashtable<String, BaseMapping>();
+        mAndroidSettings = new Hashtable<String, BaseMapping>();
+
+        // Add all the mappings.
+        addMapping(new ScreenTimeoutMapping());
+        addMapping(new WallpaperMapping(mContext));
+    }
+
+    void addMapping(BaseMapping mapping) {
+        String[] props = mapping.getGeckoSettings();
+        for (int i = 0; i < props.length; i++) {
+            mGeckoSettings.put(props[i], mapping);
+        }
+
+        props = mapping.getAndroidSettings();
+        for (int i = 0; i < props.length; i++) {
+            mAndroidSettings.put(props[i], mapping);
+        }
+    }
+
+    void destroy() {
+        EventDispatcher.getInstance()
+                       .unregisterGeckoThreadListener(this,
+                                                      "Settings:Change");
+        mGeckoSettings.clear();
+        mGeckoSettings = null;
+        mAndroidSettings.clear();
+        mAndroidSettings = null;
+        mContext.getContentResolver().unregisterContentObserver(this);
+    }
+
+    public void handleMessage(String event, JSONObject message) {
+        Log.w(LOGTAG, "Received " + event);
+
+        try {
+            String setting = message.getString("setting");
+            BaseMapping mapping = mGeckoSettings.get(setting);
+            if (mapping != null) {
+                Log.d(LOGTAG, "Changing gecko setting " + setting);
+                mapping.onGeckoChange(setting, message);
+            } else {
+                Log.d(LOGTAG, "No gecko mapping registered for " + setting);
+            }
+        } catch(Exception ex) {
+            Log.d(LOGTAG, "Error getting setting name", ex);
+        }
+    }
+
+    // ContentObserver, see
+    // http://developer.android.com/reference/android/database/ContentObserver.html
+    @Override
+    public boolean deliverSelfNotifications() {
+        return false;
+    }
+
+    @Override
+    public void onChange(boolean selfChange) {
+        onChange(selfChange, null);
+    }
+
+    @Override
+    public void onChange(boolean selfChange, Uri uri) {
+        super.onChange(selfChange);
+        Log.d(LOGTAG, "Settings change detected uri=" + uri);
+        BaseMapping mapping = mAndroidSettings.get(uri.toString());
+        if (mapping != null) {
+            Log.d(LOGTAG, "Changing android setting " + uri);
+            mapping.onAndroidChange(uri);
+        } else {
+            Log.d(LOGTAG, "No android mapping registered for " + uri);
+        }
+    }
+
+}
--- a/mobile/android/b2gdroid/components/MessagesBridge.jsm
+++ b/mobile/android/b2gdroid/components/MessagesBridge.jsm
@@ -1,45 +1,54 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * 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/. */
 
 this.EXPORTED_SYMBOLS = ["MessagesBridge"];
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/SystemAppProxy.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "settings",
+                                   "@mozilla.org/settingsService;1",
+                                   "nsISettingsService");
 
 // This module receives messages from Launcher.java as observer notifications.
+// It also listens for settings changes to relay them back to Android.
 
 function debug() {
   dump("-*- MessagesBridge " + Array.slice(arguments) + "\n");
 }
 
+function getWindow() {
+  return SystemAppProxy.getFrame().contentWindow ||
+         Services.wm.getMostRecentWindow("navigator:browser");
+}
+
+// To prevent roundtrips like android -> gecko -> android we keep track of
+// in flight setting changes.
+let _blockedSettings = new Set();
+
 this.MessagesBridge = {
   init: function() {
-    Services.obs.addObserver(this, "Android:Launcher", false);
+    Services.obs.addObserver(this.onAndroidMessage, "Android:Launcher", false);
+    Services.obs.addObserver(this.onAndroidSetting, "Android:Setting", false);
+    Services.obs.addObserver(this.onSettingChange, "mozsettings-changed", false);
     Services.obs.addObserver(this, "xpcom-shutdown", false);
   },
 
-  observe: function(aSubject, aTopic, aData) {
-    if (aTopic == "xpcom-shutdown") {
-      Services.obs.removeObserver(this, "Android:Launcher");
-      Services.obs.removeObserver(this, "xpcom-shutdown");
-    }
-
-    if (aTopic != "Android:Launcher") {
-      return;
-    }
-
+  onAndroidMessage: function(aSubject, aTopic, aData) {
     let data = JSON.parse(aData);
     debug(`Got Android:Launcher message ${data.action}`);
 
-    let window = SystemAppProxy.getFrame().contentWindow;
+    let window = getWindow();
     switch (data.action) {
       case "screen_on":
       case "screen_off":
         // In both cases, make it look like pressing the power button
         // by dispatching keydown & keyup on the system app window.
         window.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Power" }));
         window.dispatchEvent(new window.KeyboardEvent("keyup", { key: "Power" }));
         break;
@@ -48,12 +57,63 @@ this.MessagesBridge = {
                                          data: { type: "url",
                                                  url: data.url } });
         break;
       case "home-key":
         window.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Home" }));
         window.dispatchEvent(new window.KeyboardEvent("keyup", { key: "Home" }));
         break;
     }
+  },
+
+  onAndroidSetting: function(aSubject, aTopic, aData) {
+    let data = JSON.parse(aData);
+    let lock = settings.createLock();
+    let key = Object.keys(data)[0];
+    debug(`Got Android:Setting message ${key} -> ${data[key]}`);
+    // Don't relay back to android the same setting change.
+    _blockedSettings.add(key);
+    lock.set(key, data[key], null);
+  },
+
+  onSettingChange: function(aSubject, aTopic, aData) {
+    if ("wrappedJSObject" in aSubject) {
+      aSubject = aSubject.wrappedJSObject;
+    }
+    if (aSubject) {
+      debug("Got setting change: " + aSubject.key + " -> " + aSubject.value);
+
+      if (_blockedSettings.has(aSubject.key)) {
+        _blockedSettings.delete(aSubject.key);
+        debug("Rejecting blocked setting change for " + aSubject.key);
+        return;
+      }
+
+      let window = getWindow();
+
+      if (aSubject.value instanceof window.Blob) {
+        debug(aSubject.key + " is a Blob");
+        let reader = new window.FileReader();
+        reader.readAsDataURL(aSubject.value);
+        reader.onloadend = function() {
+          Messaging.sendRequest({ type: "Settings:Change",
+                                  setting: aSubject.key,
+                                  value: reader.result });
+        }
+      } else {
+        Messaging.sendRequest({ type: "Settings:Change",
+                                setting: aSubject.key,
+                                value: aSubject.value });
+      }
+    }
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic == "xpcom-shutdown") {
+      Services.obs.removeObserver(this.onAndroidMessage, "Android:Launcher");
+      Services.obs.removeObserver(this.onAndroidSetting, "Android:Setting");
+      Services.obs.removeObserver(this.onSettingChange, "mozsettings-changed");
+      Services.obs.removeObserver(this, "xpcom-shutdown");
+    }
   }
 }
 
 this.MessagesBridge.init();