Android Open Source - CustomersChoice Customers Choice






From Project

Back to project page CustomersChoice.

License

The source code is released under:

Apache License

If you think the Android project CustomersChoice listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
# * Copyright 2012 Hasan Hosgel/*from ww  w . ja  v a  2  s .c  om*/
 *
 * 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 de.alosdev.android.customerschoice;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
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.HashMap;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import org.apache.http.HttpStatus;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import de.alosdev.android.customerschoice.logger.ChainedLogger;
import de.alosdev.android.customerschoice.logger.Logger;
import de.alosdev.android.customerschoice.logger.NoLogger;
import de.alosdev.android.customerschoice.reporter.ChainedReporter;
import de.alosdev.android.customerschoice.reporter.NoReporter;
import de.alosdev.android.customerschoice.reporter.Reporter;


/**
 * <h1>Customer's Choice</h1>
 * <p>
 * This library can be used to make simple usability tests on live Android
 * applications, so you can find the best choice, if you have two or more
 * different solutions. The Customer choose the best solution. For Example, if
 * it is better to have a red or blue purchase button.
 * </p>
 *
 * <h2>USAGE</h2>
 * <p>CustomersChoice.getVariant(context, "Variant name");</p>
 *
 * <h3>adding a {@link Variant} by code with a spreading of 50:50 with
 * {@link CustomersChoice#addVariant(Variant)}</h3>
 * <p>CustomersChoice.addVariant(new
 * VariantBuilder("Variant name").setSpreading(new int[] { 50, 50 }).build());</p>
 *
 * <h3>JSON structure for configurations</h3>
 * <pre>
 * {
 *   "resetAll": true,
 *   "variants": [
 *     {
 *       "startTime": 51,
 *       "spreading": [ 1, 2 ],
 *       "name": "Variant1",
 *       "reset": true;
 *     },
 *     {
 *       "endTime": 53,
 *       "spreading": [ 3, 3 ],
 *       "name": "Variant2"
 *     }
 *   ]
 * }
 * </pre>
 *
 * <h4>adding several {@link Variant}s by a {@link String} resource with
 * {@link CustomersChoice#configureByResource(Context, int)}.</h4>
 * <p>CustomersChoice.configureByResource(context, R.string.resource);</p>
 * <p>With this you can add different configurations by locale, density and/or size.</p>
 *
 * <h4>adding several {@link Variant}s by a file one the SD Card {@link CustomersChoice#configureBySD(String)}</h4>
 * <p>CustomersChoice.configureBySD("FilepathWithFileName");</p>
 *
 * <h4>adding several {@link Variant}s by a network {@link CustomersChoice#configureByNetwork(Context, String)}</h4>
 * <p>CustomersChoice.configureByNetwork(context, "configurationURL");</p>
 *
 * <h3>adding loggers</h3>
 * <p>CustomersChoice.addLoggers(new AndroidLogger(), new CustomLogger());</p>
 *
 * <h3>adding reporters</h3>
 * <p>CustomersChoice.addReporters(new LogReporter(new AndroidLogger), new CustomReporter());</p>
 *
 * <h3>report of reached Goal</h3>
 * <p>CustomersChoice.reachesGoal("Variant name");</p>
 *
 * <h3>overwriting of the variant for a test scenario</h3>
 * <p>add the {@link BroadcastReceiver} configuration into the manifest:</p>
 * <h4>Attention</h4>
 * <p>Add also permissions to the broadcast receiver in the manifest, if you want to use it on a live application.</p>
 * <pre>
 * &lt;receiver android:name="de.alosdev.android.customerschoice.broadcast.OverwriteVariantBroadCastReceiver">
 *   &lt;intent-filter>
 *     &lt;action android:name="de.alosdev.android.customerschoice.demo.broadcast" />
 *   &lt;/intent-filter>
 * &lt;/receiver>
 * </pre>
 * <p>Write a broadcast Intent for overwriting the {@link Variant} in an {@link Activity}</p>
 * <pre>
 * Intent intent = new Intent("de.alosdev.android.customerschoice.demo.broadcast");
 * intent.putExtra(OverwriteVariantBroadCastReceiver.KEY_OVERWRITE_VARIANT, "Variant name");
 * intent.putExtra(OverwriteVariantBroadCastReceiver.KEY_FORCE_VARIANT, 2);
 * sendBroadcast(intent);
 * </pre>
 * <h3>setting {@link LifeTime} of {@link Variant}s</h3>
 * <p>You can change the {@link LifeTime} of the variant by {@link CustomersChoice#setLifeTime(Context, LifeTime)}.</p>
 * <li>{@link LifeTime#Session} - only persisted in memory</li>
 * <li>{@link LifeTime#Persistent} - persisted in preferences</li>
 * <br/><br/>
 * @author Hasan Hosgel
 *
 */
