Bug 1207719 - (Part 2) Change Switchboard to combine network requests for experiments and server configurations into a single network fetch. r=sebastian
MozReview-Commit-ID: c38pPvDrT8
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -812,37 +812,32 @@ public class BrowserApp extends GeckoApp
private void initSwitchboard(Intent intent) {
if (Experiments.isDisabled(new SafeIntent(intent)) || !AppConstants.MOZ_SWITCHBOARD) {
return;
}
final String hostExtra = ContextUtils.getStringExtra(intent, INTENT_KEY_SWITCHBOARD_HOST);
final String host = TextUtils.isEmpty(hostExtra) ? DEFAULT_SWITCHBOARD_HOST : hostExtra;
- final String configServerUpdateUrl;
- final String configServerUrl;
+ final String serverUrl;
try {
- configServerUpdateUrl = new URL("https", host, "urls").toString();
- configServerUrl = new URL("https", host, "v1").toString();
+ serverUrl = new URL("https", host, "v2").toString();
} catch (MalformedURLException e) {
Log.e(LOGTAG, "Error creating Switchboard server URL", e);
return;
}
- SwitchBoard.initDefaultServerUrls(configServerUpdateUrl, configServerUrl, true);
-
final String switchboardUUID = ContextUtils.getStringExtra(intent, INTENT_KEY_SWITCHBOARD_UUID);
SwitchBoard.setUUIDFromExtra(switchboardUUID);
- // Looks at the server if there are changes in the server URL that should be used in the future
- new AsyncConfigLoader(this, AsyncConfigLoader.UPDATE_SERVER, switchboardUUID).execute();
-
- // Loads the actual config. This can be done on app start or on app onResume() depending
- // how often you want to update the config.
- new AsyncConfigLoader(this, AsyncConfigLoader.CONFIG_SERVER, switchboardUUID).execute();
+ // Loads the Switchboard config from the specified server URL. Eventually, we
+ // should use the endpoint returned by the server URL, to support migrating
+ // to a new endpoint. However, if we want to do that, we'll need to find a different
+ // solution for dynamically changing the server URL from the intent.
+ new AsyncConfigLoader(this, switchboardUUID, serverUrl).execute();
}
private void showUpdaterPermissionSnackbar() {
SnackbarHelper.SnackbarCallback allowCallback = new SnackbarHelper.SnackbarCallback() {
@Override
public void onClick(View v) {
Permissions.from(BrowserApp.this)
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
--- a/mobile/android/thirdparty/com/keepsafe/switchboard/AsyncConfigLoader.java
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/AsyncConfigLoader.java
@@ -13,72 +13,45 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.keepsafe.switchboard;
import android.content.Context;
import android.os.AsyncTask;
-import android.util.Log;
/**
* An async loader to load user config in background thread based on internal generated UUID.
*
* Call <code>AsyncConfigLoader.execute()</code> to load SwitchBoard.loadConfig() with own ID.
* To use your custom UUID call <code>AsyncConfigLoader.execute(uuid)</code> with uuid being your unique user id
* as a String
*
* @author Philipp Berner
*
*/
public class AsyncConfigLoader extends AsyncTask<Void, Void, Void> {
- private String TAG = "AsyncConfigLoader";
-
- public static final int UPDATE_SERVER = 1;
- public static final int CONFIG_SERVER = 2;
-
private Context context;
- private int configToLoad;
private String uuid;
-
- /**
- * Sets the params for async loading either SwitchBoard.updateConfigServerUrl()
- * or SwitchBoard.loadConfig.
- * @param c Application context
- * @param configType Either UPDATE_SERVER or CONFIG_SERVER
- */
- public AsyncConfigLoader(Context c, int configType) {
- this(c, configType, null);
- }
+ private String defaultServerUrl;
/**
* Sets the params for async loading either SwitchBoard.updateConfigServerUrl()
* or SwitchBoard.loadConfig.
* Loads config with a custom UUID
* @param c Application context
- * @param configType Either UPDATE_SERVER or CONFIG_SERVER
* @param uuid Custom UUID
+ * @param defaultServerUrl Default URL endpoint for Switchboard config.
*/
- public AsyncConfigLoader(Context c, int configType, String uuid) {
+ public AsyncConfigLoader(Context c, String uuid, String defaultServerUrl) {
this.context = c;
- this.configToLoad = configType;
this.uuid = uuid;
+ this.defaultServerUrl = defaultServerUrl;
}
@Override
protected Void doInBackground(Void... params) {
-
- if(configToLoad == UPDATE_SERVER) {
- SwitchBoard.updateConfigServerUrl(context);
- }
- else {
- if(uuid == null)
- SwitchBoard.loadConfig(context);
- else
- SwitchBoard.loadConfig(context, uuid);
- }
-
+ SwitchBoard.loadConfig(context, uuid, defaultServerUrl);
return null;
}
-
-}
\ No newline at end of file
+}
--- a/mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java
@@ -13,104 +13,66 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.keepsafe.switchboard;
import android.content.Context;
import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
+import android.support.annotation.Nullable;
/**
* Application preferences for SwitchBoard.
* @author Philipp Berner
*
*/
public class Preferences {
- private static final String TAG = "Preferences";
private static final String switchBoardSettings = "com.keepsafe.switchboard.settings";
- //dynamic config
private static final String kDynamicConfigServerUrl = "dynamic-config-server-url";
- private static final String kDynamicConfigServerUpdateUrl = "dynamic-config-server-update-url";
private static final String kDynamicConfig = "dynamic-config";
-
-
- //dynamic config
- /** TODO check this!!!
- * Returns a JSON string array with <br />
- * position 0 = updateserverUrl <br />
- * Fields a null if not existent.
- * @param c
- * @return
+ /**
+ * Returns the stored config server URL.
+ * @param c Context
+ * @return URL for config endpoint.
*/
- public static String getDynamicUpdateServerUrl(Context c) {
- SharedPreferences settings = (SharedPreferences) Preferences.getPreferenceObject(c, false);
- return settings.getString(kDynamicConfigServerUpdateUrl, null);
- }
-
- /**
- * Returns a JSON string array with <br />
- * postiion 1 = configServerUrl <br />
- * Fields a null if not existent.
- * @param c
- * @return
- */
- public static String getDynamicConfigServerUrl(Context c) {
- SharedPreferences settings = (SharedPreferences) Preferences.getPreferenceObject(c, false);
- return settings.getString(kDynamicConfigServerUrl, null);
+ @Nullable public static String getDynamicConfigServerUrl(Context c) {
+ final SharedPreferences prefs = c.getApplicationContext().getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE);
+ return prefs.getString(kDynamicConfigServerUrl, null);
}
/**
* Stores the config servers URL.
- * @param c
- * @param updateServerUrl Url end point to get the current config server location
- * @param configServerUrl UR: end point to get the current endpoint for the apps config file
- * @return true if saved successful
+ * @param c Context
+ * @param configServerUrl URL for config endpoint.
*/
- public static boolean setDynamicConfigServerUrl(Context c, String updateServerUrl, String configServerUrl) {
-
- SharedPreferences.Editor settings = (Editor) Preferences.getPreferenceObject(c, true);
- settings.putString(kDynamicConfigServerUpdateUrl, updateServerUrl);
- settings.putString(kDynamicConfigServerUrl, configServerUrl);
- return settings.commit();
+ public static void setDynamicConfigServerUrl(Context c, String configServerUrl) {
+ final SharedPreferences.Editor editor = c.getApplicationContext().
+ getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
+ editor.putString(kDynamicConfigServerUrl, configServerUrl);
+ editor.apply();
}
/**
* Gets the user config as a JSON string.
- * @param c
- * @return
+ * @param c Context
+ * @return Config JSON
*/
- public static String getDynamicConfigJson(Context c) {
- SharedPreferences settings = (SharedPreferences) Preferences.getPreferenceObject(c, false);
- return settings.getString(kDynamicConfig, null);
+ @Nullable public static String getDynamicConfigJson(Context c) {
+ final SharedPreferences prefs = c.getApplicationContext().getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE);
+ return prefs.getString(kDynamicConfig, null);
}
/**
* Saves the user config as a JSON sting.
- * @param c
- * @param configJson
- * @return
+ * @param c Context
+ * @param configJson Config JSON
*/
- public static boolean setDynamicConfigJson(Context c, String configJson) {
- SharedPreferences.Editor settings = (Editor) Preferences.getPreferenceObject(c, true);
- settings.putString(kDynamicConfig, configJson);
- return settings.commit();
- }
-
- static private Object getPreferenceObject(Context ctx, boolean writeable) {
-
- Object returnValue = null;
-
- Context sharedDelegate = ctx.getApplicationContext();
-
- if(!writeable) {
- returnValue = sharedDelegate.getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE);
- } else {
- returnValue = sharedDelegate.getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
- }
-
- return returnValue;
+ public static void setDynamicConfigJson(Context c, String configJson) {
+ final SharedPreferences.Editor editor = c.getApplicationContext().
+ getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
+ editor.putString(kDynamicConfig, configJson);
+ editor.apply();
}
}
--- a/mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java
@@ -15,35 +15,36 @@
*/
package com.keepsafe.switchboard;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
-import java.net.ProtocolException;
+import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.zip.CRC32;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
-import android.support.v4.content.LocalBroadcastManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.util.Log;
+
/**
* SwitchBoard is the core class of the KeepSafe Switchboard mobile A/B testing framework.
* This class provides a bunch of static methods that can be used in your app to run A/B tests.
*
* The SwitchBoard supports production and staging environment.
*
* For usage <code>initDefaultServerUrls</code> for first time usage. Server URLs can be updates from
* a remote location with <code>initConfigServerUrl</code>.
@@ -55,198 +56,115 @@ import android.util.Log;
*
* @author Philipp Berner
*
*/
public class SwitchBoard {
private static final String TAG = "SwitchBoard";
- /** Set if the application is run in debug mode. DynamicConfig runs against staging server when in debug and production when not */
+ /** Set if the application is run in debug mode. */
public static boolean DEBUG = true;
- /** Production server to update the remote server URLs. http://staging.domain/path_to/SwitchboardURLs.php */
- private static String DYNAMIC_CONFIG_SERVER_URL_UPDATE;
-
- /** Production server for getting the actual config file. http://staging.domain/path_to/SwitchboardDriver.php */
- private static String DYNAMIC_CONFIG_SERVER_DEFAULT_URL;
-
- public static final String ACTION_CONFIG_FETCHED = ".SwitchBoard.CONFIG_FETCHED";
-
- private static final String kUpdateServerUrl = "updateServerUrl";
- private static final String kConfigServerUrl = "configServerUrl";
-
private static final String IS_EXPERIMENT_ACTIVE = "isActive";
private static final String EXPERIMENT_VALUES = "values";
- private static String uuidExtra = null;
-
+ private static final String KEY_SERVER_URL = "mainServerUrl";
+ private static final String KEY_CONFIG_RESULTS = "results";
- /**
- * Basic initialization with one server.
- * @param configServerUpdateUrl Url to: http://staging.domain/path_to/SwitchboardURLs.php
- * @param configServerUrl Url to: http://staging.domain/path_to/SwitchboardDriver.php - the acutall config
- * @param isDebug Is the application running in debug mode. This will add log messages.
- */
- public static void initDefaultServerUrls(String configServerUpdateUrl, String configServerUrl,
- boolean isDebug) {
-
- DYNAMIC_CONFIG_SERVER_URL_UPDATE = configServerUpdateUrl;
- DYNAMIC_CONFIG_SERVER_DEFAULT_URL = configServerUrl;
- DEBUG = isDebug;
- }
+ private static String uuidExtra = null;
public static void setUUIDFromExtra(String uuid) {
uuidExtra = uuid;
}
- /**
- * Advanced initialization that supports a production and staging environment without changing the server URLs manually.
- * SwitchBoard will connect to the staging environment in debug mode. This makes it very simple to test new experiements
- * during development.
- * @param configServerUpdateUrlStaging Url to http://staging.domain/path_to/SwitchboardURLs.php in staging environment
- * @param configServerUrlStaging Url to: http://staging.domain/path_to/SwitchboardDriver.php in production - the acutall config
- * @param configServerUpdateUrl Url to http://staging.domain/path_to/SwitchboardURLs.php in production environment
- * @param configServerUrl Url to: http://staging.domain/path_to/SwitchboardDriver.php in production - the acutall config
- * @param isDebug Defines if the app runs in debug.
- */
- public static void initDefaultServerUrls(String configServerUpdateUrlStaging, String configServerUrlStaging,
- String configServerUpdateUrl, String configServerUrl,
- boolean isDebug) {
-
- if(isDebug) {
- DYNAMIC_CONFIG_SERVER_URL_UPDATE = configServerUpdateUrlStaging;
- DYNAMIC_CONFIG_SERVER_DEFAULT_URL = configServerUrlStaging;
- } else {
- DYNAMIC_CONFIG_SERVER_URL_UPDATE = configServerUpdateUrl;
- DYNAMIC_CONFIG_SERVER_DEFAULT_URL = configServerUrl;
- }
-
- DEBUG = isDebug;
- }
-
- /**
- * Updates the server URLs from remote and stores it locally in the app. This allows to move the server side
- * whith users already using Switchboard.
- * When there is no internet connection it will continue to use the URLs from the last time or
- * default URLS that have been set with <code>initDefaultServerUrls</code>.
- *
- * This methode should always be executed in a background thread to not block the UI.
- *
- * @param c Application context
- */
- public static void updateConfigServerUrl(Context c) {
- if(DEBUG) Log.d(TAG, "start initConfigServerUrl");
-
- if(DEBUG) {
- //set default value that is set in code for debug mode.
- Preferences.setDynamicConfigServerUrl(c, DYNAMIC_CONFIG_SERVER_URL_UPDATE, DYNAMIC_CONFIG_SERVER_DEFAULT_URL);
- return;
- }
-
- //lookup new config server url from the one that is in shared prefs
- String updateServerUrl = Preferences.getDynamicUpdateServerUrl(c);
-
- //set to default when not set in preferences
- if(updateServerUrl == null)
- updateServerUrl = DYNAMIC_CONFIG_SERVER_URL_UPDATE;
-
- try {
- String result = readFromUrlGET(updateServerUrl, "");
- if(DEBUG) Log.d(TAG, "Result String: " + result);
-
- if(result != null){
- JSONObject a = new JSONObject(result);
-
- Preferences.setDynamicConfigServerUrl(c, (String)a.get(kUpdateServerUrl), (String)a.get(kConfigServerUrl));
-
- if(DEBUG) Log.d(TAG, "Update Server Url: " + (String)a.get(kUpdateServerUrl));
- if(DEBUG) Log.d(TAG, "Config Server Url: " + (String)a.get(kConfigServerUrl));
- } else {
- storeDefaultUrlsInPreferences(c);
- }
-
- } catch (JSONException e) {
- e.printStackTrace();
- }
-
- if(DEBUG) Log.d(TAG, "end initConfigServerUrl");
- }
-
- /**
- * Loads a new config file for the specific user from current config server. Uses internal unique user ID.
- * Use this method only in background thread as network connections are involved that block UI thread.
- * Use AsyncConfigLoader() for easy background threading.
- * @param c ApplicationContext
- */
- public static void loadConfig(Context c) {
- loadConfig(c, null);
- }
/**
* Loads a new config for a user. This method allows you to pass your own unique user ID instead of using
* the SwitchBoard internal user ID.
* Don't call method direct for background threading reasons.
* @param c ApplicationContext
* @param uuid Custom unique user ID
+ * @param defaultServerUrl Default server URL endpoint.
*/
- public static void loadConfig(Context c, String uuid) {
+ static void loadConfig(Context c, String uuid, @NonNull String defaultServerUrl) {
+
+ // Eventually, we want to check `Preferences.getDynamicConfigServerUrl(c);` before
+ // falling back to the default server URL. However, this will require figuring
+ // out a new solution for dynamically specifying a new server from the intent.
+ String serverUrl = defaultServerUrl;
+
+ final URL requestUrl = buildConfigRequestUrl(c, uuid, serverUrl);
+ if (requestUrl == null) {
+ return;
+ }
+
+ if (DEBUG) Log.d(TAG, requestUrl.toString());
+
+ final String result = readFromUrlGET(requestUrl);
+ if (DEBUG) Log.d(TAG, result);
+
+ if (result == null) {
+ return;
+ }
try {
+ final JSONObject json = new JSONObject(result);
- //get uuid
- if(uuid == null) {
- DeviceUuidFactory df = new DeviceUuidFactory(c);
- uuid = df.getDeviceUuid().toString();
+ // Update the server URL if necessary.
+ final String newServerUrl = json.getString(KEY_SERVER_URL);
+ if (!defaultServerUrl.equals(newServerUrl)) {
+ Preferences.setDynamicConfigServerUrl(c, newServerUrl);
}
- String device = Build.DEVICE;
- String manufacturer = Build.MANUFACTURER;
- String lang = "unknown";
- try {
- lang = Locale.getDefault().getISO3Language();
- } catch (MissingResourceException e) {
- e.printStackTrace();
- }
- String country = "unknown";
- try {
- country = Locale.getDefault().getISO3Country();
- } catch (MissingResourceException e) {
- e.printStackTrace();
- }
- String packageName = c.getPackageName();
- String versionName = "none";
- try {
- versionName = c.getPackageManager().getPackageInfo(c.getPackageName(), 0).versionName;
- } catch (NameNotFoundException e) {
- e.printStackTrace();
- }
+ // Store the config in shared prefs.
+ final String config = json.getString(KEY_CONFIG_RESULTS);
+ Preferences.setDynamicConfigJson(c, config);
+ } catch (JSONException e) {
+ Log.e(TAG, "Exception parsing server result", e);
+ }
+ }
- //load config, includes all experiments
- String serverUrl = Preferences.getDynamicConfigServerUrl(c);
-
- if(serverUrl != null) {
- String params = "uuid="+uuid+"&device="+device+"&lang="+lang+"&country="+country
- +"&manufacturer="+manufacturer+"&appId="+packageName+"&version="+versionName;
- if(DEBUG) Log.d(TAG, "Read from server URL: " + serverUrl + "?" + params);
- String serverConfig = readFromUrlGET(serverUrl, params);
+ @Nullable private static URL buildConfigRequestUrl(Context c, String uuid, String serverUrl) {
+ if (uuid == null) {
+ DeviceUuidFactory df = new DeviceUuidFactory(c);
+ uuid = df.getDeviceUuid().toString();
+ }
- if(DEBUG) Log.d(TAG, serverConfig);
-
- //store experiments in shared prefs (one variable)
- if(serverConfig != null)
- Preferences.setDynamicConfigJson(c, serverConfig);
- }
-
- } catch (NullPointerException e) {
+ final String device = Build.DEVICE;
+ final String manufacturer = Build.MANUFACTURER;
+ String lang = "unknown";
+ try {
+ lang = Locale.getDefault().getISO3Language();
+ } catch (MissingResourceException e) {
+ e.printStackTrace();
+ }
+ String country = "unknown";
+ try {
+ country = Locale.getDefault().getISO3Country();
+ } catch (MissingResourceException e) {
e.printStackTrace();
}
- //notify listeners that the config fetch has completed
- Intent i = new Intent(ACTION_CONFIG_FETCHED);
- LocalBroadcastManager.getInstance(c).sendBroadcast(i);
+ final String packageName = c.getPackageName();
+ String versionName = "none";
+ try {
+ versionName = c.getPackageManager().getPackageInfo(c.getPackageName(), 0).versionName;
+ } catch (NameNotFoundException e) {
+ e.printStackTrace();
+ }
+
+ final String params = "uuid="+uuid+"&device="+device+"&lang="+lang+"&country="+country
+ +"&manufacturer="+manufacturer+"&appId="+packageName+"&version="+versionName;
+
+ try {
+ return new URL(serverUrl + "?" + params);
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ return null;
+ }
}
public static boolean isInBucket(Context c, int low, int high) {
int userBucket = getUserBucket(c);
if (userBucket >= low && userBucket < high)
return true;
else
return false;
@@ -369,69 +287,42 @@ public class SwitchBoard {
e.printStackTrace();
Log.e(TAG, "Could not create JSON object from config string", e);
}
return null;
}
/**
- * Sets config server URLs in shared prefs to defaul when not set already. It keeps
- * URLs when already set in shared preferences.
- * @param c
- */
- private static void storeDefaultUrlsInPreferences(Context c) {
- String configUrl = Preferences.getDynamicConfigServerUrl(c);
- String updateUrl = Preferences.getDynamicUpdateServerUrl(c);
-
- if(configUrl == null)
- configUrl = DYNAMIC_CONFIG_SERVER_DEFAULT_URL;
-
- if(updateUrl == null)
- updateUrl = DYNAMIC_CONFIG_SERVER_URL_UPDATE;
-
- Preferences.setDynamicConfigServerUrl(c, updateUrl, configUrl);
- }
-
- /**
* Returns a String containing the server response from a GET request
- * @param address Valid http addess.
- * @param params String of params. Multiple params seperated with &. No leading ? in string
+ * @param url URL for GET request.
* @return Returns String from server or null when failed.
*/
- private static String readFromUrlGET(String address, String params) {
- if(address == null || params == null)
- return null;
-
- String completeUrl = address + "?" + params;
- if(DEBUG) Log.d(TAG, "readFromUrl(): " + completeUrl);
+ @Nullable private static String readFromUrlGET(URL url) {
+ if (DEBUG) Log.d(TAG, "readFromUrl(): " + url);
try {
- URL url = new URL(completeUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setUseCaches(false);
- // get response
InputStream is = connection.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(is);
BufferedReader bufferReader = new BufferedReader(inputStreamReader, 8192);
String line = "";
- StringBuffer resultContent = new StringBuffer();
+ StringBuilder resultContent = new StringBuilder();
while ((line = bufferReader.readLine()) != null) {
if(DEBUG) Log.d(TAG, line);
resultContent.append(line);
}
bufferReader.close();
if(DEBUG) Log.d(TAG, "readFromUrl() result: " + resultContent.toString());
return resultContent.toString();
- } catch (ProtocolException e) {
- e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**