summaryrefslogtreecommitdiffstats
path: root/mobile/android/thirdparty/com/keepsafe/switchboard
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/thirdparty/com/keepsafe/switchboard')
-rw-r--r--mobile/android/thirdparty/com/keepsafe/switchboard/AsyncConfigLoader.java54
-rw-r--r--mobile/android/thirdparty/com/keepsafe/switchboard/DeviceUuidFactory.java70
-rw-r--r--mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java105
-rw-r--r--mobile/android/thirdparty/com/keepsafe/switchboard/Switch.java72
-rw-r--r--mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java390
5 files changed, 691 insertions, 0 deletions
diff --git a/mobile/android/thirdparty/com/keepsafe/switchboard/AsyncConfigLoader.java b/mobile/android/thirdparty/com/keepsafe/switchboard/AsyncConfigLoader.java
new file mode 100644
index 000000000..2cff4b4c3
--- /dev/null
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/AsyncConfigLoader.java
@@ -0,0 +1,54 @@
+/*
+ Copyright 2012 KeepSafe Software Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ 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;
+
+/**
+ * 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 Context context;
+ 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 defaultServerUrl Default URL endpoint for Switchboard config.
+ */
+ public AsyncConfigLoader(Context c, String defaultServerUrl) {
+ this.context = c;
+ this.defaultServerUrl = defaultServerUrl;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ SwitchBoard.loadConfig(context, defaultServerUrl);
+ return null;
+ }
+}
diff --git a/mobile/android/thirdparty/com/keepsafe/switchboard/DeviceUuidFactory.java b/mobile/android/thirdparty/com/keepsafe/switchboard/DeviceUuidFactory.java
new file mode 100644
index 000000000..c4476d2cd
--- /dev/null
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/DeviceUuidFactory.java
@@ -0,0 +1,70 @@
+/*
+ Copyright 2012 KeepSafe Software Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+package com.keepsafe.switchboard;
+
+import java.util.UUID;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+/**
+ * Generates a UUID and stores is persistent as in the apps shared preferences.
+ *
+ * @author Philipp Berner
+ */
+public class DeviceUuidFactory {
+ protected static final String PREFS_FILE = "com.keepsafe.switchboard.uuid";
+ protected static final String PREFS_DEVICE_ID = "device_id";
+
+ private static UUID uuid = null;
+
+ public DeviceUuidFactory(Context context) {
+ if (uuid == null) {
+ synchronized (DeviceUuidFactory.class) {
+ if (uuid == null) {
+ final SharedPreferences prefs = context
+ .getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
+ final String id = prefs.getString(PREFS_DEVICE_ID, null);
+
+ if (id != null) {
+ // Use the ids previously computed and stored in the prefs file
+ uuid = UUID.fromString(id);
+ } else {
+ uuid = UUID.randomUUID();
+
+ // Write the value out to the prefs file
+ prefs.edit().putString(PREFS_DEVICE_ID, uuid.toString()).apply();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a unique UUID for the current android device. As with all UUIDs,
+ * this unique ID is "very highly likely" to be unique across all Android
+ * devices. Much more so than ANDROID_ID is.
+ *
+ * The UUID is generated with <code>UUID.randomUUID()</code>.
+ *
+ * @return a UUID that may be used to uniquely identify your device for most
+ * purposes.
+ */
+ public UUID getDeviceUuid() {
+ return uuid;
+ }
+
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java b/mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java
new file mode 100644
index 000000000..f7f6f7cb7
--- /dev/null
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java
@@ -0,0 +1,105 @@
+/*
+ Copyright 2012 KeepSafe Software Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ 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.support.annotation.Nullable;
+
+/**
+ * Application preferences for SwitchBoard.
+ * @author Philipp Berner
+ *
+ */
+public class Preferences {
+
+ private static final String switchBoardSettings = "com.keepsafe.switchboard.settings";
+
+ private static final String CONFIG_JSON = "config-json";
+ private static final String OVERRIDE_PREFIX = "experiment.override.";
+
+
+ /**
+ * Gets the user config as a JSON string.
+ * @param c Context
+ * @return Config JSON
+ */
+ @Nullable public static String getDynamicConfigJson(Context c) {
+ final SharedPreferences prefs = c.getApplicationContext().getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE);
+ return prefs.getString(CONFIG_JSON, null);
+ }
+
+ /**
+ * Saves the user config as a JSON sting.
+ * @param c Context
+ * @param configJson Config JSON
+ */
+ public static void setDynamicConfigJson(Context c, String configJson) {
+ final SharedPreferences.Editor editor = c.getApplicationContext().
+ getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
+ editor.putString(CONFIG_JSON, configJson);
+ editor.apply();
+ }
+
+ /**
+ * Gets the override value for an experiment.
+ *
+ * @param c Context
+ * @param experimentName Experiment name
+ * @return Whether or not the experiment should be enabled, or null if there is no override.
+ */
+ @Nullable public static Boolean getOverrideValue(Context c, String experimentName) {
+ final SharedPreferences prefs = c.getApplicationContext().
+ getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE);
+
+ final String key = OVERRIDE_PREFIX + experimentName;
+ if (prefs.contains(key)) {
+ // This will never fall back to the default value.
+ return prefs.getBoolean(key, false);
+ }
+
+ // Default to returning null if no override was found.
+ return null;
+ }
+
+ /**
+ * Saves an override value for an experiment.
+ *
+ * @param c Context
+ * @param experimentName Experiment name
+ * @param isEnabled Whether or not to enable the experiment
+ */
+ public static void setOverrideValue(Context c, String experimentName, boolean isEnabled) {
+ final SharedPreferences.Editor editor = c.getApplicationContext().
+ getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
+ editor.putBoolean(OVERRIDE_PREFIX + experimentName, isEnabled);
+ editor.apply();
+ }
+
+ /**
+ * Clears the override value for an experiment.
+ *
+ * @param c Context
+ * @param experimentName Experiment name
+ */
+ public static void clearOverrideValue(Context c, String experimentName) {
+ final SharedPreferences.Editor editor = c.getApplicationContext().
+ getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
+ editor.remove(OVERRIDE_PREFIX + experimentName);
+ editor.apply();
+ }
+}
diff --git a/mobile/android/thirdparty/com/keepsafe/switchboard/Switch.java b/mobile/android/thirdparty/com/keepsafe/switchboard/Switch.java
new file mode 100644
index 000000000..5307750bb
--- /dev/null
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/Switch.java
@@ -0,0 +1,72 @@
+/*
+ Copyright 2012 KeepSafe Software Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+package com.keepsafe.switchboard;
+
+import org.json.JSONObject;
+
+import android.content.Context;
+
+/**
+ * Single instance of an existing experiment for easier and cleaner code.
+ *
+ * @author Philipp Berner
+ *
+ */
+public class Switch {
+
+ private Context context;
+ private String experimentName;
+
+ /**
+ * Creates an instance of a single experiment to give more convenient access to its values.
+ * When the given experiment does not exist, it will give back default valued that can be found
+ * in <code>Switchboard</code>. Developer has to know that experiment exists when using it.
+ * @param c Application context
+ * @param experimentName Name of the experiment as defined on the server
+ */
+ public Switch(Context c, String experimentName) {
+ this.context = c;
+ this.experimentName = experimentName;
+ }
+
+ /**
+ * Returns true if the experiment is active for this particular user.
+ * @return Status of the experiment and false when experiment does not exist.
+ */
+ public boolean isActive() {
+ return SwitchBoard.isInExperiment(context, experimentName);
+ }
+
+ /**
+ * Returns true if the experiment has additional values.
+ * @return true when values exist
+ */
+ public boolean hasValues() {
+ return SwitchBoard.hasExperimentValues(context, experimentName);
+ }
+
+ /**
+ * Gives back all the experiment values in a JSONObject. This function checks if
+ * values exists. If no values exist, it returns null.
+ * @return Values in JSONObject or null if non
+ */
+ public JSONObject getValues() {
+ if(hasValues())
+ return SwitchBoard.getExperimentValuesFromJson(context, experimentName);
+ else
+ return null;
+ }
+}
diff --git a/mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java b/mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java
new file mode 100644
index 000000000..e99144045
--- /dev/null
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java
@@ -0,0 +1,390 @@
+/*
+ Copyright 2012 KeepSafe Software Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+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.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 org.json.JSONArray;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Build;
+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>.
+ *
+ * To run a experiment use <code>isInExperiment()</code>. The experiment name has to match the one you
+ * setup on the server.
+ * All functions are design to be safe for programming mistakes and network connection issues. If the
+ * experiment does not exists it will return false and pretend the user is not part of it.
+ *
+ * @author Philipp Berner
+ *
+ */
+public class SwitchBoard {
+
+ private static final String TAG = "SwitchBoard";
+
+ /** Set if the application is run in debug mode. */
+ public static boolean DEBUG = true;
+
+ // Top-level experiment keys.
+ private static final String KEY_DATA = "data";
+ private static final String KEY_NAME = "name";
+ private static final String KEY_MATCH = "match";
+ private static final String KEY_BUCKETS = "buckets";
+ private static final String KEY_VALUES = "values";
+
+ // Match keys.
+ private static final String KEY_APP_ID = "appId";
+ private static final String KEY_COUNTRY = "country";
+ private static final String KEY_DEVICE = "device";
+ private static final String KEY_LANG = "lang";
+ private static final String KEY_MANUFACTURER = "manufacturer";
+ private static final String KEY_VERSION = "version";
+
+ // Bucket keys.
+ private static final String KEY_MIN = "min";
+ private static final String KEY_MAX = "max";
+
+ /**
+ * Loads a new config for a user. This method does network I/O, so it
+ * should not be called on the main thread.
+ *
+ * @param c ApplicationContext
+ * @param serverUrl Server URL endpoint.
+ */
+ static void loadConfig(Context c, @NonNull String serverUrl) {
+ final URL url;
+ try {
+ url = new URL(serverUrl);
+ } catch (MalformedURLException e) {
+ Log.e(TAG, "Exception creating server URL", e);
+ return;
+ }
+
+ final String result = readFromUrlGET(url);
+ if (DEBUG) Log.d(TAG, "Result: " + result);
+ if (result == null) {
+ return;
+ }
+
+ // Cache result locally in shared preferences.
+ Preferences.setDynamicConfigJson(c, result);
+ }
+
+ public static boolean isInBucket(Context c, int low, int high) {
+ final int userBucket = getUserBucket(c);
+ return (userBucket >= low) && (userBucket < high);
+ }
+
+ /**
+ * Looks up in config if user is in certain experiment. Returns false as a default value when experiment
+ * does not exist.
+ * Experiment names are defined server side as Key in array for return values.
+ * @param experimentName Name of the experiment to lookup
+ * @return returns value for experiment or false if experiment does not exist.
+ */
+ public static boolean isInExperiment(Context c, String experimentName) {
+ final Boolean override = Preferences.getOverrideValue(c, experimentName);
+ if (override != null) {
+ return override;
+ }
+
+ final String config = Preferences.getDynamicConfigJson(c);
+ if (config == null) {
+ return false;
+ }
+
+ try {
+ // TODO: cache the array into a mapping so we don't do a loop everytime we are looking for a experiment key
+ final JSONArray experiments = new JSONObject(config).getJSONArray(KEY_DATA);
+ JSONObject experiment = null;
+
+ for (int i = 0; i < experiments.length(); i++) {
+ JSONObject entry = experiments.getJSONObject(i);
+ final String name = entry.getString(KEY_NAME);
+ if (name.equals(experimentName)) {
+ experiment = entry;
+ break;
+ }
+ }
+
+ if (experiment == null) {
+ return false;
+ }
+
+ if (!isMatch(c, experiment.optJSONObject(KEY_MATCH))) {
+ return false;
+ }
+
+ final JSONObject buckets = experiment.getJSONObject(KEY_BUCKETS);
+ final boolean inExperiment = isInBucket(c, buckets.getInt(KEY_MIN), buckets.getInt(KEY_MAX));
+
+ if (DEBUG) {
+ Log.d(TAG, experimentName + " = " + inExperiment);
+ }
+ return inExperiment;
+ } catch (JSONException e) {
+ // If the experiment name is not found in the JSON, just return false.
+ // There is no need to log an error, since we don't really care if an
+ // inactive experiment is missing from the config.
+ return false;
+ }
+ }
+
+ private static List<String> getExperimentNames(Context c) throws JSONException {
+ // TODO: cache the array into a mapping so we don't do a loop everytime we are looking for a experiment key
+ final List<String> returnList = new ArrayList<>();
+ final String config = Preferences.getDynamicConfigJson(c);
+ final JSONArray experiments = new JSONObject(config).getJSONArray(KEY_DATA);
+
+ for (int i = 0; i < experiments.length(); i++) {
+ JSONObject entry = experiments.getJSONObject(i);
+ returnList.add(entry.getString(KEY_NAME));
+ }
+ return returnList;
+ }
+
+ @Nullable
+ private static JSONObject getExperiment(Context c, String experimentName) throws JSONException {
+ // TODO: cache the array into a mapping so we don't do a loop everytime we are looking for a experiment key
+ final String config = Preferences.getDynamicConfigJson(c);
+ final JSONArray experiments = new JSONObject(config).getJSONArray(KEY_DATA);
+ JSONObject experiment = null;
+
+ for (int i = 0; i < experiments.length(); i++) {
+ JSONObject entry = experiments.getJSONObject(i);
+ if (entry.getString(KEY_NAME).equals(experimentName)) {
+ experiment = entry;
+ break;
+ }
+ }
+ return experiment;
+ }
+
+ private static boolean isMatch(Context c, @Nullable JSONObject matchKeys) {
+ // If no match keys are specified, default to enabling the experiment.
+ if (matchKeys == null) {
+ return true;
+ }
+
+ if (matchKeys.has(KEY_APP_ID)) {
+ final String packageName = c.getPackageName();
+ try {
+ if (!packageName.matches(matchKeys.getString(KEY_APP_ID))) {
+ return false;
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Exception matching appId", e);
+ }
+ }
+
+ if (matchKeys.has(KEY_COUNTRY)) {
+ try {
+ final String country = Locale.getDefault().getISO3Country();
+ if (!country.matches(matchKeys.getString(KEY_COUNTRY))) {
+ return false;
+ }
+ } catch (MissingResourceException|JSONException e) {
+ Log.e(TAG, "Exception matching country", e);
+ }
+ }
+
+ if (matchKeys.has(KEY_DEVICE)) {
+ final String device = Build.DEVICE;
+ try {
+ if (!device.matches(matchKeys.getString(KEY_DEVICE))) {
+ return false;
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Exception matching device", e);
+ }
+
+ }
+ if (matchKeys.has(KEY_LANG)) {
+ try {
+ final String lang = Locale.getDefault().getISO3Language();
+ if (!lang.matches(matchKeys.getString(KEY_LANG))) {
+ return false;
+ }
+ } catch (MissingResourceException|JSONException e) {
+ Log.e(TAG, "Exception matching lang", e);
+ }
+ }
+ if (matchKeys.has(KEY_MANUFACTURER)) {
+ final String manufacturer = Build.MANUFACTURER;
+ try {
+ if (!manufacturer.matches(matchKeys.getString(KEY_MANUFACTURER))) {
+ return false;
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Exception matching manufacturer", e);
+ }
+ }
+
+ if (matchKeys.has(KEY_VERSION)) {
+ try {
+ final String version = c.getPackageManager().getPackageInfo(c.getPackageName(), 0).versionName;
+ if (!version.matches(matchKeys.getString(KEY_VERSION))) {
+ return false;
+ }
+ } catch (NameNotFoundException|JSONException e) {
+ Log.e(TAG, "Exception matching version", e);
+ }
+ }
+
+ // Default to return true if no matches failed.
+ return true;
+ }
+
+ /**
+ * @return a list of all active experiments.
+ */
+ public static List<String> getActiveExperiments(Context c) {
+ final List<String> returnList = new ArrayList<>();
+
+ final String config = Preferences.getDynamicConfigJson(c);
+ if (config == null) {
+ return returnList;
+ }
+
+ try {
+ final JSONObject data = new JSONObject(config);
+ final List<String> experiments = getExperimentNames(c);
+
+ for (int i = 0; i < experiments.size(); i++) {
+ final String name = experiments.get(i);
+
+ // Check override value before reading saved JSON.
+ Boolean isActive = Preferences.getOverrideValue(c, name);
+ if (isActive == null) {
+ // TODO: This is inefficient because it will check all the match cases on all experiments.
+ isActive = isInExperiment(c, name);
+ }
+ if (isActive) {
+ returnList.add(name);
+ }
+ }
+ } catch (JSONException e) {
+ // Something went wrong!
+ }
+
+ return returnList;
+ }
+
+ /**
+ * Checks if a certain experiment has additional values.
+ * @param c ApplicationContext
+ * @param experimentName Name of the experiment
+ * @return true when experiment exists
+ */
+ public static boolean hasExperimentValues(Context c, String experimentName) {
+ return getExperimentValuesFromJson(c, experimentName) != null;
+ }
+
+ /**
+ * Returns the experiment value as a JSONObject.
+ * @param experimentName Name of the experiment
+ * @return Experiment value as String, null if experiment does not exist.
+ */
+ @Nullable
+ public static JSONObject getExperimentValuesFromJson(Context c, String experimentName) {
+ final String config = Preferences.getDynamicConfigJson(c);
+
+ if (config == null) {
+ return null;
+ }
+
+ try {
+ final JSONObject experiment = getExperiment(c, experimentName);
+ if (experiment == null) {
+ return null;
+ }
+ return experiment.getJSONObject(KEY_VALUES);
+ } catch (JSONException e) {
+ Log.e(TAG, "Could not create JSON object from config string", e);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a String containing the server response from a GET request
+ * @param url URL for GET request.
+ * @return Returns String from server or null when failed.
+ */
+ @Nullable private static String readFromUrlGET(URL url) {
+ try {
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+ connection.setUseCaches(false);
+
+ InputStream is = connection.getInputStream();
+ InputStreamReader inputStreamReader = new InputStreamReader(is);
+ BufferedReader bufferReader = new BufferedReader(inputStreamReader, 8192);
+ String line;
+ StringBuilder resultContent = new StringBuilder();
+ while ((line = bufferReader.readLine()) != null) {
+ resultContent.append(line);
+ }
+ bufferReader.close();
+
+ return resultContent.toString();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the bucket number of the user. There are 100 possible buckets.
+ */
+ private static int getUserBucket(Context c) {
+ final DeviceUuidFactory df = new DeviceUuidFactory(c);
+ final String uuid = df.getDeviceUuid().toString();
+
+ CRC32 crc = new CRC32();
+ crc.update(uuid.getBytes());
+ long checksum = crc.getValue();
+ return (int)(checksum % 100L);
+ }
+}