public final class CustomersChoice {
  private static final String FIELD_VARIANTS = "variants";
  private static final String FIELD_LAST_MODIFIED = "lastModified";
  private static final String FIELD_ETAG = "etag";
  private static final String KEY_SPREADING = "spreading";
  private static final String KEY_END_TIME = "endTime";
  private static final String KEY_START_TIME = "startTime";
  private static final String KEY_NAME = "name";
  private static final String KEY_VARIANTS = FIELD_VARIANTS;
  private static final String KEY_RESET_All = "resetAll";
  private static final String KEY_RESET = "reset";
  public static final String TAG = CustomersChoice.class.getSimpleName();
  private static CustomersChoice instance;
  private LifeTime lifeTime = LifeTime.Session;
  private HashMap<String, Variant> variants;
  private Random random;
  private Logger log;
  private Reporter report;

  /**
   * Definition of LifeTime of a {@link Variant}, whose default is
   * {@link #Session}, which cannot be changed in the moment
   *
   * @author Hasan Hosgel
   *
   */
  public enum LifeTime {
    Session, Persistent
  }

  /**
   * used for initialization
   */
  public static void init() {
    checkInstance();
  }

  private CustomersChoice() {
    variants = new HashMap<String, Variant>();
    random = new Random(System.currentTimeMillis());
    log = new NoLogger();
    report = new NoReporter();
  }

  /**
   * returns the Variant, which is chosen by the internal logic, which can be
   * used for your cases(if,switch).
   * @param context
   */
  public static int getVariant(Context context, final String name) {
    checkInstance();

    return instance.getInternalVariant(context, name);
  }

  /**
   * sets the {@link Logger}s for the library. If none is set the default {@link Logger}
   * {@link NoLogger} is used.
   *
   * @param log
   *          if the parameter is NULL or empty, the {@link NoLogger} is used.
   */
  public static void addLoggers(Logger... loggers) {
    checkInstance();

    final Logger log;
    if ((null == loggers) || (loggers.length < 1)) {
      log = new NoLogger();
    } else if (loggers.length == 1) {
      log = loggers[0];
    } else {
      log = new ChainedLogger(loggers);
    }
    instance.log = log;
  }

  /**
   * sets the report s for the library. If none is set the default {@link Logger}
   * {@link NoLogger} is used.
   *
   * @param log
   *          if the parameter is NULL or empty, the {@link NoLogger} is used.
   */
  public static void addReporters(Reporter... reporters) {
    checkInstance();

    final Reporter report;
    if ((null == reporters) || (reporters.length < 1)) {
      report = new NoReporter();
    } else if (reporters.length == 1) {
      report = reporters[0];
    } else {
      report = new ChainedReporter(reporters);
    }
    instance.report = report;
  }

  public static Logger getLogger() {
    checkInstance();
    return instance.log;
  }

  private int getInternalVariant(Context context, String name) {
    int choosedVariant = 1;
    Variant variant = instance.variants.get(name);
    final long currentTime = System.currentTimeMillis();
    if ((null != variant) && (variant.start < currentTime) && (variant.end > currentTime)) {
      report.startVariant(variant);
      log.d(TAG, "choosed for ", name, " Variant: ", choosedVariant);
      if (variant.currentVariant < 1) {
        int complete = 0;
        for (int spreadingItem : variant.spreading) {
          complete += spreadingItem;
        }

        int nextInt = random.nextInt(complete);
        for (int i = 1; i <= variant.spreading.length; i++) {
          nextInt -= variant.spreading[i - 1];
          if (nextInt < 1) {
            variant.currentVariant = i;
            break;
          }
        }
        persistVariants(context);
      }
      choosedVariant = variant.currentVariant;
    }
    return choosedVariant;
  }

