/*
* 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.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
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;
import static com.squareup.picasso.MemoryPolicy.shouldWriteToMemoryCache;
import static com.squareup.picasso.Utils.OWNER_DISPATCHER;
import static com.squareup.picasso.Utils.VERB_BATCHED;
import static com.squareup.picasso.Utils.VERB_CANCELED;
import static com.squareup.picasso.Utils.VERB_DELIVERED;
import static com.squareup.picasso.Utils.VERB_ENQUEUED;
import static com.squareup.picasso.Utils.VERB_IGNORED;
import static com.squareup.picasso.Utils.VERB_PAUSED;
import static com.squareup.picasso.Utils.VERB_REPLAYING;
import static com.squareup.picasso.Utils.VERB_RETRYING;
import static com.squareup.picasso.Utils.getLogIdsForHunter;
import static com.squareup.picasso.Utils.getService;
import static com.squareup.picasso.Utils.hasPermission;
import static com.squareup.picasso.Utils.log;
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;
static final int TAG_PAUSE = 11;
static final int TAG_RESUME = 12;
static final int REQUEST_BATCH_RESUME = 13;
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 Map<Object, Action> failedActions;
final Map<Object, Action> pausedActions;
final Set<Object> pausedTags;
final Handler handler;
final Handler mainThreadHandler;
final Cache cache;
final Stats stats;
final List<BitmapHunter> batch;
final NetworkBroadcastReceiver receiver;
final boolean scansNetworkChanges;
boolean airplaneMode;
Dispatcher(Context context, ExecutorService service, Handler mainThreadHandler,
Downloader downloader, Cache cache, Stats stats) {
this.dispatcherThread = new DispatcherThread();
this.dispatcherThread.start();
Utils.flushStackLocalLeaks(dispatcherThread.getLooper());
this.context = context;
this.service = service;
this.hunterMap = new LinkedHashMap<>();
this.failedActions = new WeakHashMap<>();
this.pausedActions = new WeakHashMap<>();
this.pausedTags = new HashSet<>();
this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
this.downloader = downloader;
this.mainThreadHandler = mainThreadHandler;
this.cache = cache;
this.stats = stats;
this.batch = new ArrayList<>(4);
this.airplaneMode = Utils.isAirplaneModeOn(this.context);
this.scansNetworkChanges = hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE);
this.receiver = new NetworkBroadcastReceiver(this);
receiver.register();
}
void shutdown() {
// Shutdown the thread pool only if it is the one created by Picasso.
if (service instanceof PicassoExecutorService) {
service.shutdown();
}
downloader.shutdown();
dispatcherThread.quit();
// Unregister network broadcast receiver on the main thread.
Picasso.HANDLER.post(new Runnable() {
@Override public void run() {
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 dispatchPauseTag(Object tag) {
handler.sendMessage(handler.obtainMessage(TAG_PAUSE, tag));
}
void dispatchResumeTag(Object tag) {
handler.sendMessage(handler.obtainMessage(TAG_RESUME, tag));
}
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) {
performSubmit(action, true);
}
void performSubmit(Action action, boolean dismissFailed) {
if (pausedTags.contains(action.getTag())) {
pausedActions.put(action.getTarget(), action);
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_PAUSED, action.request.logId(),
"because tag '" + action.getTag() + "' is paused");
}
return;
}
BitmapHunter hunter = hunterMap.get(action.getKey());
if (hunter != null) {
hunter.attach(action);
return;
}
if (service.isShutdown()) {
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_IGNORED, action.request.logId(), "because shut down");
}
return;
}
hunter = forRequest(action.getPicasso(), this, cache, stats, action);
hunter.future = service.submit(hunter);
hunterMap.put(action.getKey(), hunter);
if (dismissFailed) {
failedActions.remove(action.getTarget());
}
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_ENQUEUED, action.request.logId());
}
}
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);
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_CANCELED, action.getRequest().logId());
}
}
}
if (pausedTags.contains(action.getTag())) {
pausedActions.remove(action.getTarget());
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_CANCELED, action.getRequest().logId(),
"because paused request got canceled");
}
}
Action remove = failedActions.remove(action.getTarget());
if (remove != null && remove.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_CANCELED, remove.getRequest().logId(), "from replaying");
}
}
void performPauseTag(Object tag) {
// Trying to pause a tag that is already paused.
if (!pausedTags.add(tag)) {
return;
}
// Go through all active hunters and detach/pause the requests
// that have the paused tag.
for (Iterator<BitmapHunter> it = hunterMap.values().iterator(); it.hasNext();) {
BitmapHunter hunter = it.next();
boolean loggingEnabled = hunter.getPicasso().loggingEnabled;
Action single = hunter.getAction();
List<Action> joined = hunter.getActions();
boolean hasMultiple = joined != null && !joined.isEmpty();
// Hunter has no requests, bail early.
if (single == null && !hasMultiple) {
continue;
}
if (single != null && single.getTag().equals(tag)) {
hunter.detach(single);
pausedActions.put(single.getTarget(), single);
if (loggingEnabled) {
log(OWNER_DISPATCHER, VERB_PAUSED, single.request.logId(),
"because tag '" + tag + "' was paused");
}
}
if (hasMultiple) {
for (int i = joined.size() - 1; i >= 0; i--) {
Action action = joined.get(i);
if (!action.getTag().equals(tag)) {
continue;
}
hunter.detach(action);
pausedActions.put(action.getTarget(), action);
if (loggingEnabled) {
log(OWNER_DISPATCHER, VERB_PAUSED, action.request.logId(),
"because tag '" + tag + "' was paused");
}
}
}
// Check if the hunter can be cancelled in case all its requests
// had the tag being paused here.
if (hunter.cancel()) {
it.remove();
if (loggingEnabled) {
log(OWNER_DISPATCHER, VERB_CANCELED, getLogIdsForHunter(hunter), "all actions paused");
}
}
}
}
void performResumeTag(Object tag) {
// Trying to resume a tag that is not paused.
if (!pausedTags.remove(tag)) {
return;
}
List<Action> batch = null;
for (Iterator<Action> i = pausedActions.values().iterator(); i.hasNext();) {
Action action = i.next();
if (action.getTag().equals(tag)) {
if (batch == null) {
batch = new ArrayList<>();
}
batch.add(action);
i.remove();
}
}
if (batch != null) {
mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(REQUEST_BATCH_RESUME, batch));
}
}
void performRetry(BitmapHunter hunter) {
if (hunter.isCancelled()) return;
if (service.isShutdown()) {
performError(hunter, false);
return;
}
NetworkInfo networkInfo = null;
if (scansNetworkChanges) {
ConnectivityManager connectivityManager = getService(context, CONNECTIVITY_SERVICE);
networkInfo = connectivityManager.getActiveNetworkInfo();
}
if (hunter.shouldRetry(airplaneMode, networkInfo)) {
if (hunter.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_RETRYING, getLogIdsForHunter(hunter));
}
if (hunter.getException() instanceof NetworkRequestHandler.ContentLengthException) {
hunter.networkPolicy |= NetworkPolicy.NO_CACHE.index;
}
hunter.future = service.submit(hunter);
} else {
// Mark for replay only if we observe network info changes and support replay.
boolean willReplay = scansNetworkChanges && hunter.supportsReplay();
performError(hunter, willReplay);
if (willReplay) {
markForReplay(hunter);
}
}
}
void performComplete(BitmapHunter hunter) {
if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
cache.set(hunter.getKey(), hunter.getResult());
}
hunterMap.remove(hunter.getKey());
batch(hunter);
if (hunter.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter), "for completion");
}
}
void performBatchComplete() {
List<BitmapHunter> copy = new ArrayList<>(batch);
batch.clear();
mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy));
logBatch(copy);
}
void performError(BitmapHunter hunter, boolean willReplay) {
if (hunter.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter),
"for error" + (willReplay ? " (will replay)" : ""));
}
hunterMap.remove(hunter.getKey());
batch(hunter);
}
void performAirplaneModeChange(boolean airplaneMode) {
this.airplaneMode = airplaneMode;
}
void performNetworkStateChange(NetworkInfo info) {
if (service instanceof PicassoExecutorService) {
((PicassoExecutorService) service).adjustThreadCount(info);
}
// Intentionally check only if isConnected() here before we flush out failed actions.
if (info != null && info.isConnected()) {
flushFailedActions();
}
}
private void flushFailedActions() {
if (!failedActions.isEmpty()) {
Iterator<Action> iterator = failedActions.values().iterator();
while (iterator.hasNext()) {
Action action = iterator.next();
iterator.remove();
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_REPLAYING, action.getRequest().logId());
}
performSubmit(action, false);
}
}
}
private void markForReplay(BitmapHunter hunter) {
Action action = hunter.getAction();
if (action != null) {
markForReplay(action);
}
List<Action> joined = hunter.getActions();
if (joined != null) {
//noinspection ForLoopReplaceableByForEach
for (int i = 0, n = joined.size(); i < n; i++) {
Action join = joined.get(i);
markForReplay(join);
}
}
}
private void markForReplay(Action action) {
Object target = action.getTarget();
if (target != null) {
action.willReplay = true;
failedActions.put(target, action);
}
}
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 void logBatch(List<BitmapHunter> copy) {
if (copy == null || copy.isEmpty()) return;
BitmapHunter hunter = copy.get(0);
Picasso picasso = hunter.getPicasso();
if (picasso.loggingEnabled) {
StringBuilder builder = new StringBuilder();
for (BitmapHunter bitmapHunter : copy) {
if (builder.length() > 0) builder.append(", ");
builder.append(Utils.getLogIdsForHunter(bitmapHunter));
}
log(OWNER_DISPATCHER, VERB_DELIVERED, builder.toString());
}
}
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 TAG_PAUSE: {
Object tag = msg.obj;
dispatcher.performPauseTag(tag);
break;
}
case TAG_RESUME: {
Object tag = msg.obj;
dispatcher.performResumeTag(tag);
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, false);
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);
}
}
static class NetworkBroadcastReceiver extends BroadcastReceiver {
static final String EXTRA_AIRPLANE_STATE = "state";
private final Dispatcher dispatcher;
NetworkBroadcastReceiver(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
void register() {
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_AIRPLANE_MODE_CHANGED);
if (dispatcher.scansNetworkChanges) {
filter.addAction(CONNECTIVITY_ACTION);
}
dispatcher.context.registerReceiver(this, filter);
}
void unregister() {
dispatcher.context.unregisterReceiver(this);
}
@Override public void onReceive(Context context, Intent intent) {
// On some versions of Android this may be called with a null Intent,
// also without extras (getExtras() == null), in such case we use defaults.
if (intent == null) {
return;
}
final String action = intent.getAction();
if (ACTION_AIRPLANE_MODE_CHANGED.equals(action)) {
if (!intent.hasExtra(EXTRA_AIRPLANE_STATE)) {
return; // No airplane state, ignore it. Should we query Utils.isAirplaneModeOn?
}
dispatcher.dispatchAirplaneModeChange(intent.getBooleanExtra(EXTRA_AIRPLANE_STATE, false));
} else if (CONNECTIVITY_ACTION.equals(action)) {
ConnectivityManager connectivityManager = getService(context, CONNECTIVITY_SERVICE);
dispatcher.dispatchNetworkStateChange(connectivityManager.getActiveNetworkInfo());
}
}
}
}