summaryrefslogtreecommitdiffstats
path: root/mobile/android/thirdparty/com
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/thirdparty/com')
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java781
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ActivityKind.java35
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ActivityPackage.java100
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ActivityState.java151
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Adjust.java79
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustAttribution.java62
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustConfig.java128
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustEvent.java112
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java141
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustInstance.java86
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustReferrerReceiver.java35
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java155
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Constants.java53
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/DeviceInfo.java290
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/IActivityHandler.java36
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/IAttributionHandler.java20
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ILogger.java20
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/IPackageHandler.java27
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/IRequestHandler.java9
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/LICENSE21
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/LogLevel.java19
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Logger.java107
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/OnAttributionChangedListener.java5
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/PackageBuilder.java291
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/PackageHandler.java274
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Reflection.java210
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/RequestHandler.java210
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/UnitTestActivity.java38
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Util.java202
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/plugin/AndroidIdUtil.java10
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/plugin/MacAddressUtil.java82
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/plugin/Plugin.java12
-rw-r--r--mobile/android/thirdparty/com/jakewharton/disklrucache/DiskLruCache.java943
-rw-r--r--mobile/android/thirdparty/com/jakewharton/disklrucache/StrictLineReader.java196
-rw-r--r--mobile/android/thirdparty/com/jakewharton/disklrucache/Util.java77
-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
-rw-r--r--mobile/android/thirdparty/com/squareup/leakcanary/LeakCanary.java21
-rw-r--r--mobile/android/thirdparty/com/squareup/leakcanary/RefWatcher.java20
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Action.java83
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/AssetBitmapHunter.java51
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/BitmapHunter.java357
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Cache.java64
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Callback.java31
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/ContactsPhotoBitmapHunter.java130
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/ContentStreamBitmapHunter.java67
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/DeferredRequestCreator.java70
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Dispatcher.java315
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Downloader.java99
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/FetchAction.java30
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/FileBitmapHunter.java57
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/GetAction.java30
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/ImageViewAction.java75
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/LruCache.java146
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/MarkableInputStream.java157
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/MediaStoreBitmapHunter.java116
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/NetworkBitmapHunter.java113
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Picasso.java522
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/PicassoDrawable.java186
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/PicassoExecutorService.java81
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Request.java307
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/RequestCreator.java374
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/ResourceBitmapHunter.java55
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Stats.java143
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/StatsSnapshot.java120
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Target.java45
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/TargetAction.java46
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Transformation.java34
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/UrlConnectionDownloader.java100
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Utils.java304
73 files changed, 10057 insertions, 0 deletions
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java b/mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java
new file mode 100644
index 000000000..8c7858b97
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java
@@ -0,0 +1,781 @@
+//
+// ActivityHandler.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import org.json.JSONObject;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static com.adjust.sdk.Constants.ACTIVITY_STATE_FILENAME;
+import static com.adjust.sdk.Constants.ATTRIBUTION_FILENAME;
+import static com.adjust.sdk.Constants.LOGTAG;
+
+public class ActivityHandler extends HandlerThread implements IActivityHandler {
+
+ private static long TIMER_INTERVAL;
+ private static long TIMER_START;
+ private static long SESSION_INTERVAL;
+ private static long SUBSESSION_INTERVAL;
+ private static final String TIME_TRAVEL = "Time travel!";
+ private static final String ADJUST_PREFIX = "adjust_";
+ private static final String ACTIVITY_STATE_NAME = "Activity state";
+ private static final String ATTRIBUTION_NAME = "Attribution";
+
+ private SessionHandler sessionHandler;
+ private IPackageHandler packageHandler;
+ private ActivityState activityState;
+ private ILogger logger;
+ private static ScheduledExecutorService timer;
+ private boolean enabled;
+ private boolean offline;
+
+ private DeviceInfo deviceInfo;
+ private AdjustConfig adjustConfig; // always valid after construction
+ private AdjustAttribution attribution;
+ private IAttributionHandler attributionHandler;
+
+ private ActivityHandler(AdjustConfig adjustConfig) {
+ super(LOGTAG, MIN_PRIORITY);
+ setDaemon(true);
+ start();
+
+ logger = AdjustFactory.getLogger();
+ sessionHandler = new SessionHandler(getLooper(), this);
+ enabled = true;
+ init(adjustConfig);
+
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.INIT;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void init(AdjustConfig adjustConfig) {
+ this.adjustConfig = adjustConfig;
+ }
+
+ public static ActivityHandler getInstance(AdjustConfig adjustConfig) {
+ if (adjustConfig == null) {
+ AdjustFactory.getLogger().error("AdjustConfig missing");
+ return null;
+ }
+
+ if (!adjustConfig.isValid()) {
+ AdjustFactory.getLogger().error("AdjustConfig not initialized correctly");
+ return null;
+ }
+
+ ActivityHandler activityHandler = new ActivityHandler(adjustConfig);
+ return activityHandler;
+ }
+
+ @Override
+ public void trackSubsessionStart() {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.START;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void trackSubsessionEnd() {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.END;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void trackEvent(AdjustEvent event) {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.EVENT;
+ message.obj = event;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void finishedTrackingActivity(JSONObject jsonResponse) {
+ if (jsonResponse == null) {
+ return;
+ }
+
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.FINISH_TRACKING;
+ message.obj = jsonResponse;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (enabled == this.enabled) {
+ if (enabled) {
+ logger.debug("Adjust already enabled");
+ } else {
+ logger.debug("Adjust already disabled");
+ }
+ return;
+ }
+ this.enabled = enabled;
+ if (activityState != null) {
+ activityState.enabled = enabled;
+ }
+ if (enabled) {
+ if (toPause()) {
+ logger.info("Package and attribution handler remain paused due to the SDK is offline");
+ } else {
+ logger.info("Resuming package handler and attribution handler to enabled the SDK");
+ }
+ trackSubsessionStart();
+ } else {
+ logger.info("Pausing package handler and attribution handler to disable the SDK");
+ trackSubsessionEnd();
+ }
+ }
+
+ @Override
+ public void setOfflineMode(boolean offline) {
+ if (offline == this.offline) {
+ if (offline) {
+ logger.debug("Adjust already in offline mode");
+ } else {
+ logger.debug("Adjust already in online mode");
+ }
+ return;
+ }
+ this.offline = offline;
+ if (offline) {
+ logger.info("Pausing package and attribution handler to put in offline mode");
+ } else {
+ if (toPause()) {
+ logger.info("Package and attribution handler remain paused because the SDK is disabled");
+ } else {
+ logger.info("Resuming package handler and attribution handler to put in online mode");
+ }
+ }
+ updateStatus();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ if (activityState != null) {
+ return activityState.enabled;
+ } else {
+ return enabled;
+ }
+ }
+
+ @Override
+ public void readOpenUrl(Uri url, long clickTime) {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.DEEP_LINK;
+ UrlClickTime urlClickTime = new UrlClickTime(url, clickTime);
+ message.obj = urlClickTime;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public boolean tryUpdateAttribution(AdjustAttribution attribution) {
+ if (attribution == null) return false;
+
+ if (attribution.equals(this.attribution)) {
+ return false;
+ }
+
+ saveAttribution(attribution);
+ launchAttributionListener();
+ return true;
+ }
+
+ private void saveAttribution(AdjustAttribution attribution) {
+ this.attribution = attribution;
+ writeAttribution();
+ }
+
+ private void launchAttributionListener() {
+ if (adjustConfig.onAttributionChangedListener == null) {
+ return;
+ }
+ Handler handler = new Handler(adjustConfig.context.getMainLooper());
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ adjustConfig.onAttributionChangedListener.onAttributionChanged(attribution);
+ }
+ };
+ handler.post(runnable);
+ }
+
+ @Override
+ public void setAskingAttribution(boolean askingAttribution) {
+ activityState.askingAttribution = askingAttribution;
+ writeActivityState();
+ }
+
+ @Override
+ public ActivityPackage getAttributionPackage() {
+ long now = System.currentTimeMillis();
+ PackageBuilder attributionBuilder = new PackageBuilder(adjustConfig,
+ deviceInfo,
+ activityState,
+ now);
+ return attributionBuilder.buildAttributionPackage();
+ }
+
+ @Override
+ public void sendReferrer(String referrer, long clickTime) {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.SEND_REFERRER;
+ ReferrerClickTime referrerClickTime = new ReferrerClickTime(referrer, clickTime);
+ message.obj = referrerClickTime;
+ sessionHandler.sendMessage(message);
+ }
+
+ private class UrlClickTime {
+ Uri url;
+ long clickTime;
+
+ UrlClickTime(Uri url, long clickTime) {
+ this.url = url;
+ this.clickTime = clickTime;
+ }
+ }
+
+ private class ReferrerClickTime {
+ String referrer;
+ long clickTime;
+
+ ReferrerClickTime(String referrer, long clickTime) {
+ this.referrer = referrer;
+ this.clickTime = clickTime;
+ }
+ }
+
+ private void updateStatus() {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.UPDATE_STATUS;
+ sessionHandler.sendMessage(message);
+ }
+
+ private static final class SessionHandler extends Handler {
+ private static final int BASE_ADDRESS = 72630;
+ private static final int INIT = BASE_ADDRESS + 1;
+ private static final int START = BASE_ADDRESS + 2;
+ private static final int END = BASE_ADDRESS + 3;
+ private static final int EVENT = BASE_ADDRESS + 4;
+ private static final int FINISH_TRACKING = BASE_ADDRESS + 5;
+ private static final int DEEP_LINK = BASE_ADDRESS + 6;
+ private static final int SEND_REFERRER = BASE_ADDRESS + 7;
+ private static final int UPDATE_STATUS = BASE_ADDRESS + 8;
+
+ private final WeakReference<ActivityHandler> sessionHandlerReference;
+
+ protected SessionHandler(Looper looper, ActivityHandler sessionHandler) {
+ super(looper);
+ this.sessionHandlerReference = new WeakReference<ActivityHandler>(sessionHandler);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ super.handleMessage(message);
+
+ ActivityHandler sessionHandler = sessionHandlerReference.get();
+ if (sessionHandler == null) {
+ return;
+ }
+
+ switch (message.arg1) {
+ case INIT:
+ sessionHandler.initInternal();
+ break;
+ case START:
+ sessionHandler.startInternal();
+ break;
+ case END:
+ sessionHandler.endInternal();
+ break;
+ case EVENT:
+ AdjustEvent event = (AdjustEvent) message.obj;
+ sessionHandler.trackEventInternal(event);
+ break;
+ case FINISH_TRACKING:
+ JSONObject jsonResponse = (JSONObject) message.obj;
+ sessionHandler.finishedTrackingActivityInternal(jsonResponse);
+ break;
+ case DEEP_LINK:
+ UrlClickTime urlClickTime = (UrlClickTime) message.obj;
+ sessionHandler.readOpenUrlInternal(urlClickTime.url, urlClickTime.clickTime);
+ break;
+ case SEND_REFERRER:
+ ReferrerClickTime referrerClickTime = (ReferrerClickTime) message.obj;
+ sessionHandler.sendReferrerInternal(referrerClickTime.referrer, referrerClickTime.clickTime);
+ break;
+ case UPDATE_STATUS:
+ sessionHandler.updateStatusInternal();
+ break;
+ }
+ }
+ }
+
+ private void initInternal() {
+ TIMER_INTERVAL = AdjustFactory.getTimerInterval();
+ TIMER_START = AdjustFactory.getTimerStart();
+ SESSION_INTERVAL = AdjustFactory.getSessionInterval();
+ SUBSESSION_INTERVAL = AdjustFactory.getSubsessionInterval();
+
+ deviceInfo = new DeviceInfo(adjustConfig.context, adjustConfig.sdkPrefix);
+
+ if (adjustConfig.environment == AdjustConfig.ENVIRONMENT_PRODUCTION) {
+ logger.setLogLevel(LogLevel.ASSERT);
+ } else {
+ logger.setLogLevel(adjustConfig.logLevel);
+ }
+
+ if (adjustConfig.eventBufferingEnabled) {
+ logger.info("Event buffering is enabled");
+ }
+
+ String playAdId = Util.getPlayAdId(adjustConfig.context);
+ if (playAdId == null) {
+ logger.info("Unable to get Google Play Services Advertising ID at start time");
+ }
+
+ if (adjustConfig.defaultTracker != null) {
+ logger.info("Default tracker: '%s'", adjustConfig.defaultTracker);
+ }
+
+ if (adjustConfig.referrer != null) {
+ sendReferrer(adjustConfig.referrer, adjustConfig.referrerClickTime); // send to background queue to make sure that activityState is valid
+ }
+
+ readAttribution();
+ readActivityState();
+
+ packageHandler = AdjustFactory.getPackageHandler(this, adjustConfig.context, toPause());
+
+ startInternal();
+ }
+
+ private void startInternal() {
+ // it shouldn't start if it was disabled after a first session
+ if (activityState != null
+ && !activityState.enabled) {
+ return;
+ }
+
+ updateStatusInternal();
+
+ processSession();
+
+ checkAttributionState();
+
+ startTimer();
+ }
+
+ private void processSession() {
+ long now = System.currentTimeMillis();
+
+ // very first session
+ if (activityState == null) {
+ activityState = new ActivityState();
+ activityState.sessionCount = 1; // this is the first session
+
+ transferSessionPackage(now);
+ activityState.resetSessionAttributes(now);
+ activityState.enabled = this.enabled;
+ writeActivityState();
+ return;
+ }
+
+ long lastInterval = now - activityState.lastActivity;
+
+ if (lastInterval < 0) {
+ logger.error(TIME_TRAVEL);
+ activityState.lastActivity = now;
+ writeActivityState();
+ return;
+ }
+
+ // new session
+ if (lastInterval > SESSION_INTERVAL) {
+ activityState.sessionCount++;
+ activityState.lastInterval = lastInterval;
+
+ transferSessionPackage(now);
+ activityState.resetSessionAttributes(now);
+ writeActivityState();
+ return;
+ }
+
+ // new subsession
+ if (lastInterval > SUBSESSION_INTERVAL) {
+ activityState.subsessionCount++;
+ activityState.sessionLength += lastInterval;
+ activityState.lastActivity = now;
+ writeActivityState();
+ logger.info("Started subsession %d of session %d",
+ activityState.subsessionCount,
+ activityState.sessionCount);
+ }
+ }
+
+ private void checkAttributionState() {
+ // if there is no attribution saved, or there is one being asked
+ if (attribution == null || activityState.askingAttribution) {
+ getAttributionHandler().getAttribution();
+ }
+ }
+
+ private void endInternal() {
+ packageHandler.pauseSending();
+ getAttributionHandler().pauseSending();
+ stopTimer();
+ if (updateActivityState(System.currentTimeMillis())) {
+ writeActivityState();
+ }
+ }
+
+ private void trackEventInternal(AdjustEvent event) {
+ if (!checkEvent(event)) return;
+ if (!activityState.enabled) return;
+
+ long now = System.currentTimeMillis();
+
+ activityState.eventCount++;
+ updateActivityState(now);
+
+ PackageBuilder eventBuilder = new PackageBuilder(adjustConfig, deviceInfo, activityState, now);
+ ActivityPackage eventPackage = eventBuilder.buildEventPackage(event);
+ packageHandler.addPackage(eventPackage);
+
+ if (adjustConfig.eventBufferingEnabled) {
+ logger.info("Buffered event %s", eventPackage.getSuffix());
+ } else {
+ packageHandler.sendFirstPackage();
+ }
+
+ writeActivityState();
+ }
+
+ private void finishedTrackingActivityInternal(JSONObject jsonResponse) {
+ if (jsonResponse == null) {
+ return;
+ }
+
+ String deeplink = jsonResponse.optString("deeplink", null);
+ launchDeeplinkMain(deeplink);
+ getAttributionHandler().checkAttribution(jsonResponse);
+ }
+
+ private void sendReferrerInternal(String referrer, long clickTime) {
+ ActivityPackage clickPackage = buildQueryStringClickPackage(referrer,
+ "reftag",
+ clickTime);
+ if (clickPackage == null) {
+ return;
+ }
+
+ getAttributionHandler().getAttribution();
+
+ packageHandler.sendClickPackage(clickPackage);
+ }
+
+ private void readOpenUrlInternal(Uri url, long clickTime) {
+ if (url == null) {
+ return;
+ }
+
+ String queryString = url.getQuery();
+
+ ActivityPackage clickPackage = buildQueryStringClickPackage(queryString, "deeplink", clickTime);
+ if (clickPackage == null) {
+ return;
+ }
+
+ getAttributionHandler().getAttribution();
+
+ packageHandler.sendClickPackage(clickPackage);
+ }
+
+ private ActivityPackage buildQueryStringClickPackage(String queryString, String source, long clickTime) {
+ if (queryString == null) {
+ return null;
+ }
+
+ long now = System.currentTimeMillis();
+ Map<String, String> queryStringParameters = new HashMap<String, String>();
+ AdjustAttribution queryStringAttribution = new AdjustAttribution();
+ boolean hasAdjustTags = false;
+
+ String[] queryPairs = queryString.split("&");
+ for (String pair : queryPairs) {
+ if (readQueryString(pair, queryStringParameters, queryStringAttribution)) {
+ hasAdjustTags = true;
+ }
+ }
+
+ if (!hasAdjustTags) {
+ return null;
+ }
+
+ String reftag = queryStringParameters.remove("reftag");
+
+ PackageBuilder builder = new PackageBuilder(adjustConfig, deviceInfo, activityState, now);
+ builder.extraParameters = queryStringParameters;
+ builder.attribution = queryStringAttribution;
+ builder.reftag = reftag;
+ ActivityPackage clickPackage = builder.buildClickPackage(source, clickTime);
+ return clickPackage;
+ }
+
+ private boolean readQueryString(String queryString,
+ Map<String, String> extraParameters,
+ AdjustAttribution queryStringAttribution) {
+ String[] pairComponents = queryString.split("=");
+ if (pairComponents.length != 2) return false;
+
+ String key = pairComponents[0];
+ if (!key.startsWith(ADJUST_PREFIX)) return false;
+
+ String value = pairComponents[1];
+ if (value.length() == 0) return false;
+
+ String keyWOutPrefix = key.substring(ADJUST_PREFIX.length());
+ if (keyWOutPrefix.length() == 0) return false;
+
+ if (!trySetAttribution(queryStringAttribution, keyWOutPrefix, value)) {
+ extraParameters.put(keyWOutPrefix, value);
+ }
+
+ return true;
+ }
+
+ private boolean trySetAttribution(AdjustAttribution queryStringAttribution,
+ String key,
+ String value) {
+ if (key.equals("tracker")) {
+ queryStringAttribution.trackerName = value;
+ return true;
+ }
+
+ if (key.equals("campaign")) {
+ queryStringAttribution.campaign = value;
+ return true;
+ }
+
+ if (key.equals("adgroup")) {
+ queryStringAttribution.adgroup = value;
+ return true;
+ }
+
+ if (key.equals("creative")) {
+ queryStringAttribution.creative = value;
+ return true;
+ }
+
+ return false;
+ }
+
+ private void updateStatusInternal() {
+ updateAttributionHandlerStatus();
+ updatePackageHandlerStatus();
+ }
+
+ private void updateAttributionHandlerStatus() {
+ if (attributionHandler == null) {
+ return;
+ }
+ if (toPause()) {
+ attributionHandler.pauseSending();
+ } else {
+ attributionHandler.resumeSending();
+ }
+ }
+
+ private void updatePackageHandlerStatus() {
+ if (packageHandler == null) {
+ return;
+ }
+ if (toPause()) {
+ packageHandler.pauseSending();
+ } else {
+ packageHandler.resumeSending();
+ }
+ }
+
+ private void launchDeeplinkMain(String deeplink) {
+ if (deeplink == null) return;
+
+ Uri location = Uri.parse(deeplink);
+ Intent mapIntent = new Intent(Intent.ACTION_VIEW, location);
+ mapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Verify it resolves
+ PackageManager packageManager = adjustConfig.context.getPackageManager();
+ List<ResolveInfo> activities = packageManager.queryIntentActivities(mapIntent, 0);
+ boolean isIntentSafe = activities.size() > 0;
+
+ // Start an activity if it's safe
+ if (!isIntentSafe) {
+ logger.error("Unable to open deep link (%s)", deeplink);
+ return;
+ }
+
+ logger.info("Open deep link (%s)", deeplink);
+ adjustConfig.context.startActivity(mapIntent);
+ }
+
+ private boolean updateActivityState(long now) {
+ long lastInterval = now - activityState.lastActivity;
+ // ignore late updates
+ if (lastInterval > SESSION_INTERVAL) {
+ return false;
+ }
+ activityState.lastActivity = now;
+
+ if (lastInterval < 0) {
+ logger.error(TIME_TRAVEL);
+ } else {
+ activityState.sessionLength += lastInterval;
+ activityState.timeSpent += lastInterval;
+ }
+ return true;
+ }
+
+ public static boolean deleteActivityState(Context context) {
+ return context.deleteFile(ACTIVITY_STATE_FILENAME);
+ }
+
+ public static boolean deleteAttribution(Context context) {
+ return context.deleteFile(ATTRIBUTION_FILENAME);
+ }
+
+ private void transferSessionPackage(long now) {
+ PackageBuilder builder = new PackageBuilder(adjustConfig, deviceInfo, activityState, now);
+ ActivityPackage sessionPackage = builder.buildSessionPackage();
+ packageHandler.addPackage(sessionPackage);
+ packageHandler.sendFirstPackage();
+ }
+
+ private void startTimer() {
+ stopTimer();
+
+ if (!activityState.enabled) {
+ return;
+ }
+ timer = Executors.newSingleThreadScheduledExecutor();
+ timer.scheduleWithFixedDelay(new Runnable() {
+ @Override
+ public void run() {
+ timerFired();
+ }
+ }, TIMER_START, TIMER_INTERVAL, TimeUnit.MILLISECONDS);
+ }
+
+ private void stopTimer() {
+ if (timer != null) {
+ timer.shutdown();
+ timer = null;
+ }
+ }
+
+ private void timerFired() {
+ if (!activityState.enabled) {
+ stopTimer();
+ return;
+ }
+
+ packageHandler.sendFirstPackage();
+
+ if (updateActivityState(System.currentTimeMillis())) {
+ writeActivityState();
+ }
+ }
+
+ private void readActivityState() {
+ try {
+ /**
+ * Mozilla:
+ * readObject is a generic object, and can therefore return arbitrary generic objects
+ * that might not match the expected type. Therefore there will be an implicit cast
+ * here, which can fail. Therefore we have to add the catch (ClassCastException)
+ * Note: this has been fixed in upstream, we only need this for the version we are still shipping.
+ */
+ activityState = Util.readObject(adjustConfig.context, ACTIVITY_STATE_FILENAME, ACTIVITY_STATE_NAME);
+ } catch (ClassCastException e) {
+ activityState = null;
+ }
+ }
+
+ private void readAttribution() {
+ try {
+ /**
+ * Mozilla: (same as in readActivityState() )
+ * readObject is a generic object, and can therefore return arbitrary generic objects
+ * that might not match the expected type. Therefore there will be an implicit cast
+ * here, which can fail. Therefore we have to add the catch (ClassCastException)
+ * Note: this has been fixed in upstream, we only need this for the version we are still shipping.
+ */
+ attribution = Util.readObject(adjustConfig.context, ATTRIBUTION_FILENAME, ATTRIBUTION_NAME);
+ } catch (ClassCastException e) {
+ activityState = null;
+ }
+ }
+
+ private void writeActivityState() {
+ Util.writeObject(activityState, adjustConfig.context, ACTIVITY_STATE_FILENAME, ACTIVITY_STATE_NAME);
+ }
+
+ private void writeAttribution() {
+ Util.writeObject(attribution, adjustConfig.context, ATTRIBUTION_FILENAME, ATTRIBUTION_NAME);
+ }
+
+ private boolean checkEvent(AdjustEvent event) {
+ if (event == null) {
+ logger.error("Event missing");
+ return false;
+ }
+
+ if (!event.isValid()) {
+ logger.error("Event not initialized correctly");
+ return false;
+ }
+
+ return true;
+ }
+
+ // lazy initialization to prevent null activity state before first session
+ private IAttributionHandler getAttributionHandler() {
+ if (attributionHandler == null) {
+ ActivityPackage attributionPackage = getAttributionPackage();
+ attributionHandler = AdjustFactory.getAttributionHandler(this,
+ attributionPackage,
+ toPause());
+ }
+ return attributionHandler;
+ }
+
+ private boolean toPause() {
+ return offline || !isEnabled();
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ActivityKind.java b/mobile/android/thirdparty/com/adjust/sdk/ActivityKind.java
new file mode 100644
index 000000000..a255b83a9
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityKind.java
@@ -0,0 +1,35 @@
+package com.adjust.sdk;
+
+public enum ActivityKind {
+ UNKNOWN, SESSION, EVENT, CLICK, ATTRIBUTION;
+
+ public static ActivityKind fromString(String string) {
+ if ("session".equals(string)) {
+ return SESSION;
+ } else if ("event".equals(string)) {
+ return EVENT;
+ } else if ("click".equals(string)) {
+ return CLICK;
+ } else if ("attribution".equals(string)) {
+ return ATTRIBUTION;
+ } else {
+ return UNKNOWN;
+ }
+ }
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case SESSION:
+ return "session";
+ case EVENT:
+ return "event";
+ case CLICK:
+ return "click";
+ case ATTRIBUTION:
+ return "attribution";
+ default:
+ return "unknown";
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ActivityPackage.java b/mobile/android/thirdparty/com/adjust/sdk/ActivityPackage.java
new file mode 100644
index 000000000..27ab969fd
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityPackage.java
@@ -0,0 +1,100 @@
+//
+// ActivityPackage.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import java.io.Serializable;
+import java.util.Map;
+
+public class ActivityPackage implements Serializable {
+ private static final long serialVersionUID = -35935556512024097L;
+
+ // data
+ private String path;
+ private String clientSdk;
+ private Map<String, String> parameters;
+
+ // logs
+ private ActivityKind activityKind;
+ private String suffix;
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public String getClientSdk() {
+ return clientSdk;
+ }
+
+ public void setClientSdk(String clientSdk) {
+ this.clientSdk = clientSdk;
+ }
+
+ public Map<String, String> getParameters() {
+ return parameters;
+ }
+
+ public void setParameters(Map<String, String> parameters) {
+ this.parameters = parameters;
+ }
+
+ public ActivityKind getActivityKind() {
+ return activityKind;
+ }
+
+ public void setActivityKind(ActivityKind activityKind) {
+ this.activityKind = activityKind;
+ }
+
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public void setSuffix(String suffix) {
+ this.suffix = suffix;
+ }
+
+ public String toString() {
+ return String.format("%s%s", activityKind.toString(), suffix);
+ }
+
+ public String getExtendedString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(String.format("Path: %s\n", path));
+ builder.append(String.format("ClientSdk: %s\n", clientSdk));
+
+ if (parameters != null) {
+ builder.append("Parameters:");
+ for (Map.Entry<String, String> entry : parameters.entrySet()) {
+ builder.append(String.format("\n\t%-16s %s", entry.getKey(), entry.getValue()));
+ }
+ }
+ return builder.toString();
+ }
+
+ protected String getSuccessMessage() {
+ try {
+ return String.format("Tracked %s%s", activityKind.toString(), suffix);
+ } catch (NullPointerException e) {
+ return "Tracked ???";
+ }
+ }
+
+ protected String getFailureMessage() {
+ try {
+ return String.format("Failed to track %s%s", activityKind.toString(), suffix);
+ } catch (NullPointerException e) {
+ return "Failed to track ???";
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ActivityState.java b/mobile/android/thirdparty/com/adjust/sdk/ActivityState.java
new file mode 100644
index 000000000..41ad2ca3b
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityState.java
@@ -0,0 +1,151 @@
+//
+// ActivityState.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectInputStream.GetField;
+import java.io.Serializable;
+import java.util.Calendar;
+import java.util.Locale;
+
+public class ActivityState implements Serializable, Cloneable {
+ private static final long serialVersionUID = 9039439291143138148L;
+ private transient String readErrorMessage = "Unable to read '%s' field in migration device with message (%s)";
+ private transient ILogger logger;
+
+ // persistent data
+ protected String uuid;
+ protected boolean enabled;
+ protected boolean askingAttribution;
+
+ // global counters
+ protected int eventCount;
+ protected int sessionCount;
+
+ // session attributes
+ protected int subsessionCount;
+ protected long sessionLength; // all durations in milliseconds
+ protected long timeSpent;
+ protected long lastActivity; // all times in milliseconds since 1970
+
+ protected long lastInterval;
+
+ protected ActivityState() {
+ logger = AdjustFactory.getLogger();
+ // create UUID for new devices
+ uuid = Util.createUuid();
+ enabled = true;
+ askingAttribution = false;
+
+ eventCount = 0; // no events yet
+ sessionCount = 0; // the first session just started
+ subsessionCount = -1; // we don't know how many subsessions this first session will have
+ sessionLength = -1; // same for session length and time spent
+ timeSpent = -1; // this information will be collected and attached to the next session
+ lastActivity = -1;
+ lastInterval = -1;
+ }
+
+ protected void resetSessionAttributes(long now) {
+ subsessionCount = 1; // first subsession
+ sessionLength = 0; // no session length yet
+ timeSpent = 0; // no time spent yet
+ lastActivity = now;
+ lastInterval = -1;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US,
+ "ec:%d sc:%d ssc:%d sl:%.1f ts:%.1f la:%s uuid:%s",
+ eventCount, sessionCount, subsessionCount,
+ sessionLength / 1000.0, timeSpent / 1000.0,
+ stamp(lastActivity), uuid);
+ }
+
+ @Override
+ public ActivityState clone() {
+ try {
+ return (ActivityState) super.clone();
+ } catch (CloneNotSupportedException e) {
+ return null;
+ }
+ }
+
+
+ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
+ GetField fields = stream.readFields();
+
+ eventCount = readIntField(fields, "eventCount", 0);
+ sessionCount = readIntField(fields, "sessionCount", 0);
+ subsessionCount = readIntField(fields, "subsessionCount", -1);
+ sessionLength = readLongField(fields, "sessionLength", -1l);
+ timeSpent = readLongField(fields, "timeSpent", -1l);
+ lastActivity = readLongField(fields, "lastActivity", -1l);
+ lastInterval = readLongField(fields, "lastInterval", -1l);
+
+ // new fields
+ uuid = readStringField(fields, "uuid", null);
+ enabled = readBooleanField(fields, "enabled", true);
+ askingAttribution = readBooleanField(fields, "askingAttribution", false);
+
+ // create UUID for migrating devices
+ if (uuid == null) {
+ uuid = Util.createUuid();
+ }
+ }
+
+ private String readStringField(GetField fields, String name, String defaultValue) {
+ try {
+ return (String) fields.get(name, defaultValue);
+ } catch (Exception e) {
+ logger.debug(readErrorMessage, name, e.getMessage());
+ return defaultValue;
+ }
+ }
+
+ private boolean readBooleanField(GetField fields, String name, boolean defaultValue) {
+ try {
+ return fields.get(name, defaultValue);
+ } catch (Exception e) {
+ logger.debug(readErrorMessage, name, e.getMessage());
+ return defaultValue;
+ }
+ }
+
+ private int readIntField(GetField fields, String name, int defaultValue) {
+ try {
+ return fields.get(name, defaultValue);
+ } catch (Exception e) {
+ logger.debug(readErrorMessage, name, e.getMessage());
+ return defaultValue;
+ }
+ }
+
+ private long readLongField(GetField fields, String name, long defaultValue) {
+ try {
+ return fields.get(name, defaultValue);
+ } catch (Exception e) {
+ logger.debug(readErrorMessage, name, e.getMessage());
+ return defaultValue;
+ }
+ }
+
+ private static String stamp(long dateMillis) {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(dateMillis);
+ return String.format(Locale.US,
+ "%02d:%02d:%02d",
+ calendar.HOUR_OF_DAY,
+ calendar.MINUTE,
+ calendar.SECOND);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Adjust.java b/mobile/android/thirdparty/com/adjust/sdk/Adjust.java
new file mode 100644
index 000000000..3b81a077b
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Adjust.java
@@ -0,0 +1,79 @@
+//
+// Adjust.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2012-10-11.
+// Copyright (c) 2012-2014 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.net.Uri;
+
+/**
+ * The main interface to Adjust.
+ * Use the methods of this class to tell Adjust about the usage of your app.
+ * See the README for details.
+ */
+public class Adjust {
+
+ private static AdjustInstance defaultInstance;
+
+ private Adjust() {
+ }
+
+ public static synchronized AdjustInstance getDefaultInstance() {
+ if (defaultInstance == null) {
+ defaultInstance = new AdjustInstance();
+ }
+ return defaultInstance;
+ }
+
+ public static void onCreate(AdjustConfig adjustConfig) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.onCreate(adjustConfig);
+ }
+
+ public static void trackEvent(AdjustEvent event) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.trackEvent(event);
+ }
+
+ public static void onResume() {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.onResume();
+ }
+
+ public static void onPause() {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.onPause();
+ }
+
+ public static void setEnabled(boolean enabled) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.setEnabled(enabled);
+ }
+
+ public static boolean isEnabled() {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ return adjustInstance.isEnabled();
+ }
+
+ public static void appWillOpenUrl(Uri url) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.appWillOpenUrl(url);
+ }
+
+ public static void setReferrer(String referrer) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.sendReferrer(referrer);
+ }
+
+ public static void setOfflineMode(boolean enabled) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.setOfflineMode(enabled);
+ }
+}
+
+
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustAttribution.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustAttribution.java
new file mode 100644
index 000000000..4e3abb017
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustAttribution.java
@@ -0,0 +1,62 @@
+package com.adjust.sdk;
+
+import org.json.JSONObject;
+
+import java.io.Serializable;
+
+/**
+ * Created by pfms on 07/11/14.
+ */
+public class AdjustAttribution implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ public String trackerToken;
+ public String trackerName;
+ public String network;
+ public String campaign;
+ public String adgroup;
+ public String creative;
+
+ public static AdjustAttribution fromJson(JSONObject jsonObject) {
+ if (jsonObject == null) return null;
+
+ AdjustAttribution attribution = new AdjustAttribution();
+
+ attribution.trackerToken = jsonObject.optString("tracker_token", null);
+ attribution.trackerName = jsonObject.optString("tracker_name", null);
+ attribution.network = jsonObject.optString("network", null);
+ attribution.campaign = jsonObject.optString("campaign", null);
+ attribution.adgroup = jsonObject.optString("adgroup", null);
+ attribution.creative = jsonObject.optString("creative", null);
+
+ return attribution;
+ }
+
+ public boolean equals(Object other) {
+ if (other == this) return true;
+ if (other == null) return false;
+ if (getClass() != other.getClass()) return false;
+ AdjustAttribution otherAttribution = (AdjustAttribution) other;
+
+ if (!equalString(trackerToken, otherAttribution.trackerToken)) return false;
+ if (!equalString(trackerName, otherAttribution.trackerName)) return false;
+ if (!equalString(network, otherAttribution.network)) return false;
+ if (!equalString(campaign, otherAttribution.campaign)) return false;
+ if (!equalString(adgroup, otherAttribution.adgroup)) return false;
+ if (!equalString(creative, otherAttribution.creative)) return false;
+ return true;
+ }
+
+ private boolean equalString(String first, String second) {
+ if (first == null || second == null) {
+ return first == null && second == null;
+ }
+ return first.equals(second);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("tt:%s tn:%s net:%s cam:%s adg:%s cre:%s",
+ trackerToken, trackerName, network, campaign, adgroup, creative);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustConfig.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustConfig.java
new file mode 100644
index 000000000..148a5f670
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustConfig.java
@@ -0,0 +1,128 @@
+package com.adjust.sdk;
+
+import android.content.Context;
+
+/**
+ * Created by pfms on 06/11/14.
+ */
+public class AdjustConfig {
+ Context context;
+ String appToken;
+ String environment;
+ LogLevel logLevel;
+ String sdkPrefix;
+ Boolean eventBufferingEnabled;
+ String defaultTracker;
+ OnAttributionChangedListener onAttributionChangedListener;
+ String referrer;
+ long referrerClickTime;
+ Boolean knownDevice;
+
+ public static final String ENVIRONMENT_SANDBOX = "sandbox";
+ public static final String ENVIRONMENT_PRODUCTION = "production";
+
+ public AdjustConfig(Context context, String appToken, String environment) {
+ if (!isValid(context, appToken, environment)) {
+ return;
+ }
+
+ this.context = context.getApplicationContext();
+ this.appToken = appToken;
+ this.environment = environment;
+
+ // default values
+ this.logLevel = LogLevel.INFO;
+ this.eventBufferingEnabled = false;
+ }
+
+ public void setEventBufferingEnabled(Boolean eventBufferingEnabled) {
+ this.eventBufferingEnabled = eventBufferingEnabled;
+ }
+
+ public void setLogLevel(LogLevel logLevel) {
+ this.logLevel = logLevel;
+ }
+
+ public void setSdkPrefix(String sdkPrefix) {
+ this.sdkPrefix = sdkPrefix;
+ }
+
+ public void setDefaultTracker(String defaultTracker) {
+ this.defaultTracker = defaultTracker;
+ }
+
+ public void setOnAttributionChangedListener(OnAttributionChangedListener onAttributionChangedListener) {
+ this.onAttributionChangedListener = onAttributionChangedListener;
+ }
+
+ public boolean hasListener() {
+ return onAttributionChangedListener != null;
+ }
+
+ public boolean isValid() {
+ return appToken != null;
+ }
+
+ private boolean isValid(Context context, String appToken, String environment) {
+ if (!checkAppToken(appToken)) return false;
+ if (!checkEnvironment(environment)) return false;
+ if (!checkContext(context)) return false;
+
+ return true;
+ }
+
+ private static boolean checkContext(Context context) {
+ ILogger logger = AdjustFactory.getLogger();
+ if (context == null) {
+ logger.error("Missing context");
+ return false;
+ }
+
+ if (!Util.checkPermission(context, android.Manifest.permission.INTERNET)) {
+ logger.error("Missing permission: INTERNET");
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean checkAppToken(String appToken) {
+ ILogger logger = AdjustFactory.getLogger();
+ if (appToken == null) {
+ logger.error("Missing App Token.");
+ return false;
+ }
+
+ if (appToken.length() != 12) {
+ logger.error("Malformed App Token '%s'", appToken);
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean checkEnvironment(String environment) {
+ ILogger logger = AdjustFactory.getLogger();
+ if (environment == null) {
+ logger.error("Missing environment");
+ return false;
+ }
+
+ if (environment == AdjustConfig.ENVIRONMENT_SANDBOX) {
+ logger.Assert("SANDBOX: Adjust is running in Sandbox mode. " +
+ "Use this setting for testing. " +
+ "Don't forget to set the environment to `production` before publishing!");
+ return true;
+ }
+ if (environment == AdjustConfig.ENVIRONMENT_PRODUCTION) {
+ logger.Assert(
+ "PRODUCTION: Adjust is running in Production mode. " +
+ "Use this setting only for the build that you want to publish. " +
+ "Set the environment to `sandbox` if you want to test your app!");
+ return true;
+ }
+
+ logger.error("Unknown environment '%s'", environment);
+ return false;
+ }
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustEvent.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustEvent.java
new file mode 100644
index 000000000..f03718183
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustEvent.java
@@ -0,0 +1,112 @@
+package com.adjust.sdk;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by pfms on 05/11/14.
+ */
+public class AdjustEvent {
+ String eventToken;
+ Double revenue;
+ String currency;
+ Map<String, String> callbackParameters;
+ Map<String, String> partnerParameters;
+
+ private static ILogger logger = AdjustFactory.getLogger();
+
+ public AdjustEvent(String eventToken) {
+ if (!checkEventToken(eventToken, logger)) return;
+
+ this.eventToken = eventToken;
+ }
+
+ public void setRevenue(double revenue, String currency) {
+ if (!checkRevenue(revenue, currency)) return;
+
+ this.revenue = revenue;
+ this.currency = currency;
+ }
+
+ public void addCallbackParameter(String key, String value) {
+ if (!isValidParameter(key, "key", "Callback")) return;
+ if (!isValidParameter(value, "value", "Callback")) return;
+
+ if (callbackParameters == null) {
+ callbackParameters = new HashMap<String, String>();
+ }
+
+ String previousValue = callbackParameters.put(key, value);
+
+ if (previousValue != null) {
+ logger.warn("key %s was overwritten", key);
+ }
+ }
+
+ public void addPartnerParameter(String key, String value) {
+ if (!isValidParameter(key, "key", "Partner")) return;
+ if (!isValidParameter(value, "value", "Partner")) return;
+
+ if (partnerParameters == null) {
+ partnerParameters = new HashMap<String, String>();
+ }
+
+ String previousValue = partnerParameters.put(key, value);
+
+ if (previousValue != null) {
+ logger.warn("key %s was overwritten", key);
+ }
+ }
+
+ public boolean isValid() {
+ return eventToken != null;
+ }
+
+ private static boolean checkEventToken(String eventToken, ILogger logger) {
+ if (eventToken == null) {
+ logger.error("Missing Event Token");
+ return false;
+ }
+ if (eventToken.length() != 6) {
+ logger.error("Malformed Event Token '%s'", eventToken);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean checkRevenue(Double revenue, String currency) {
+ if (revenue != null) {
+ if (revenue < 0.0) {
+ logger.error("Invalid amount %.4f", revenue);
+ return false;
+ }
+
+ if (currency == null) {
+ logger.error("Currency must be set with revenue");
+ return false;
+ }
+ if (currency == "") {
+ logger.error("Currency is empty");
+ return false;
+ }
+
+ } else if (currency != null) {
+ logger.error("Revenue must be set with currency");
+ return false;
+ }
+ return true;
+ }
+
+ private boolean isValidParameter(String attribute, String attributeType, String parameterName) {
+ if (attribute == null) {
+ logger.error("%s parameter %s is missing", parameterName, attributeType);
+ return false;
+ }
+ if (attribute == "") {
+ logger.error("%s parameter %s is empty", parameterName, attributeType);
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java
new file mode 100644
index 000000000..802af6416
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java
@@ -0,0 +1,141 @@
+package com.adjust.sdk;
+
+import android.content.Context;
+
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+public class AdjustFactory {
+ private static IPackageHandler packageHandler = null;
+ private static IRequestHandler requestHandler = null;
+ private static IAttributionHandler attributionHandler = null;
+ private static IActivityHandler activityHandler = null;
+ private static ILogger logger = null;
+ private static HttpClient httpClient = null;
+
+ private static long timerInterval = -1;
+ private static long timerStart = -1;
+ private static long sessionInterval = -1;
+ private static long subsessionInterval = -1;
+
+ public static IPackageHandler getPackageHandler(ActivityHandler activityHandler,
+ Context context,
+ boolean startPaused) {
+ if (packageHandler == null) {
+ return new PackageHandler(activityHandler, context, startPaused);
+ }
+ packageHandler.init(activityHandler, context, startPaused);
+ return packageHandler;
+ }
+
+ public static IRequestHandler getRequestHandler(IPackageHandler packageHandler) {
+ if (requestHandler == null) {
+ return new RequestHandler(packageHandler);
+ }
+ requestHandler.init(packageHandler);
+ return requestHandler;
+ }
+
+ public static ILogger getLogger() {
+ if (logger == null) {
+ // Logger needs to be "static" to retain the configuration throughout the app
+ logger = new Logger();
+ }
+ return logger;
+ }
+
+ public static HttpClient getHttpClient(HttpParams params) {
+ if (httpClient == null) {
+ return new DefaultHttpClient(params);
+ }
+ return httpClient;
+ }
+
+ public static long getTimerInterval() {
+ if (timerInterval == -1) {
+ return Constants.ONE_MINUTE;
+ }
+ return timerInterval;
+ }
+
+ public static long getTimerStart() {
+ if (timerStart == -1) {
+ return 0;
+ }
+ return timerStart;
+ }
+
+ public static long getSessionInterval() {
+ if (sessionInterval == -1) {
+ return Constants.THIRTY_MINUTES;
+ }
+ return sessionInterval;
+ }
+
+ public static long getSubsessionInterval() {
+ if (subsessionInterval == -1) {
+ return Constants.ONE_SECOND;
+ }
+ return subsessionInterval;
+ }
+
+ public static IActivityHandler getActivityHandler(AdjustConfig config) {
+ if (activityHandler == null) {
+ return ActivityHandler.getInstance(config);
+ }
+ activityHandler.init(config);
+ return activityHandler;
+ }
+
+ public static IAttributionHandler getAttributionHandler(IActivityHandler activityHandler,
+ ActivityPackage attributionPackage,
+ boolean startPaused) {
+ if (attributionHandler == null) {
+ return new AttributionHandler(activityHandler, attributionPackage, startPaused);
+ }
+ attributionHandler.init(activityHandler, attributionPackage, startPaused);
+ return attributionHandler;
+ }
+
+ public static void setPackageHandler(IPackageHandler packageHandler) {
+ AdjustFactory.packageHandler = packageHandler;
+ }
+
+ public static void setRequestHandler(IRequestHandler requestHandler) {
+ AdjustFactory.requestHandler = requestHandler;
+ }
+
+ public static void setLogger(ILogger logger) {
+ AdjustFactory.logger = logger;
+ }
+
+ public static void setHttpClient(HttpClient httpClient) {
+ AdjustFactory.httpClient = httpClient;
+ }
+
+ public static void setTimerInterval(long timerInterval) {
+ AdjustFactory.timerInterval = timerInterval;
+ }
+
+ public static void setTimerStart(long timerStart) {
+ AdjustFactory.timerStart = timerStart;
+ }
+
+ public static void setSessionInterval(long sessionInterval) {
+ AdjustFactory.sessionInterval = sessionInterval;
+ }
+
+ public static void setSubsessionInterval(long subsessionInterval) {
+ AdjustFactory.subsessionInterval = subsessionInterval;
+ }
+
+ public static void setActivityHandler(IActivityHandler activityHandler) {
+ AdjustFactory.activityHandler = activityHandler;
+ }
+
+ public static void setAttributionHandler(IAttributionHandler attributionHandler) {
+ AdjustFactory.attributionHandler = attributionHandler;
+ }
+
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustInstance.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustInstance.java
new file mode 100644
index 000000000..158fb7ca1
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustInstance.java
@@ -0,0 +1,86 @@
+package com.adjust.sdk;
+
+import android.net.Uri;
+
+/**
+ * Created by pfms on 04/12/14.
+ */
+public class AdjustInstance {
+
+ private String referrer;
+ private long referrerClickTime;
+ private ActivityHandler activityHandler;
+
+ private static ILogger getLogger() {
+ return AdjustFactory.getLogger();
+ }
+
+ public void onCreate(AdjustConfig adjustConfig) {
+ if (activityHandler != null) {
+ getLogger().error("Adjust already initialized");
+ return;
+ }
+
+ adjustConfig.referrer = this.referrer;
+ adjustConfig.referrerClickTime = this.referrerClickTime;
+
+ activityHandler = ActivityHandler.getInstance(adjustConfig);
+ }
+
+ public void trackEvent(AdjustEvent event) {
+ if (!checkActivityHandler()) return;
+ activityHandler.trackEvent(event);
+ }
+
+ public void onResume() {
+ if (!checkActivityHandler()) return;
+ activityHandler.trackSubsessionStart();
+ }
+
+ public void onPause() {
+ if (!checkActivityHandler()) return;
+ activityHandler.trackSubsessionEnd();
+ }
+
+ public void setEnabled(boolean enabled) {
+ if (!checkActivityHandler()) return;
+ activityHandler.setEnabled(enabled);
+ }
+
+ public boolean isEnabled() {
+ if (!checkActivityHandler()) return false;
+ return activityHandler.isEnabled();
+ }
+
+ public void appWillOpenUrl(Uri url) {
+ if (!checkActivityHandler()) return;
+ long clickTime = System.currentTimeMillis();
+ activityHandler.readOpenUrl(url, clickTime);
+ }
+
+ public void sendReferrer(String referrer) {
+ long clickTime = System.currentTimeMillis();
+ // sendReferrer might be triggered before Adjust
+ if (activityHandler == null) {
+ // save it to inject in the config before launch
+ this.referrer = referrer;
+ this.referrerClickTime = clickTime;
+ } else {
+ activityHandler.sendReferrer(referrer, clickTime);
+ }
+ }
+
+ public void setOfflineMode(boolean enabled) {
+ if (!checkActivityHandler()) return;
+ activityHandler.setOfflineMode(enabled);
+ }
+
+ private boolean checkActivityHandler() {
+ if (activityHandler == null) {
+ getLogger().error("Please initialize Adjust by calling 'onCreate' before");
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustReferrerReceiver.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustReferrerReceiver.java
new file mode 100644
index 000000000..cfeecd8d0
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustReferrerReceiver.java
@@ -0,0 +1,35 @@
+package com.adjust.sdk;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+
+import static com.adjust.sdk.Constants.ENCODING;
+import static com.adjust.sdk.Constants.MALFORMED;
+import static com.adjust.sdk.Constants.REFERRER;
+
+// support multiple BroadcastReceivers for the INSTALL_REFERRER:
+// http://blog.appington.com/2012/08/01/giving-credit-for-android-app-installs
+
+public class AdjustReferrerReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String rawReferrer = intent.getStringExtra(REFERRER);
+ if (null == rawReferrer) {
+ return;
+ }
+
+ String referrer;
+ try {
+ referrer = URLDecoder.decode(rawReferrer, ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ referrer = MALFORMED;
+ }
+
+ AdjustInstance adjust = Adjust.getDefaultInstance();
+ adjust.sendReferrer(referrer);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java b/mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java
new file mode 100644
index 000000000..0d550a83a
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java
@@ -0,0 +1,155 @@
+package com.adjust.sdk;
+
+import android.net.Uri;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import org.json.JSONObject;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Created by pfms on 07/11/14.
+ */
+public class AttributionHandler implements IAttributionHandler {
+ private ScheduledExecutorService scheduler;
+ private IActivityHandler activityHandler;
+ private ILogger logger;
+ private ActivityPackage attributionPackage;
+ private ScheduledFuture waitingTask;
+ private HttpClient httpClient;
+ private boolean paused;
+
+ public AttributionHandler(IActivityHandler activityHandler,
+ ActivityPackage attributionPackage,
+ boolean startPaused) {
+ scheduler = Executors.newSingleThreadScheduledExecutor();
+ logger = AdjustFactory.getLogger();
+ httpClient = Util.getHttpClient();
+ init(activityHandler, attributionPackage, startPaused);
+ }
+
+ @Override
+ public void init(IActivityHandler activityHandler,
+ ActivityPackage attributionPackage,
+ boolean startPaused) {
+ this.activityHandler = activityHandler;
+ this.attributionPackage = attributionPackage;
+ this.paused = startPaused;
+ }
+
+ @Override
+ public void getAttribution() {
+ getAttribution(0);
+ }
+
+ @Override
+ public void checkAttribution(final JSONObject jsonResponse) {
+ scheduler.submit(new Runnable() {
+ @Override
+ public void run() {
+ checkAttributionInternal(jsonResponse);
+ }
+ });
+ }
+
+ @Override
+ public void pauseSending() {
+ paused = true;
+ }
+
+ @Override
+ public void resumeSending() {
+ paused = false;
+ }
+
+ private void getAttribution(int delayInMilliseconds) {
+ if (waitingTask != null) {
+ waitingTask.cancel(false);
+ }
+
+ if (delayInMilliseconds != 0) {
+ logger.debug("Waiting to query attribution in %d milliseconds", delayInMilliseconds);
+ }
+
+ waitingTask = scheduler.schedule(new Runnable() {
+ @Override
+ public void run() {
+ getAttributionInternal();
+ }
+ }, delayInMilliseconds, TimeUnit.MILLISECONDS);
+ }
+
+ private void checkAttributionInternal(JSONObject jsonResponse) {
+ if (jsonResponse == null) return;
+
+ JSONObject attributionJson = jsonResponse.optJSONObject("attribution");
+ AdjustAttribution attribution = AdjustAttribution.fromJson(attributionJson);
+
+ int timerMilliseconds = jsonResponse.optInt("ask_in", -1);
+
+ // without ask_in attribute
+ if (timerMilliseconds < 0) {
+ activityHandler.tryUpdateAttribution(attribution);
+
+ activityHandler.setAskingAttribution(false);
+
+ return;
+ }
+
+ activityHandler.setAskingAttribution(true);
+
+ getAttribution(timerMilliseconds);
+ }
+
+ private void getAttributionInternal() {
+ if (paused) {
+ logger.debug("Attribution Handler is paused");
+ return;
+ }
+ logger.verbose("%s", attributionPackage.getExtendedString());
+ HttpResponse httpResponse = null;
+ try {
+ HttpGet request = getRequest(attributionPackage);
+ httpResponse = httpClient.execute(request);
+ } catch (Exception e) {
+ logger.error("Failed to get attribution (%s)", e.getMessage());
+ return;
+ }
+
+ JSONObject jsonResponse = Util.parseJsonResponse(httpResponse, logger);
+
+ checkAttributionInternal(jsonResponse);
+ }
+
+ private Uri buildUri(ActivityPackage attributionPackage) {
+ Uri.Builder uriBuilder = new Uri.Builder();
+
+ uriBuilder.scheme(Constants.SCHEME);
+ uriBuilder.authority(Constants.AUTHORITY);
+ uriBuilder.appendPath(attributionPackage.getPath());
+
+ for (Map.Entry<String, String> entry : attributionPackage.getParameters().entrySet()) {
+ uriBuilder.appendQueryParameter(entry.getKey(), entry.getValue());
+ }
+
+ return uriBuilder.build();
+ }
+
+ private HttpGet getRequest(ActivityPackage attributionPackage) throws URISyntaxException {
+ HttpGet request = new HttpGet();
+ Uri uri = buildUri(attributionPackage);
+ request.setURI(new URI(uri.toString()));
+
+ request.addHeader("Client-SDK", attributionPackage.getClientSdk());
+
+ return request;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Constants.java b/mobile/android/thirdparty/com/adjust/sdk/Constants.java
new file mode 100644
index 000000000..7a97cb2f4
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Constants.java
@@ -0,0 +1,53 @@
+//
+// Constants.java
+// Adjust
+//
+// Created by keyboardsurfer on 2013-11-08.
+// Copyright (c) 2012-2014 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author keyboardsurfer
+ * @since 8.11.13
+ */
+public interface Constants {
+ int ONE_SECOND = 1000;
+ int ONE_MINUTE = 60 * ONE_SECOND;
+ int THIRTY_MINUTES = 30 * ONE_MINUTE;
+
+ int CONNECTION_TIMEOUT = Constants.ONE_MINUTE;
+ int SOCKET_TIMEOUT = Constants.ONE_MINUTE;
+
+ String BASE_URL = "https://app.adjust.com";
+ String SCHEME = "https";
+ String AUTHORITY = "app.adjust.com";
+ String CLIENT_SDK = "android4.0.0";
+ String LOGTAG = "Adjust";
+
+ String ACTIVITY_STATE_FILENAME = "AdjustIoActivityState";
+ String ATTRIBUTION_FILENAME = "AdjustAttribution";
+
+ String MALFORMED = "malformed";
+ String SMALL = "small";
+ String NORMAL = "normal";
+ String LONG = "long";
+ String LARGE = "large";
+ String XLARGE = "xlarge";
+ String LOW = "low";
+ String MEDIUM = "medium";
+ String HIGH = "high";
+ String REFERRER = "referrer";
+
+ String ENCODING = "UTF-8";
+ String MD5 = "MD5";
+ String SHA1 = "SHA-1";
+
+ // List of known plugins, possibly not active
+ List<String> PLUGINS = Arrays.asList();
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/DeviceInfo.java b/mobile/android/thirdparty/com/adjust/sdk/DeviceInfo.java
new file mode 100644
index 000000000..5cccb77f4
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/DeviceInfo.java
@@ -0,0 +1,290 @@
+package com.adjust.sdk;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.util.DisplayMetrics;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.util.Locale;
+import java.util.Map;
+
+import static com.adjust.sdk.Constants.ENCODING;
+import static com.adjust.sdk.Constants.HIGH;
+import static com.adjust.sdk.Constants.LARGE;
+import static com.adjust.sdk.Constants.LONG;
+import static com.adjust.sdk.Constants.LOW;
+import static com.adjust.sdk.Constants.MD5;
+import static com.adjust.sdk.Constants.MEDIUM;
+import static com.adjust.sdk.Constants.NORMAL;
+import static com.adjust.sdk.Constants.SHA1;
+import static com.adjust.sdk.Constants.SMALL;
+import static com.adjust.sdk.Constants.XLARGE;
+
+/**
+ * Created by pfms on 06/11/14.
+ */
+class DeviceInfo {
+ String macSha1;
+ String macShortMd5;
+ String androidId;
+ String fbAttributionId;
+ String clientSdk;
+ String packageName;
+ String appVersion;
+ String deviceType;
+ String deviceName;
+ String deviceManufacturer;
+ String osName;
+ String osVersion;
+ String language;
+ String country;
+ String screenSize;
+ String screenFormat;
+ String screenDensity;
+ String displayWidth;
+ String displayHeight;
+ Map<String, String> pluginKeys;
+
+ DeviceInfo(Context context, String sdkPrefix) {
+ Resources resources = context.getResources();
+ DisplayMetrics displayMetrics = resources.getDisplayMetrics();
+ Configuration configuration = resources.getConfiguration();
+ Locale locale = configuration.locale;
+ int screenLayout = configuration.screenLayout;
+ boolean isGooglePlayServicesAvailable = Reflection.isGooglePlayServicesAvailable(context);
+ String macAddress = getMacAddress(context, isGooglePlayServicesAvailable);
+
+ packageName = getPackageName(context);
+ appVersion = getAppVersion(context);
+ deviceType = getDeviceType(screenLayout);
+ deviceName = getDeviceName();
+ deviceManufacturer = getDeviceManufacturer();
+ osName = getOsName();
+ osVersion = getOsVersion();
+ language = getLanguage(locale);
+ country = getCountry(locale);
+ screenSize = getScreenSize(screenLayout);
+ screenFormat = getScreenFormat(screenLayout);
+ screenDensity = getScreenDensity(displayMetrics);
+ displayWidth = getDisplayWidth(displayMetrics);
+ displayHeight = getDisplayHeight(displayMetrics);
+ clientSdk = getClientSdk(sdkPrefix);
+ androidId = getAndroidId(context, isGooglePlayServicesAvailable);
+ fbAttributionId = getFacebookAttributionId(context);
+ pluginKeys = Reflection.getPluginKeys(context);
+ macSha1 = getMacSha1(macAddress);
+ macShortMd5 = getMacShortMd5(macAddress);
+ }
+
+ private String getMacAddress(Context context, boolean isGooglePlayServicesAvailable) {
+ if (!isGooglePlayServicesAvailable) {
+ if (!!Util.checkPermission(context, android.Manifest.permission.ACCESS_WIFI_STATE)) {
+ AdjustFactory.getLogger().warn("Missing permission: ACCESS_WIFI_STATE");
+ }
+ return Reflection.getMacAddress(context);
+ } else {
+ return null;
+ }
+ }
+
+ private String getPackageName(Context context) {
+ return context.getPackageName();
+ }
+
+ private String getAppVersion(Context context) {
+ try {
+ PackageManager packageManager = context.getPackageManager();
+ String name = context.getPackageName();
+ PackageInfo info = packageManager.getPackageInfo(name, 0);
+ return info.versionName;
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ private String getDeviceType(int screenLayout) {
+ int screenSize = screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
+
+ switch (screenSize) {
+ case Configuration.SCREENLAYOUT_SIZE_SMALL:
+ case Configuration.SCREENLAYOUT_SIZE_NORMAL:
+ return "phone";
+ case Configuration.SCREENLAYOUT_SIZE_LARGE:
+ case 4:
+ return "tablet";
+ default:
+ return null;
+ }
+ }
+
+ private String getDeviceName() {
+ return Build.MODEL;
+ }
+
+ private String getDeviceManufacturer() {
+ return Build.MANUFACTURER;
+ }
+
+ private String getOsName() {
+ return "android";
+ }
+
+ private String getOsVersion() {
+ return osVersion = "" + Build.VERSION.SDK_INT;
+ }
+
+ private String getLanguage(Locale locale) {
+ return locale.getLanguage();
+ }
+
+ private String getCountry(Locale locale) {
+ return locale.getCountry();
+ }
+
+ private String getScreenSize(int screenLayout) {
+ int screenSize = screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
+
+ switch (screenSize) {
+ case Configuration.SCREENLAYOUT_SIZE_SMALL:
+ return SMALL;
+ case Configuration.SCREENLAYOUT_SIZE_NORMAL:
+ return NORMAL;
+ case Configuration.SCREENLAYOUT_SIZE_LARGE:
+ return LARGE;
+ case 4:
+ return XLARGE;
+ default:
+ return null;
+ }
+ }
+
+ private String getScreenFormat(int screenLayout) {
+ int screenFormat = screenLayout & Configuration.SCREENLAYOUT_LONG_MASK;
+
+ switch (screenFormat) {
+ case Configuration.SCREENLAYOUT_LONG_YES:
+ return LONG;
+ case Configuration.SCREENLAYOUT_LONG_NO:
+ return NORMAL;
+ default:
+ return null;
+ }
+ }
+
+ private String getScreenDensity(DisplayMetrics displayMetrics) {
+ int density = displayMetrics.densityDpi;
+ int low = (DisplayMetrics.DENSITY_MEDIUM + DisplayMetrics.DENSITY_LOW) / 2;
+ int high = (DisplayMetrics.DENSITY_MEDIUM + DisplayMetrics.DENSITY_HIGH) / 2;
+
+ if (0 == density) {
+ return null;
+ } else if (density < low) {
+ return LOW;
+ } else if (density > high) {
+ return HIGH;
+ }
+ return MEDIUM;
+ }
+
+ private String getDisplayWidth(DisplayMetrics displayMetrics) {
+ return String.valueOf(displayMetrics.widthPixels);
+ }
+
+ private String getDisplayHeight(DisplayMetrics displayMetrics) {
+ return String.valueOf(displayMetrics.heightPixels);
+ }
+
+ private String getClientSdk(String sdkPrefix) {
+ if (sdkPrefix == null) {
+ return Constants.CLIENT_SDK;
+ } else {
+ return String.format("%s@%s", sdkPrefix, Constants.CLIENT_SDK);
+ }
+ }
+
+ private String getMacSha1(String macAddress) {
+ if (macAddress == null) {
+ return null;
+ }
+ String macSha1 = sha1(macAddress);
+
+ return macSha1;
+ }
+
+ private String getMacShortMd5(String macAddress) {
+ if (macAddress == null) {
+ return null;
+ }
+ String macShort = macAddress.replaceAll(":", "");
+ String macShortMd5 = md5(macShort);
+
+ return macShortMd5;
+ }
+
+ private String getAndroidId(Context context, boolean isGooglePlayServicesAvailable) {
+ if (!isGooglePlayServicesAvailable) {
+ return Reflection.getAndroidId(context);
+ } else {
+ return null;
+ }
+ }
+
+ private String sha1(final String text) {
+ return hash(text, SHA1);
+ }
+
+ private String md5(final String text) {
+ return hash(text, MD5);
+ }
+
+ private String hash(final String text, final String method) {
+ String hashString = null;
+ try {
+ final byte[] bytes = text.getBytes(ENCODING);
+ final MessageDigest mesd = MessageDigest.getInstance(method);
+ mesd.update(bytes, 0, bytes.length);
+ final byte[] hash = mesd.digest();
+ hashString = convertToHex(hash);
+ } catch (Exception e) {
+ }
+ return hashString;
+ }
+
+ private static String convertToHex(final byte[] bytes) {
+ final BigInteger bigInt = new BigInteger(1, bytes);
+ final String formatString = "%0" + (bytes.length << 1) + "x";
+ return String.format(formatString, bigInt);
+ }
+
+ private String getFacebookAttributionId(final Context context) {
+ try {
+ final ContentResolver contentResolver = context.getContentResolver();
+ final Uri uri = Uri.parse("content://com.facebook.katana.provider.AttributionIdProvider");
+ final String columnName = "aid";
+ final String[] projection = {columnName};
+ final Cursor cursor = contentResolver.query(uri, projection, null, null, null);
+
+ if (null == cursor) {
+ return null;
+ }
+ if (!cursor.moveToFirst()) {
+ cursor.close();
+ return null;
+ }
+
+ final String attributionId = cursor.getString(cursor.getColumnIndex(columnName));
+ cursor.close();
+ return attributionId;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/IActivityHandler.java b/mobile/android/thirdparty/com/adjust/sdk/IActivityHandler.java
new file mode 100644
index 000000000..10b92205d
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/IActivityHandler.java
@@ -0,0 +1,36 @@
+package com.adjust.sdk;
+
+import android.net.Uri;
+
+import org.json.JSONObject;
+
+/**
+ * Created by pfms on 15/12/14.
+ */
+public interface IActivityHandler {
+ public void init(AdjustConfig config);
+
+ public void trackSubsessionStart();
+
+ public void trackSubsessionEnd();
+
+ public void trackEvent(AdjustEvent event);
+
+ public void finishedTrackingActivity(JSONObject jsonResponse);
+
+ public void setEnabled(boolean enabled);
+
+ public boolean isEnabled();
+
+ public void readOpenUrl(Uri url, long clickTime);
+
+ public boolean tryUpdateAttribution(AdjustAttribution attribution);
+
+ public void sendReferrer(String referrer, long clickTime);
+
+ public void setOfflineMode(boolean enabled);
+
+ public void setAskingAttribution(boolean askingAttribution);
+
+ public ActivityPackage getAttributionPackage();
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/IAttributionHandler.java b/mobile/android/thirdparty/com/adjust/sdk/IAttributionHandler.java
new file mode 100644
index 000000000..d4e701f75
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/IAttributionHandler.java
@@ -0,0 +1,20 @@
+package com.adjust.sdk;
+
+import org.json.JSONObject;
+
+/**
+ * Created by pfms on 15/12/14.
+ */
+public interface IAttributionHandler {
+ public void init(IActivityHandler activityHandler,
+ ActivityPackage attributionPackage,
+ boolean startPaused);
+
+ public void getAttribution();
+
+ public void checkAttribution(JSONObject jsonResponse);
+
+ public void pauseSending();
+
+ public void resumeSending();
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ILogger.java b/mobile/android/thirdparty/com/adjust/sdk/ILogger.java
new file mode 100644
index 000000000..28f92af4b
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ILogger.java
@@ -0,0 +1,20 @@
+package com.adjust.sdk;
+
+public interface ILogger {
+ public void setLogLevel(LogLevel logLevel);
+
+ public void setLogLevelString(String logLevelString);
+
+ public void verbose(String message, Object... parameters);
+
+ public void debug(String message, Object... parameters);
+
+ public void info(String message, Object... parameters);
+
+ public void warn(String message, Object... parameters);
+
+ public void error(String message, Object... parameters);
+
+ public void Assert(String message, Object... parameters);
+
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/IPackageHandler.java b/mobile/android/thirdparty/com/adjust/sdk/IPackageHandler.java
new file mode 100644
index 000000000..99c300364
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/IPackageHandler.java
@@ -0,0 +1,27 @@
+package com.adjust.sdk;
+
+import android.content.Context;
+
+import org.json.JSONObject;
+
+public interface IPackageHandler {
+ public void init(IActivityHandler activityHandler, Context context, boolean startPaused);
+
+ public void addPackage(ActivityPackage pack);
+
+ public void sendFirstPackage();
+
+ public void sendNextPackage();
+
+ public void closeFirstPackage();
+
+ public void pauseSending();
+
+ public void resumeSending();
+
+ public String getFailureMessage();
+
+ public void finishedTrackingActivity(JSONObject jsonResponse);
+
+ public void sendClickPackage(ActivityPackage clickPackage);
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/IRequestHandler.java b/mobile/android/thirdparty/com/adjust/sdk/IRequestHandler.java
new file mode 100644
index 000000000..5b18e2ee9
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/IRequestHandler.java
@@ -0,0 +1,9 @@
+package com.adjust.sdk;
+
+public interface IRequestHandler {
+ public void init(IPackageHandler packageHandler);
+
+ public void sendPackage(ActivityPackage pack);
+
+ public void sendClickPackage(ActivityPackage clickPackage);
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/LICENSE b/mobile/android/thirdparty/com/adjust/sdk/LICENSE
new file mode 100644
index 000000000..25e1d5eb5
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2012-2014 adjust GmbH,
+http://www.adjust.com
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/mobile/android/thirdparty/com/adjust/sdk/LogLevel.java b/mobile/android/thirdparty/com/adjust/sdk/LogLevel.java
new file mode 100644
index 000000000..5c0b410c2
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/LogLevel.java
@@ -0,0 +1,19 @@
+package com.adjust.sdk;
+
+import android.util.Log;
+
+/**
+ * Created by pfms on 11/03/15.
+ */
+public enum LogLevel {
+ VERBOSE(Log.VERBOSE), DEBUG(Log.DEBUG), INFO(Log.INFO), WARN(Log.WARN), ERROR(Log.ERROR), ASSERT(Log.ASSERT);
+ final int androidLogLevel;
+
+ LogLevel(final int androidLogLevel) {
+ this.androidLogLevel = androidLogLevel;
+ }
+
+ public int getAndroidLogLevel() {
+ return androidLogLevel;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Logger.java b/mobile/android/thirdparty/com/adjust/sdk/Logger.java
new file mode 100644
index 000000000..86a644d4a
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Logger.java
@@ -0,0 +1,107 @@
+//
+// Logger.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-04-18.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+import static com.adjust.sdk.Constants.LOGTAG;
+
+public class Logger implements ILogger {
+
+ private LogLevel logLevel;
+ private static String formatErrorMessage = "Error formating log message: %s, with params: %s";
+
+ public Logger() {
+ setLogLevel(LogLevel.INFO);
+ }
+
+ @Override
+ public void setLogLevel(LogLevel logLevel) {
+ this.logLevel = logLevel;
+ }
+
+ @Override
+ public void setLogLevelString(String logLevelString) {
+ if (null != logLevelString) {
+ try {
+ setLogLevel(LogLevel.valueOf(logLevelString.toUpperCase(Locale.US)));
+ } catch (IllegalArgumentException iae) {
+ error("Malformed logLevel '%s', falling back to 'info'", logLevelString);
+ }
+ }
+ }
+
+ @Override
+ public void verbose(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.VERBOSE) {
+ try {
+ Log.v(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void debug(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.DEBUG) {
+ try {
+ Log.d(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void info(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.INFO) {
+ try {
+ Log.i(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void warn(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.WARN) {
+ try {
+ Log.w(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void error(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.ERROR) {
+ try {
+ Log.e(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void Assert(String message, Object... parameters) {
+ try {
+ Log.println(Log.ASSERT, LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/OnAttributionChangedListener.java b/mobile/android/thirdparty/com/adjust/sdk/OnAttributionChangedListener.java
new file mode 100644
index 000000000..137d50d4d
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/OnAttributionChangedListener.java
@@ -0,0 +1,5 @@
+package com.adjust.sdk;
+
+public interface OnAttributionChangedListener {
+ public void onAttributionChanged(AdjustAttribution attribution);
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/PackageBuilder.java b/mobile/android/thirdparty/com/adjust/sdk/PackageBuilder.java
new file mode 100644
index 000000000..3a43045fd
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/PackageBuilder.java
@@ -0,0 +1,291 @@
+//
+// PackageBuilder.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+class PackageBuilder {
+ private AdjustConfig adjustConfig;
+ private DeviceInfo deviceInfo;
+ private ActivityState activityState;
+ private long createdAt;
+
+ // reattributions
+ Map<String, String> extraParameters;
+ AdjustAttribution attribution;
+ String reftag;
+
+ private static ILogger logger = AdjustFactory.getLogger();
+
+ public PackageBuilder(AdjustConfig adjustConfig,
+ DeviceInfo deviceInfo,
+ ActivityState activityState,
+ long createdAt) {
+ this.adjustConfig = adjustConfig;
+ this.deviceInfo = deviceInfo;
+ this.activityState = activityState.clone();
+ this.createdAt = createdAt;
+ }
+
+ public ActivityPackage buildSessionPackage() {
+ Map<String, String> parameters = getDefaultParameters();
+ addDuration(parameters, "last_interval", activityState.lastInterval);
+ addString(parameters, "default_tracker", adjustConfig.defaultTracker);
+
+ ActivityPackage sessionPackage = getDefaultActivityPackage();
+ sessionPackage.setPath("/session");
+ sessionPackage.setActivityKind(ActivityKind.SESSION);
+ sessionPackage.setSuffix("");
+ sessionPackage.setParameters(parameters);
+
+ return sessionPackage;
+ }
+
+ public ActivityPackage buildEventPackage(AdjustEvent event) {
+ Map<String, String> parameters = getDefaultParameters();
+ addInt(parameters, "event_count", activityState.eventCount);
+ addString(parameters, "event_token", event.eventToken);
+ addDouble(parameters, "revenue", event.revenue);
+ addString(parameters, "currency", event.currency);
+ addMapJson(parameters, "callback_params", event.callbackParameters);
+ addMapJson(parameters, "partner_params", event.partnerParameters);
+
+ ActivityPackage eventPackage = getDefaultActivityPackage();
+ eventPackage.setPath("/event");
+ eventPackage.setActivityKind(ActivityKind.EVENT);
+ eventPackage.setSuffix(getEventSuffix(event));
+ eventPackage.setParameters(parameters);
+
+ return eventPackage;
+ }
+
+ public ActivityPackage buildClickPackage(String source, long clickTime) {
+ Map<String, String> parameters = getDefaultParameters();
+
+ addString(parameters, "source", source);
+ addDate(parameters, "click_time", clickTime);
+ addString(parameters, "reftag", reftag);
+ addMapJson(parameters, "params", extraParameters);
+ injectAttribution(parameters);
+
+ ActivityPackage clickPackage = getDefaultActivityPackage();
+ clickPackage.setPath("/sdk_click");
+ clickPackage.setActivityKind(ActivityKind.CLICK);
+ clickPackage.setSuffix("");
+ clickPackage.setParameters(parameters);
+
+ return clickPackage;
+ }
+
+ public ActivityPackage buildAttributionPackage() {
+ Map<String, String> parameters = getIdsParameters();
+
+ ActivityPackage attributionPackage = getDefaultActivityPackage();
+ attributionPackage.setPath("attribution"); // does not contain '/' because of Uri.Builder.appendPath
+ attributionPackage.setActivityKind(ActivityKind.ATTRIBUTION);
+ attributionPackage.setSuffix("");
+ attributionPackage.setParameters(parameters);
+
+ return attributionPackage;
+ }
+
+ private ActivityPackage getDefaultActivityPackage() {
+ ActivityPackage activityPackage = new ActivityPackage();
+ activityPackage.setClientSdk(deviceInfo.clientSdk);
+ return activityPackage;
+ }
+
+ private Map<String, String> getDefaultParameters() {
+ Map<String, String> parameters = new HashMap<String, String>();
+
+ injectDeviceInfo(parameters);
+ injectConfig(parameters);
+ injectActivityState(parameters);
+ addDate(parameters, "created_at", createdAt);
+
+ // general
+ checkDeviceIds(parameters);
+
+ return parameters;
+ }
+
+ private Map<String, String> getIdsParameters() {
+ Map<String, String> parameters = new HashMap<String, String>();
+
+ injectDeviceInfoIds(parameters);
+ injectConfig(parameters);
+ injectActivityStateIds(parameters);
+
+ checkDeviceIds(parameters);
+
+ return parameters;
+ }
+
+ private void injectDeviceInfo(Map<String, String> parameters) {
+ injectDeviceInfoIds(parameters);
+ addString(parameters, "fb_id", deviceInfo.fbAttributionId);
+ addString(parameters, "package_name", deviceInfo.packageName);
+ addString(parameters, "app_version", deviceInfo.appVersion);
+ addString(parameters, "device_type", deviceInfo.deviceType);
+ addString(parameters, "device_name", deviceInfo.deviceName);
+ addString(parameters, "device_manufacturer", deviceInfo.deviceManufacturer);
+ addString(parameters, "os_name", deviceInfo.osName);
+ addString(parameters, "os_version", deviceInfo.osVersion);
+ addString(parameters, "language", deviceInfo.language);
+ addString(parameters, "country", deviceInfo.country);
+ addString(parameters, "screen_size", deviceInfo.screenSize);
+ addString(parameters, "screen_format", deviceInfo.screenFormat);
+ addString(parameters, "screen_density", deviceInfo.screenDensity);
+ addString(parameters, "display_width", deviceInfo.displayWidth);
+ addString(parameters, "display_height", deviceInfo.displayHeight);
+ fillPluginKeys(parameters);
+ }
+
+ private void injectDeviceInfoIds(Map<String, String> parameters) {
+ addString(parameters, "mac_sha1", deviceInfo.macSha1);
+ addString(parameters, "mac_md5", deviceInfo.macShortMd5);
+ addString(parameters, "android_id", deviceInfo.androidId);
+ }
+
+ private void injectConfig(Map<String, String> parameters) {
+ addString(parameters, "app_token", adjustConfig.appToken);
+ addString(parameters, "environment", adjustConfig.environment);
+ addBoolean(parameters, "device_known", adjustConfig.knownDevice);
+ addBoolean(parameters, "needs_attribution_data", adjustConfig.hasListener());
+
+ String playAdId = Util.getPlayAdId(adjustConfig.context);
+ addString(parameters, "gps_adid", playAdId);
+ Boolean isTrackingEnabled = Util.isPlayTrackingEnabled(adjustConfig.context);
+ addBoolean(parameters, "tracking_enabled", isTrackingEnabled);
+ }
+
+ private void injectActivityState(Map<String, String> parameters) {
+ injectActivityStateIds(parameters);
+ addInt(parameters, "session_count", activityState.sessionCount);
+ addInt(parameters, "subsession_count", activityState.subsessionCount);
+ addDuration(parameters, "session_length", activityState.sessionLength);
+ addDuration(parameters, "time_spent", activityState.timeSpent);
+ }
+
+ private void injectActivityStateIds(Map<String, String> parameters) {
+ addString(parameters, "android_uuid", activityState.uuid);
+ }
+
+ private void injectAttribution(Map<String, String> parameters) {
+ if (attribution == null) {
+ return;
+ }
+ addString(parameters, "tracker", attribution.trackerName);
+ addString(parameters, "campaign", attribution.campaign);
+ addString(parameters, "adgroup", attribution.adgroup);
+ addString(parameters, "creative", attribution.creative);
+ }
+
+ private void checkDeviceIds(Map<String, String> parameters) {
+ if (!parameters.containsKey("mac_sha1")
+ && !parameters.containsKey("mac_md5")
+ && !parameters.containsKey("android_id")
+ && !parameters.containsKey("gps_adid")) {
+ logger.error("Missing device id's. Please check if Proguard is correctly set with Adjust SDK");
+ }
+ }
+
+ private void fillPluginKeys(Map<String, String> parameters) {
+ if (deviceInfo.pluginKeys == null) {
+ return;
+ }
+
+ for (Map.Entry<String, String> entry : deviceInfo.pluginKeys.entrySet()) {
+ addString(parameters, entry.getKey(), entry.getValue());
+ }
+ }
+
+ private String getEventSuffix(AdjustEvent event) {
+ if (event.revenue == null) {
+ return String.format(" '%s'", event.eventToken);
+ } else {
+ return String.format(Locale.US, " (%.4f %s, '%s')", event.revenue, event.currency, event.eventToken);
+ }
+ }
+
+ private void addString(Map<String, String> parameters, String key, String value) {
+ if (TextUtils.isEmpty(value)) {
+ return;
+ }
+
+ parameters.put(key, value);
+ }
+
+ private void addInt(Map<String, String> parameters, String key, long value) {
+ if (value < 0) {
+ return;
+ }
+
+ String valueString = Long.toString(value);
+ addString(parameters, key, valueString);
+ }
+
+ private void addDate(Map<String, String> parameters, String key, long value) {
+ if (value < 0) {
+ return;
+ }
+
+ String dateString = Util.dateFormat(value);
+ addString(parameters, key, dateString);
+ }
+
+ private void addDuration(Map<String, String> parameters, String key, long durationInMilliSeconds) {
+ if (durationInMilliSeconds < 0) {
+ return;
+ }
+
+ long durationInSeconds = (durationInMilliSeconds + 500) / 1000;
+ addInt(parameters, key, durationInSeconds);
+ }
+
+ private void addMapJson(Map<String, String> parameters, String key, Map<String, String> map) {
+ if (map == null) {
+ return;
+ }
+
+ if (map.size() == 0) {
+ return;
+ }
+
+ JSONObject jsonObject = new JSONObject(map);
+ String jsonString = jsonObject.toString();
+
+ addString(parameters, key, jsonString);
+ }
+
+ private void addBoolean(Map<String, String> parameters, String key, Boolean value) {
+ if (value == null) {
+ return;
+ }
+
+ int intValue = value ? 1 : 0;
+
+ addInt(parameters, key, intValue);
+ }
+
+ private void addDouble(Map<String, String> parameters, String key, Double value) {
+ if (value == null) return;
+
+ String doubleString = String.format("%.5f", value);
+
+ addString(parameters, key, doubleString);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/PackageHandler.java b/mobile/android/thirdparty/com/adjust/sdk/PackageHandler.java
new file mode 100644
index 000000000..d0a84ccd1
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/PackageHandler.java
@@ -0,0 +1,274 @@
+//
+// PackageHandler.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OptionalDataException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+// persistent
+public class PackageHandler extends HandlerThread implements IPackageHandler {
+ private static final String PACKAGE_QUEUE_FILENAME = "AdjustIoPackageQueue";
+
+ private final InternalHandler internalHandler;
+ private IRequestHandler requestHandler;
+ private IActivityHandler activityHandler;
+ private List<ActivityPackage> packageQueue;
+ private AtomicBoolean isSending;
+ private boolean paused;
+ private Context context;
+ private ILogger logger;
+
+ public PackageHandler(IActivityHandler activityHandler,
+ Context context,
+ boolean startPaused) {
+ super(Constants.LOGTAG, MIN_PRIORITY);
+ setDaemon(true);
+ start();
+ this.internalHandler = new InternalHandler(getLooper(), this);
+ this.logger = AdjustFactory.getLogger();
+
+ init(activityHandler, context, startPaused);
+
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.INIT;
+ internalHandler.sendMessage(message);
+ }
+
+ @Override
+ public void init(IActivityHandler activityHandler, Context context, boolean startPaused) {
+ this.activityHandler = activityHandler;
+ this.context = context;
+ this.paused = startPaused;
+ }
+
+ // add a package to the queue
+ @Override
+ public void addPackage(ActivityPackage pack) {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.ADD;
+ message.obj = pack;
+ internalHandler.sendMessage(message);
+ }
+
+ // try to send the oldest package
+ @Override
+ public void sendFirstPackage() {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.SEND_FIRST;
+ internalHandler.sendMessage(message);
+ }
+
+ // remove oldest package and try to send the next one
+ // (after success or possibly permanent failure)
+ @Override
+ public void sendNextPackage() {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.SEND_NEXT;
+ internalHandler.sendMessage(message);
+ }
+
+ // close the package to retry in the future (after temporary failure)
+ @Override
+ public void closeFirstPackage() {
+ isSending.set(false);
+ }
+
+ // interrupt the sending loop after the current request has finished
+ @Override
+ public void pauseSending() {
+ paused = true;
+ }
+
+ // allow sending requests again
+ @Override
+ public void resumeSending() {
+ paused = false;
+ }
+
+ // short info about how failing packages are handled
+ @Override
+ public String getFailureMessage() {
+ return "Will retry later.";
+ }
+
+ @Override
+ public void finishedTrackingActivity(JSONObject jsonResponse) {
+ activityHandler.finishedTrackingActivity(jsonResponse);
+ }
+
+ @Override
+ public void sendClickPackage(ActivityPackage clickPackage) {
+ logger.debug("Sending click package (%s)", clickPackage);
+ logger.verbose("%s", clickPackage.getExtendedString());
+ requestHandler.sendClickPackage(clickPackage);
+ }
+
+ private static final class InternalHandler extends Handler {
+ private static final int INIT = 1;
+ private static final int ADD = 2;
+ private static final int SEND_NEXT = 3;
+ private static final int SEND_FIRST = 4;
+
+ private final WeakReference<PackageHandler> packageHandlerReference;
+
+ protected InternalHandler(Looper looper, PackageHandler packageHandler) {
+ super(looper);
+ this.packageHandlerReference = new WeakReference<PackageHandler>(packageHandler);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ super.handleMessage(message);
+
+ PackageHandler packageHandler = packageHandlerReference.get();
+ if (null == packageHandler) {
+ return;
+ }
+
+ switch (message.arg1) {
+ case INIT:
+ packageHandler.initInternal();
+ break;
+ case ADD:
+ ActivityPackage activityPackage = (ActivityPackage) message.obj;
+ packageHandler.addInternal(activityPackage);
+ break;
+ case SEND_FIRST:
+ packageHandler.sendFirstInternal();
+ break;
+ case SEND_NEXT:
+ packageHandler.sendNextInternal();
+ break;
+ }
+ }
+ }
+
+ // internal methods run in dedicated queue thread
+
+ private void initInternal() {
+ requestHandler = AdjustFactory.getRequestHandler(this);
+
+ isSending = new AtomicBoolean();
+
+ readPackageQueue();
+ }
+
+ private void addInternal(ActivityPackage newPackage) {
+ packageQueue.add(newPackage);
+ logger.debug("Added package %d (%s)", packageQueue.size(), newPackage);
+ logger.verbose("%s", newPackage.getExtendedString());
+
+ writePackageQueue();
+ }
+
+ private void sendFirstInternal() {
+ if (packageQueue.isEmpty()) {
+ return;
+ }
+
+ if (paused) {
+ logger.debug("Package handler is paused");
+ return;
+ }
+ if (isSending.getAndSet(true)) {
+ logger.verbose("Package handler is already sending");
+ return;
+ }
+
+ ActivityPackage firstPackage = packageQueue.get(0);
+ requestHandler.sendPackage(firstPackage);
+ }
+
+ private void sendNextInternal() {
+ packageQueue.remove(0);
+ writePackageQueue();
+ isSending.set(false);
+ sendFirstInternal();
+ }
+
+ private void readPackageQueue() {
+ try {
+ FileInputStream inputStream = context.openFileInput(PACKAGE_QUEUE_FILENAME);
+ BufferedInputStream bufferedStream = new BufferedInputStream(inputStream);
+ ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);
+
+ try {
+ Object object = objectStream.readObject();
+ @SuppressWarnings("unchecked")
+ List<ActivityPackage> packageQueue = (List<ActivityPackage>) object;
+ logger.debug("Package handler read %d packages", packageQueue.size());
+ this.packageQueue = packageQueue;
+ return;
+ } catch (ClassNotFoundException e) {
+ logger.error("Failed to find package queue class");
+ } catch (OptionalDataException e) {
+ /* no-op */
+ } catch (IOException e) {
+ logger.error("Failed to read package queue object");
+ } catch (ClassCastException e) {
+ logger.error("Failed to cast package queue object");
+ } finally {
+ objectStream.close();
+ }
+ } catch (FileNotFoundException e) {
+ logger.verbose("Package queue file not found");
+ } catch (Exception e) {
+ logger.error("Failed to read package queue file");
+ }
+
+ // start with a fresh package queue in case of any exception
+ packageQueue = new ArrayList<ActivityPackage>();
+ }
+
+ public static Boolean deletePackageQueue(Context context) {
+ return context.deleteFile(PACKAGE_QUEUE_FILENAME);
+ }
+
+
+ private void writePackageQueue() {
+ try {
+ FileOutputStream outputStream = context.openFileOutput(PACKAGE_QUEUE_FILENAME, Context.MODE_PRIVATE);
+ BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream);
+ ObjectOutputStream objectStream = new ObjectOutputStream(bufferedStream);
+
+ try {
+ objectStream.writeObject(packageQueue);
+ logger.debug("Package handler wrote %d packages", packageQueue.size());
+ } catch (NotSerializableException e) {
+ logger.error("Failed to serialize packages");
+ } finally {
+ objectStream.close();
+ }
+ } catch (Exception e) {
+ logger.error("Failed to write packages (%s)", e.getLocalizedMessage());
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Reflection.java b/mobile/android/thirdparty/com/adjust/sdk/Reflection.java
new file mode 100644
index 000000000..d9d9a9dbc
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Reflection.java
@@ -0,0 +1,210 @@
+package com.adjust.sdk;
+
+import android.content.Context;
+
+import com.adjust.sdk.plugin.Plugin;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.adjust.sdk.Constants.PLUGINS;
+
+public class Reflection {
+
+ public static String getPlayAdId(Context context) {
+ try {
+ Object AdvertisingInfoObject = getAdvertisingInfoObject(context);
+
+ String playAdid = (String) invokeInstanceMethod(AdvertisingInfoObject, "getId", null);
+
+ return playAdid;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static Boolean isPlayTrackingEnabled(Context context) {
+ try {
+ Object AdvertisingInfoObject = getAdvertisingInfoObject(context);
+
+ Boolean isLimitedTrackingEnabled = (Boolean) invokeInstanceMethod(AdvertisingInfoObject, "isLimitAdTrackingEnabled", null);
+
+ return !isLimitedTrackingEnabled;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static boolean isGooglePlayServicesAvailable(Context context) {
+ try {
+ Integer isGooglePlayServicesAvailableStatusCode = (Integer) invokeStaticMethod(
+ "com.google.android.gms.common.GooglePlayServicesUtil",
+ "isGooglePlayServicesAvailable",
+ new Class[]{Context.class}, context
+ );
+
+ boolean isGooglePlayServicesAvailable = (Boolean) isConnectionResultSuccess(isGooglePlayServicesAvailableStatusCode);
+
+ return isGooglePlayServicesAvailable;
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ public static String getMacAddress(Context context) {
+ try {
+ String macSha1 = (String) invokeStaticMethod(
+ "com.adjust.sdk.plugin.MacAddressUtil",
+ "getMacAddress",
+ new Class[]{Context.class}, context
+ );
+
+ return macSha1;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static String getAndroidId(Context context) {
+ try {
+ String androidId = (String) invokeStaticMethod("com.adjust.sdk.plugin.AndroidIdUtil", "getAndroidId"
+ , new Class[]{Context.class}, context);
+
+ return androidId;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static String getSha1EmailAddress(Context context, String key) {
+ try {
+ String sha1EmailAddress = (String) invokeStaticMethod("com.adjust.sdk.plugin.EmailUtil", "getSha1EmailAddress"
+ , new Class[]{Context.class, String.class}, context, key);
+
+ return sha1EmailAddress;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ private static Object getAdvertisingInfoObject(Context context)
+ throws Exception {
+ return invokeStaticMethod("com.google.android.gms.ads.identifier.AdvertisingIdClient",
+ "getAdvertisingIdInfo",
+ new Class[]{Context.class}, context
+ );
+ }
+
+ private static boolean isConnectionResultSuccess(Integer statusCode) {
+ if (statusCode == null) {
+ return false;
+ }
+
+ try {
+ Class ConnectionResultClass = Class.forName("com.google.android.gms.common.ConnectionResult");
+
+ Field SuccessField = ConnectionResultClass.getField("SUCCESS");
+
+ int successStatusCode = SuccessField.getInt(null);
+
+ return successStatusCode == statusCode;
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ public static Class forName(String className) {
+ try {
+ Class classObject = Class.forName(className);
+ return classObject;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static Object createDefaultInstance(String className) {
+ Class classObject = forName(className);
+ Object instance = createDefaultInstance(classObject);
+ return instance;
+ }
+
+ public static Object createDefaultInstance(Class classObject) {
+ try {
+ Object instance = classObject.newInstance();
+ return instance;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static Object createInstance(String className, Class[] cArgs, Object... args) {
+ try {
+ Class classObject = Class.forName(className);
+ @SuppressWarnings("unchecked")
+ Constructor constructor = classObject.getConstructor(cArgs);
+ Object instance = constructor.newInstance(args);
+ return instance;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static Object invokeStaticMethod(String className, String methodName, Class[] cArgs, Object... args)
+ throws Exception {
+ Class classObject = Class.forName(className);
+
+ return invokeMethod(classObject, methodName, null, cArgs, args);
+ }
+
+ public static Object invokeInstanceMethod(Object instance, String methodName, Class[] cArgs, Object... args)
+ throws Exception {
+ Class classObject = instance.getClass();
+
+ return invokeMethod(classObject, methodName, instance, cArgs, args);
+ }
+
+ public static Object invokeMethod(Class classObject, String methodName, Object instance, Class[] cArgs, Object... args)
+ throws Exception {
+ @SuppressWarnings("unchecked")
+ Method methodObject = classObject.getMethod(methodName, cArgs);
+
+ Object resultObject = methodObject.invoke(instance, args);
+
+ return resultObject;
+ }
+
+ public static Map<String, String> getPluginKeys(Context context) {
+ Map<String, String> pluginKeys = new HashMap<String, String>();
+
+ for (Plugin plugin : getPlugins()) {
+ Map.Entry<String, String> pluginEntry = plugin.getParameter(context);
+ if (pluginEntry != null) {
+ pluginKeys.put(pluginEntry.getKey(), pluginEntry.getValue());
+ }
+ }
+
+ if (pluginKeys.size() == 0) {
+ return null;
+ } else {
+ return pluginKeys;
+ }
+ }
+
+ private static List<Plugin> getPlugins() {
+ List<Plugin> plugins = new ArrayList<Plugin>(PLUGINS.size());
+
+ for (String pluginName : PLUGINS) {
+ Object pluginObject = Reflection.createDefaultInstance(pluginName);
+ if (pluginObject != null && pluginObject instanceof Plugin) {
+ plugins.add((Plugin) pluginObject);
+ }
+ }
+
+ return plugins;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/RequestHandler.java b/mobile/android/thirdparty/com/adjust/sdk/RequestHandler.java
new file mode 100644
index 000000000..84d45d0ce
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/RequestHandler.java
@@ -0,0 +1,210 @@
+//
+// RequestHandler.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity;
+import ch.boye.httpclientandroidlib.client.methods.HttpPost;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.client.utils.URLEncodedUtils;
+import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.ref.WeakReference;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class RequestHandler extends HandlerThread implements IRequestHandler {
+ private InternalHandler internalHandler;
+ private IPackageHandler packageHandler;
+ private HttpClient httpClient;
+ private ILogger logger;
+
+ public RequestHandler(IPackageHandler packageHandler) {
+ super(Constants.LOGTAG, MIN_PRIORITY);
+ setDaemon(true);
+ start();
+
+ this.logger = AdjustFactory.getLogger();
+ this.internalHandler = new InternalHandler(getLooper(), this);
+ init(packageHandler);
+
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.INIT;
+ internalHandler.sendMessage(message);
+ }
+
+ @Override
+ public void init(IPackageHandler packageHandler) {
+ this.packageHandler = packageHandler;
+ }
+
+ @Override
+ public void sendPackage(ActivityPackage pack) {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.SEND;
+ message.obj = pack;
+ internalHandler.sendMessage(message);
+ }
+
+ @Override
+ public void sendClickPackage(ActivityPackage clickPackage) {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.SEND_CLICK;
+ message.obj = clickPackage;
+ internalHandler.sendMessage(message);
+
+ }
+
+ private static final class InternalHandler extends Handler {
+ private static final int INIT = 72401;
+ private static final int SEND = 72400;
+ private static final int SEND_CLICK = 72402;
+
+ private final WeakReference<RequestHandler> requestHandlerReference;
+
+ protected InternalHandler(Looper looper, RequestHandler requestHandler) {
+ super(looper);
+ this.requestHandlerReference = new WeakReference<RequestHandler>(requestHandler);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ super.handleMessage(message);
+
+ RequestHandler requestHandler = requestHandlerReference.get();
+ if (null == requestHandler) {
+ return;
+ }
+
+ switch (message.arg1) {
+ case INIT:
+ requestHandler.initInternal();
+ break;
+ case SEND:
+ ActivityPackage activityPackage = (ActivityPackage) message.obj;
+ requestHandler.sendInternal(activityPackage, true);
+ break;
+ case SEND_CLICK:
+ ActivityPackage clickPackage = (ActivityPackage) message.obj;
+ requestHandler.sendInternal(clickPackage, false);
+ break;
+ }
+ }
+ }
+
+ private void initInternal() {
+ httpClient = Util.getHttpClient();
+ }
+
+ private void sendInternal(ActivityPackage activityPackage, boolean sendToPackageHandler) {
+ try {
+ HttpUriRequest request = getRequest(activityPackage);
+ HttpResponse response = httpClient.execute(request);
+ requestFinished(response, sendToPackageHandler);
+ } catch (UnsupportedEncodingException e) {
+ sendNextPackage(activityPackage, "Failed to encode parameters", e, sendToPackageHandler);
+ } catch (ClientProtocolException e) {
+ closePackage(activityPackage, "Client protocol error", e, sendToPackageHandler);
+ } catch (SocketTimeoutException e) {
+ closePackage(activityPackage, "Request timed out", e, sendToPackageHandler);
+ } catch (IOException e) {
+ closePackage(activityPackage, "Request failed", e, sendToPackageHandler);
+ } catch (Throwable e) {
+ sendNextPackage(activityPackage, "Runtime exception", e, sendToPackageHandler);
+ }
+ }
+
+ private void requestFinished(HttpResponse response, boolean sendToPackageHandler) {
+ JSONObject jsonResponse = Util.parseJsonResponse(response, logger);
+
+ if (jsonResponse == null) {
+ if (sendToPackageHandler) {
+ packageHandler.closeFirstPackage();
+ }
+ return;
+ }
+
+ packageHandler.finishedTrackingActivity(jsonResponse);
+ if (sendToPackageHandler) {
+ packageHandler.sendNextPackage();
+ }
+ }
+
+ // close current package because it failed
+ private void closePackage(ActivityPackage activityPackage, String message, Throwable throwable, boolean sendToPackageHandler) {
+ final String packageMessage = activityPackage.getFailureMessage();
+ final String handlerMessage = packageHandler.getFailureMessage();
+ final String reasonString = getReasonString(message, throwable);
+ logger.error("%s. (%s) %s", packageMessage, reasonString, handlerMessage);
+
+ if (sendToPackageHandler) {
+ packageHandler.closeFirstPackage();
+ }
+ }
+
+ // send next package because the current package failed
+ private void sendNextPackage(ActivityPackage activityPackage, String message, Throwable throwable, boolean sendToPackageHandler) {
+ final String failureMessage = activityPackage.getFailureMessage();
+ final String reasonString = getReasonString(message, throwable);
+ logger.error("%s. (%s)", failureMessage, reasonString);
+
+ if (sendToPackageHandler) {
+ packageHandler.sendNextPackage();
+ }
+ }
+
+ private String getReasonString(String message, Throwable throwable) {
+ if (throwable != null) {
+ return String.format("%s: %s", message, throwable);
+ } else {
+ return String.format("%s", message);
+ }
+ }
+
+ private HttpUriRequest getRequest(ActivityPackage activityPackage) throws UnsupportedEncodingException {
+ String url = Constants.BASE_URL + activityPackage.getPath();
+ HttpPost request = new HttpPost(url);
+
+ String language = Locale.getDefault().getLanguage();
+ request.addHeader("Client-SDK", activityPackage.getClientSdk());
+ request.addHeader("Accept-Language", language);
+
+ List<NameValuePair> pairs = new ArrayList<NameValuePair>();
+ for (Map.Entry<String, String> entry : activityPackage.getParameters().entrySet()) {
+ NameValuePair pair = new BasicNameValuePair(entry.getKey(), entry.getValue());
+ pairs.add(pair);
+ }
+
+ long now = System.currentTimeMillis();
+ String dateString = Util.dateFormat(now);
+ NameValuePair sentAtPair = new BasicNameValuePair("sent_at", dateString);
+ pairs.add(sentAtPair);
+
+ UrlEncodedFormEntity entity = new UrlEncodedFormEntity(pairs);
+ entity.setContentType(URLEncodedUtils.CONTENT_TYPE);
+ request.setEntity(entity);
+
+ return request;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/UnitTestActivity.java b/mobile/android/thirdparty/com/adjust/sdk/UnitTestActivity.java
new file mode 100644
index 000000000..799fb8982
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/UnitTestActivity.java
@@ -0,0 +1,38 @@
+package com.adjust.sdk;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class UnitTestActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ //setContentView(com.adjust.sdk.test.R.layout.activity_unit_test);
+ }
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ //getMenuInflater().inflate(com.adjust.sdk.test.R.menu.menu_unit_test, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+/* int id = item.getItemId();
+
+ //noinspection SimplifiableIfStatement
+ if (id == com.adjust.sdk.test.R.id.action_settings) {
+ return true;
+ }
+*/
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Util.java b/mobile/android/thirdparty/com/adjust/sdk/Util.java
new file mode 100644
index 000000000..84c47f87e
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Util.java
@@ -0,0 +1,202 @@
+//
+// Util.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2012-10-11.
+// Copyright (c) 2012-2014 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.params.BasicHttpParams;
+import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OptionalDataException;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Collects utility functions used by Adjust.
+ */
+public class Util {
+
+ private static SimpleDateFormat dateFormat;
+ private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'Z";
+
+ protected static String createUuid() {
+ return UUID.randomUUID().toString();
+ }
+
+ public static String quote(String string) {
+ if (string == null) {
+ return null;
+ }
+
+ Pattern pattern = Pattern.compile("\\s");
+ Matcher matcher = pattern.matcher(string);
+ if (!matcher.find()) {
+ return string;
+ }
+
+ return String.format("'%s'", string);
+ }
+
+ public static String dateFormat(long date) {
+ if (null == dateFormat) {
+ dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US);
+ }
+ return dateFormat.format(date);
+ }
+
+ public static String getPlayAdId(Context context) {
+ return Reflection.getPlayAdId(context);
+ }
+
+ public static Boolean isPlayTrackingEnabled(Context context) {
+ return Reflection.isPlayTrackingEnabled(context);
+ }
+
+ public static <T> T readObject(Context context, String filename, String objectName) {
+ ILogger logger = AdjustFactory.getLogger();
+ try {
+ FileInputStream inputStream = context.openFileInput(filename);
+ BufferedInputStream bufferedStream = new BufferedInputStream(inputStream);
+ ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);
+
+ try {
+ @SuppressWarnings("unchecked")
+ T t = (T) objectStream.readObject();
+ logger.debug("Read %s: %s", objectName, t);
+ return t;
+ } catch (ClassNotFoundException e) {
+ logger.error("Failed to find %s class", objectName);
+ } catch (OptionalDataException e) {
+ /* no-op */
+ } catch (IOException e) {
+ logger.error("Failed to read %s object", objectName);
+ } catch (ClassCastException e) {
+ logger.error("Failed to cast %s object", objectName);
+ } finally {
+ objectStream.close();
+ }
+
+ } catch (FileNotFoundException e) {
+ logger.verbose("%s file not found", objectName);
+ } catch (Exception e) {
+ logger.error("Failed to open %s file for reading (%s)", objectName, e);
+ }
+
+ return null;
+ }
+
+ public static <T> void writeObject(T object, Context context, String filename, String objectName) {
+ ILogger logger = AdjustFactory.getLogger();
+ try {
+ FileOutputStream outputStream = context.openFileOutput(filename, Context.MODE_PRIVATE);
+ BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream);
+ ObjectOutputStream objectStream = new ObjectOutputStream(bufferedStream);
+
+ try {
+ objectStream.writeObject(object);
+ logger.debug("Wrote %s: %s", objectName, object);
+ } catch (NotSerializableException e) {
+ logger.error("Failed to serialize %s", objectName);
+ } finally {
+ objectStream.close();
+ }
+
+ } catch (Exception e) {
+ logger.error("Failed to open %s for writing (%s)", objectName, e);
+ }
+ }
+
+ public static String parseResponse(HttpResponse httpResponse, ILogger logger) {
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ httpResponse.getEntity().writeTo(out);
+ out.close();
+ String response = out.toString().trim();
+ logger.verbose("Response: %s", response);
+ return response;
+ } catch (Exception e) {
+ logger.error("Failed to parse response (%s)", e);
+ return null;
+ }
+ }
+
+ public static JSONObject parseJsonResponse(HttpResponse httpResponse, ILogger logger) {
+ if (httpResponse == null) {
+ return null;
+ }
+ String stringResponse = null;
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ httpResponse.getEntity().writeTo(out);
+ out.close();
+ stringResponse = out.toString().trim();
+ } catch (Exception e) {
+ logger.error("Failed to parse response (%s)", e.getMessage());
+ }
+
+ logger.verbose("Response: %s", stringResponse);
+ if (stringResponse == null) return null;
+
+ JSONObject jsonResponse = null;
+ try {
+ jsonResponse = new JSONObject(stringResponse);
+ } catch (JSONException e) {
+ logger.error("Failed to parse json response: %s (%s)", stringResponse, e.getMessage());
+ }
+
+ if (jsonResponse == null) return null;
+
+ String message = jsonResponse.optString("message", null);
+
+ if (message == null) {
+ message = "No message found";
+ }
+
+ if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ logger.info("%s", message);
+ } else {
+ logger.error("%s", message);
+ }
+
+ return jsonResponse;
+ }
+
+ public static HttpClient getHttpClient() {
+ HttpParams httpParams = new BasicHttpParams();
+ HttpConnectionParams.setConnectionTimeout(httpParams, Constants.CONNECTION_TIMEOUT);
+ HttpConnectionParams.setSoTimeout(httpParams, Constants.SOCKET_TIMEOUT);
+ return AdjustFactory.getHttpClient(httpParams);
+ }
+
+ public static boolean checkPermission(Context context, String permission) {
+ int result = context.checkCallingOrSelfPermission(permission);
+ return result == PackageManager.PERMISSION_GRANTED;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/plugin/AndroidIdUtil.java b/mobile/android/thirdparty/com/adjust/sdk/plugin/AndroidIdUtil.java
new file mode 100644
index 000000000..96a072287
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/plugin/AndroidIdUtil.java
@@ -0,0 +1,10 @@
+package com.adjust.sdk.plugin;
+
+import android.content.Context;
+import android.provider.Settings.Secure;
+
+public class AndroidIdUtil {
+ public static String getAndroidId(final Context context) {
+ return Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/plugin/MacAddressUtil.java b/mobile/android/thirdparty/com/adjust/sdk/plugin/MacAddressUtil.java
new file mode 100644
index 000000000..c8bdbadd7
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/plugin/MacAddressUtil.java
@@ -0,0 +1,82 @@
+package com.adjust.sdk.plugin;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.text.TextUtils;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Locale;
+
+public class MacAddressUtil {
+ public static String getMacAddress(Context context) {
+ final String rawAddress = getRawMacAddress(context);
+ if (rawAddress == null) {
+ return null;
+ }
+ final String upperAddress = rawAddress.toUpperCase(Locale.US);
+ return removeSpaceString(upperAddress);
+ }
+
+ private static String getRawMacAddress(Context context) {
+ // android devices should have a wlan address
+ final String wlanAddress = loadAddress("wlan0");
+ if (wlanAddress != null) {
+ return wlanAddress;
+ }
+
+ // emulators should have an ethernet address
+ final String ethAddress = loadAddress("eth0");
+ if (ethAddress != null) {
+ return ethAddress;
+ }
+
+ // query the wifi manager (requires the ACCESS_WIFI_STATE permission)
+ try {
+ final WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ final String wifiAddress = wifiManager.getConnectionInfo().getMacAddress();
+ if (wifiAddress != null) {
+ return wifiAddress;
+ }
+ } catch (Exception e) {
+ /* no-op */
+ }
+
+ return null;
+ }
+
+ private static String loadAddress(final String interfaceName) {
+ try {
+ final String filePath = "/sys/class/net/" + interfaceName + "/address";
+ final StringBuilder fileData = new StringBuilder(1000);
+ final BufferedReader reader = new BufferedReader(new FileReader(filePath), 1024);
+ final char[] buf = new char[1024];
+ int numRead;
+
+ String readData;
+ while ((numRead = reader.read(buf)) != -1) {
+ readData = String.valueOf(buf, 0, numRead);
+ fileData.append(readData);
+ }
+
+ reader.close();
+ return fileData.toString();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private static String removeSpaceString(final String inputString) {
+ if (inputString == null) {
+ return null;
+ }
+
+ String outputString = inputString.replaceAll("\\s", "");
+ if (TextUtils.isEmpty(outputString)) {
+ return null;
+ }
+
+ return outputString;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/plugin/Plugin.java b/mobile/android/thirdparty/com/adjust/sdk/plugin/Plugin.java
new file mode 100644
index 000000000..ab704e6d3
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/plugin/Plugin.java
@@ -0,0 +1,12 @@
+package com.adjust.sdk.plugin;
+
+import android.content.Context;
+
+import java.util.Map;
+
+/**
+ * Created by pfms on 18/09/14.
+ */
+public interface Plugin {
+ Map.Entry<String, String> getParameter(Context context);
+}
diff --git a/mobile/android/thirdparty/com/jakewharton/disklrucache/DiskLruCache.java b/mobile/android/thirdparty/com/jakewharton/disklrucache/DiskLruCache.java
new file mode 100644
index 000000000..e81e7fbcc
--- /dev/null
+++ b/mobile/android/thirdparty/com/jakewharton/disklrucache/DiskLruCache.java
@@ -0,0 +1,943 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.jakewharton.disklrucache;
+
+import java.io.BufferedWriter;
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A cache that uses a bounded amount of space on a filesystem. Each cache
+ * entry has a string key and a fixed number of values. Each key must match
+ * the regex <strong>[a-z0-9_-]{1,120}</strong>. Values are byte sequences,
+ * accessible as streams or files. Each value must be between {@code 0} and
+ * {@code Integer.MAX_VALUE} bytes in length.
+ *
+ * <p>The cache stores its data in a directory on the filesystem. This
+ * directory must be exclusive to the cache; the cache may delete or overwrite
+ * files from its directory. It is an error for multiple processes to use the
+ * same cache directory at the same time.
+ *
+ * <p>This cache limits the number of bytes that it will store on the
+ * filesystem. When the number of stored bytes exceeds the limit, the cache will
+ * remove entries in the background until the limit is satisfied. The limit is
+ * not strict: the cache may temporarily exceed it while waiting for files to be
+ * deleted. The limit does not include filesystem overhead or the cache
+ * journal so space-sensitive applications should set a conservative limit.
+ *
+ * <p>Clients call {@link #edit} to create or update the values of an entry. An
+ * entry may have only one editor at one time; if a value is not available to be
+ * edited then {@link #edit} will return null.
+ * <ul>
+ * <li>When an entry is being <strong>created</strong> it is necessary to
+ * supply a full set of values; the empty value should be used as a
+ * placeholder if necessary.
+ * <li>When an entry is being <strong>edited</strong>, it is not necessary
+ * to supply data for every value; values default to their previous
+ * value.
+ * </ul>
+ * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
+ * or {@link Editor#abort}. Committing is atomic: a read observes the full set
+ * of values as they were before or after the commit, but never a mix of values.
+ *
+ * <p>Clients call {@link #get} to read a snapshot of an entry. The read will
+ * observe the value at the time that {@link #get} was called. Updates and
+ * removals after the call do not impact ongoing reads.
+ *
+ * <p>This class is tolerant of some I/O errors. If files are missing from the
+ * filesystem, the corresponding entries will be dropped from the cache. If
+ * an error occurs while writing a cache value, the edit will fail silently.
+ * Callers should handle other problems by catching {@code IOException} and
+ * responding appropriately.
+ */
+public final class DiskLruCache implements Closeable {
+ static final String JOURNAL_FILE = "journal";
+ static final String JOURNAL_FILE_TEMP = "journal.tmp";
+ static final String JOURNAL_FILE_BACKUP = "journal.bkp";
+ static final String MAGIC = "libcore.io.DiskLruCache";
+ static final String VERSION_1 = "1";
+ static final long ANY_SEQUENCE_NUMBER = -1;
+ static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}";
+ static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN);
+ private static final String CLEAN = "CLEAN";
+ private static final String DIRTY = "DIRTY";
+ private static final String REMOVE = "REMOVE";
+ private static final String READ = "READ";
+
+ /*
+ * This cache uses a journal file named "journal". A typical journal file
+ * looks like this:
+ * libcore.io.DiskLruCache
+ * 1
+ * 100
+ * 2
+ *
+ * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
+ * DIRTY 335c4c6028171cfddfbaae1a9c313c52
+ * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
+ * REMOVE 335c4c6028171cfddfbaae1a9c313c52
+ * DIRTY 1ab96a171faeeee38496d8b330771a7a
+ * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
+ * READ 335c4c6028171cfddfbaae1a9c313c52
+ * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
+ *
+ * The first five lines of the journal form its header. They are the
+ * constant string "libcore.io.DiskLruCache", the disk cache's version,
+ * the application's version, the value count, and a blank line.
+ *
+ * Each of the subsequent lines in the file is a record of the state of a
+ * cache entry. Each line contains space-separated values: a state, a key,
+ * and optional state-specific values.
+ * o DIRTY lines track that an entry is actively being created or updated.
+ * Every successful DIRTY action should be followed by a CLEAN or REMOVE
+ * action. DIRTY lines without a matching CLEAN or REMOVE indicate that
+ * temporary files may need to be deleted.
+ * o CLEAN lines track a cache entry that has been successfully published
+ * and may be read. A publish line is followed by the lengths of each of
+ * its values.
+ * o READ lines track accesses for LRU.
+ * o REMOVE lines track entries that have been deleted.
+ *
+ * The journal file is appended to as cache operations occur. The journal may
+ * occasionally be compacted by dropping redundant lines. A temporary file named
+ * "journal.tmp" will be used during compaction; that file should be deleted if
+ * it exists when the cache is opened.
+ */
+
+ private final File directory;
+ private final File journalFile;
+ private final File journalFileTmp;
+ private final File journalFileBackup;
+ private final int appVersion;
+ private long maxSize;
+ private final int valueCount;
+ private long size = 0;
+ private Writer journalWriter;
+ private final LinkedHashMap<String, Entry> lruEntries =
+ new LinkedHashMap<String, Entry>(0, 0.75f, true);
+ private int redundantOpCount;
+
+ /**
+ * To differentiate between old and current snapshots, each entry is given
+ * a sequence number each time an edit is committed. A snapshot is stale if
+ * its sequence number is not equal to its entry's sequence number.
+ */
+ private long nextSequenceNumber = 0;
+
+ /** This cache uses a single background thread to evict entries. */
+ final ThreadPoolExecutor executorService =
+ new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+ private final Callable<Void> cleanupCallable = new Callable<Void>() {
+ public Void call() throws Exception {
+ synchronized (DiskLruCache.this) {
+ if (journalWriter == null) {
+ return null; // Closed.
+ }
+ trimToSize();
+ if (journalRebuildRequired()) {
+ rebuildJournal();
+ redundantOpCount = 0;
+ }
+ }
+ return null;
+ }
+ };
+
+ private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
+ this.directory = directory;
+ this.appVersion = appVersion;
+ this.journalFile = new File(directory, JOURNAL_FILE);
+ this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
+ this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
+ this.valueCount = valueCount;
+ this.maxSize = maxSize;
+ }
+
+ /**
+ * Opens the cache in {@code directory}, creating a cache if none exists
+ * there.
+ *
+ * @param directory a writable directory
+ * @param valueCount the number of values per cache entry. Must be positive.
+ * @param maxSize the maximum number of bytes this cache should use to store
+ * @throws IOException if reading or writing the cache directory fails
+ */
+ public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
+ throws IOException {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("maxSize <= 0");
+ }
+ if (valueCount <= 0) {
+ throw new IllegalArgumentException("valueCount <= 0");
+ }
+
+ // If a bkp file exists, use it instead.
+ File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
+ if (backupFile.exists()) {
+ File journalFile = new File(directory, JOURNAL_FILE);
+ // If journal file also exists just delete backup file.
+ if (journalFile.exists()) {
+ backupFile.delete();
+ } else {
+ renameTo(backupFile, journalFile, false);
+ }
+ }
+
+ // Prefer to pick up where we left off.
+ DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+ if (cache.journalFile.exists()) {
+ try {
+ cache.readJournal();
+ cache.processJournal();
+ return cache;
+ } catch (IOException journalIsCorrupt) {
+ System.out
+ .println("DiskLruCache "
+ + directory
+ + " is corrupt: "
+ + journalIsCorrupt.getMessage()
+ + ", removing");
+ cache.delete();
+ }
+ }
+
+ // Create a new empty cache.
+ directory.mkdirs();
+ cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+ cache.rebuildJournal();
+ return cache;
+ }
+
+ private void readJournal() throws IOException {
+ StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
+ try {
+ String magic = reader.readLine();
+ String version = reader.readLine();
+ String appVersionString = reader.readLine();
+ String valueCountString = reader.readLine();
+ String blank = reader.readLine();
+ if (!MAGIC.equals(magic)
+ || !VERSION_1.equals(version)
+ || !Integer.toString(appVersion).equals(appVersionString)
+ || !Integer.toString(valueCount).equals(valueCountString)
+ || !"".equals(blank)) {
+ throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+ + valueCountString + ", " + blank + "]");
+ }
+
+ int lineCount = 0;
+ while (true) {
+ try {
+ readJournalLine(reader.readLine());
+ lineCount++;
+ } catch (EOFException endOfJournal) {
+ break;
+ }
+ }
+ redundantOpCount = lineCount - lruEntries.size();
+
+ // If we ended on a truncated line, rebuild the journal before appending to it.
+ if (reader.hasUnterminatedLine()) {
+ rebuildJournal();
+ } else {
+ journalWriter = new BufferedWriter(new OutputStreamWriter(
+ new FileOutputStream(journalFile, true), Util.US_ASCII));
+ }
+ } finally {
+ Util.closeQuietly(reader);
+ }
+ }
+
+ private void readJournalLine(String line) throws IOException {
+ int firstSpace = line.indexOf(' ');
+ if (firstSpace == -1) {
+ throw new IOException("unexpected journal line: " + line);
+ }
+
+ int keyBegin = firstSpace + 1;
+ int secondSpace = line.indexOf(' ', keyBegin);
+ final String key;
+ if (secondSpace == -1) {
+ key = line.substring(keyBegin);
+ if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
+ lruEntries.remove(key);
+ return;
+ }
+ } else {
+ key = line.substring(keyBegin, secondSpace);
+ }
+
+ Entry entry = lruEntries.get(key);
+ if (entry == null) {
+ entry = new Entry(key);
+ lruEntries.put(key, entry);
+ }
+
+ if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
+ String[] parts = line.substring(secondSpace + 1).split(" ");
+ entry.readable = true;
+ entry.currentEditor = null;
+ entry.setLengths(parts);
+ } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
+ entry.currentEditor = new Editor(entry);
+ } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
+ // This work was already done by calling lruEntries.get().
+ } else {
+ throw new IOException("unexpected journal line: " + line);
+ }
+ }
+
+ /**
+ * Computes the initial size and collects garbage as a part of opening the
+ * cache. Dirty entries are assumed to be inconsistent and will be deleted.
+ */
+ private void processJournal() throws IOException {
+ deleteIfExists(journalFileTmp);
+ for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
+ Entry entry = i.next();
+ if (entry.currentEditor == null) {
+ for (int t = 0; t < valueCount; t++) {
+ size += entry.lengths[t];
+ }
+ } else {
+ entry.currentEditor = null;
+ for (int t = 0; t < valueCount; t++) {
+ deleteIfExists(entry.getCleanFile(t));
+ deleteIfExists(entry.getDirtyFile(t));
+ }
+ i.remove();
+ }
+ }
+ }
+
+ /**
+ * Creates a new journal that omits redundant information. This replaces the
+ * current journal if it exists.
+ */
+ private synchronized void rebuildJournal() throws IOException {
+ if (journalWriter != null) {
+ journalWriter.close();
+ }
+
+ Writer writer = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
+ try {
+ writer.write(MAGIC);
+ writer.write("\n");
+ writer.write(VERSION_1);
+ writer.write("\n");
+ writer.write(Integer.toString(appVersion));
+ writer.write("\n");
+ writer.write(Integer.toString(valueCount));
+ writer.write("\n");
+ writer.write("\n");
+
+ for (Entry entry : lruEntries.values()) {
+ if (entry.currentEditor != null) {
+ writer.write(DIRTY + ' ' + entry.key + '\n');
+ } else {
+ writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ }
+ }
+ } finally {
+ writer.close();
+ }
+
+ if (journalFile.exists()) {
+ renameTo(journalFile, journalFileBackup, true);
+ }
+ renameTo(journalFileTmp, journalFile, false);
+ journalFileBackup.delete();
+
+ journalWriter = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
+ }
+
+ private static void deleteIfExists(File file) throws IOException {
+ if (file.exists() && !file.delete()) {
+ throw new IOException();
+ }
+ }
+
+ private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
+ if (deleteDestination) {
+ deleteIfExists(to);
+ }
+ if (!from.renameTo(to)) {
+ throw new IOException();
+ }
+ }
+
+ /**
+ * Returns a snapshot of the entry named {@code key}, or null if it doesn't
+ * exist is not currently readable. If a value is returned, it is moved to
+ * the head of the LRU queue.
+ */
+ public synchronized Snapshot get(String key) throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ Entry entry = lruEntries.get(key);
+ if (entry == null) {
+ return null;
+ }
+
+ if (!entry.readable) {
+ return null;
+ }
+
+ // Open all streams eagerly to guarantee that we see a single published
+ // snapshot. If we opened streams lazily then the streams could come
+ // from different edits.
+ InputStream[] ins = new InputStream[valueCount];
+ try {
+ for (int i = 0; i < valueCount; i++) {
+ ins[i] = new FileInputStream(entry.getCleanFile(i));
+ }
+ } catch (FileNotFoundException e) {
+ // A file must have been deleted manually!
+ for (int i = 0; i < valueCount; i++) {
+ if (ins[i] != null) {
+ Util.closeQuietly(ins[i]);
+ } else {
+ break;
+ }
+ }
+ return null;
+ }
+
+ redundantOpCount++;
+ journalWriter.append(READ + ' ' + key + '\n');
+ if (journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+
+ return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
+ }
+
+ /**
+ * Returns an editor for the entry named {@code key}, or null if another
+ * edit is in progress.
+ */
+ public Editor edit(String key) throws IOException {
+ return edit(key, ANY_SEQUENCE_NUMBER);
+ }
+
+ private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ Entry entry = lruEntries.get(key);
+ if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
+ || entry.sequenceNumber != expectedSequenceNumber)) {
+ return null; // Snapshot is stale.
+ }
+ if (entry == null) {
+ entry = new Entry(key);
+ lruEntries.put(key, entry);
+ } else if (entry.currentEditor != null) {
+ return null; // Another edit is in progress.
+ }
+
+ Editor editor = new Editor(entry);
+ entry.currentEditor = editor;
+
+ // Flush the journal before creating files to prevent file leaks.
+ journalWriter.write(DIRTY + ' ' + key + '\n');
+ journalWriter.flush();
+ return editor;
+ }
+
+ /** Returns the directory where this cache stores its data. */
+ public File getDirectory() {
+ return directory;
+ }
+
+ /**
+ * Returns the maximum number of bytes that this cache should use to store
+ * its data.
+ */
+ public synchronized long getMaxSize() {
+ return maxSize;
+ }
+
+ /**
+ * Changes the maximum number of bytes the cache can store and queues a job
+ * to trim the existing store, if necessary.
+ */
+ public synchronized void setMaxSize(long maxSize) {
+ this.maxSize = maxSize;
+ executorService.submit(cleanupCallable);
+ }
+
+ /**
+ * Returns the number of bytes currently being used to store the values in
+ * this cache. This may be greater than the max size if a background
+ * deletion is pending.
+ */
+ public synchronized long size() {
+ return size;
+ }
+
+ private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
+ Entry entry = editor.entry;
+ if (entry.currentEditor != editor) {
+ throw new IllegalStateException();
+ }
+
+ // If this edit is creating the entry for the first time, every index must have a value.
+ if (success && !entry.readable) {
+ for (int i = 0; i < valueCount; i++) {
+ if (!editor.written[i]) {
+ editor.abort();
+ throw new IllegalStateException("Newly created entry didn't create value for index " + i);
+ }
+ if (!entry.getDirtyFile(i).exists()) {
+ editor.abort();
+ return;
+ }
+ }
+ }
+
+ for (int i = 0; i < valueCount; i++) {
+ File dirty = entry.getDirtyFile(i);
+ if (success) {
+ if (dirty.exists()) {
+ File clean = entry.getCleanFile(i);
+ dirty.renameTo(clean);
+ long oldLength = entry.lengths[i];
+ long newLength = clean.length();
+ entry.lengths[i] = newLength;
+ size = size - oldLength + newLength;
+ }
+ } else {
+ deleteIfExists(dirty);
+ }
+ }
+
+ redundantOpCount++;
+ entry.currentEditor = null;
+ if (entry.readable | success) {
+ entry.readable = true;
+ journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ if (success) {
+ entry.sequenceNumber = nextSequenceNumber++;
+ }
+ } else {
+ lruEntries.remove(entry.key);
+ journalWriter.write(REMOVE + ' ' + entry.key + '\n');
+ }
+ journalWriter.flush();
+
+ if (size > maxSize || journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+ }
+
+ /**
+ * We only rebuild the journal when it will halve the size of the journal
+ * and eliminate at least 2000 ops.
+ */
+ private boolean journalRebuildRequired() {
+ final int redundantOpCompactThreshold = 2000;
+ return redundantOpCount >= redundantOpCompactThreshold //
+ && redundantOpCount >= lruEntries.size();
+ }
+
+ /**
+ * Drops the entry for {@code key} if it exists and can be removed. Entries
+ * actively being edited cannot be removed.
+ *
+ * @return true if an entry was removed.
+ */
+ public synchronized boolean remove(String key) throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ Entry entry = lruEntries.get(key);
+ if (entry == null || entry.currentEditor != null) {
+ return false;
+ }
+
+ for (int i = 0; i < valueCount; i++) {
+ File file = entry.getCleanFile(i);
+ if (file.exists() && !file.delete()) {
+ throw new IOException("failed to delete " + file);
+ }
+ size -= entry.lengths[i];
+ entry.lengths[i] = 0;
+ }
+
+ redundantOpCount++;
+ journalWriter.append(REMOVE + ' ' + key + '\n');
+ lruEntries.remove(key);
+
+ if (journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+
+ return true;
+ }
+
+ /** Returns true if this cache has been closed. */
+ public synchronized boolean isClosed() {
+ return journalWriter == null;
+ }
+
+ private void checkNotClosed() {
+ if (journalWriter == null) {
+ throw new IllegalStateException("cache is closed");
+ }
+ }
+
+ /** Force buffered operations to the filesystem. */
+ public synchronized void flush() throws IOException {
+ checkNotClosed();
+ trimToSize();
+ journalWriter.flush();
+ }
+
+ /** Closes this cache. Stored values will remain on the filesystem. */
+ public synchronized void close() throws IOException {
+ if (journalWriter == null) {
+ return; // Already closed.
+ }
+ for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
+ if (entry.currentEditor != null) {
+ entry.currentEditor.abort();
+ }
+ }
+ trimToSize();
+ journalWriter.close();
+ journalWriter = null;
+ }
+
+ private void trimToSize() throws IOException {
+ while (size > maxSize) {
+ Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
+ remove(toEvict.getKey());
+ }
+ }
+
+ /**
+ * Closes the cache and deletes all of its stored values. This will delete
+ * all files in the cache directory including files that weren't created by
+ * the cache.
+ */
+ public void delete() throws IOException {
+ close();
+ Util.deleteContents(directory);
+ }
+
+ private void validateKey(String key) {
+ Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("keys must match regex "
+ + STRING_KEY_PATTERN + ": \"" + key + "\"");
+ }
+ }
+
+ private static String inputStreamToString(InputStream in) throws IOException {
+ return Util.readFully(new InputStreamReader(in, Util.UTF_8));
+ }
+
+ /** A snapshot of the values for an entry. */
+ public final class Snapshot implements Closeable {
+ private final String key;
+ private final long sequenceNumber;
+ private final InputStream[] ins;
+ private final long[] lengths;
+
+ private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
+ this.key = key;
+ this.sequenceNumber = sequenceNumber;
+ this.ins = ins;
+ this.lengths = lengths;
+ }
+
+ /**
+ * Returns an editor for this snapshot's entry, or null if either the
+ * entry has changed since this snapshot was created or if another edit
+ * is in progress.
+ */
+ public Editor edit() throws IOException {
+ return DiskLruCache.this.edit(key, sequenceNumber);
+ }
+
+ /** Returns the unbuffered stream with the value for {@code index}. */
+ public InputStream getInputStream(int index) {
+ return ins[index];
+ }
+
+ /** Returns the string value for {@code index}. */
+ public String getString(int index) throws IOException {
+ return inputStreamToString(getInputStream(index));
+ }
+
+ /** Returns the byte length of the value for {@code index}. */
+ public long getLength(int index) {
+ return lengths[index];
+ }
+
+ public void close() {
+ for (InputStream in : ins) {
+ Util.closeQuietly(in);
+ }
+ }
+ }
+
+ private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ // Eat all writes silently. Nom nom.
+ }
+ };
+
+ /** Edits the values for an entry. */
+ public final class Editor {
+ private final Entry entry;
+ private final boolean[] written;
+ private boolean hasErrors;
+ private boolean committed;
+
+ private Editor(Entry entry) {
+ this.entry = entry;
+ this.written = (entry.readable) ? null : new boolean[valueCount];
+ }
+
+ /**
+ * Returns an unbuffered input stream to read the last committed value,
+ * or null if no value has been committed.
+ */
+ public InputStream newInputStream(int index) throws IOException {
+ synchronized (DiskLruCache.this) {
+ if (entry.currentEditor != this) {
+ throw new IllegalStateException();
+ }
+ if (!entry.readable) {
+ return null;
+ }
+ try {
+ return new FileInputStream(entry.getCleanFile(index));
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Returns the last committed value as a string, or null if no value
+ * has been committed.
+ */
+ public String getString(int index) throws IOException {
+ InputStream in = newInputStream(index);
+ return in != null ? inputStreamToString(in) : null;
+ }
+
+ /**
+ * Returns a new unbuffered output stream to write the value at
+ * {@code index}. If the underlying output stream encounters errors
+ * when writing to the filesystem, this edit will be aborted when
+ * {@link #commit} is called. The returned output stream does not throw
+ * IOExceptions.
+ */
+ public OutputStream newOutputStream(int index) throws IOException {
+ if (index < 0 || index >= valueCount) {
+ throw new IllegalArgumentException("Expected index " + index + " to "
+ + "be greater than 0 and less than the maximum value count "
+ + "of " + valueCount);
+ }
+ synchronized (DiskLruCache.this) {
+ if (entry.currentEditor != this) {
+ throw new IllegalStateException();
+ }
+ if (!entry.readable) {
+ written[index] = true;
+ }
+ File dirtyFile = entry.getDirtyFile(index);
+ FileOutputStream outputStream;
+ try {
+ outputStream = new FileOutputStream(dirtyFile);
+ } catch (FileNotFoundException e) {
+ // Attempt to recreate the cache directory.
+ directory.mkdirs();
+ try {
+ outputStream = new FileOutputStream(dirtyFile);
+ } catch (FileNotFoundException e2) {
+ // We are unable to recover. Silently eat the writes.
+ return NULL_OUTPUT_STREAM;
+ }
+ }
+ return new FaultHidingOutputStream(outputStream);
+ }
+ }
+
+ /** Sets the value at {@code index} to {@code value}. */
+ public void set(int index, String value) throws IOException {
+ Writer writer = null;
+ try {
+ writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
+ writer.write(value);
+ } finally {
+ Util.closeQuietly(writer);
+ }
+ }
+
+ /**
+ * Commits this edit so it is visible to readers. This releases the
+ * edit lock so another edit may be started on the same key.
+ */
+ public void commit() throws IOException {
+ if (hasErrors) {
+ completeEdit(this, false);
+ remove(entry.key); // The previous entry is stale.
+ } else {
+ completeEdit(this, true);
+ }
+ committed = true;
+ }
+
+ /**
+ * Aborts this edit. This releases the edit lock so another edit may be
+ * started on the same key.
+ */
+ public void abort() throws IOException {
+ completeEdit(this, false);
+ }
+
+ public void abortUnlessCommitted() {
+ if (!committed) {
+ try {
+ abort();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+
+ private class FaultHidingOutputStream extends FilterOutputStream {
+ private FaultHidingOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ @Override public void write(int oneByte) {
+ try {
+ out.write(oneByte);
+ } catch (IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override public void write(byte[] buffer, int offset, int length) {
+ try {
+ out.write(buffer, offset, length);
+ } catch (IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override public void close() {
+ try {
+ out.close();
+ } catch (IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override public void flush() {
+ try {
+ out.flush();
+ } catch (IOException e) {
+ hasErrors = true;
+ }
+ }
+ }
+ }
+
+ private final class Entry {
+ private final String key;
+
+ /** Lengths of this entry's files. */
+ private final long[] lengths;
+
+ /** True if this entry has ever been published. */
+ private boolean readable;
+
+ /** The ongoing edit or null if this entry is not being edited. */
+ private Editor currentEditor;
+
+ /** The sequence number of the most recently committed edit to this entry. */
+ private long sequenceNumber;
+
+ private Entry(String key) {
+ this.key = key;
+ this.lengths = new long[valueCount];
+ }
+
+ public String getLengths() throws IOException {
+ StringBuilder result = new StringBuilder();
+ for (long size : lengths) {
+ result.append(' ').append(size);
+ }
+ return result.toString();
+ }
+
+ /** Set lengths using decimal numbers like "10123". */
+ private void setLengths(String[] strings) throws IOException {
+ if (strings.length != valueCount) {
+ throw invalidLengths(strings);
+ }
+
+ try {
+ for (int i = 0; i < strings.length; i++) {
+ lengths[i] = Long.parseLong(strings[i]);
+ }
+ } catch (NumberFormatException e) {
+ throw invalidLengths(strings);
+ }
+ }
+
+ private IOException invalidLengths(String[] strings) throws IOException {
+ throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
+ }
+
+ public File getCleanFile(int i) {
+ return new File(directory, key + "." + i);
+ }
+
+ public File getDirtyFile(int i) {
+ return new File(directory, key + "." + i + ".tmp");
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/jakewharton/disklrucache/StrictLineReader.java b/mobile/android/thirdparty/com/jakewharton/disklrucache/StrictLineReader.java
new file mode 100644
index 000000000..c90691ce4
--- /dev/null
+++ b/mobile/android/thirdparty/com/jakewharton/disklrucache/StrictLineReader.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.jakewharton.disklrucache;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+/**
+ * Buffers input from an {@link InputStream} for reading lines.
+ *
+ * <p>This class is used for buffered reading of lines. For purposes of this class, a line ends
+ * with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
+ * line at end of input is invalid and will be ignored, the caller may use {@code
+ * hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
+ *
+ * <p>This class is intended for reading input that strictly consists of lines, such as line-based
+ * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
+ * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
+ * end-of-input reporting and a more restrictive definition of a line.
+ *
+ * <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
+ * and 10, respectively, and the representation of no other character contains these values.
+ * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
+ * The default charset is US_ASCII.
+ */
+class StrictLineReader implements Closeable {
+ private static final byte CR = (byte) '\r';
+ private static final byte LF = (byte) '\n';
+
+ private final InputStream in;
+ private final Charset charset;
+
+ /*
+ * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
+ * and the data in the range [pos, end) is buffered for reading. At end of input, if there is
+ * an unterminated line, we set end == -1, otherwise end == pos. If the underlying
+ * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
+ */
+ private byte[] buf;
+ private int pos;
+ private int end;
+
+ /**
+ * Constructs a new {@code LineReader} with the specified charset and the default capacity.
+ *
+ * @param in the {@code InputStream} to read data from.
+ * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
+ * supported.
+ * @throws NullPointerException if {@code in} or {@code charset} is null.
+ * @throws IllegalArgumentException if the specified charset is not supported.
+ */
+ public StrictLineReader(InputStream in, Charset charset) {
+ this(in, 8192, charset);
+ }
+
+ /**
+ * Constructs a new {@code LineReader} with the specified capacity and charset.
+ *
+ * @param in the {@code InputStream} to read data from.
+ * @param capacity the capacity of the buffer.
+ * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
+ * supported.
+ * @throws NullPointerException if {@code in} or {@code charset} is null.
+ * @throws IllegalArgumentException if {@code capacity} is negative or zero
+ * or the specified charset is not supported.
+ */
+ public StrictLineReader(InputStream in, int capacity, Charset charset) {
+ if (in == null || charset == null) {
+ throw new NullPointerException();
+ }
+ if (capacity < 0) {
+ throw new IllegalArgumentException("capacity <= 0");
+ }
+ if (!(charset.equals(Util.US_ASCII))) {
+ throw new IllegalArgumentException("Unsupported encoding");
+ }
+
+ this.in = in;
+ this.charset = charset;
+ buf = new byte[capacity];
+ }
+
+ /**
+ * Closes the reader by closing the underlying {@code InputStream} and
+ * marking this reader as closed.
+ *
+ * @throws IOException for errors when closing the underlying {@code InputStream}.
+ */
+ public void close() throws IOException {
+ synchronized (in) {
+ if (buf != null) {
+ buf = null;
+ in.close();
+ }
+ }
+ }
+
+ /**
+ * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
+ * this end of line marker is not included in the result.
+ *
+ * @return the next line from the input.
+ * @throws IOException for underlying {@code InputStream} errors.
+ * @throws EOFException for the end of source stream.
+ */
+ public String readLine() throws IOException {
+ synchronized (in) {
+ if (buf == null) {
+ throw new IOException("LineReader is closed");
+ }
+
+ // Read more data if we are at the end of the buffered data.
+ // Though it's an error to read after an exception, we will let {@code fillBuf()}
+ // throw again if that happens; thus we need to handle end == -1 as well as end == pos.
+ if (pos >= end) {
+ fillBuf();
+ }
+ // Try to find LF in the buffered data and return the line if successful.
+ for (int i = pos; i != end; ++i) {
+ if (buf[i] == LF) {
+ int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
+ String res = new String(buf, pos, lineEnd - pos, charset.name());
+ pos = i + 1;
+ return res;
+ }
+ }
+
+ // Let's anticipate up to 80 characters on top of those already read.
+ ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
+ @Override
+ public String toString() {
+ int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
+ try {
+ return new String(buf, 0, length, charset.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e); // Since we control the charset this will never happen.
+ }
+ }
+ };
+
+ while (true) {
+ out.write(buf, pos, end - pos);
+ // Mark unterminated line in case fillBuf throws EOFException or IOException.
+ end = -1;
+ fillBuf();
+ // Try to find LF in the buffered data and return the line if successful.
+ for (int i = pos; i != end; ++i) {
+ if (buf[i] == LF) {
+ if (i != pos) {
+ out.write(buf, pos, i - pos);
+ }
+ pos = i + 1;
+ return out.toString();
+ }
+ }
+ }
+ }
+ }
+
+ public boolean hasUnterminatedLine() {
+ return end == -1;
+ }
+
+ /**
+ * Reads new input data into the buffer. Call only with pos == end or end == -1,
+ * depending on the desired outcome if the function throws.
+ */
+ private void fillBuf() throws IOException {
+ int result = in.read(buf, 0, buf.length);
+ if (result == -1) {
+ throw new EOFException();
+ }
+ pos = 0;
+ end = result;
+ }
+}
+
diff --git a/mobile/android/thirdparty/com/jakewharton/disklrucache/Util.java b/mobile/android/thirdparty/com/jakewharton/disklrucache/Util.java
new file mode 100644
index 000000000..0a7dba9bc
--- /dev/null
+++ b/mobile/android/thirdparty/com/jakewharton/disklrucache/Util.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.jakewharton.disklrucache;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.nio.charset.Charset;
+
+/** Junk drawer of utility methods. */
+final class Util {
+ static final Charset US_ASCII = Charset.forName("US-ASCII");
+ static final Charset UTF_8 = Charset.forName("UTF-8");
+
+ private Util() {
+ }
+
+ static String readFully(Reader reader) throws IOException {
+ try {
+ StringWriter writer = new StringWriter();
+ char[] buffer = new char[1024];
+ int count;
+ while ((count = reader.read(buffer)) != -1) {
+ writer.write(buffer, 0, count);
+ }
+ return writer.toString();
+ } finally {
+ reader.close();
+ }
+ }
+
+ /**
+ * Deletes the contents of {@code dir}. Throws an IOException if any file
+ * could not be deleted, or if {@code dir} is not a readable directory.
+ */
+ static void deleteContents(File dir) throws IOException {
+ File[] files = dir.listFiles();
+ if (files == null) {
+ throw new IOException("not a readable directory: " + dir);
+ }
+ for (File file : files) {
+ if (file.isDirectory()) {
+ deleteContents(file);
+ }
+ if (!file.delete()) {
+ throw new IOException("failed to delete file: " + file);
+ }
+ }
+ }
+
+ static void closeQuietly(/*Auto*/Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (RuntimeException rethrown) {
+ throw rethrown;
+ } catch (Exception ignored) {
+ }
+ }
+ }
+}
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);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/leakcanary/LeakCanary.java b/mobile/android/thirdparty/com/squareup/leakcanary/LeakCanary.java
new file mode 100644
index 000000000..a9e44437d
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/leakcanary/LeakCanary.java
@@ -0,0 +1,21 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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 com.squareup.leakcanary;
+
+import android.app.Application;
+
+/**
+ * A no-op version of {@link LeakCanary} that can be used in release builds.
+ */
+public final class LeakCanary {
+ public static RefWatcher install(Application application) {
+ return RefWatcher.DISABLED;
+ }
+
+ private LeakCanary() {
+ throw new AssertionError();
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/leakcanary/RefWatcher.java b/mobile/android/thirdparty/com/squareup/leakcanary/RefWatcher.java
new file mode 100644
index 000000000..3c75dcdee
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/leakcanary/RefWatcher.java
@@ -0,0 +1,20 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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 com.squareup.leakcanary;
+
+/**
+ * No-op implementation of {@link RefWatcher} for release builds. Please use {@link
+ * RefWatcher#DISABLED}.
+ */
+public final class RefWatcher {
+ public static final RefWatcher DISABLED = new RefWatcher();
+
+ private RefWatcher() {}
+
+ public void watch(Object watchedReference) {}
+
+ public void watch(Object watchedReference, String referenceName) {}
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Action.java b/mobile/android/thirdparty/com/squareup/picasso/Action.java
new file mode 100644
index 000000000..5f176d770
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Action.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+
+abstract class Action<T> {
+ static class RequestWeakReference<T> extends WeakReference<T> {
+ final Action action;
+
+ public RequestWeakReference(Action action, T referent, ReferenceQueue<? super T> q) {
+ super(referent, q);
+ this.action = action;
+ }
+ }
+
+ final Picasso picasso;
+ final Request data;
+ final WeakReference<T> target;
+ final boolean skipCache;
+ final boolean noFade;
+ final int errorResId;
+ final Drawable errorDrawable;
+ final String key;
+
+ boolean cancelled;
+
+ Action(Picasso picasso, T target, Request data, boolean skipCache, boolean noFade,
+ int errorResId, Drawable errorDrawable, String key) {
+ this.picasso = picasso;
+ this.data = data;
+ this.target = new RequestWeakReference<T>(this, target, picasso.referenceQueue);
+ this.skipCache = skipCache;
+ this.noFade = noFade;
+ this.errorResId = errorResId;
+ this.errorDrawable = errorDrawable;
+ this.key = key;
+ }
+
+ abstract void complete(Bitmap result, Picasso.LoadedFrom from);
+
+ abstract void error();
+
+ void cancel() {
+ cancelled = true;
+ }
+
+ Request getData() {
+ return data;
+ }
+
+ T getTarget() {
+ return target.get();
+ }
+
+ String getKey() {
+ return key;
+ }
+
+ boolean isCancelled() {
+ return cancelled;
+ }
+
+ Picasso getPicasso() {
+ return picasso;
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/AssetBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/AssetBitmapHunter.java
new file mode 100644
index 000000000..c0245ed3f
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/AssetBitmapHunter.java
@@ -0,0 +1,51 @@
+package com.squareup.picasso;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
+
+class AssetBitmapHunter extends BitmapHunter {
+ private AssetManager assetManager;
+
+ public AssetBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
+ Stats stats, Action action) {
+ super(picasso, dispatcher, cache, stats, action);
+ assetManager = context.getAssets();
+ }
+
+ @Override Bitmap decode(Request data) throws IOException {
+ String filePath = data.uri.toString().substring(ASSET_PREFIX_LENGTH);
+ return decodeAsset(filePath);
+ }
+
+ @Override Picasso.LoadedFrom getLoadedFrom() {
+ return DISK;
+ }
+
+ Bitmap decodeAsset(String filePath) throws IOException {
+ BitmapFactory.Options options = null;
+ if (data.hasSize()) {
+ options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ InputStream is = null;
+ try {
+ is = assetManager.open(filePath);
+ BitmapFactory.decodeStream(is, null, options);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ calculateInSampleSize(data.targetWidth, data.targetHeight, options);
+ }
+ InputStream is = assetManager.open(filePath);
+ try {
+ return BitmapFactory.decodeStream(is, null, options);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/BitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/BitmapHunter.java
new file mode 100644
index 000000000..b8aa87c48
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/BitmapHunter.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.provider.MediaStore;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+
+import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE;
+import static android.content.ContentResolver.SCHEME_CONTENT;
+import static android.content.ContentResolver.SCHEME_FILE;
+import static android.provider.ContactsContract.Contacts;
+import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;
+
+abstract class BitmapHunter implements Runnable {
+
+ /**
+ * Global lock for bitmap decoding to ensure that we are only are decoding one at a time. Since
+ * this will only ever happen in background threads we help avoid excessive memory thrashing as
+ * well as potential OOMs. Shamelessly stolen from Volley.
+ */
+ private static final Object DECODE_LOCK = new Object();
+ private static final String ANDROID_ASSET = "android_asset";
+ protected static final int ASSET_PREFIX_LENGTH =
+ (SCHEME_FILE + ":///" + ANDROID_ASSET + "/").length();
+
+ final Picasso picasso;
+ final Dispatcher dispatcher;
+ final Cache cache;
+ final Stats stats;
+ final String key;
+ final Request data;
+ final List<Action> actions;
+ final boolean skipMemoryCache;
+
+ Bitmap result;
+ Future<?> future;
+ Picasso.LoadedFrom loadedFrom;
+ Exception exception;
+ int exifRotation; // Determined during decoding of original resource.
+
+ BitmapHunter(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action) {
+ this.picasso = picasso;
+ this.dispatcher = dispatcher;
+ this.cache = cache;
+ this.stats = stats;
+ this.key = action.getKey();
+ this.data = action.getData();
+ this.skipMemoryCache = action.skipCache;
+ this.actions = new ArrayList<Action>(4);
+ attach(action);
+ }
+
+ protected void setExifRotation(int exifRotation) {
+ this.exifRotation = exifRotation;
+ }
+
+ @Override public void run() {
+ try {
+ Thread.currentThread().setName(Utils.THREAD_PREFIX + data.getName());
+
+ result = hunt();
+
+ if (result == null) {
+ dispatcher.dispatchFailed(this);
+ } else {
+ dispatcher.dispatchComplete(this);
+ }
+ } catch (Downloader.ResponseException e) {
+ exception = e;
+ dispatcher.dispatchFailed(this);
+ } catch (IOException e) {
+ exception = e;
+ dispatcher.dispatchRetry(this);
+ } catch (OutOfMemoryError e) {
+ StringWriter writer = new StringWriter();
+ stats.createSnapshot().dump(new PrintWriter(writer));
+ exception = new RuntimeException(writer.toString(), e);
+ dispatcher.dispatchFailed(this);
+ } catch (Exception e) {
+ exception = e;
+ dispatcher.dispatchFailed(this);
+ } finally {
+ Thread.currentThread().setName(Utils.THREAD_IDLE_NAME);
+ }
+ }
+
+ abstract Bitmap decode(Request data) throws IOException;
+
+ Bitmap hunt() throws IOException {
+ Bitmap bitmap;
+
+ if (!skipMemoryCache) {
+ bitmap = cache.get(key);
+ if (bitmap != null) {
+ stats.dispatchCacheHit();
+ loadedFrom = MEMORY;
+ return bitmap;
+ }
+ }
+
+ bitmap = decode(data);
+
+ if (bitmap != null) {
+ stats.dispatchBitmapDecoded(bitmap);
+ if (data.needsTransformation() || exifRotation != 0) {
+ synchronized (DECODE_LOCK) {
+ if (data.needsMatrixTransform() || exifRotation != 0) {
+ bitmap = transformResult(data, bitmap, exifRotation);
+ }
+ if (data.hasCustomTransformations()) {
+ bitmap = applyCustomTransformations(data.transformations, bitmap);
+ }
+ }
+ stats.dispatchBitmapTransformed(bitmap);
+ }
+ }
+
+ return bitmap;
+ }
+
+ void attach(Action action) {
+ actions.add(action);
+ }
+
+ void detach(Action action) {
+ actions.remove(action);
+ }
+
+ boolean cancel() {
+ return actions.isEmpty() && future != null && future.cancel(false);
+ }
+
+ boolean isCancelled() {
+ return future != null && future.isCancelled();
+ }
+
+ boolean shouldSkipMemoryCache() {
+ return skipMemoryCache;
+ }
+
+ boolean shouldRetry(boolean airplaneMode, NetworkInfo info) {
+ return false;
+ }
+
+ Bitmap getResult() {
+ return result;
+ }
+
+ String getKey() {
+ return key;
+ }
+
+ Request getData() {
+ return data;
+ }
+
+ List<Action> getActions() {
+ return actions;
+ }
+
+ Exception getException() {
+ return exception;
+ }
+
+ Picasso.LoadedFrom getLoadedFrom() {
+ return loadedFrom;
+ }
+
+ static BitmapHunter forRequest(Context context, Picasso picasso, Dispatcher dispatcher,
+ Cache cache, Stats stats, Action action, Downloader downloader) {
+ if (action.getData().resourceId != 0) {
+ return new ResourceBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ }
+ Uri uri = action.getData().uri;
+ String scheme = uri.getScheme();
+ if (SCHEME_CONTENT.equals(scheme)) {
+ if (Contacts.CONTENT_URI.getHost().equals(uri.getHost()) //
+ && !uri.getPathSegments().contains(Contacts.Photo.CONTENT_DIRECTORY)) {
+ return new ContactsPhotoBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ } else if (MediaStore.AUTHORITY.equals(uri.getAuthority())) {
+ return new MediaStoreBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ } else {
+ return new ContentStreamBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ }
+ } else if (SCHEME_FILE.equals(scheme)) {
+ if (!uri.getPathSegments().isEmpty() && ANDROID_ASSET.equals(uri.getPathSegments().get(0))) {
+ return new AssetBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ }
+ return new FileBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ } else if (SCHEME_ANDROID_RESOURCE.equals(scheme)) {
+ return new ResourceBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ } else {
+ return new NetworkBitmapHunter(picasso, dispatcher, cache, stats, action, downloader);
+ }
+ }
+
+ static void calculateInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options) {
+ calculateInSampleSize(reqWidth, reqHeight, options.outWidth, options.outHeight, options);
+ }
+
+ static void calculateInSampleSize(int reqWidth, int reqHeight, int width, int height,
+ BitmapFactory.Options options) {
+ int sampleSize = 1;
+ if (height > reqHeight || width > reqWidth) {
+ final int heightRatio = Math.round((float) height / (float) reqHeight);
+ final int widthRatio = Math.round((float) width / (float) reqWidth);
+ sampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
+ }
+
+ options.inSampleSize = sampleSize;
+ options.inJustDecodeBounds = false;
+ }
+
+ static Bitmap applyCustomTransformations(List<Transformation> transformations, Bitmap result) {
+ for (int i = 0, count = transformations.size(); i < count; i++) {
+ final Transformation transformation = transformations.get(i);
+ Bitmap newResult = transformation.transform(result);
+
+ if (newResult == null) {
+ final StringBuilder builder = new StringBuilder() //
+ .append("Transformation ")
+ .append(transformation.key())
+ .append(" returned null after ")
+ .append(i)
+ .append(" previous transformation(s).\n\nTransformation list:\n");
+ for (Transformation t : transformations) {
+ builder.append(t.key()).append('\n');
+ }
+ Picasso.HANDLER.post(new Runnable() {
+ @Override public void run() {
+ throw new NullPointerException(builder.toString());
+ }
+ });
+ return null;
+ }
+
+ if (newResult == result && result.isRecycled()) {
+ Picasso.HANDLER.post(new Runnable() {
+ @Override public void run() {
+ throw new IllegalStateException("Transformation "
+ + transformation.key()
+ + " returned input Bitmap but recycled it.");
+ }
+ });
+ return null;
+ }
+
+ // If the transformation returned a new bitmap ensure they recycled the original.
+ if (newResult != result && !result.isRecycled()) {
+ Picasso.HANDLER.post(new Runnable() {
+ @Override public void run() {
+ throw new IllegalStateException("Transformation "
+ + transformation.key()
+ + " mutated input Bitmap but failed to recycle the original.");
+ }
+ });
+ return null;
+ }
+
+ result = newResult;
+ }
+ return result;
+ }
+
+ static Bitmap transformResult(Request data, Bitmap result, int exifRotation) {
+ int inWidth = result.getWidth();
+ int inHeight = result.getHeight();
+
+ int drawX = 0;
+ int drawY = 0;
+ int drawWidth = inWidth;
+ int drawHeight = inHeight;
+
+ Matrix matrix = new Matrix();
+
+ if (data.needsMatrixTransform()) {
+ int targetWidth = data.targetWidth;
+ int targetHeight = data.targetHeight;
+
+ float targetRotation = data.rotationDegrees;
+ if (targetRotation != 0) {
+ if (data.hasRotationPivot) {
+ matrix.setRotate(targetRotation, data.rotationPivotX, data.rotationPivotY);
+ } else {
+ matrix.setRotate(targetRotation);
+ }
+ }
+
+ if (data.centerCrop) {
+ float widthRatio = targetWidth / (float) inWidth;
+ float heightRatio = targetHeight / (float) inHeight;
+ float scale;
+ if (widthRatio > heightRatio) {
+ scale = widthRatio;
+ int newSize = (int) Math.ceil(inHeight * (heightRatio / widthRatio));
+ drawY = (inHeight - newSize) / 2;
+ drawHeight = newSize;
+ } else {
+ scale = heightRatio;
+ int newSize = (int) Math.ceil(inWidth * (widthRatio / heightRatio));
+ drawX = (inWidth - newSize) / 2;
+ drawWidth = newSize;
+ }
+ matrix.preScale(scale, scale);
+ } else if (data.centerInside) {
+ float widthRatio = targetWidth / (float) inWidth;
+ float heightRatio = targetHeight / (float) inHeight;
+ float scale = widthRatio < heightRatio ? widthRatio : heightRatio;
+ matrix.preScale(scale, scale);
+ } else if (targetWidth != 0 && targetHeight != 0 //
+ && (targetWidth != inWidth || targetHeight != inHeight)) {
+ // If an explicit target size has been specified and they do not match the results bounds,
+ // pre-scale the existing matrix appropriately.
+ float sx = targetWidth / (float) inWidth;
+ float sy = targetHeight / (float) inHeight;
+ matrix.preScale(sx, sy);
+ }
+ }
+
+ if (exifRotation != 0) {
+ matrix.preRotate(exifRotation);
+ }
+
+ Bitmap newResult =
+ Bitmap.createBitmap(result, drawX, drawY, drawWidth, drawHeight, matrix, true);
+ if (newResult != result) {
+ result.recycle();
+ result = newResult;
+ }
+
+ return result;
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Cache.java b/mobile/android/thirdparty/com/squareup/picasso/Cache.java
new file mode 100644
index 000000000..bce297f9e
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Cache.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.graphics.Bitmap;
+
+/**
+ * A memory cache for storing the most recently used images.
+ * <p/>
+ * <em>Note:</em> The {@link Cache} is accessed by multiple threads. You must ensure
+ * your {@link Cache} implementation is thread safe when {@link Cache#get(String)} or {@link
+ * Cache#set(String, android.graphics.Bitmap)} is called.
+ */
+public interface Cache {
+ /** Retrieve an image for the specified {@code key} or {@code null}. */
+ Bitmap get(String key);
+
+ /** Store an image in the cache for the specified {@code key}. */
+ void set(String key, Bitmap bitmap);
+
+ /** Returns the current size of the cache in bytes. */
+ int size();
+
+ /** Returns the maximum size in bytes that the cache can hold. */
+ int maxSize();
+
+ /** Clears the cache. */
+ void clear();
+
+ /** A cache which does not store any values. */
+ Cache NONE = new Cache() {
+ @Override public Bitmap get(String key) {
+ return null;
+ }
+
+ @Override public void set(String key, Bitmap bitmap) {
+ // Ignore.
+ }
+
+ @Override public int size() {
+ return 0;
+ }
+
+ @Override public int maxSize() {
+ return 0;
+ }
+
+ @Override public void clear() {
+ }
+ };
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Callback.java b/mobile/android/thirdparty/com/squareup/picasso/Callback.java
new file mode 100644
index 000000000..d93620889
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Callback.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+public interface Callback {
+ void onSuccess();
+
+ void onError();
+
+ public static class EmptyCallback implements Callback {
+
+ @Override public void onSuccess() {
+ }
+
+ @Override public void onError() {
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/ContactsPhotoBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/ContactsPhotoBitmapHunter.java
new file mode 100644
index 000000000..9444167b4
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/ContactsPhotoBitmapHunter.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
+import static android.provider.ContactsContract.Contacts.openContactPhotoInputStream;
+import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
+
+class ContactsPhotoBitmapHunter extends BitmapHunter {
+ /** A lookup uri (e.g. content://com.android.contacts/contacts/lookup/3570i61d948d30808e537) */
+ private static final int ID_LOOKUP = 1;
+ /** A contact thumbnail uri (e.g. content://com.android.contacts/contacts/38/photo) */
+ private static final int ID_THUMBNAIL = 2;
+ /** A contact uri (e.g. content://com.android.contacts/contacts/38) */
+ private static final int ID_CONTACT = 3;
+ /**
+ * A contact display photo (high resolution) uri
+ * (e.g. content://com.android.contacts/display_photo/5)
+ */
+ private static final int ID_DISPLAY_PHOTO = 4;
+
+ private static final UriMatcher matcher;
+
+ static {
+ matcher = new UriMatcher(UriMatcher.NO_MATCH);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", ID_LOOKUP);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", ID_LOOKUP);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", ID_THUMBNAIL);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", ID_CONTACT);
+ matcher.addURI(ContactsContract.AUTHORITY, "display_photo/#", ID_DISPLAY_PHOTO);
+ }
+
+ final Context context;
+
+ ContactsPhotoBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
+ Stats stats, Action action) {
+ super(picasso, dispatcher, cache, stats, action);
+ this.context = context;
+ }
+
+ @Override Bitmap decode(Request data) throws IOException {
+ InputStream is = null;
+ try {
+ is = getInputStream();
+ return decodeStream(is, data);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ }
+
+ @Override Picasso.LoadedFrom getLoadedFrom() {
+ return DISK;
+ }
+
+ private InputStream getInputStream() throws IOException {
+ ContentResolver contentResolver = context.getContentResolver();
+ Uri uri = getData().uri;
+ switch (matcher.match(uri)) {
+ case ID_LOOKUP:
+ uri = ContactsContract.Contacts.lookupContact(contentResolver, uri);
+ if (uri == null) {
+ return null;
+ }
+ // Resolved the uri to a contact uri, intentionally fall through to process the resolved uri
+ case ID_CONTACT:
+ if (SDK_INT < ICE_CREAM_SANDWICH) {
+ return openContactPhotoInputStream(contentResolver, uri);
+ } else {
+ return ContactPhotoStreamIcs.get(contentResolver, uri);
+ }
+ case ID_THUMBNAIL:
+ case ID_DISPLAY_PHOTO:
+ return contentResolver.openInputStream(uri);
+ default:
+ throw new IllegalStateException("Invalid uri: " + uri);
+ }
+ }
+
+ private Bitmap decodeStream(InputStream stream, Request data) throws IOException {
+ if (stream == null) {
+ return null;
+ }
+ BitmapFactory.Options options = null;
+ if (data.hasSize()) {
+ options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ InputStream is = getInputStream();
+ try {
+ BitmapFactory.decodeStream(is, null, options);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ calculateInSampleSize(data.targetWidth, data.targetHeight, options);
+ }
+ return BitmapFactory.decodeStream(stream, null, options);
+ }
+
+ @TargetApi(ICE_CREAM_SANDWICH)
+ private static class ContactPhotoStreamIcs {
+ static InputStream get(ContentResolver contentResolver, Uri uri) {
+ return openContactPhotoInputStream(contentResolver, uri, true);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/ContentStreamBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/ContentStreamBitmapHunter.java
new file mode 100644
index 000000000..624ffe078
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/ContentStreamBitmapHunter.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
+
+class ContentStreamBitmapHunter extends BitmapHunter {
+ final Context context;
+
+ ContentStreamBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
+ Stats stats, Action action) {
+ super(picasso, dispatcher, cache, stats, action);
+ this.context = context;
+ }
+
+ @Override Bitmap decode(Request data)
+ throws IOException {
+ return decodeContentStream(data);
+ }
+
+ @Override Picasso.LoadedFrom getLoadedFrom() {
+ return DISK;
+ }
+
+ protected Bitmap decodeContentStream(Request data) throws IOException {
+ ContentResolver contentResolver = context.getContentResolver();
+ BitmapFactory.Options options = null;
+ if (data.hasSize()) {
+ options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ InputStream is = null;
+ try {
+ is = contentResolver.openInputStream(data.uri);
+ BitmapFactory.decodeStream(is, null, options);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ calculateInSampleSize(data.targetWidth, data.targetHeight, options);
+ }
+ InputStream is = contentResolver.openInputStream(data.uri);
+ try {
+ return BitmapFactory.decodeStream(is, null, options);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/DeferredRequestCreator.java b/mobile/android/thirdparty/com/squareup/picasso/DeferredRequestCreator.java
new file mode 100644
index 000000000..fbdaab1c3
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/DeferredRequestCreator.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.view.ViewTreeObserver;
+import android.widget.ImageView;
+import java.lang.ref.WeakReference;
+
+class DeferredRequestCreator implements ViewTreeObserver.OnPreDrawListener {
+
+ final RequestCreator creator;
+ final WeakReference<ImageView> target;
+ Callback callback;
+
+ DeferredRequestCreator(RequestCreator creator, ImageView target, Callback callback) {
+ this.creator = creator;
+ this.target = new WeakReference<ImageView>(target);
+ this.callback = callback;
+ target.getViewTreeObserver().addOnPreDrawListener(this);
+ }
+
+ @Override public boolean onPreDraw() {
+ ImageView target = this.target.get();
+ if (target == null) {
+ return true;
+ }
+ ViewTreeObserver vto = target.getViewTreeObserver();
+ if (!vto.isAlive()) {
+ return true;
+ }
+
+ int width = target.getMeasuredWidth();
+ int height = target.getMeasuredHeight();
+
+ if (width <= 0 || height <= 0) {
+ return true;
+ }
+
+ vto.removeOnPreDrawListener(this);
+
+ this.creator.unfit().resize(width, height).into(target, callback);
+ return true;
+ }
+
+ void cancel() {
+ callback = null;
+ ImageView target = this.target.get();
+ if (target == null) {
+ return;
+ }
+ ViewTreeObserver vto = target.getViewTreeObserver();
+ if (!vto.isAlive()) {
+ return;
+ }
+ vto.removeOnPreDrawListener(this);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Dispatcher.java b/mobile/android/thirdparty/com/squareup/picasso/Dispatcher.java
new file mode 100644
index 000000000..6401431fb
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Dispatcher.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.Manifest;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+
+import static android.content.Context.CONNECTIVITY_SERVICE;
+import static android.content.Intent.ACTION_AIRPLANE_MODE_CHANGED;
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+import static com.squareup.picasso.BitmapHunter.forRequest;
+
+class Dispatcher {
+ private static final int RETRY_DELAY = 500;
+ private static final int AIRPLANE_MODE_ON = 1;
+ private static final int AIRPLANE_MODE_OFF = 0;
+
+ static final int REQUEST_SUBMIT = 1;
+ static final int REQUEST_CANCEL = 2;
+ static final int REQUEST_GCED = 3;
+ static final int HUNTER_COMPLETE = 4;
+ static final int HUNTER_RETRY = 5;
+ static final int HUNTER_DECODE_FAILED = 6;
+ static final int HUNTER_DELAY_NEXT_BATCH = 7;
+ static final int HUNTER_BATCH_COMPLETE = 8;
+ static final int NETWORK_STATE_CHANGE = 9;
+ static final int AIRPLANE_MODE_CHANGE = 10;
+
+ private static final String DISPATCHER_THREAD_NAME = "Dispatcher";
+ private static final int BATCH_DELAY = 200; // ms
+
+ final DispatcherThread dispatcherThread;
+ final Context context;
+ final ExecutorService service;
+ final Downloader downloader;
+ final Map<String, BitmapHunter> hunterMap;
+ final Handler handler;
+ final Handler mainThreadHandler;
+ final Cache cache;
+ final Stats stats;
+ final List<BitmapHunter> batch;
+ final NetworkBroadcastReceiver receiver;
+
+ NetworkInfo networkInfo;
+ boolean airplaneMode;
+
+ Dispatcher(Context context, ExecutorService service, Handler mainThreadHandler,
+ Downloader downloader, Cache cache, Stats stats) {
+ this.dispatcherThread = new DispatcherThread();
+ this.dispatcherThread.start();
+ this.context = context;
+ this.service = service;
+ this.hunterMap = new LinkedHashMap<String, BitmapHunter>();
+ this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
+ this.downloader = downloader;
+ this.mainThreadHandler = mainThreadHandler;
+ this.cache = cache;
+ this.stats = stats;
+ this.batch = new ArrayList<BitmapHunter>(4);
+ this.airplaneMode = Utils.isAirplaneModeOn(this.context);
+ this.receiver = new NetworkBroadcastReceiver(this.context);
+ receiver.register();
+ }
+
+ void shutdown() {
+ service.shutdown();
+ dispatcherThread.quit();
+ receiver.unregister();
+ }
+
+ void dispatchSubmit(Action action) {
+ handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
+ }
+
+ void dispatchCancel(Action action) {
+ handler.sendMessage(handler.obtainMessage(REQUEST_CANCEL, action));
+ }
+
+ void dispatchComplete(BitmapHunter hunter) {
+ handler.sendMessage(handler.obtainMessage(HUNTER_COMPLETE, hunter));
+ }
+
+ void dispatchRetry(BitmapHunter hunter) {
+ handler.sendMessageDelayed(handler.obtainMessage(HUNTER_RETRY, hunter), RETRY_DELAY);
+ }
+
+ void dispatchFailed(BitmapHunter hunter) {
+ handler.sendMessage(handler.obtainMessage(HUNTER_DECODE_FAILED, hunter));
+ }
+
+ void dispatchNetworkStateChange(NetworkInfo info) {
+ handler.sendMessage(handler.obtainMessage(NETWORK_STATE_CHANGE, info));
+ }
+
+ void dispatchAirplaneModeChange(boolean airplaneMode) {
+ handler.sendMessage(handler.obtainMessage(AIRPLANE_MODE_CHANGE,
+ airplaneMode ? AIRPLANE_MODE_ON : AIRPLANE_MODE_OFF, 0));
+ }
+
+ void performSubmit(Action action) {
+ BitmapHunter hunter = hunterMap.get(action.getKey());
+ if (hunter != null) {
+ hunter.attach(action);
+ return;
+ }
+
+ if (service.isShutdown()) {
+ return;
+ }
+
+ hunter = forRequest(context, action.getPicasso(), this, cache, stats, action, downloader);
+ hunter.future = service.submit(hunter);
+ hunterMap.put(action.getKey(), hunter);
+ }
+
+ void performCancel(Action action) {
+ String key = action.getKey();
+ BitmapHunter hunter = hunterMap.get(key);
+ if (hunter != null) {
+ hunter.detach(action);
+ if (hunter.cancel()) {
+ hunterMap.remove(key);
+ }
+ }
+ }
+
+ void performRetry(BitmapHunter hunter) {
+ if (hunter.isCancelled()) return;
+
+ if (service.isShutdown()) {
+ performError(hunter);
+ return;
+ }
+
+ if (hunter.shouldRetry(airplaneMode, networkInfo)) {
+ hunter.future = service.submit(hunter);
+ } else {
+ performError(hunter);
+ }
+ }
+
+ void performComplete(BitmapHunter hunter) {
+ if (!hunter.shouldSkipMemoryCache()) {
+ cache.set(hunter.getKey(), hunter.getResult());
+ }
+ hunterMap.remove(hunter.getKey());
+ batch(hunter);
+ }
+
+ void performBatchComplete() {
+ List<BitmapHunter> copy = new ArrayList<BitmapHunter>(batch);
+ batch.clear();
+ mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy));
+ }
+
+ void performError(BitmapHunter hunter) {
+ hunterMap.remove(hunter.getKey());
+ batch(hunter);
+ }
+
+ void performAirplaneModeChange(boolean airplaneMode) {
+ this.airplaneMode = airplaneMode;
+ }
+
+ void performNetworkStateChange(NetworkInfo info) {
+ networkInfo = info;
+ if (service instanceof PicassoExecutorService) {
+ ((PicassoExecutorService) service).adjustThreadCount(info);
+ }
+ }
+
+ private void batch(BitmapHunter hunter) {
+ if (hunter.isCancelled()) {
+ return;
+ }
+ batch.add(hunter);
+ if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) {
+ handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY);
+ }
+ }
+
+ private static class DispatcherHandler extends Handler {
+ private final Dispatcher dispatcher;
+
+ public DispatcherHandler(Looper looper, Dispatcher dispatcher) {
+ super(looper);
+ this.dispatcher = dispatcher;
+ }
+
+ @Override public void handleMessage(final Message msg) {
+ switch (msg.what) {
+ case REQUEST_SUBMIT: {
+ Action action = (Action) msg.obj;
+ dispatcher.performSubmit(action);
+ break;
+ }
+ case REQUEST_CANCEL: {
+ Action action = (Action) msg.obj;
+ dispatcher.performCancel(action);
+ break;
+ }
+ case HUNTER_COMPLETE: {
+ BitmapHunter hunter = (BitmapHunter) msg.obj;
+ dispatcher.performComplete(hunter);
+ break;
+ }
+ case HUNTER_RETRY: {
+ BitmapHunter hunter = (BitmapHunter) msg.obj;
+ dispatcher.performRetry(hunter);
+ break;
+ }
+ case HUNTER_DECODE_FAILED: {
+ BitmapHunter hunter = (BitmapHunter) msg.obj;
+ dispatcher.performError(hunter);
+ break;
+ }
+ case HUNTER_DELAY_NEXT_BATCH: {
+ dispatcher.performBatchComplete();
+ break;
+ }
+ case NETWORK_STATE_CHANGE: {
+ NetworkInfo info = (NetworkInfo) msg.obj;
+ dispatcher.performNetworkStateChange(info);
+ break;
+ }
+ case AIRPLANE_MODE_CHANGE: {
+ dispatcher.performAirplaneModeChange(msg.arg1 == AIRPLANE_MODE_ON);
+ break;
+ }
+ default:
+ Picasso.HANDLER.post(new Runnable() {
+ @Override public void run() {
+ throw new AssertionError("Unknown handler message received: " + msg.what);
+ }
+ });
+ }
+ }
+ }
+
+ static class DispatcherThread extends HandlerThread {
+ DispatcherThread() {
+ super(Utils.THREAD_PREFIX + DISPATCHER_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
+ }
+ }
+
+ private class NetworkBroadcastReceiver extends BroadcastReceiver {
+ private static final String EXTRA_AIRPLANE_STATE = "state";
+
+ private final ConnectivityManager connectivityManager;
+
+ NetworkBroadcastReceiver(Context context) {
+ connectivityManager = (ConnectivityManager) context.getSystemService(CONNECTIVITY_SERVICE);
+ }
+
+ void register() {
+ boolean shouldScanState = service instanceof PicassoExecutorService && //
+ Utils.hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_AIRPLANE_MODE_CHANGED);
+ if (shouldScanState) {
+ filter.addAction(CONNECTIVITY_ACTION);
+ }
+ context.registerReceiver(this, filter);
+ }
+
+ void unregister() {
+ context.unregisterReceiver(this);
+ }
+
+ @Override public void onReceive(Context context, Intent intent) {
+ // On some versions of Android this may be called with a null Intent
+ if (null == intent) {
+ return;
+ }
+
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+
+ if (ACTION_AIRPLANE_MODE_CHANGED.equals(action)) {
+ dispatchAirplaneModeChange(extras.getBoolean(EXTRA_AIRPLANE_STATE, false));
+ } else if (CONNECTIVITY_ACTION.equals(action)) {
+ dispatchNetworkStateChange(connectivityManager.getActiveNetworkInfo());
+ }
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Downloader.java b/mobile/android/thirdparty/com/squareup/picasso/Downloader.java
new file mode 100644
index 000000000..33a909371
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Downloader.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** A mechanism to load images from external resources such as a disk cache and/or the internet. */
+public interface Downloader {
+ /**
+ * Download the specified image {@code url} from the internet.
+ *
+ * @param uri Remote image URL.
+ * @param localCacheOnly If {@code true} the URL should only be loaded if available in a local
+ * disk cache.
+ * @return {@link Response} containing either a {@link Bitmap} representation of the request or an
+ * {@link InputStream} for the image data. {@code null} can be returned to indicate a problem
+ * loading the bitmap.
+ * @throws IOException if the requested URL cannot successfully be loaded.
+ */
+ Response load(Uri uri, boolean localCacheOnly) throws IOException;
+
+ /** Thrown for non-2XX responses. */
+ class ResponseException extends IOException {
+ public ResponseException(String message) {
+ super(message);
+ }
+ }
+
+ /** Response stream or bitmap and info. */
+ class Response {
+ final InputStream stream;
+ final Bitmap bitmap;
+ final boolean cached;
+
+ /**
+ * Response image and info.
+ *
+ * @param bitmap Image.
+ * @param loadedFromCache {@code true} if the source of the image is from a local disk cache.
+ */
+ public Response(Bitmap bitmap, boolean loadedFromCache) {
+ if (bitmap == null) {
+ throw new IllegalArgumentException("Bitmap may not be null.");
+ }
+ this.stream = null;
+ this.bitmap = bitmap;
+ this.cached = loadedFromCache;
+ }
+
+ /**
+ * Response stream and info.
+ *
+ * @param stream Image data stream.
+ * @param loadedFromCache {@code true} if the source of the stream is from a local disk cache.
+ */
+ public Response(InputStream stream, boolean loadedFromCache) {
+ if (stream == null) {
+ throw new IllegalArgumentException("Stream may not be null.");
+ }
+ this.stream = stream;
+ this.bitmap = null;
+ this.cached = loadedFromCache;
+ }
+
+ /**
+ * Input stream containing image data.
+ * <p>
+ * If this returns {@code null}, image data will be available via {@link #getBitmap()}.
+ */
+ public InputStream getInputStream() {
+ return stream;
+ }
+
+ /**
+ * Bitmap representing the image.
+ * <p>
+ * If this returns {@code null}, image data will be available via {@link #getInputStream()}.
+ */
+ public Bitmap getBitmap() {
+ return bitmap;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/FetchAction.java b/mobile/android/thirdparty/com/squareup/picasso/FetchAction.java
new file mode 100644
index 000000000..d8f1c3fb4
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/FetchAction.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.graphics.Bitmap;
+
+class FetchAction extends Action<Void> {
+ FetchAction(Picasso picasso, Request data, boolean skipCache, String key) {
+ super(picasso, null, data, skipCache, false, 0, null, key);
+ }
+
+ @Override void complete(Bitmap result, Picasso.LoadedFrom from) {
+ }
+
+ @Override public void error() {
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/FileBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/FileBitmapHunter.java
new file mode 100644
index 000000000..dac38fb80
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/FileBitmapHunter.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.media.ExifInterface;
+import android.net.Uri;
+import java.io.IOException;
+
+import static android.media.ExifInterface.ORIENTATION_NORMAL;
+import static android.media.ExifInterface.ORIENTATION_ROTATE_180;
+import static android.media.ExifInterface.ORIENTATION_ROTATE_270;
+import static android.media.ExifInterface.ORIENTATION_ROTATE_90;
+import static android.media.ExifInterface.TAG_ORIENTATION;
+
+class FileBitmapHunter extends ContentStreamBitmapHunter {
+
+ FileBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
+ Stats stats, Action action) {
+ super(context, picasso, dispatcher, cache, stats, action);
+ }
+
+ @Override Bitmap decode(Request data)
+ throws IOException {
+ setExifRotation(getFileExifRotation(data.uri));
+ return super.decode(data);
+ }
+
+ static int getFileExifRotation(Uri uri) throws IOException {
+ ExifInterface exifInterface = new ExifInterface(uri.getPath());
+ int orientation = exifInterface.getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
+ switch (orientation) {
+ case ORIENTATION_ROTATE_90:
+ return 90;
+ case ORIENTATION_ROTATE_180:
+ return 180;
+ case ORIENTATION_ROTATE_270:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/GetAction.java b/mobile/android/thirdparty/com/squareup/picasso/GetAction.java
new file mode 100644
index 000000000..e30024e89
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/GetAction.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.graphics.Bitmap;
+
+class GetAction extends Action<Void> {
+ GetAction(Picasso picasso, Request data, boolean skipCache, String key) {
+ super(picasso, null, data, skipCache, false, 0, null, key);
+ }
+
+ @Override void complete(Bitmap result, Picasso.LoadedFrom from) {
+ }
+
+ @Override public void error() {
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/ImageViewAction.java b/mobile/android/thirdparty/com/squareup/picasso/ImageViewAction.java
new file mode 100644
index 000000000..16f550907
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/ImageViewAction.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.widget.ImageView;
+
+class ImageViewAction extends Action<ImageView> {
+
+ Callback callback;
+
+ ImageViewAction(Picasso picasso, ImageView imageView, Request data, boolean skipCache,
+ boolean noFade, int errorResId, Drawable errorDrawable, String key, Callback callback) {
+ super(picasso, imageView, data, skipCache, noFade, errorResId, errorDrawable, key);
+ this.callback = callback;
+ }
+
+ @Override public void complete(Bitmap result, Picasso.LoadedFrom from) {
+ if (result == null) {
+ throw new AssertionError(
+ String.format("Attempted to complete action with no result!\n%s", this));
+ }
+
+ ImageView target = this.target.get();
+ if (target == null) {
+ return;
+ }
+
+ Context context = picasso.context;
+ boolean debugging = picasso.debugging;
+ PicassoDrawable.setBitmap(target, context, result, from, noFade, debugging);
+
+ if (callback != null) {
+ callback.onSuccess();
+ }
+ }
+
+ @Override public void error() {
+ ImageView target = this.target.get();
+ if (target == null) {
+ return;
+ }
+ if (errorResId != 0) {
+ target.setImageResource(errorResId);
+ } else if (errorDrawable != null) {
+ target.setImageDrawable(errorDrawable);
+ }
+
+ if (callback != null) {
+ callback.onError();
+ }
+ }
+
+ @Override void cancel() {
+ super.cancel();
+ if (callback != null) {
+ callback = null;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/LruCache.java b/mobile/android/thirdparty/com/squareup/picasso/LruCache.java
new file mode 100644
index 000000000..5d5f07fcb
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/LruCache.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.squareup.picasso;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** A memory cache which uses a least-recently used eviction policy. */
+public class LruCache implements Cache {
+ final LinkedHashMap<String, Bitmap> map;
+ private final int maxSize;
+
+ private int size;
+ private int putCount;
+ private int evictionCount;
+ private int hitCount;
+ private int missCount;
+
+ /** Create a cache using an appropriate portion of the available RAM as the maximum size. */
+ public LruCache(Context context) {
+ this(Utils.calculateMemoryCacheSize(context));
+ }
+
+ /** Create a cache with a given maximum size in bytes. */
+ public LruCache(int maxSize) {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("Max size must be positive.");
+ }
+ this.maxSize = maxSize;
+ this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
+ }
+
+ @Override public Bitmap get(String key) {
+ if (key == null) {
+ throw new NullPointerException("key == null");
+ }
+
+ Bitmap mapValue;
+ synchronized (this) {
+ mapValue = map.get(key);
+ if (mapValue != null) {
+ hitCount++;
+ return mapValue;
+ }
+ missCount++;
+ }
+
+ return null;
+ }
+
+ @Override public void set(String key, Bitmap bitmap) {
+ if (key == null || bitmap == null) {
+ throw new NullPointerException("key == null || bitmap == null");
+ }
+
+ Bitmap previous;
+ synchronized (this) {
+ putCount++;
+ size += Utils.getBitmapBytes(bitmap);
+ previous = map.put(key, bitmap);
+ if (previous != null) {
+ size -= Utils.getBitmapBytes(previous);
+ }
+ }
+
+ trimToSize(maxSize);
+ }
+
+ private void trimToSize(int maxSize) {
+ while (true) {
+ String key;
+ Bitmap value;
+ synchronized (this) {
+ if (size < 0 || (map.isEmpty() && size != 0)) {
+ throw new IllegalStateException(
+ getClass().getName() + ".sizeOf() is reporting inconsistent results!");
+ }
+
+ if (size <= maxSize || map.isEmpty()) {
+ break;
+ }
+
+ Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
+ key = toEvict.getKey();
+ value = toEvict.getValue();
+ map.remove(key);
+ size -= Utils.getBitmapBytes(value);
+ evictionCount++;
+ }
+ }
+ }
+
+ /** Clear the cache. */
+ public final void evictAll() {
+ trimToSize(-1); // -1 will evict 0-sized elements
+ }
+
+ /** Returns the sum of the sizes of the entries in this cache. */
+ public final synchronized int size() {
+ return size;
+ }
+
+ /** Returns the maximum sum of the sizes of the entries in this cache. */
+ public final synchronized int maxSize() {
+ return maxSize;
+ }
+
+ public final synchronized void clear() {
+ evictAll();
+ }
+
+ /** Returns the number of times {@link #get} returned a value. */
+ public final synchronized int hitCount() {
+ return hitCount;
+ }
+
+ /** Returns the number of times {@link #get} returned {@code null}. */
+ public final synchronized int missCount() {
+ return missCount;
+ }
+
+ /** Returns the number of times {@link #set(String, Bitmap)} was called. */
+ public final synchronized int putCount() {
+ return putCount;
+ }
+
+ /** Returns the number of values that have been evicted. */
+ public final synchronized int evictionCount() {
+ return evictionCount;
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/MarkableInputStream.java b/mobile/android/thirdparty/com/squareup/picasso/MarkableInputStream.java
new file mode 100644
index 000000000..17043a1b0
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/MarkableInputStream.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An input stream wrapper that supports unlimited independent cursors for
+ * marking and resetting. Each cursor is a token, and it's the caller's
+ * responsibility to keep track of these.
+ */
+final class MarkableInputStream extends InputStream {
+ private final InputStream in;
+
+ private long offset;
+ private long reset;
+ private long limit;
+
+ private long defaultMark = -1;
+
+ public MarkableInputStream(InputStream in) {
+ if (!in.markSupported()) {
+ in = new BufferedInputStream(in);
+ }
+ this.in = in;
+ }
+
+ /** Marks this place in the stream so we can reset back to it later. */
+ @Override public void mark(int readLimit) {
+ defaultMark = savePosition(readLimit);
+ }
+
+ /**
+ * Returns an opaque token representing the current position in the stream.
+ * Call {@link #reset(long)} to return to this position in the stream later.
+ * It is an error to call {@link #reset(long)} after consuming more than
+ * {@code readLimit} bytes from this stream.
+ */
+ public long savePosition(int readLimit) {
+ long offsetLimit = offset + readLimit;
+ if (limit < offsetLimit) {
+ setLimit(offsetLimit);
+ }
+ return offset;
+ }
+
+ /**
+ * Makes sure that the underlying stream can backtrack the full range from
+ * {@code reset} thru {@code limit}. Since we can't call {@code mark()}
+ * without also adjusting the reset-to-position on the underlying stream this
+ * method resets first and then marks the union of the two byte ranges. On
+ * buffered streams this additional cursor motion shouldn't result in any
+ * additional I/O.
+ */
+ private void setLimit(long limit) {
+ try {
+ if (reset < offset && offset <= this.limit) {
+ in.reset();
+ in.mark((int) (limit - reset));
+ skip(reset, offset);
+ } else {
+ reset = offset;
+ in.mark((int) (limit - offset));
+ }
+ this.limit = limit;
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to mark: " + e);
+ }
+ }
+
+ /** Resets the stream to the most recent {@link #mark mark}. */
+ @Override public void reset() throws IOException {
+ reset(defaultMark);
+ }
+
+ /** Resets the stream to the position recorded by {@code token}. */
+ public void reset(long token) throws IOException {
+ if (offset > limit || token < reset) {
+ throw new IOException("Cannot reset");
+ }
+ in.reset();
+ skip(reset, token);
+ offset = token;
+ }
+
+ /** Skips {@code target - current} bytes and returns. */
+ private void skip(long current, long target) throws IOException {
+ while (current < target) {
+ long skipped = in.skip(target - current);
+ if (skipped == 0) {
+ if (read() == -1) {
+ break; // EOF
+ } else {
+ skipped = 1;
+ }
+ }
+ current += skipped;
+ }
+ }
+
+ @Override public int read() throws IOException {
+ int result = in.read();
+ if (result != -1) {
+ offset++;
+ }
+ return result;
+ }
+
+ @Override public int read(byte[] buffer) throws IOException {
+ int count = in.read(buffer);
+ if (count != -1) {
+ offset += count;
+ }
+ return count;
+ }
+
+ @Override public int read(byte[] buffer, int offset, int length) throws IOException {
+ int count = in.read(buffer, offset, length);
+ if (count != -1) {
+ this.offset += count;
+ }
+ return count;
+ }
+
+ @Override public long skip(long byteCount) throws IOException {
+ long skipped = in.skip(byteCount);
+ offset += skipped;
+ return skipped;
+ }
+
+ @Override public int available() throws IOException {
+ return in.available();
+ }
+
+ @Override public void close() throws IOException {
+ in.close();
+ }
+
+ @Override public boolean markSupported() {
+ return in.markSupported();
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/MediaStoreBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/MediaStoreBitmapHunter.java
new file mode 100644
index 000000000..4f8ae1c24
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/MediaStoreBitmapHunter.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2014 Square, 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.squareup.picasso;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.MediaStore;
+import java.io.IOException;
+
+import static android.content.ContentUris.parseId;
+import static android.provider.MediaStore.Images.Thumbnails.FULL_SCREEN_KIND;
+import static android.provider.MediaStore.Images.Thumbnails.MICRO_KIND;
+import static android.provider.MediaStore.Images.Thumbnails.MINI_KIND;
+import static android.provider.MediaStore.Images.Thumbnails.getThumbnail;
+import static com.squareup.picasso.MediaStoreBitmapHunter.PicassoKind.FULL;
+import static com.squareup.picasso.MediaStoreBitmapHunter.PicassoKind.MICRO;
+import static com.squareup.picasso.MediaStoreBitmapHunter.PicassoKind.MINI;
+
+class MediaStoreBitmapHunter extends ContentStreamBitmapHunter {
+ private static final String[] CONTENT_ORIENTATION = new String[] {
+ MediaStore.Images.ImageColumns.ORIENTATION
+ };
+
+ MediaStoreBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
+ Stats stats, Action action) {
+ super(context, picasso, dispatcher, cache, stats, action);
+ }
+
+ @Override Bitmap decode(Request data) throws IOException {
+ ContentResolver contentResolver = context.getContentResolver();
+ setExifRotation(getExitOrientation(contentResolver, data.uri));
+
+ if (data.hasSize()) {
+ PicassoKind picassoKind = getPicassoKind(data.targetWidth, data.targetHeight);
+ if (picassoKind == FULL) {
+ return super.decode(data);
+ }
+
+ long id = parseId(data.uri);
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+
+ calculateInSampleSize(data.targetWidth, data.targetHeight, picassoKind.width,
+ picassoKind.height, options);
+
+ Bitmap result = getThumbnail(contentResolver, id, picassoKind.androidKind, options);
+
+ if (result != null) {
+ return result;
+ }
+ }
+
+ return super.decode(data);
+ }
+
+ static PicassoKind getPicassoKind(int targetWidth, int targetHeight) {
+ if (targetWidth <= MICRO.width && targetHeight <= MICRO.height) {
+ return MICRO;
+ } else if (targetWidth <= MINI.width && targetHeight <= MINI.height) {
+ return MINI;
+ }
+ return FULL;
+ }
+
+ static int getExitOrientation(ContentResolver contentResolver, Uri uri) {
+ Cursor cursor = null;
+ try {
+ cursor = contentResolver.query(uri, CONTENT_ORIENTATION, null, null, null);
+ if (cursor == null || !cursor.moveToFirst()) {
+ return 0;
+ }
+ return cursor.getInt(0);
+ } catch (RuntimeException ignored) {
+ // If the orientation column doesn't exist, assume no rotation.
+ return 0;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ enum PicassoKind {
+ MICRO(MICRO_KIND, 96, 96),
+ MINI(MINI_KIND, 512, 384),
+ FULL(FULL_SCREEN_KIND, -1, -1);
+
+ final int androidKind;
+ final int width;
+ final int height;
+
+ PicassoKind(int androidKind, int width, int height) {
+ this.androidKind = androidKind;
+ this.width = width;
+ this.height = height;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/NetworkBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/NetworkBitmapHunter.java
new file mode 100644
index 000000000..6d148211d
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/NetworkBitmapHunter.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.NetworkInfo;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static com.squareup.picasso.Downloader.Response;
+import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
+import static com.squareup.picasso.Picasso.LoadedFrom.NETWORK;
+
+class NetworkBitmapHunter extends BitmapHunter {
+ static final int DEFAULT_RETRY_COUNT = 2;
+ private static final int MARKER = 65536;
+
+ private final Downloader downloader;
+
+ int retryCount;
+
+ public NetworkBitmapHunter(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats,
+ Action action, Downloader downloader) {
+ super(picasso, dispatcher, cache, stats, action);
+ this.downloader = downloader;
+ this.retryCount = DEFAULT_RETRY_COUNT;
+ }
+
+ @Override Bitmap decode(Request data) throws IOException {
+ boolean loadFromLocalCacheOnly = retryCount == 0;
+
+ Response response = downloader.load(data.uri, loadFromLocalCacheOnly);
+ if (response == null) {
+ return null;
+ }
+
+ loadedFrom = response.cached ? DISK : NETWORK;
+
+ Bitmap result = response.getBitmap();
+ if (result != null) {
+ return result;
+ }
+
+ InputStream is = response.getInputStream();
+ try {
+ return decodeStream(is, data);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ }
+
+ @Override boolean shouldRetry(boolean airplaneMode, NetworkInfo info) {
+ boolean hasRetries = retryCount > 0;
+ if (!hasRetries) {
+ return false;
+ }
+ retryCount--;
+ return info == null || info.isConnectedOrConnecting();
+ }
+
+ private Bitmap decodeStream(InputStream stream, Request data) throws IOException {
+ if (stream == null) {
+ return null;
+ }
+ MarkableInputStream markStream = new MarkableInputStream(stream);
+ stream = markStream;
+
+ long mark = markStream.savePosition(MARKER);
+
+ boolean isWebPFile = Utils.isWebPFile(stream);
+ markStream.reset(mark);
+ // When decode WebP network stream, BitmapFactory throw JNI Exception and make app crash.
+ // Decode byte array instead
+ if (isWebPFile) {
+ byte[] bytes = Utils.toByteArray(stream);
+ BitmapFactory.Options options = null;
+ if (data.hasSize()) {
+ options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+
+ BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+ calculateInSampleSize(data.targetWidth, data.targetHeight, options);
+ }
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+ } else {
+ BitmapFactory.Options options = null;
+ if (data.hasSize()) {
+ options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+
+ BitmapFactory.decodeStream(stream, null, options);
+ calculateInSampleSize(data.targetWidth, data.targetHeight, options);
+
+ markStream.reset(mark);
+ }
+ return BitmapFactory.decodeStream(stream, null, options);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Picasso.java b/mobile/android/thirdparty/com/squareup/picasso/Picasso.java
new file mode 100644
index 000000000..9b510f977
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Picasso.java
@@ -0,0 +1,522 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.widget.ImageView;
+import java.io.File;
+import java.lang.ref.ReferenceQueue;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.ExecutorService;
+
+import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+import static com.squareup.picasso.Action.RequestWeakReference;
+import static com.squareup.picasso.Dispatcher.HUNTER_BATCH_COMPLETE;
+import static com.squareup.picasso.Dispatcher.REQUEST_GCED;
+import static com.squareup.picasso.Utils.THREAD_PREFIX;
+
+/**
+ * Image downloading, transformation, and caching manager.
+ * <p/>
+ * Use {@link #with(android.content.Context)} for the global singleton instance or construct your
+ * own instance with {@link Builder}.
+ */
+public class Picasso {
+
+ /** Callbacks for Picasso events. */
+ public interface Listener {
+ /**
+ * Invoked when an image has failed to load. This is useful for reporting image failures to a
+ * remote analytics service, for example.
+ */
+ void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception);
+ }
+
+ /**
+ * A transformer that is called immediately before every request is submitted. This can be used to
+ * modify any information about a request.
+ * <p>
+ * For example, if you use a CDN you can change the hostname for the image based on the current
+ * location of the user in order to get faster download speeds.
+ * <p>
+ * <b>NOTE:</b> This is a beta feature. The API is subject to change in a backwards incompatible
+ * way at any time.
+ */
+ public interface RequestTransformer {
+ /**
+ * Transform a request before it is submitted to be processed.
+ *
+ * @return The original request or a new request to replace it. Must not be null.
+ */
+ Request transformRequest(Request request);
+
+ /** A {@link RequestTransformer} which returns the original request. */
+ RequestTransformer IDENTITY = new RequestTransformer() {
+ @Override public Request transformRequest(Request request) {
+ return request;
+ }
+ };
+ }
+
+ static final Handler HANDLER = new Handler(Looper.getMainLooper()) {
+ @Override public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case HUNTER_BATCH_COMPLETE: {
+ @SuppressWarnings("unchecked") List<BitmapHunter> batch = (List<BitmapHunter>) msg.obj;
+ for (BitmapHunter hunter : batch) {
+ hunter.picasso.complete(hunter);
+ }
+ break;
+ }
+ case REQUEST_GCED: {
+ Action action = (Action) msg.obj;
+ action.picasso.cancelExistingRequest(action.getTarget());
+ break;
+ }
+ default:
+ throw new AssertionError("Unknown handler message received: " + msg.what);
+ }
+ }
+ };
+
+ static Picasso singleton = null;
+
+ private final Listener listener;
+ private final RequestTransformer requestTransformer;
+ private final CleanupThread cleanupThread;
+
+ final Context context;
+ final Dispatcher dispatcher;
+ final Cache cache;
+ final Stats stats;
+ final Map<Object, Action> targetToAction;
+ final Map<ImageView, DeferredRequestCreator> targetToDeferredRequestCreator;
+ final ReferenceQueue<Object> referenceQueue;
+
+ boolean debugging;
+ boolean shutdown;
+
+ Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener,
+ RequestTransformer requestTransformer, Stats stats, boolean debugging) {
+ this.context = context;
+ this.dispatcher = dispatcher;
+ this.cache = cache;
+ this.listener = listener;
+ this.requestTransformer = requestTransformer;
+ this.stats = stats;
+ this.targetToAction = new WeakHashMap<Object, Action>();
+ this.targetToDeferredRequestCreator = new WeakHashMap<ImageView, DeferredRequestCreator>();
+ this.debugging = debugging;
+ this.referenceQueue = new ReferenceQueue<Object>();
+ this.cleanupThread = new CleanupThread(referenceQueue, HANDLER);
+ this.cleanupThread.start();
+ }
+
+ /** Cancel any existing requests for the specified target {@link ImageView}. */
+ public void cancelRequest(ImageView view) {
+ cancelExistingRequest(view);
+ }
+
+ /** Cancel any existing requests for the specified {@link Target} instance. */
+ public void cancelRequest(Target target) {
+ cancelExistingRequest(target);
+ }
+
+ /**
+ * Start an image request using the specified URI.
+ * <p>
+ * Passing {@code null} as a {@code uri} will not trigger any request but will set a placeholder,
+ * if one is specified.
+ *
+ * @see #load(File)
+ * @see #load(String)
+ * @see #load(int)
+ */
+ public RequestCreator load(Uri uri) {
+ return new RequestCreator(this, uri, 0);
+ }
+
+ /**
+ * Start an image request using the specified path. This is a convenience method for calling
+ * {@link #load(Uri)}.
+ * <p>
+ * This path may be a remote URL, file resource (prefixed with {@code file:}), content resource
+ * (prefixed with {@code content:}), or android resource (prefixed with {@code
+ * android.resource:}.
+ * <p>
+ * Passing {@code null} as a {@code path} will not trigger any request but will set a
+ * placeholder, if one is specified.
+ *
+ * @see #load(Uri)
+ * @see #load(File)
+ * @see #load(int)
+ */
+ public RequestCreator load(String path) {
+ if (path == null) {
+ return new RequestCreator(this, null, 0);
+ }
+ if (path.trim().length() == 0) {
+ throw new IllegalArgumentException("Path must not be empty.");
+ }
+ return load(Uri.parse(path));
+ }
+
+ /**
+ * Start an image request using the specified image file. This is a convenience method for
+ * calling {@link #load(Uri)}.
+ * <p>
+ * Passing {@code null} as a {@code file} will not trigger any request but will set a
+ * placeholder, if one is specified.
+ *
+ * @see #load(Uri)
+ * @see #load(String)
+ * @see #load(int)
+ */
+ public RequestCreator load(File file) {
+ if (file == null) {
+ return new RequestCreator(this, null, 0);
+ }
+ return load(Uri.fromFile(file));
+ }
+
+ /**
+ * Start an image request using the specified drawable resource ID.
+ *
+ * @see #load(Uri)
+ * @see #load(String)
+ * @see #load(File)
+ */
+ public RequestCreator load(int resourceId) {
+ if (resourceId == 0) {
+ throw new IllegalArgumentException("Resource ID must not be zero.");
+ }
+ return new RequestCreator(this, null, resourceId);
+ }
+
+ /** {@code true} if debug display, logging, and statistics are enabled. */
+ @SuppressWarnings("UnusedDeclaration") public boolean isDebugging() {
+ return debugging;
+ }
+
+ /** Toggle whether debug display, logging, and statistics are enabled. */
+ @SuppressWarnings("UnusedDeclaration") public void setDebugging(boolean debugging) {
+ this.debugging = debugging;
+ }
+
+ /** Creates a {@link StatsSnapshot} of the current stats for this instance. */
+ @SuppressWarnings("UnusedDeclaration") public StatsSnapshot getSnapshot() {
+ return stats.createSnapshot();
+ }
+
+ /** Stops this instance from accepting further requests. */
+ public void shutdown() {
+ if (this == singleton) {
+ throw new UnsupportedOperationException("Default singleton instance cannot be shutdown.");
+ }
+ if (shutdown) {
+ return;
+ }
+ cache.clear();
+ cleanupThread.shutdown();
+ stats.shutdown();
+ dispatcher.shutdown();
+ for (DeferredRequestCreator deferredRequestCreator : targetToDeferredRequestCreator.values()) {
+ deferredRequestCreator.cancel();
+ }
+ targetToDeferredRequestCreator.clear();
+ shutdown = true;
+ }
+
+ Request transformRequest(Request request) {
+ Request transformed = requestTransformer.transformRequest(request);
+ if (transformed == null) {
+ throw new IllegalStateException("Request transformer "
+ + requestTransformer.getClass().getCanonicalName()
+ + " returned null for "
+ + request);
+ }
+ return transformed;
+ }
+
+ void defer(ImageView view, DeferredRequestCreator request) {
+ targetToDeferredRequestCreator.put(view, request);
+ }
+
+ void enqueueAndSubmit(Action action) {
+ Object target = action.getTarget();
+ if (target != null) {
+ cancelExistingRequest(target);
+ targetToAction.put(target, action);
+ }
+ submit(action);
+ }
+
+ void submit(Action action) {
+ dispatcher.dispatchSubmit(action);
+ }
+
+ Bitmap quickMemoryCacheCheck(String key) {
+ Bitmap cached = cache.get(key);
+ if (cached != null) {
+ stats.dispatchCacheHit();
+ } else {
+ stats.dispatchCacheMiss();
+ }
+ return cached;
+ }
+
+ void complete(BitmapHunter hunter) {
+ List<Action> joined = hunter.getActions();
+ if (joined.isEmpty()) {
+ return;
+ }
+
+ Uri uri = hunter.getData().uri;
+ Exception exception = hunter.getException();
+ Bitmap result = hunter.getResult();
+ LoadedFrom from = hunter.getLoadedFrom();
+
+ for (Action join : joined) {
+ if (join.isCancelled()) {
+ continue;
+ }
+ targetToAction.remove(join.getTarget());
+ if (result != null) {
+ if (from == null) {
+ throw new AssertionError("LoadedFrom cannot be null.");
+ }
+ join.complete(result, from);
+ } else {
+ join.error();
+ }
+ }
+
+ if (listener != null && exception != null) {
+ listener.onImageLoadFailed(this, uri, exception);
+ }
+ }
+
+ private void cancelExistingRequest(Object target) {
+ Action action = targetToAction.remove(target);
+ if (action != null) {
+ action.cancel();
+ dispatcher.dispatchCancel(action);
+ }
+ if (target instanceof ImageView) {
+ ImageView targetImageView = (ImageView) target;
+ DeferredRequestCreator deferredRequestCreator =
+ targetToDeferredRequestCreator.remove(targetImageView);
+ if (deferredRequestCreator != null) {
+ deferredRequestCreator.cancel();
+ }
+ }
+ }
+
+ private static class CleanupThread extends Thread {
+ private final ReferenceQueue<?> referenceQueue;
+ private final Handler handler;
+
+ CleanupThread(ReferenceQueue<?> referenceQueue, Handler handler) {
+ this.referenceQueue = referenceQueue;
+ this.handler = handler;
+ setDaemon(true);
+ setName(THREAD_PREFIX + "refQueue");
+ }
+
+ @Override public void run() {
+ Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
+ while (true) {
+ try {
+ RequestWeakReference<?> remove = (RequestWeakReference<?>) referenceQueue.remove();
+ handler.sendMessage(handler.obtainMessage(REQUEST_GCED, remove.action));
+ } catch (InterruptedException e) {
+ break;
+ } catch (final Exception e) {
+ handler.post(new Runnable() {
+ @Override public void run() {
+ throw new RuntimeException(e);
+ }
+ });
+ break;
+ }
+ }
+ }
+
+ void shutdown() {
+ interrupt();
+ }
+ }
+
+ /**
+ * The global default {@link Picasso} instance.
+ * <p>
+ * This instance is automatically initialized with defaults that are suitable to most
+ * implementations.
+ * <ul>
+ * <li>LRU memory cache of 15% the available application RAM</li>
+ * <li>Disk cache of 2% storage space up to 50MB but no less than 5MB. (Note: this is only
+ * available on API 14+ <em>or</em> if you are using a standalone library that provides a disk
+ * cache on all API levels like OkHttp)</li>
+ * <li>Three download threads for disk and network access.</li>
+ * </ul>
+ * <p>
+ * If these settings do not meet the requirements of your application you can construct your own
+ * instance with full control over the configuration by using {@link Picasso.Builder}.
+ */
+ public static Picasso with(Context context) {
+ if (singleton == null) {
+ singleton = new Builder(context).build();
+ }
+ return singleton;
+ }
+
+ /** Fluent API for creating {@link Picasso} instances. */
+ @SuppressWarnings("UnusedDeclaration") // Public API.
+ public static class Builder {
+ private final Context context;
+ private Downloader downloader;
+ private ExecutorService service;
+ private Cache cache;
+ private Listener listener;
+ private RequestTransformer transformer;
+ private boolean debugging;
+
+ /** Start building a new {@link Picasso} instance. */
+ public Builder(Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("Context must not be null.");
+ }
+ this.context = context.getApplicationContext();
+ }
+
+ /** Specify the {@link Downloader} that will be used for downloading images. */
+ public Builder downloader(Downloader downloader) {
+ if (downloader == null) {
+ throw new IllegalArgumentException("Downloader must not be null.");
+ }
+ if (this.downloader != null) {
+ throw new IllegalStateException("Downloader already set.");
+ }
+ this.downloader = downloader;
+ return this;
+ }
+
+ /** Specify the executor service for loading images in the background. */
+ public Builder executor(ExecutorService executorService) {
+ if (executorService == null) {
+ throw new IllegalArgumentException("Executor service must not be null.");
+ }
+ if (this.service != null) {
+ throw new IllegalStateException("Executor service already set.");
+ }
+ this.service = executorService;
+ return this;
+ }
+
+ /** Specify the memory cache used for the most recent images. */
+ public Builder memoryCache(Cache memoryCache) {
+ if (memoryCache == null) {
+ throw new IllegalArgumentException("Memory cache must not be null.");
+ }
+ if (this.cache != null) {
+ throw new IllegalStateException("Memory cache already set.");
+ }
+ this.cache = memoryCache;
+ return this;
+ }
+
+ /** Specify a listener for interesting events. */
+ public Builder listener(Listener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("Listener must not be null.");
+ }
+ if (this.listener != null) {
+ throw new IllegalStateException("Listener already set.");
+ }
+ this.listener = listener;
+ return this;
+ }
+
+ /**
+ * Specify a transformer for all incoming requests.
+ * <p>
+ * <b>NOTE:</b> This is a beta feature. The API is subject to change in a backwards incompatible
+ * way at any time.
+ */
+ public Builder requestTransformer(RequestTransformer transformer) {
+ if (transformer == null) {
+ throw new IllegalArgumentException("Transformer must not be null.");
+ }
+ if (this.transformer != null) {
+ throw new IllegalStateException("Transformer already set.");
+ }
+ this.transformer = transformer;
+ return this;
+ }
+
+ /** Whether debugging is enabled or not. */
+ public Builder debugging(boolean debugging) {
+ this.debugging = debugging;
+ return this;
+ }
+
+ /** Create the {@link Picasso} instance. */
+ public Picasso build() {
+ Context context = this.context;
+
+ if (downloader == null) {
+ downloader = Utils.createDefaultDownloader(context);
+ }
+ if (cache == null) {
+ cache = new LruCache(context);
+ }
+ if (service == null) {
+ service = new PicassoExecutorService();
+ }
+ if (transformer == null) {
+ transformer = RequestTransformer.IDENTITY;
+ }
+
+ Stats stats = new Stats(cache);
+
+ Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);
+
+ return new Picasso(context, dispatcher, cache, listener, transformer, stats, debugging);
+ }
+ }
+
+ /** Describes where the image was loaded from. */
+ public enum LoadedFrom {
+ MEMORY(Color.GREEN),
+ DISK(Color.YELLOW),
+ NETWORK(Color.RED);
+
+ final int debugColor;
+
+ private LoadedFrom(int debugColor) {
+ this.debugColor = debugColor;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/PicassoDrawable.java b/mobile/android/thirdparty/com/squareup/picasso/PicassoDrawable.java
new file mode 100644
index 000000000..07f762c31
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/PicassoDrawable.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
+import android.widget.ImageView;
+
+import static android.graphics.Color.WHITE;
+import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;
+
+final class PicassoDrawable extends Drawable {
+ // Only accessed from main thread.
+ private static final Paint DEBUG_PAINT = new Paint();
+
+ private static final float FADE_DURATION = 200f; //ms
+
+ /**
+ * Create or update the drawable on the target {@link ImageView} to display the supplied bitmap
+ * image.
+ */
+ static void setBitmap(ImageView target, Context context, Bitmap bitmap,
+ Picasso.LoadedFrom loadedFrom, boolean noFade, boolean debugging) {
+ Drawable placeholder = target.getDrawable();
+ if (placeholder instanceof AnimationDrawable) {
+ ((AnimationDrawable) placeholder).stop();
+ }
+ PicassoDrawable drawable =
+ new PicassoDrawable(context, placeholder, bitmap, loadedFrom, noFade, debugging);
+ target.setImageDrawable(drawable);
+ }
+
+ /**
+ * Create or update the drawable on the target {@link ImageView} to display the supplied
+ * placeholder image.
+ */
+ static void setPlaceholder(ImageView target, int placeholderResId, Drawable placeholderDrawable) {
+ if (placeholderResId != 0) {
+ target.setImageResource(placeholderResId);
+ } else {
+ target.setImageDrawable(placeholderDrawable);
+ }
+ if (target.getDrawable() instanceof AnimationDrawable) {
+ ((AnimationDrawable) target.getDrawable()).start();
+ }
+ }
+
+ private final boolean debugging;
+ private final float density;
+ private final Picasso.LoadedFrom loadedFrom;
+ final BitmapDrawable image;
+
+ Drawable placeholder;
+
+ long startTimeMillis;
+ boolean animating;
+ int alpha = 0xFF;
+
+ PicassoDrawable(Context context, Drawable placeholder, Bitmap bitmap,
+ Picasso.LoadedFrom loadedFrom, boolean noFade, boolean debugging) {
+ Resources res = context.getResources();
+
+ this.debugging = debugging;
+ this.density = res.getDisplayMetrics().density;
+
+ this.loadedFrom = loadedFrom;
+
+ this.image = new BitmapDrawable(res, bitmap);
+
+ boolean fade = loadedFrom != MEMORY && !noFade;
+ if (fade) {
+ this.placeholder = placeholder;
+ animating = true;
+ startTimeMillis = SystemClock.uptimeMillis();
+ }
+ }
+
+ @Override public void draw(Canvas canvas) {
+ if (!animating) {
+ image.draw(canvas);
+ } else {
+ float normalized = (SystemClock.uptimeMillis() - startTimeMillis) / FADE_DURATION;
+ if (normalized >= 1f) {
+ animating = false;
+ placeholder = null;
+ image.draw(canvas);
+ } else {
+ if (placeholder != null) {
+ placeholder.draw(canvas);
+ }
+
+ int partialAlpha = (int) (alpha * normalized);
+ image.setAlpha(partialAlpha);
+ image.draw(canvas);
+ image.setAlpha(alpha);
+ invalidateSelf();
+ }
+ }
+
+ if (debugging) {
+ drawDebugIndicator(canvas);
+ }
+ }
+
+ @Override public int getIntrinsicWidth() {
+ return image.getIntrinsicWidth();
+ }
+
+ @Override public int getIntrinsicHeight() {
+ return image.getIntrinsicHeight();
+ }
+
+ @Override public void setAlpha(int alpha) {
+ this.alpha = alpha;
+ if (placeholder != null) {
+ placeholder.setAlpha(alpha);
+ }
+ image.setAlpha(alpha);
+ }
+
+ @Override public void setColorFilter(ColorFilter cf) {
+ if (placeholder != null) {
+ placeholder.setColorFilter(cf);
+ }
+ image.setColorFilter(cf);
+ }
+
+ @Override public int getOpacity() {
+ return image.getOpacity();
+ }
+
+ @Override protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+
+ image.setBounds(bounds);
+ if (placeholder != null) {
+ placeholder.setBounds(bounds);
+ }
+ }
+
+ private void drawDebugIndicator(Canvas canvas) {
+ DEBUG_PAINT.setColor(WHITE);
+ Path path = getTrianglePath(new Point(0, 0), (int) (16 * density));
+ canvas.drawPath(path, DEBUG_PAINT);
+
+ DEBUG_PAINT.setColor(loadedFrom.debugColor);
+ path = getTrianglePath(new Point(0, 0), (int) (15 * density));
+ canvas.drawPath(path, DEBUG_PAINT);
+ }
+
+ private static Path getTrianglePath(Point p1, int width) {
+ Point p2 = new Point(p1.x + width, p1.y);
+ Point p3 = new Point(p1.x, p1.y + width);
+
+ Path path = new Path();
+ path.moveTo(p1.x, p1.y);
+ path.lineTo(p2.x, p2.y);
+ path.lineTo(p3.x, p3.y);
+
+ return path;
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/PicassoExecutorService.java b/mobile/android/thirdparty/com/squareup/picasso/PicassoExecutorService.java
new file mode 100644
index 000000000..875dd2dda
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/PicassoExecutorService.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.telephony.TelephonyManager;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The default {@link java.util.concurrent.ExecutorService} used for new {@link Picasso} instances.
+ * <p/>
+ * Exists as a custom type so that we can differentiate the use of defaults versus a user-supplied
+ * instance.
+ */
+class PicassoExecutorService extends ThreadPoolExecutor {
+ private static final int DEFAULT_THREAD_COUNT = 3;
+
+ PicassoExecutorService() {
+ super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<Runnable>(), new Utils.PicassoThreadFactory());
+ }
+
+ void adjustThreadCount(NetworkInfo info) {
+ if (info == null || !info.isConnectedOrConnecting()) {
+ setThreadCount(DEFAULT_THREAD_COUNT);
+ return;
+ }
+ switch (info.getType()) {
+ case ConnectivityManager.TYPE_WIFI:
+ case ConnectivityManager.TYPE_WIMAX:
+ case ConnectivityManager.TYPE_ETHERNET:
+ setThreadCount(4);
+ break;
+ case ConnectivityManager.TYPE_MOBILE:
+ switch (info.getSubtype()) {
+ case TelephonyManager.NETWORK_TYPE_LTE: // 4G
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ setThreadCount(3);
+ break;
+ case TelephonyManager.NETWORK_TYPE_UMTS: // 3G
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ setThreadCount(2);
+ break;
+ case TelephonyManager.NETWORK_TYPE_GPRS: // 2G
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ setThreadCount(1);
+ break;
+ default:
+ setThreadCount(DEFAULT_THREAD_COUNT);
+ }
+ break;
+ default:
+ setThreadCount(DEFAULT_THREAD_COUNT);
+ }
+ }
+
+ private void setThreadCount(int threadCount) {
+ setCorePoolSize(threadCount);
+ setMaximumPoolSize(threadCount);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Request.java b/mobile/android/thirdparty/com/squareup/picasso/Request.java
new file mode 100644
index 000000000..8e9c32460
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Request.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.net.Uri;
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.util.Collections.unmodifiableList;
+
+/** Immutable data about an image and the transformations that will be applied to it. */
+public final class Request {
+ /**
+ * The image URI.
+ * <p>
+ * This is mutually exclusive with {@link #resourceId}.
+ */
+ public final Uri uri;
+ /**
+ * The image resource ID.
+ * <p>
+ * This is mutually exclusive with {@link #uri}.
+ */
+ public final int resourceId;
+ /** List of custom transformations to be applied after the built-in transformations. */
+ public final List<Transformation> transformations;
+ /** Target image width for resizing. */
+ public final int targetWidth;
+ /** Target image height for resizing. */
+ public final int targetHeight;
+ /**
+ * True if the final image should use the 'centerCrop' scale technique.
+ * <p>
+ * This is mutually exclusive with {@link #centerInside}.
+ */
+ public final boolean centerCrop;
+ /**
+ * True if the final image should use the 'centerInside' scale technique.
+ * <p>
+ * This is mutually exclusive with {@link #centerCrop}.
+ */
+ public final boolean centerInside;
+ /** Amount to rotate the image in degrees. */
+ public final float rotationDegrees;
+ /** Rotation pivot on the X axis. */
+ public final float rotationPivotX;
+ /** Rotation pivot on the Y axis. */
+ public final float rotationPivotY;
+ /** Whether or not {@link #rotationPivotX} and {@link #rotationPivotY} are set. */
+ public final boolean hasRotationPivot;
+
+ private Request(Uri uri, int resourceId, List<Transformation> transformations, int targetWidth,
+ int targetHeight, boolean centerCrop, boolean centerInside, float rotationDegrees,
+ float rotationPivotX, float rotationPivotY, boolean hasRotationPivot) {
+ this.uri = uri;
+ this.resourceId = resourceId;
+ if (transformations == null) {
+ this.transformations = null;
+ } else {
+ this.transformations = unmodifiableList(transformations);
+ }
+ this.targetWidth = targetWidth;
+ this.targetHeight = targetHeight;
+ this.centerCrop = centerCrop;
+ this.centerInside = centerInside;
+ this.rotationDegrees = rotationDegrees;
+ this.rotationPivotX = rotationPivotX;
+ this.rotationPivotY = rotationPivotY;
+ this.hasRotationPivot = hasRotationPivot;
+ }
+
+ String getName() {
+ if (uri != null) {
+ return uri.getPath();
+ }
+ return Integer.toHexString(resourceId);
+ }
+
+ public boolean hasSize() {
+ return targetWidth != 0;
+ }
+
+ boolean needsTransformation() {
+ return needsMatrixTransform() || hasCustomTransformations();
+ }
+
+ boolean needsMatrixTransform() {
+ return targetWidth != 0 || rotationDegrees != 0;
+ }
+
+ boolean hasCustomTransformations() {
+ return transformations != null;
+ }
+
+ public Builder buildUpon() {
+ return new Builder(this);
+ }
+
+ /** Builder for creating {@link Request} instances. */
+ public static final class Builder {
+ private Uri uri;
+ private int resourceId;
+ private int targetWidth;
+ private int targetHeight;
+ private boolean centerCrop;
+ private boolean centerInside;
+ private float rotationDegrees;
+ private float rotationPivotX;
+ private float rotationPivotY;
+ private boolean hasRotationPivot;
+ private List<Transformation> transformations;
+
+ /** Start building a request using the specified {@link Uri}. */
+ public Builder(Uri uri) {
+ setUri(uri);
+ }
+
+ /** Start building a request using the specified resource ID. */
+ public Builder(int resourceId) {
+ setResourceId(resourceId);
+ }
+
+ Builder(Uri uri, int resourceId) {
+ this.uri = uri;
+ this.resourceId = resourceId;
+ }
+
+ private Builder(Request request) {
+ uri = request.uri;
+ resourceId = request.resourceId;
+ targetWidth = request.targetWidth;
+ targetHeight = request.targetHeight;
+ centerCrop = request.centerCrop;
+ centerInside = request.centerInside;
+ rotationDegrees = request.rotationDegrees;
+ rotationPivotX = request.rotationPivotX;
+ rotationPivotY = request.rotationPivotY;
+ hasRotationPivot = request.hasRotationPivot;
+ if (request.transformations != null) {
+ transformations = new ArrayList<Transformation>(request.transformations);
+ }
+ }
+
+ boolean hasImage() {
+ return uri != null || resourceId != 0;
+ }
+
+ boolean hasSize() {
+ return targetWidth != 0;
+ }
+
+ /**
+ * Set the target image Uri.
+ * <p>
+ * This will clear an image resource ID if one is set.
+ */
+ public Builder setUri(Uri uri) {
+ if (uri == null) {
+ throw new IllegalArgumentException("Image URI may not be null.");
+ }
+ this.uri = uri;
+ this.resourceId = 0;
+ return this;
+ }
+
+ /**
+ * Set the target image resource ID.
+ * <p>
+ * This will clear an image Uri if one is set.
+ */
+ public Builder setResourceId(int resourceId) {
+ if (resourceId == 0) {
+ throw new IllegalArgumentException("Image resource ID may not be 0.");
+ }
+ this.resourceId = resourceId;
+ this.uri = null;
+ return this;
+ }
+
+ /** Resize the image to the specified size in pixels. */
+ public Builder resize(int targetWidth, int targetHeight) {
+ if (targetWidth <= 0) {
+ throw new IllegalArgumentException("Width must be positive number.");
+ }
+ if (targetHeight <= 0) {
+ throw new IllegalArgumentException("Height must be positive number.");
+ }
+ this.targetWidth = targetWidth;
+ this.targetHeight = targetHeight;
+ return this;
+ }
+
+ /** Clear the resize transformation, if any. This will also clear center crop/inside if set. */
+ public Builder clearResize() {
+ targetWidth = 0;
+ targetHeight = 0;
+ centerCrop = false;
+ centerInside = false;
+ return this;
+ }
+
+ /**
+ * Crops an image inside of the bounds specified by {@link #resize(int, int)} rather than
+ * distorting the aspect ratio. This cropping technique scales the image so that it fills the
+ * requested bounds and then crops the extra.
+ */
+ public Builder centerCrop() {
+ if (centerInside) {
+ throw new IllegalStateException("Center crop can not be used after calling centerInside");
+ }
+ centerCrop = true;
+ return this;
+ }
+
+ /** Clear the center crop transformation flag, if set. */
+ public Builder clearCenterCrop() {
+ centerCrop = false;
+ return this;
+ }
+
+ /**
+ * Centers an image inside of the bounds specified by {@link #resize(int, int)}. This scales
+ * the image so that both dimensions are equal to or less than the requested bounds.
+ */
+ public Builder centerInside() {
+ if (centerCrop) {
+ throw new IllegalStateException("Center inside can not be used after calling centerCrop");
+ }
+ centerInside = true;
+ return this;
+ }
+
+ /** Clear the center inside transformation flag, if set. */
+ public Builder clearCenterInside() {
+ centerInside = false;
+ return this;
+ }
+
+ /** Rotate the image by the specified degrees. */
+ public Builder rotate(float degrees) {
+ rotationDegrees = degrees;
+ return this;
+ }
+
+ /** Rotate the image by the specified degrees around a pivot point. */
+ public Builder rotate(float degrees, float pivotX, float pivotY) {
+ rotationDegrees = degrees;
+ rotationPivotX = pivotX;
+ rotationPivotY = pivotY;
+ hasRotationPivot = true;
+ return this;
+ }
+
+ /** Clear the rotation transformation, if any. */
+ public Builder clearRotation() {
+ rotationDegrees = 0;
+ rotationPivotX = 0;
+ rotationPivotY = 0;
+ hasRotationPivot = false;
+ return this;
+ }
+
+ /**
+ * Add a custom transformation to be applied to the image.
+ * <p/>
+ * Custom transformations will always be run after the built-in transformations.
+ */
+ public Builder transform(Transformation transformation) {
+ if (transformation == null) {
+ throw new IllegalArgumentException("Transformation must not be null.");
+ }
+ if (transformations == null) {
+ transformations = new ArrayList<Transformation>(2);
+ }
+ transformations.add(transformation);
+ return this;
+ }
+
+ /** Create the immutable {@link Request} object. */
+ public Request build() {
+ if (centerInside && centerCrop) {
+ throw new IllegalStateException("Center crop and center inside can not be used together.");
+ }
+ if (centerCrop && targetWidth == 0) {
+ throw new IllegalStateException("Center crop requires calling resize.");
+ }
+ if (centerInside && targetWidth == 0) {
+ throw new IllegalStateException("Center inside requires calling resize.");
+ }
+ return new Request(uri, resourceId, transformations, targetWidth, targetHeight, centerCrop,
+ centerInside, rotationDegrees, rotationPivotX, rotationPivotY, hasRotationPivot);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/RequestCreator.java b/mobile/android/thirdparty/com/squareup/picasso/RequestCreator.java
new file mode 100644
index 000000000..3a5ca3a9f
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/RequestCreator.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.widget.ImageView;
+import java.io.IOException;
+
+import static com.squareup.picasso.BitmapHunter.forRequest;
+import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;
+import static com.squareup.picasso.Utils.checkNotMain;
+import static com.squareup.picasso.Utils.createKey;
+
+/** Fluent API for building an image download request. */
+@SuppressWarnings("UnusedDeclaration") // Public API.
+public class RequestCreator {
+ private final Picasso picasso;
+ private final Request.Builder data;
+
+ private boolean skipMemoryCache;
+ private boolean noFade;
+ private boolean deferred;
+ private int placeholderResId;
+ private Drawable placeholderDrawable;
+ private int errorResId;
+ private Drawable errorDrawable;
+
+ RequestCreator(Picasso picasso, Uri uri, int resourceId) {
+ if (picasso.shutdown) {
+ throw new IllegalStateException(
+ "Picasso instance already shut down. Cannot submit new requests.");
+ }
+ this.picasso = picasso;
+ this.data = new Request.Builder(uri, resourceId);
+ }
+
+ /**
+ * A placeholder drawable to be used while the image is being loaded. If the requested image is
+ * not immediately available in the memory cache then this resource will be set on the target
+ * {@link ImageView}.
+ */
+ public RequestCreator placeholder(int placeholderResId) {
+ if (placeholderResId == 0) {
+ throw new IllegalArgumentException("Placeholder image resource invalid.");
+ }
+ if (placeholderDrawable != null) {
+ throw new IllegalStateException("Placeholder image already set.");
+ }
+ this.placeholderResId = placeholderResId;
+ return this;
+ }
+
+ /**
+ * A placeholder drawable to be used while the image is being loaded. If the requested image is
+ * not immediately available in the memory cache then this resource will be set on the target
+ * {@link ImageView}.
+ * <p>
+ * If you are not using a placeholder image but want to clear an existing image (such as when
+ * used in an {@link android.widget.Adapter adapter}), pass in {@code null}.
+ */
+ public RequestCreator placeholder(Drawable placeholderDrawable) {
+ if (placeholderResId != 0) {
+ throw new IllegalStateException("Placeholder image already set.");
+ }
+ this.placeholderDrawable = placeholderDrawable;
+ return this;
+ }
+
+ /** An error drawable to be used if the request image could not be loaded. */
+ public RequestCreator error(int errorResId) {
+ if (errorResId == 0) {
+ throw new IllegalArgumentException("Error image resource invalid.");
+ }
+ if (errorDrawable != null) {
+ throw new IllegalStateException("Error image already set.");
+ }
+ this.errorResId = errorResId;
+ return this;
+ }
+
+ /** An error drawable to be used if the request image could not be loaded. */
+ public RequestCreator error(Drawable errorDrawable) {
+ if (errorDrawable == null) {
+ throw new IllegalArgumentException("Error image may not be null.");
+ }
+ if (errorResId != 0) {
+ throw new IllegalStateException("Error image already set.");
+ }
+ this.errorDrawable = errorDrawable;
+ return this;
+ }
+
+ /**
+ * Attempt to resize the image to fit exactly into the target {@link ImageView}'s bounds. This
+ * will result in delayed execution of the request until the {@link ImageView} has been measured.
+ * <p/>
+ * <em>Note:</em> This method works only when your target is an {@link ImageView}.
+ */
+ public RequestCreator fit() {
+ deferred = true;
+ return this;
+ }
+
+ /** Internal use only. Used by {@link DeferredRequestCreator}. */
+ RequestCreator unfit() {
+ deferred = false;
+ return this;
+ }
+
+ /** Resize the image to the specified dimension size. */
+ public RequestCreator resizeDimen(int targetWidthResId, int targetHeightResId) {
+ Resources resources = picasso.context.getResources();
+ int targetWidth = resources.getDimensionPixelSize(targetWidthResId);
+ int targetHeight = resources.getDimensionPixelSize(targetHeightResId);
+ return resize(targetWidth, targetHeight);
+ }
+
+ /** Resize the image to the specified size in pixels. */
+ public RequestCreator resize(int targetWidth, int targetHeight) {
+ data.resize(targetWidth, targetHeight);
+ return this;
+ }
+
+ /**
+ * Crops an image inside of the bounds specified by {@link #resize(int, int)} rather than
+ * distorting the aspect ratio. This cropping technique scales the image so that it fills the
+ * requested bounds and then crops the extra.
+ */
+ public RequestCreator centerCrop() {
+ data.centerCrop();
+ return this;
+ }
+
+ /**
+ * Centers an image inside of the bounds specified by {@link #resize(int, int)}. This scales
+ * the image so that both dimensions are equal to or less than the requested bounds.
+ */
+ public RequestCreator centerInside() {
+ data.centerInside();
+ return this;
+ }
+
+ /** Rotate the image by the specified degrees. */
+ public RequestCreator rotate(float degrees) {
+ data.rotate(degrees);
+ return this;
+ }
+
+ /** Rotate the image by the specified degrees around a pivot point. */
+ public RequestCreator rotate(float degrees, float pivotX, float pivotY) {
+ data.rotate(degrees, pivotX, pivotY);
+ return this;
+ }
+
+ /**
+ * Add a custom transformation to be applied to the image.
+ * <p/>
+ * Custom transformations will always be run after the built-in transformations.
+ */
+ // TODO show example of calling resize after a transform in the javadoc
+ public RequestCreator transform(Transformation transformation) {
+ data.transform(transformation);
+ return this;
+ }
+
+ /**
+ * Indicate that this action should not use the memory cache for attempting to load or save the
+ * image. This can be useful when you know an image will only ever be used once (e.g., loading
+ * an image from the filesystem and uploading to a remote server).
+ */
+ public RequestCreator skipMemoryCache() {
+ skipMemoryCache = true;
+ return this;
+ }
+
+ /** Disable brief fade in of images loaded from the disk cache or network. */
+ public RequestCreator noFade() {
+ noFade = true;
+ return this;
+ }
+
+ /** Synchronously fulfill this request. Must not be called from the main thread. */
+ public Bitmap get() throws IOException {
+ checkNotMain();
+ if (deferred) {
+ throw new IllegalStateException("Fit cannot be used with get.");
+ }
+ if (!data.hasImage()) {
+ return null;
+ }
+
+ Request finalData = picasso.transformRequest(data.build());
+ String key = createKey(finalData);
+
+ Action action = new GetAction(picasso, finalData, skipMemoryCache, key);
+ return forRequest(picasso.context, picasso, picasso.dispatcher, picasso.cache, picasso.stats,
+ action, picasso.dispatcher.downloader).hunt();
+ }
+
+ /**
+ * Asynchronously fulfills the request without a {@link ImageView} or {@link Target}. This is
+ * useful when you want to warm up the cache with an image.
+ */
+ public void fetch() {
+ if (deferred) {
+ throw new IllegalStateException("Fit cannot be used with fetch.");
+ }
+ if (data.hasImage()) {
+ Request finalData = picasso.transformRequest(data.build());
+ String key = createKey(finalData);
+
+ Action action = new FetchAction(picasso, finalData, skipMemoryCache, key);
+ picasso.enqueueAndSubmit(action);
+ }
+ }
+
+ /**
+ * Asynchronously fulfills the request into the specified {@link Target}. In most cases, you
+ * should use this when you are dealing with a custom {@link android.view.View View} or view
+ * holder which should implement the {@link Target} interface.
+ * <p>
+ * Implementing on a {@link android.view.View View}:
+ * <blockquote><pre>
+ * public class ProfileView extends FrameLayout implements Target {
+ * {@literal @}Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
+ * setBackgroundDrawable(new BitmapDrawable(bitmap));
+ * }
+ *
+ * {@literal @}Override public void onBitmapFailed() {
+ * setBackgroundResource(R.drawable.profile_error);
+ * }
+ * }
+ * </pre></blockquote>
+ * Implementing on a view holder object for use inside of an adapter:
+ * <blockquote><pre>
+ * public class ViewHolder implements Target {
+ * public FrameLayout frame;
+ * public TextView name;
+ *
+ * {@literal @}Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
+ * frame.setBackgroundDrawable(new BitmapDrawable(bitmap));
+ * }
+ *
+ * {@literal @}Override public void onBitmapFailed() {
+ * frame.setBackgroundResource(R.drawable.profile_error);
+ * }
+ * }
+ * </pre></blockquote>
+ * <p>
+ * <em>Note:</em> This method keeps a weak reference to the {@link Target} instance and will be
+ * garbage collected if you do not keep a strong reference to it. To receive callbacks when an
+ * image is loaded use {@link #into(android.widget.ImageView, Callback)}.
+ */
+ public void into(Target target) {
+ if (target == null) {
+ throw new IllegalArgumentException("Target must not be null.");
+ }
+ if (deferred) {
+ throw new IllegalStateException("Fit cannot be used with a Target.");
+ }
+
+ Drawable drawable =
+ placeholderResId != 0 ? picasso.context.getResources().getDrawable(placeholderResId)
+ : placeholderDrawable;
+
+ if (!data.hasImage()) {
+ picasso.cancelRequest(target);
+ target.onPrepareLoad(drawable);
+ return;
+ }
+
+ Request finalData = picasso.transformRequest(data.build());
+ String requestKey = createKey(finalData);
+
+ if (!skipMemoryCache) {
+ Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
+ if (bitmap != null) {
+ picasso.cancelRequest(target);
+ target.onBitmapLoaded(bitmap, MEMORY);
+ return;
+ }
+ }
+
+ target.onPrepareLoad(drawable);
+
+ Action action = new TargetAction(picasso, target, finalData, skipMemoryCache, requestKey);
+ picasso.enqueueAndSubmit(action);
+ }
+
+ /**
+ * Asynchronously fulfills the request into the specified {@link ImageView}.
+ * <p/>
+ * <em>Note:</em> This method keeps a weak reference to the {@link ImageView} instance and will
+ * automatically support object recycling.
+ */
+ public void into(ImageView target) {
+ into(target, null);
+ }
+
+ /**
+ * Asynchronously fulfills the request into the specified {@link ImageView} and invokes the
+ * target {@link Callback} if it's not {@code null}.
+ * <p/>
+ * <em>Note:</em> The {@link Callback} param is a strong reference and will prevent your
+ * {@link android.app.Activity} or {@link android.app.Fragment} from being garbage collected. If
+ * you use this method, it is <b>strongly</b> recommended you invoke an adjacent
+ * {@link Picasso#cancelRequest(android.widget.ImageView)} call to prevent temporary leaking.
+ */
+ public void into(ImageView target, Callback callback) {
+ if (target == null) {
+ throw new IllegalArgumentException("Target must not be null.");
+ }
+
+ if (!data.hasImage()) {
+ picasso.cancelRequest(target);
+ PicassoDrawable.setPlaceholder(target, placeholderResId, placeholderDrawable);
+ return;
+ }
+
+ if (deferred) {
+ if (data.hasSize()) {
+ throw new IllegalStateException("Fit cannot be used with resize.");
+ }
+ int measuredWidth = target.getMeasuredWidth();
+ int measuredHeight = target.getMeasuredHeight();
+ if (measuredWidth == 0 || measuredHeight == 0) {
+ PicassoDrawable.setPlaceholder(target, placeholderResId, placeholderDrawable);
+ picasso.defer(target, new DeferredRequestCreator(this, target, callback));
+ return;
+ }
+ data.resize(measuredWidth, measuredHeight);
+ }
+
+ Request finalData = picasso.transformRequest(data.build());
+ String requestKey = createKey(finalData);
+
+ if (!skipMemoryCache) {
+ Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
+ if (bitmap != null) {
+ picasso.cancelRequest(target);
+ PicassoDrawable.setBitmap(target, picasso.context, bitmap, MEMORY, noFade,
+ picasso.debugging);
+ if (callback != null) {
+ callback.onSuccess();
+ }
+ return;
+ }
+ }
+
+ PicassoDrawable.setPlaceholder(target, placeholderResId, placeholderDrawable);
+
+ Action action =
+ new ImageViewAction(picasso, target, finalData, skipMemoryCache, noFade, errorResId,
+ errorDrawable, requestKey, callback);
+
+ picasso.enqueueAndSubmit(action);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/ResourceBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/ResourceBitmapHunter.java
new file mode 100644
index 000000000..fee76b200
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/ResourceBitmapHunter.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import java.io.IOException;
+
+import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
+
+class ResourceBitmapHunter extends BitmapHunter {
+ private final Context context;
+
+ ResourceBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
+ Stats stats, Action action) {
+ super(picasso, dispatcher, cache, stats, action);
+ this.context = context;
+ }
+
+ @Override Bitmap decode(Request data) throws IOException {
+ Resources res = Utils.getResources(context, data);
+ int id = Utils.getResourceId(res, data);
+ return decodeResource(res, id, data);
+ }
+
+ @Override Picasso.LoadedFrom getLoadedFrom() {
+ return DISK;
+ }
+
+ private Bitmap decodeResource(Resources resources, int id, Request data) {
+ BitmapFactory.Options bitmapOptions = null;
+ if (data.hasSize()) {
+ bitmapOptions = new BitmapFactory.Options();
+ bitmapOptions.inJustDecodeBounds = true;
+ BitmapFactory.decodeResource(resources, id, bitmapOptions);
+ calculateInSampleSize(data.targetWidth, data.targetHeight, bitmapOptions);
+ }
+ return BitmapFactory.decodeResource(resources, id, bitmapOptions);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Stats.java b/mobile/android/thirdparty/com/squareup/picasso/Stats.java
new file mode 100644
index 000000000..3eaac0249
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Stats.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.graphics.Bitmap;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+
+class Stats {
+ private static final int CACHE_HIT = 0;
+ private static final int CACHE_MISS = 1;
+ private static final int BITMAP_DECODE_FINISHED = 2;
+ private static final int BITMAP_TRANSFORMED_FINISHED = 3;
+
+ private static final String STATS_THREAD_NAME = Utils.THREAD_PREFIX + "Stats";
+
+ final HandlerThread statsThread;
+ final Cache cache;
+ final Handler handler;
+
+ long cacheHits;
+ long cacheMisses;
+ long totalOriginalBitmapSize;
+ long totalTransformedBitmapSize;
+ long averageOriginalBitmapSize;
+ long averageTransformedBitmapSize;
+ int originalBitmapCount;
+ int transformedBitmapCount;
+
+ Stats(Cache cache) {
+ this.cache = cache;
+ this.statsThread = new HandlerThread(STATS_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
+ this.statsThread.start();
+ this.handler = new StatsHandler(statsThread.getLooper(), this);
+ }
+
+ void dispatchBitmapDecoded(Bitmap bitmap) {
+ processBitmap(bitmap, BITMAP_DECODE_FINISHED);
+ }
+
+ void dispatchBitmapTransformed(Bitmap bitmap) {
+ processBitmap(bitmap, BITMAP_TRANSFORMED_FINISHED);
+ }
+
+ void dispatchCacheHit() {
+ handler.sendEmptyMessage(CACHE_HIT);
+ }
+
+ void dispatchCacheMiss() {
+ handler.sendEmptyMessage(CACHE_MISS);
+ }
+
+ void shutdown() {
+ statsThread.quit();
+ }
+
+ void performCacheHit() {
+ cacheHits++;
+ }
+
+ void performCacheMiss() {
+ cacheMisses++;
+ }
+
+ void performBitmapDecoded(long size) {
+ originalBitmapCount++;
+ totalOriginalBitmapSize += size;
+ averageOriginalBitmapSize = getAverage(originalBitmapCount, totalOriginalBitmapSize);
+ }
+
+ void performBitmapTransformed(long size) {
+ transformedBitmapCount++;
+ totalTransformedBitmapSize += size;
+ averageTransformedBitmapSize = getAverage(originalBitmapCount, totalTransformedBitmapSize);
+ }
+
+ synchronized StatsSnapshot createSnapshot() {
+ return new StatsSnapshot(cache.maxSize(), cache.size(), cacheHits, cacheMisses,
+ totalOriginalBitmapSize, totalTransformedBitmapSize, averageOriginalBitmapSize,
+ averageTransformedBitmapSize, originalBitmapCount, transformedBitmapCount,
+ System.currentTimeMillis());
+ }
+
+ private void processBitmap(Bitmap bitmap, int what) {
+ // Never send bitmaps to the handler as they could be recycled before we process them.
+ int bitmapSize = Utils.getBitmapBytes(bitmap);
+ handler.sendMessage(handler.obtainMessage(what, bitmapSize, 0));
+ }
+
+ private static long getAverage(int count, long totalSize) {
+ return totalSize / count;
+ }
+
+ private static class StatsHandler extends Handler {
+
+ private final Stats stats;
+
+ public StatsHandler(Looper looper, Stats stats) {
+ super(looper);
+ this.stats = stats;
+ }
+
+ @Override public void handleMessage(final Message msg) {
+ switch (msg.what) {
+ case CACHE_HIT:
+ stats.performCacheHit();
+ break;
+ case CACHE_MISS:
+ stats.performCacheMiss();
+ break;
+ case BITMAP_DECODE_FINISHED:
+ stats.performBitmapDecoded(msg.arg1);
+ break;
+ case BITMAP_TRANSFORMED_FINISHED:
+ stats.performBitmapTransformed(msg.arg1);
+ break;
+ default:
+ Picasso.HANDLER.post(new Runnable() {
+ @Override public void run() {
+ throw new AssertionError("Unhandled stats message." + msg.what);
+ }
+ });
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/com/squareup/picasso/StatsSnapshot.java b/mobile/android/thirdparty/com/squareup/picasso/StatsSnapshot.java
new file mode 100644
index 000000000..5f276ebf2
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/StatsSnapshot.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.util.Log;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/** Represents all stats for a {@link Picasso} instance at a single point in time. */
+public class StatsSnapshot {
+ private static final String TAG = "Picasso";
+
+ public final int maxSize;
+ public final int size;
+ public final long cacheHits;
+ public final long cacheMisses;
+ public final long totalOriginalBitmapSize;
+ public final long totalTransformedBitmapSize;
+ public final long averageOriginalBitmapSize;
+ public final long averageTransformedBitmapSize;
+ public final int originalBitmapCount;
+ public final int transformedBitmapCount;
+
+ public final long timeStamp;
+
+ public StatsSnapshot(int maxSize, int size, long cacheHits, long cacheMisses,
+ long totalOriginalBitmapSize, long totalTransformedBitmapSize, long averageOriginalBitmapSize,
+ long averageTransformedBitmapSize, int originalBitmapCount, int transformedBitmapCount,
+ long timeStamp) {
+ this.maxSize = maxSize;
+ this.size = size;
+ this.cacheHits = cacheHits;
+ this.cacheMisses = cacheMisses;
+ this.totalOriginalBitmapSize = totalOriginalBitmapSize;
+ this.totalTransformedBitmapSize = totalTransformedBitmapSize;
+ this.averageOriginalBitmapSize = averageOriginalBitmapSize;
+ this.averageTransformedBitmapSize = averageTransformedBitmapSize;
+ this.originalBitmapCount = originalBitmapCount;
+ this.transformedBitmapCount = transformedBitmapCount;
+ this.timeStamp = timeStamp;
+ }
+
+ /** Prints out this {@link StatsSnapshot} into log. */
+ public void dump() {
+ StringWriter logWriter = new StringWriter();
+ dump(new PrintWriter(logWriter));
+ Log.i(TAG, logWriter.toString());
+ }
+
+ /** Prints out this {@link StatsSnapshot} with the the provided {@link PrintWriter}. */
+ public void dump(PrintWriter writer) {
+ writer.println("===============BEGIN PICASSO STATS ===============");
+ writer.println("Memory Cache Stats");
+ writer.print(" Max Cache Size: ");
+ writer.println(maxSize);
+ writer.print(" Cache Size: ");
+ writer.println(size);
+ writer.print(" Cache % Full: ");
+ writer.println((int) Math.ceil((float) size / maxSize * 100));
+ writer.print(" Cache Hits: ");
+ writer.println(cacheHits);
+ writer.print(" Cache Misses: ");
+ writer.println(cacheMisses);
+ writer.println("Bitmap Stats");
+ writer.print(" Total Bitmaps Decoded: ");
+ writer.println(originalBitmapCount);
+ writer.print(" Total Bitmap Size: ");
+ writer.println(totalOriginalBitmapSize);
+ writer.print(" Total Transformed Bitmaps: ");
+ writer.println(transformedBitmapCount);
+ writer.print(" Total Transformed Bitmap Size: ");
+ writer.println(totalTransformedBitmapSize);
+ writer.print(" Average Bitmap Size: ");
+ writer.println(averageOriginalBitmapSize);
+ writer.print(" Average Transformed Bitmap Size: ");
+ writer.println(averageTransformedBitmapSize);
+ writer.println("===============END PICASSO STATS ===============");
+ writer.flush();
+ }
+
+ @Override public String toString() {
+ return "StatsSnapshot{"
+ + "maxSize="
+ + maxSize
+ + ", size="
+ + size
+ + ", cacheHits="
+ + cacheHits
+ + ", cacheMisses="
+ + cacheMisses
+ + ", totalOriginalBitmapSize="
+ + totalOriginalBitmapSize
+ + ", totalTransformedBitmapSize="
+ + totalTransformedBitmapSize
+ + ", averageOriginalBitmapSize="
+ + averageOriginalBitmapSize
+ + ", averageTransformedBitmapSize="
+ + averageTransformedBitmapSize
+ + ", originalBitmapCount="
+ + originalBitmapCount
+ + ", transformedBitmapCount="
+ + transformedBitmapCount
+ + ", timeStamp="
+ + timeStamp
+ + '}';
+ }
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Target.java b/mobile/android/thirdparty/com/squareup/picasso/Target.java
new file mode 100644
index 000000000..ad3ce6fcf
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Target.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+
+import static com.squareup.picasso.Picasso.LoadedFrom;
+
+/**
+ * Represents an arbitrary listener for image loading.
+ * <p/>
+ * Objects implementing this class <strong>must</strong> have a working implementation of
+ * {@link #equals(Object)} and {@link #hashCode()} for proper storage internally. Instances of this
+ * interface will also be compared to determine if view recycling is occurring. It is recommended
+ * that you add this interface directly on to a custom view type when using in an adapter to ensure
+ * correct recycling behavior.
+ */
+public interface Target {
+ /**
+ * Callback when an image has been successfully loaded.
+ * <p/>
+ * <strong>Note:</strong> You must not recycle the bitmap.
+ */
+ void onBitmapLoaded(Bitmap bitmap, LoadedFrom from);
+
+ /** Callback indicating the image could not be successfully loaded. */
+ void onBitmapFailed(Drawable errorDrawable);
+
+ /** Callback invoked right before your request is submitted. */
+ void onPrepareLoad(Drawable placeHolderDrawable);
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/TargetAction.java b/mobile/android/thirdparty/com/squareup/picasso/TargetAction.java
new file mode 100644
index 000000000..77a40f51d
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/TargetAction.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.graphics.Bitmap;
+
+final class TargetAction extends Action<Target> {
+
+ TargetAction(Picasso picasso, Target target, Request data, boolean skipCache, String key) {
+ super(picasso, target, data, skipCache, false, 0, null, key);
+ }
+
+ @Override void complete(Bitmap result, Picasso.LoadedFrom from) {
+ if (result == null) {
+ throw new AssertionError(
+ String.format("Attempted to complete action with no result!\n%s", this));
+ }
+ Target target = getTarget();
+ if (target != null) {
+ target.onBitmapLoaded(result, from);
+ if (result.isRecycled()) {
+ throw new IllegalStateException("Target callback must not recycle bitmap!");
+ }
+ }
+ }
+
+ @Override void error() {
+ Target target = getTarget();
+ if (target != null) {
+ target.onBitmapFailed(errorDrawable);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Transformation.java b/mobile/android/thirdparty/com/squareup/picasso/Transformation.java
new file mode 100644
index 000000000..2c59f160c
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Transformation.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.graphics.Bitmap;
+
+/** Image transformation. */
+public interface Transformation {
+ /**
+ * Transform the source bitmap into a new bitmap. If you create a new bitmap instance, you must
+ * call {@link android.graphics.Bitmap#recycle()} on {@code source}. You may return the original
+ * if no transformation is required.
+ */
+ Bitmap transform(Bitmap source);
+
+ /**
+ * Returns a unique key for the transformation, used for caching purposes. If the transformation
+ * has parameters (e.g. size, scale factor, etc) then these should be part of the key.
+ */
+ String key();
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/UrlConnectionDownloader.java b/mobile/android/thirdparty/com/squareup/picasso/UrlConnectionDownloader.java
new file mode 100644
index 000000000..50f9b2b98
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/UrlConnectionDownloader.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.content.Context;
+import android.net.Uri;
+import android.net.http.HttpResponseCache;
+import android.os.Build;
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import static com.squareup.picasso.Utils.parseResponseSourceHeader;
+
+/**
+ * A {@link Downloader} which uses {@link HttpURLConnection} to download images. A disk cache of 2%
+ * of the total available space will be used (capped at 50MB) will automatically be installed in the
+ * application's cache directory, when available.
+ */
+public class UrlConnectionDownloader implements Downloader {
+ static final String RESPONSE_SOURCE = "X-Android-Response-Source";
+
+ private static final Object lock = new Object();
+ static volatile Object cache;
+
+ private final Context context;
+
+ public UrlConnectionDownloader(Context context) {
+ this.context = context.getApplicationContext();
+ }
+
+ protected HttpURLConnection openConnection(Uri path) throws IOException {
+ HttpURLConnection connection = (HttpURLConnection) new URL(path.toString()).openConnection();
+ connection.setConnectTimeout(Utils.DEFAULT_CONNECT_TIMEOUT);
+ connection.setReadTimeout(Utils.DEFAULT_READ_TIMEOUT);
+ return connection;
+ }
+
+ @Override public Response load(Uri uri, boolean localCacheOnly) throws IOException {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ installCacheIfNeeded(context);
+ }
+
+ HttpURLConnection connection = openConnection(uri);
+ connection.setUseCaches(true);
+ if (localCacheOnly) {
+ connection.setRequestProperty("Cache-Control", "only-if-cached,max-age=" + Integer.MAX_VALUE);
+ }
+
+ int responseCode = connection.getResponseCode();
+ if (responseCode >= 300) {
+ connection.disconnect();
+ throw new ResponseException(responseCode + " " + connection.getResponseMessage());
+ }
+
+ boolean fromCache = parseResponseSourceHeader(connection.getHeaderField(RESPONSE_SOURCE));
+
+ return new Response(connection.getInputStream(), fromCache);
+ }
+
+ private static void installCacheIfNeeded(Context context) {
+ // DCL + volatile should be safe after Java 5.
+ if (cache == null) {
+ try {
+ synchronized (lock) {
+ if (cache == null) {
+ cache = ResponseCacheIcs.install(context);
+ }
+ }
+ } catch (IOException ignored) {
+ }
+ }
+ }
+
+ private static class ResponseCacheIcs {
+ static Object install(Context context) throws IOException {
+ File cacheDir = Utils.createDefaultCacheDir(context);
+ HttpResponseCache cache = HttpResponseCache.getInstalled();
+ if (cache == null) {
+ long maxSize = Utils.calculateDiskCacheSize(cacheDir);
+ cache = HttpResponseCache.install(cacheDir, maxSize);
+ }
+ return cache;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Utils.java b/mobile/android/thirdparty/com/squareup/picasso/Utils.java
new file mode 100644
index 000000000..bafe93f98
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Utils.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2013 Square, 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.squareup.picasso;
+
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.os.Looper;
+import android.os.Process;
+import android.os.StatFs;
+import android.provider.Settings;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.concurrent.ThreadFactory;
+
+import static android.content.Context.ACTIVITY_SERVICE;
+import static android.content.pm.ApplicationInfo.FLAG_LARGE_HEAP;
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.HONEYCOMB;
+import static android.os.Build.VERSION_CODES.HONEYCOMB_MR1;
+import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+import static android.provider.Settings.System.AIRPLANE_MODE_ON;
+
+final class Utils {
+ static final String THREAD_PREFIX = "Picasso-";
+ static final String THREAD_IDLE_NAME = THREAD_PREFIX + "Idle";
+ static final int DEFAULT_READ_TIMEOUT = 20 * 1000; // 20s
+ static final int DEFAULT_CONNECT_TIMEOUT = 15 * 1000; // 15s
+ private static final String PICASSO_CACHE = "picasso-cache";
+ private static final int KEY_PADDING = 50; // Determined by exact science.
+ private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB
+ private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
+
+ /* WebP file header
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | 'R' | 'I' | 'F' | 'F' |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | File Size |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | 'W' | 'E' | 'B' | 'P' |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+ private static final int WEBP_FILE_HEADER_SIZE = 12;
+ private static final String WEBP_FILE_HEADER_RIFF = "RIFF";
+ private static final String WEBP_FILE_HEADER_WEBP = "WEBP";
+
+ private Utils() {
+ // No instances.
+ }
+
+ static int getBitmapBytes(Bitmap bitmap) {
+ int result;
+ if (SDK_INT >= HONEYCOMB_MR1) {
+ result = BitmapHoneycombMR1.getByteCount(bitmap);
+ } else {
+ result = bitmap.getRowBytes() * bitmap.getHeight();
+ }
+ if (result < 0) {
+ throw new IllegalStateException("Negative size: " + bitmap);
+ }
+ return result;
+ }
+
+ static void checkNotMain() {
+ if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
+ throw new IllegalStateException("Method call should not happen from the main thread.");
+ }
+ }
+
+ static String createKey(Request data) {
+ StringBuilder builder;
+
+ if (data.uri != null) {
+ String path = data.uri.toString();
+ builder = new StringBuilder(path.length() + KEY_PADDING);
+ builder.append(path);
+ } else {
+ builder = new StringBuilder(KEY_PADDING);
+ builder.append(data.resourceId);
+ }
+ builder.append('\n');
+
+ if (data.rotationDegrees != 0) {
+ builder.append("rotation:").append(data.rotationDegrees);
+ if (data.hasRotationPivot) {
+ builder.append('@').append(data.rotationPivotX).append('x').append(data.rotationPivotY);
+ }
+ builder.append('\n');
+ }
+ if (data.targetWidth != 0) {
+ builder.append("resize:").append(data.targetWidth).append('x').append(data.targetHeight);
+ builder.append('\n');
+ }
+ if (data.centerCrop) {
+ builder.append("centerCrop\n");
+ } else if (data.centerInside) {
+ builder.append("centerInside\n");
+ }
+
+ if (data.transformations != null) {
+ //noinspection ForLoopReplaceableByForEach
+ for (int i = 0, count = data.transformations.size(); i < count; i++) {
+ builder.append(data.transformations.get(i).key());
+ builder.append('\n');
+ }
+ }
+
+ return builder.toString();
+ }
+
+ static void closeQuietly(InputStream is) {
+ if (is == null) return;
+ try {
+ is.close();
+ } catch (IOException ignored) {
+ }
+ }
+
+ /** Returns {@code true} if header indicates the response body was loaded from the disk cache. */
+ static boolean parseResponseSourceHeader(String header) {
+ if (header == null) {
+ return false;
+ }
+ String[] parts = header.split(" ", 2);
+ if ("CACHE".equals(parts[0])) {
+ return true;
+ }
+ if (parts.length == 1) {
+ return false;
+ }
+ try {
+ return "CONDITIONAL_CACHE".equals(parts[0]) && Integer.parseInt(parts[1]) == 304;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ static Downloader createDefaultDownloader(Context context) {
+ return new UrlConnectionDownloader(context);
+ }
+
+ static File createDefaultCacheDir(Context context) {
+ File cache = new File(context.getApplicationContext().getCacheDir(), PICASSO_CACHE);
+ if (!cache.exists()) {
+ cache.mkdirs();
+ }
+ return cache;
+ }
+
+ static long calculateDiskCacheSize(File dir) {
+ long size = MIN_DISK_CACHE_SIZE;
+
+ try {
+ StatFs statFs = new StatFs(dir.getAbsolutePath());
+ long available = ((long) statFs.getBlockCount()) * statFs.getBlockSize();
+ // Target 2% of the total space.
+ size = available / 50;
+ } catch (IllegalArgumentException ignored) {
+ }
+
+ // Bound inside min/max size for disk cache.
+ return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE);
+ }
+
+ static int calculateMemoryCacheSize(Context context) {
+ ActivityManager am = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE);
+ boolean largeHeap = (context.getApplicationInfo().flags & FLAG_LARGE_HEAP) != 0;
+ int memoryClass = am.getMemoryClass();
+ if (largeHeap && SDK_INT >= HONEYCOMB) {
+ memoryClass = ActivityManagerHoneycomb.getLargeMemoryClass(am);
+ }
+ // Target ~15% of the available heap.
+ return 1024 * 1024 * memoryClass / 7;
+ }
+
+ static boolean isAirplaneModeOn(Context context) {
+ ContentResolver contentResolver = context.getContentResolver();
+ return Settings.System.getInt(contentResolver, AIRPLANE_MODE_ON, 0) != 0;
+ }
+
+ static boolean hasPermission(Context context, String permission) {
+ return context.checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ static byte[] toByteArray(InputStream input) throws IOException {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024 * 4];
+ int n = 0;
+ while (-1 != (n = input.read(buffer))) {
+ byteArrayOutputStream.write(buffer, 0, n);
+ }
+ return byteArrayOutputStream.toByteArray();
+ }
+
+ static boolean isWebPFile(InputStream stream) throws IOException {
+ byte[] fileHeaderBytes = new byte[WEBP_FILE_HEADER_SIZE];
+ boolean isWebPFile = false;
+ if (stream.read(fileHeaderBytes, 0, WEBP_FILE_HEADER_SIZE) == WEBP_FILE_HEADER_SIZE) {
+ // If a file's header starts with RIFF and end with WEBP, the file is a WebP file
+ isWebPFile = WEBP_FILE_HEADER_RIFF.equals(new String(fileHeaderBytes, 0, 4, "US-ASCII"))
+ && WEBP_FILE_HEADER_WEBP.equals(new String(fileHeaderBytes, 8, 4, "US-ASCII"));
+ }
+ return isWebPFile;
+ }
+
+ static int getResourceId(Resources resources, Request data) throws FileNotFoundException {
+ if (data.resourceId != 0 || data.uri == null) {
+ return data.resourceId;
+ }
+
+ String pkg = data.uri.getAuthority();
+ if (pkg == null) throw new FileNotFoundException("No package provided: " + data.uri);
+
+ int id;
+ List<String> segments = data.uri.getPathSegments();
+ if (segments == null || segments.isEmpty()) {
+ throw new FileNotFoundException("No path segments: " + data.uri);
+ } else if (segments.size() == 1) {
+ try {
+ id = Integer.parseInt(segments.get(0));
+ } catch (NumberFormatException e) {
+ throw new FileNotFoundException("Last path segment is not a resource ID: " + data.uri);
+ }
+ } else if (segments.size() == 2) {
+ String type = segments.get(0);
+ String name = segments.get(1);
+
+ id = resources.getIdentifier(name, type, pkg);
+ } else {
+ throw new FileNotFoundException("More than two path segments: " + data.uri);
+ }
+ return id;
+ }
+
+ static Resources getResources(Context context, Request data) throws FileNotFoundException {
+ if (data.resourceId != 0 || data.uri == null) {
+ return context.getResources();
+ }
+
+ String pkg = data.uri.getAuthority();
+ if (pkg == null) throw new FileNotFoundException("No package provided: " + data.uri);
+ try {
+ PackageManager pm = context.getPackageManager();
+ return pm.getResourcesForApplication(pkg);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new FileNotFoundException("Unable to obtain resources for package: " + data.uri);
+ }
+ }
+
+ @TargetApi(HONEYCOMB)
+ private static class ActivityManagerHoneycomb {
+ static int getLargeMemoryClass(ActivityManager activityManager) {
+ return activityManager.getLargeMemoryClass();
+ }
+ }
+
+ static class PicassoThreadFactory implements ThreadFactory {
+ @SuppressWarnings("NullableProblems")
+ public Thread newThread(Runnable r) {
+ return new PicassoThread(r);
+ }
+ }
+
+ private static class PicassoThread extends Thread {
+ public PicassoThread(Runnable r) {
+ super(r);
+ }
+
+ @Override public void run() {
+ Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
+ super.run();
+ }
+ }
+
+ @TargetApi(HONEYCOMB_MR1)
+ private static class BitmapHoneycombMR1 {
+ static int getByteCount(Bitmap bitmap) {
+ return bitmap.getByteCount();
+ }
+ }
+}