  public static void reachesGoal(String name) {
    checkInstance();
    instance.internalReachesGoal(name);
  }

  private void internalReachesGoal(String name) {
    Variant variant = instance.variants.get(name);
    final long currentTime = System.currentTimeMillis();
    if ((null != variant) && (variant.start < currentTime) && (variant.end > currentTime)) {
      log.d(TAG, "reaches goal for ", name, " Variant: ", variant.currentVariant);
      report.reachesGoal(variant);
    }
  }

  public static void addVariant(final Variant variant) {
    addVariant(variant, true);
  }

  public static void addVariant(final Variant variant, boolean isNotReset) {
    checkInstance();

    Variant oldVariant = instance.variants.get(variant.name);
    if ((null != oldVariant) && isNotReset) {
      variant.currentVariant = oldVariant.currentVariant;
    }
    instance.variants.put(variant.name, variant);
    instance.log.d(TAG, "added variant: ", variant, " and isReset: ", !isNotReset);
  }

  private static void checkInstance() {
    if (null == instance) {
      instance = new CustomersChoice();

      // Work around pre-Froyo bugs in HTTP connection reuse.
      if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {
        System.setProperty("http.keepAlive", "false");
      }
    }
  }

  /**
   * sets the {@link LifeTime} of the Variants.<br/>
   * If the {@link LifeTime} it is {@link LifeTime#Session}, it will try to remove the saved Variants.<br/>
   * If it is {@link LifeTime#Persistent}, it will load the persisted Variants. You should call this after all
   * configuration is done
   * @param lifeTime
   * @param context
   */
  public static void setLifeTimeForVariants(Context context, LifeTime lifeTime) {
    checkInstance();
    instance.setLifeTime(context, lifeTime);
  }

  public void setLifeTime(Context context, LifeTime lifeTime) {
    final SharedPreferences preferences = getPreferences(context);
    final Set<String> foundVariants;
    switch (lifeTime) {
      case Session: {
        Editor editor = preferences.edit();
        foundVariants = preferences.getStringSet(getPreferencesKey(FIELD_VARIANTS, ""), null);
        if ((null != foundVariants) && !foundVariants.isEmpty()) {
          for (String variantName : foundVariants) {
            editor.remove(getPreferencesKey(FIELD_VARIANTS, variantName));
          }
          editor.remove(getPreferencesKey(FIELD_VARIANTS, ""));
          editor.commit();
        }
        log.d(TAG, "cleared persisted Variant");
        break;
      }

      case Persistent: {
        foundVariants = preferences.getStringSet(getPreferencesKey(FIELD_VARIANTS, ""), null);
        if ((null != foundVariants) && !foundVariants.isEmpty()) {
          for (String variantName : foundVariants) {
            forceVariant(context, variantName,
              preferences.getInt(getPreferencesKey(FIELD_VARIANTS, variantName), 0));
          }
        }
        log.d(TAG, "read persisted Variant");
        break;
      }

      default: {
        throw new IllegalArgumentException("Unknown LifeTime: " + lifeTime);
      }
    }
    this.lifeTime = lifeTime;
  }

  public LifeTime getLifeTime() {
    return lifeTime;
  }

  /**
   * configures the library with the given String resource, which has to be
   * valid JSON.
   *
   * @param context
   * @param stringResourceId
   *          resource id of the {@link String} resource containing the JSON
   */
  public static void configureByResource(Context context, int stringResourceId) {
    checkInstance();
    instance.configure(context, stringResourceId);
  }

  private void configure(Context context, int stringResourceId) {
    String jsonString = context.getString(stringResourceId);
    parseStringVariants(jsonString);
  }

