/**
* Copyright 2015-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.amazonaws.mobileconnectors.s3.transferutility;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import com.amazonaws.services.s3.AmazonS3;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* Performs background upload and download tasks. Uses a thread pool to manage
* the upload and download threads and limits the concurrent running threads.
* When there are no active tasks, TransferService will stop itself.
*/
public class TransferService extends Service {
private static final String TAG = "TransferService";
/*
* Constants of message sent to update handler.
*/
static final int MSG_EXEC = 100;
static final int MSG_CHECK = 200;
static final int MSG_DISCONNECT = 300;
private static final int MINUTE_IN_MILLIS = 60 * 1000;
/*
* Constants of intent action sent to the service.
*/
static final String INTENT_ACTION_TRANSFER_ADD = "add_transfer";
static final String INTENT_ACTION_TRANSFER_PAUSE = "pause_transfer";
static final String INTENT_ACTION_TRANSFER_RESUME = "resume_transfer";
static final String INTENT_ACTION_TRANSFER_CANCEL = "cancel_transfer";
static final String INTENT_BUNDLE_TRANSFER_ID = "id";
static final String INTENT_BUNDLE_S3_REFERENCE_KEY = "s3_reference_key";
private AmazonS3 s3;
/*
* updateHandler manages update requests in a queue. It updates transfers
* from database and start/stop threads if needed.
*/
private HandlerThread handlerThread;
private Handler updateHandler;
/*
* registers a BroadcastReceiver to receive network status change events. It
* will update transfer records in database directly.
*/
private NetworkInfoReceiver networkInfoReceiver;
/*
* A flag indicates whether a database scan is necessary. This is true when
* service starts and when network is disconnected.
*/
private boolean shouldScan = true;
/*
* A flag indicates whether the service is started the first time.
*/
private boolean isFirst = true;
/*
* A timestamp when the service is last known active. The service will stop
* after a minute of inactivity.
*/
private volatile long lastActiveTime;
private volatile int startId;
private TransferDBUtil dbUtil;
TransferStatusUpdater updater;
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Can't bind to TransferService");
}
/**
* <ul>
* <li>The service starts upon intents from transfer utility.</li>
* <li>It remains alive when there are active transfers.</li>
* <li>It also stays alive when network is disconnected and there are
* transfers waiting.</li>
* </ul>
*/
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "Starting Transfer Service");
dbUtil = new TransferDBUtil(getApplicationContext());
updater = new TransferStatusUpdater(dbUtil);
handlerThread = new HandlerThread(TAG + "-AWSTransferUpdateHandlerThread");
handlerThread.start();
setHandlerLooper(handlerThread.getLooper());
}
/**
* A Broadcast receiver to receive network connection change events.
*/
static class NetworkInfoReceiver extends BroadcastReceiver {
private final Handler handler;
private final ConnectivityManager connManager;
/**
* Constructs a NetworkInfoReceiver.
*
* @param handler a handle to send message to
*/
public NetworkInfoReceiver(Context context, Handler handler) {
this.handler = handler;
connManager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
}
@Override
public void onReceive(Context context, Intent intent) {
if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
final boolean networkConnected = isNetworkConnected();
Log.d(TAG, "Network connected: " + networkConnected);
handler.sendEmptyMessage(networkConnected ? MSG_CHECK : MSG_DISCONNECT);
}
}
/**
* Gets the status of network connectivity.
*
* @return true if network is connected, false otherwise.
*/
boolean isNetworkConnected() {
final NetworkInfo info = connManager.getActiveNetworkInfo();
return info != null && info.isConnected();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
this.startId = startId;
if (intent == null) {
return START_REDELIVER_INTENT;
}
final String keyForS3Client = intent.getStringExtra(INTENT_BUNDLE_S3_REFERENCE_KEY);
s3 = S3ClientReference.get(keyForS3Client);
if (s3 == null) {
Log.w(TAG, "TransferService can't get s3 client, and it will stop.");
stopSelf(startId);
return START_NOT_STICKY;
}
updateHandler.sendMessage(updateHandler.obtainMessage(MSG_EXEC, intent));
if (isFirst) {
registerReceiver(networkInfoReceiver, new IntentFilter(
ConnectivityManager.CONNECTIVITY_ACTION));
isFirst = false;
}
/*
* The service will not restart if it's killed by system.
*/
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
try {
unregisterReceiver(networkInfoReceiver);
} catch (final IllegalArgumentException iae) {
/*
* Ignore on purpose, just in case the service stops before
* onStartCommand where the receiver is registered.
*/
}
handlerThread.quit();
TransferThreadPool.closeThreadPool();
S3ClientReference.clear();
super.onDestroy();
}
class UpdateHandler extends Handler {
public UpdateHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_CHECK) {
// remove messages of the same type
updateHandler.removeMessages(MSG_CHECK);
checkTransfers();
} else if (msg.what == MSG_EXEC) {
execCommand((Intent) msg.obj);
} else if (msg.what == MSG_DISCONNECT) {
pauseAllForNetwork();
} else {
Log.e(TAG, "Unknown command: " + msg.what);
}
}
}
/**
* Checks two things: whether they are active transfers and whether a
* database scan is necessary.
*/
void checkTransfers() {
// scan database for previously unfinished transfers
if (shouldScan && networkInfoReceiver.isNetworkConnected() && s3 != null) {
loadTransfersFromDB();
shouldScan = false;
}
removeCompletedTransfers();
// update last active time if service is active
if (isActive()) {
lastActiveTime = System.currentTimeMillis();
// check after one minute
updateHandler.sendEmptyMessageDelayed(MSG_CHECK, MINUTE_IN_MILLIS);
} else {
/*
* Stop the service when it's been idled for more than a minute.
*/
Log.d(TAG, "Stop self");
stopSelf(startId);
}
}
/**
* Executes command received by the service.
*
* @param intent received intent
*/
void execCommand(Intent intent) {
// update last active time
lastActiveTime = System.currentTimeMillis();
final String action = intent.getAction();
final int id = intent.getIntExtra(INTENT_BUNDLE_TRANSFER_ID, 0);
if (id == 0) {
Log.e(TAG, "Invalid id: " + id);
return;
}
if (INTENT_ACTION_TRANSFER_ADD.equals(action)) {
if (updater.getTransfer(id) != null) {
Log.w(TAG, "Transfer has already been added: " + id);
} else {
/*
* only add transfer when network is available or else relies on
* the network change listener to scan the database
*/
final TransferRecord transfer = dbUtil.getTransferById(id);
if (transfer != null) {
updater.addTransfer(transfer);
transfer.start(s3, dbUtil, updater, networkInfoReceiver);
} else {
Log.e(TAG, "Can't find transfer: " + id);
}
}
} else if (INTENT_ACTION_TRANSFER_PAUSE.equals(action)) {
TransferRecord transfer = updater.getTransfer(id);
if (transfer == null) {
transfer = dbUtil.getTransferById(id);
}
if (transfer != null) {
transfer.pause(s3, updater);
}
} else if (INTENT_ACTION_TRANSFER_RESUME.equals(action)) {
TransferRecord transfer = updater.getTransfer(id);
if (transfer == null) {
transfer = dbUtil.getTransferById(id);
if (transfer != null) {
updater.addTransfer(transfer);
} else {
Log.e(TAG, "Can't find transfer: " + id);
}
}
transfer.start(s3, dbUtil, updater, networkInfoReceiver);
} else if (INTENT_ACTION_TRANSFER_CANCEL.equals(action)) {
TransferRecord transfer = updater.getTransfer(id);
if (transfer == null) {
transfer = dbUtil.getTransferById(id);
}
if (transfer != null) {
transfer.cancel(s3, updater);
}
} else {
Log.e(TAG, "Unknown action: " + action);
}
}
/**
* Checks whether the service is active. If a service is inactive, it can
* stop itself safely.
*
* @return true if service active, false otherwise
*/
private boolean isActive() {
if (shouldScan) {
return true;
}
for (final TransferRecord transfer : updater.getTransfers().values()) {
if (transfer.isRunning()) {
return true;
}
}
return System.currentTimeMillis() - lastActiveTime < MINUTE_IN_MILLIS;
}
/**
* Remove completed transfers from status updater.
*/
private void removeCompletedTransfers() {
final List<Integer> ids = new ArrayList<Integer>();
for (final TransferRecord transfer : updater.getTransfers().values()) {
if (TransferState.COMPLETED.equals(transfer.state)) {
/*
* Add completed transfers to remove. Removing transfers with
* updater.removeTransfer(transfer.id) will result in
* ConcurrentModificationException
*/
ids.add(transfer.id);
}
}
for (final Integer id : ids) {
updater.removeTransfer(id);
}
}
/**
* Loads transfers from database. These transfers are unfinished from
* previous session or are new transfers waiting for network. It skips any
* transfer that is already tracked by the status updater. Also starts
* transfers whose states indicate running but aren't.
*/
void loadTransfersFromDB() {
Log.d(TAG, "Loading transfers from database");
final Cursor c = dbUtil.queryAllTransfersWithType(TransferType.ANY);
int count = 0;
try {
while (c.moveToNext()) {
final int id = c.getInt(c.getColumnIndexOrThrow(TransferTable.COLUMN_ID));
final TransferState state = TransferState.getState(c.getString(c
.getColumnIndexOrThrow(TransferTable.COLUMN_STATE)));
final int partNumber = c.getInt(c.getColumnIndexOrThrow(TransferTable.COLUMN_PART_NUM));
// add unfinished transfers
if (partNumber == 0 && (TransferState.WAITING.equals(state)
|| TransferState.WAITING_FOR_NETWORK.equals(state)
|| TransferState.RESUMED_WAITING.equals(state))
|| TransferState.IN_PROGRESS.equals(state)) {
if (updater.getTransfer(id) == null) {
final TransferRecord transfer = new TransferRecord(id);
transfer.updateFromDB(c);
if (transfer.start(s3, dbUtil, updater, networkInfoReceiver)) {
updater.addTransfer(transfer);
count++;
}
} else {
final TransferRecord transfer = updater.getTransfer(id);
if (!transfer.isRunning()) {
transfer.start(s3, dbUtil, updater, networkInfoReceiver);
}
}
}
}
} finally {
c.close();
}
Log.d(TAG, count + " transfers are loaded from database");
}
/**
* Pause all running transfers and set state to WAITING_FOR_NETWORK.
*/
void pauseAllForNetwork() {
for (final TransferRecord transfer : updater.getTransfers().values()) {
if (s3 != null && transfer != null && transfer.pause(s3, updater)) {
// change status to waiting
updater.updateState(transfer.id, TransferState.WAITING_FOR_NETWORK);
}
}
shouldScan = true;
}
/**
* A helper method to swap a different looper for testing purpose.
*
* @param looper new looper
*/
void setHandlerLooper(Looper looper) {
updateHandler = new UpdateHandler(looper);
networkInfoReceiver = new NetworkInfoReceiver(getApplicationContext(), updateHandler);
}
@Override
protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
// only available when the application is debuggable
if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) == 0) {
return;
}
writer.printf("start id: %d\n", startId);
writer.printf("network status: %s\n", networkInfoReceiver.isNetworkConnected());
writer.printf("lastActiveTime: %s, shouldScan: %s\n", new Date(lastActiveTime), shouldScan);
final Map<Integer, TransferRecord> transfers = updater.getTransfers();
writer.printf("# of active transfers: %d\n", transfers.size());
for (final TransferRecord transfer : transfers.values()) {
writer.printf("bucket: %s, key: %s, status: %s, total size: %d, current: %d\n",
transfer.bucketName, transfer.key, transfer.state, transfer.bytesTotal,
transfer.bytesCurrent);
}
writer.flush();
}
}