From b32ea8e77ed1a761805e168710f44f1b903115e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabrice=20Desr=C3=A9?= Date: Thu, 24 Sep 2015 09:55:52 -0700 Subject: [PATCH] Bug 1207417 - Settings mapper to sync b2g and android configurations r=snorp --- mobile/android/b2gdroid/app/Makefile.in | 1 + .../b2gdroid/app/src/main/AndroidManifest.xml | 3 + .../java/org/mozilla/b2gdroid/Launcher.java | 4 + .../org/mozilla/b2gdroid/SettingsMapper.java | 247 ++++++++++++++++++ .../b2gdroid/components/MessagesBridge.jsm | 84 +++++- 5 files changed, 327 insertions(+), 12 deletions(-) create mode 100644 mobile/android/b2gdroid/app/src/main/java/org/mozilla/b2gdroid/SettingsMapper.java diff --git a/mobile/android/b2gdroid/app/Makefile.in b/mobile/android/b2gdroid/app/Makefile.in index 12d27477a037..248a954ff7e8 100644 --- a/mobile/android/b2gdroid/app/Makefile.in +++ b/mobile/android/b2gdroid/app/Makefile.in @@ -8,6 +8,7 @@ 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 diff --git a/mobile/android/b2gdroid/app/src/main/AndroidManifest.xml b/mobile/android/b2gdroid/app/src/main/AndroidManifest.xml index c5886ebad513..22f52cc38f0e 100644 --- a/mobile/android/b2gdroid/app/src/main/AndroidManifest.xml +++ b/mobile/android/b2gdroid/app/src/main/AndroidManifest.xml @@ -49,6 +49,9 @@ + + + mGeckoSettings; + private Hashtable 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(); + mAndroidSettings = new Hashtable(); + + // 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); + } + } + +} diff --git a/mobile/android/b2gdroid/components/MessagesBridge.jsm b/mobile/android/b2gdroid/components/MessagesBridge.jsm index c0cf531cc77d..3bc8be5790f5 100644 --- a/mobile/android/b2gdroid/components/MessagesBridge.jsm +++ b/mobile/android/b2gdroid/components/MessagesBridge.jsm @@ -6,35 +6,44 @@ 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": @@ -53,6 +62,57 @@ this.MessagesBridge = { 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"); + } } }