  private void parseStringVariants(String jsonString) {
    try {
      JSONObject json = new JSONObject(jsonString);

      JSONArray array = json.getJSONArray(KEY_VARIANTS);
      final int arrayLength = array.length();
      if (arrayLength > 0) {
        JSONObject variant;
        VariantBuilder builder;
        for (int i = 0; i < arrayLength; i++) {
          variant = array.getJSONObject(i);
          if (variant.has(KEY_NAME)) {
            builder = new VariantBuilder(variant.getString(KEY_NAME));
            builder.setStartTime(variant.optLong(KEY_START_TIME, 0));
            builder.setEndTime(variant.optLong(KEY_END_TIME, Long.MAX_VALUE));
            if (variant.has(KEY_SPREADING)) {
              JSONArray spreading = variant.getJSONArray(KEY_SPREADING);
              final int length = spreading.length();
              final int[] spread = new int[length];
              for (int j = 0; j < length; j++) {
                spread[j] = spreading.getInt(j);
              }
              builder.setSpreading(spread);
            }

            addVariant(builder.build(), !variant.optBoolean(KEY_RESET, false));
          } else {
            log.w(TAG, "variant has not the required name: ", variant.toString());
          }
        }
      }
      if (json.optBoolean(KEY_RESET_All, false)) {
        log.d(TAG, "reset all Variants");
        for (Variant var : variants.values()) {
          var.currentVariant = 0;
        }
      }
    } catch (JSONException e) {
      log.e(TAG, e, "cannot read string resource");
    }
  }

  /**
   * Configuring the {@link Variant} via a file on the SD Card. The file content
   * must be valid JSON.
   *
   * @param fileName
   *          the filename or relative file path to the configuration file on
   *          the SD Card.
   */
  public static void configureBySD(String fileName) {
    checkInstance();
    try {
      instance.internalConfigureBySD(fileName);
    } catch (IOException e) {
      instance.log.e(TAG, e, "error while reading file: ", fileName);
    }
  }

  private void internalConfigureBySD(String fileName) throws IOException {
    // checks if the external storage is mounted
    if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
      final String filePath = TextUtils.concat(Environment.getExternalStorageDirectory().getAbsolutePath(), "/",
        fileName).toString();

      // loads the configuration file
      final File configurationFile = new File(filePath);

      // only loads the file if it's existing.
      if (configurationFile.exists()) {
        readFromInputStream(new FileInputStream(configurationFile));
      } else {
        log.w(TAG, "file does not exist on sd root:", fileName);
      }
    }
  }

  /**
   * loading the file content into a String and then parsing it.
   *
   * @param inputStream
   * @throws FileNotFoundException
   * @throws IOException
   */
  private void readFromInputStream(InputStream inputStream) throws FileNotFoundException, IOException {
    BufferedReader reader = null;
    InputStreamReader is = null;
    try {
      is = new InputStreamReader(inputStream);
      reader = new BufferedReader(is, 8192);

      String line = null;

      final StringBuilder result = new StringBuilder();
      while ((line = reader.readLine()) != null) {
        result.append(line);
      }

      parseStringVariants(result.toString());
    } finally {
      if (null != is) {
        is.close();
      }
      if (null != reader) {
        reader.close();
      }
    }
  }

  /**
   * Dont forget to add the permission
   * <code>&lt;uses-permission android:name="android.permission.INTERNET"/&gt</code>
   * in your AndroidManifest.xml<br/>
   * Any valid URL can be used for configuring the {@link Variant}s. The only
   * requirement is valid JSON.
   *
   * @param context
   * @param fileAddress
   */
  public static void configureByNetwork(Context context, String fileAddress) {
    checkInstance();
    instance.internalConfigureByNetwork(context, fileAddress);
  }

  private void internalConfigureByNetwork(final Context context, String fileAddress) {
    new AsyncTask<String, Void, Void>() {
      @Override
      protected Void doInBackground(String... args) {
        String value = args[0];
        try {
          final SharedPreferences preferences = getPreferences(context);
          final URL url = new URL(value);
          log.d(TAG, "read from: ", value);

          final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
          conn.setReadTimeout(10000 /* milliseconds */);
          conn.setConnectTimeout(15000 /* milliseconds */);

          // set etag header if existing
          final String fieldEtag = preferences.getString(getPreferencesKey(value, FIELD_ETAG), null);
          if (null != fieldEtag) {
            conn.setRequestProperty("If-None-Match", fieldEtag);
          }

          // set modified since header if existing
          final long fieldLastModified = preferences.getLong(getPreferencesKey(value, FIELD_LAST_MODIFIED), 0);
          if (fieldLastModified > 0) {
            conn.setIfModifiedSince(fieldLastModified);
          }
          conn.connect();

          final int response = conn.getResponseCode();

          if (HttpStatus.SC_OK == response) {
            log.d(TAG, "found file");
            readFromInputStream(conn.getInputStream());

            // writing caching information into preferences
            final Editor editor = preferences.edit();
            editor.putString(getPreferencesKey(value, FIELD_ETAG), conn.getHeaderField("ETag"));
            editor.putLong(getPreferencesKey(value, FIELD_LAST_MODIFIED), conn.getHeaderFieldDate("Last-Modified", 0));
            editor.commit();
          } else if (HttpStatus.SC_NOT_MODIFIED == response) {
            log.i(TAG, "no updates, file not modified: ", value);
          } else {
            log.e(TAG, "cannot read from: ", value, " and get following response code:", response);
          }
        } catch (MalformedURLException e) {
          log.e(TAG, e, "the given URL is malformed: ", value);
        } catch (IOException e) {
          log.e(TAG, e, "Error during reading the file: ", value);
        }
        return null;
      }

    }.execute(fileAddress);
  }

  String getPreferencesKey(String value, String field) {
    final StringBuilder sb = new StringBuilder();
    sb.append(TAG).append('.').append(value).append('.').append(field);
    return sb.toString();
  }

  /**
   * forces a Variant to be a custom case.
   * @param context
   * @param variantName
   * @param forceVariant
   */

  public static void forceVariant(Context context, String variantName, int forceVariant) {
    checkInstance();

    instance.internalForceVariant(context, variantName, forceVariant);
  }

  private void internalForceVariant(Context context, String variantName, int forceVariant) {
    Variant variant = variants.get(variantName);
    if (null == variant) {
      log.w(TAG, "This Variant does not exists: ", variantName);
      return;
    }
    variant.currentVariant = forceVariant;
    persistVariants(context);
  }

  private void persistVariants(final Context context) {
    if (lifeTime == LifeTime.Persistent) {
      new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... params) {
          final Editor editor = getPreferences(context).edit();
          final HashSet<String> variantNames = new HashSet<String>();
          for (Variant variant : variants.values()) {
            variantNames.add(variant.name);
            editor.putInt(getPreferencesKey(FIELD_VARIANTS, variant.name), variant.currentVariant);
          }
          editor.putStringSet(getPreferencesKey(KEY_VARIANTS, ""), variantNames);
          editor.commit();
          log.d(TAG, "persisted Variant");
          return null;
        }
      }.execute();
    }
  }

  private SharedPreferences getPreferences(Context context) {
    return context.getSharedPreferences(TAG, Context.MODE_PRIVATE);
  }
}




Java Source Code List

de.alosdev.android.customerschoice.CustomersChoice.java
de.alosdev.android.customerschoice.VariantBuilder.java
de.alosdev.android.customerschoice.Variant.java
de.alosdev.android.customerschoice.broadcast.OverwriteVariantBroadCastReceiver.java
de.alosdev.android.customerschoice.demo.CustomersChoiceApplication.java
de.alosdev.android.customerschoice.demo.CustomersChoiceDemo.java
de.alosdev.android.customerschoice.demo.ShoppingCardActivity.java
de.alosdev.android.customerschoice.logger.AndroidLogger.java
de.alosdev.android.customerschoice.logger.ChainedLogger.java
de.alosdev.android.customerschoice.logger.Logger.java
de.alosdev.android.customerschoice.logger.NoLogger.java
de.alosdev.android.customerschoice.reporter.ChainedReporter.java
de.alosdev.android.customerschoice.reporter.LogReporter.java
de.alosdev.android.customerschoice.reporter.NoReporter.java
de.alosdev.android.customerschoice.reporter.Reporter.java