package cm4mmupdater.service;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.WifiLock;
import android.os.*;
import android.widget.RemoteViews;
import android.widget.Toast;
import cm4mmupdater.customExceptions.NotEnoughSpaceException;
import cm4mmupdater.customTypes.UpdateInfo;
import cm4mmupdater.interfaces.IDownloadService;
import cm4mmupdater.interfaces.IDownloadServiceCallback;
import cm4mmupdater.misc.Constants;
import cm4mmupdater.misc.Log;
import cm4mmupdater.ui.ApplyUpdateActivity;
import cm4mmupdater.ui.DownloadActivity;
import cm4mmupdater.ui.MainActivity;
import cm4mmupdater.ui.R;
import cm4mmupdater.utils.MD5;
import cm4mmupdater.utils.Preferences;
import cm4mmupdater.utils.SysUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.DefaultHttpClient;
import java.io.*;
import java.net.URI;
import java.util.List;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
public class DownloadService extends Service {
private static final String TAG = "DownloadService";
private Boolean showDebugOutput = false;
private final RemoteCallbackList<IDownloadServiceCallback> mCallbacks = new RemoteCallbackList<IDownloadServiceCallback>();
private boolean prepareForDownloadCancel;
private boolean mMirrorNameUpdated;
private String mMirrorName;
private boolean mDownloading = false;
private UpdateInfo mCurrentUpdate;
private WifiLock mWifiLock;
private volatile long mtotalDownloaded;
private int mcontentLength;
private long mStartTime;
private String minutesString;
private String secondsString;
private String fullUpdateFolderPath;
private Resources res;
private long localFileSize = 0;
private Preferences prefs;
@Override
public IBinder onBind(Intent arg0) {
return mBinder;
}
private final IDownloadService.Stub mBinder = new IDownloadService.Stub() {
public void Download(UpdateInfo ui) throws RemoteException {
mDownloading = true;
boolean success = checkForConnectionAndUpdate(ui);
notifyUser(ui, success);
mDownloading = false;
}
public boolean DownloadRunning() throws RemoteException {
return mDownloading;
}
public void PauseDownload() throws RemoteException {
//TODO: Pause Download
stopDownload();
}
public void cancelDownload() throws RemoteException {
cancelCurrentDownload();
}
public UpdateInfo getCurrentUpdate() throws RemoteException {
return mCurrentUpdate;
}
public String getCurrentMirrorName() throws RemoteException {
return mMirrorName;
}
public void registerCallback(IDownloadServiceCallback cb)
throws RemoteException {
if (cb != null) mCallbacks.register(cb);
}
public void unregisterCallback(IDownloadServiceCallback cb)
throws RemoteException {
if (cb != null) mCallbacks.unregister(cb);
}
};
@Override
public void onCreate() {
if (showDebugOutput) Log.d(TAG, "Download Service Created");
prefs = new Preferences(this);
showDebugOutput = prefs.displayDebugOutput();
mWifiLock = ((WifiManager) getSystemService(WIFI_SERVICE)).createWifiLock("CM Updater");
fullUpdateFolderPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + prefs.getUpdateFolder();
res = getResources();
minutesString = res.getString(R.string.minutes);
secondsString = res.getString(R.string.seconds);
}
@Override
public void onDestroy() {
mCallbacks.kill();
super.onDestroy();
}
private boolean checkForConnectionAndUpdate(UpdateInfo updateToDownload) {
if (showDebugOutput) Log.d(TAG, "Called CheckForConnectionAndUpdate");
mCurrentUpdate = updateToDownload;
boolean success;
mWifiLock.acquire();
try {
if (showDebugOutput) Log.d(TAG, "Downloading update...");
success = downloadFile(updateToDownload);
}
catch (RuntimeException ex) {
Log.e(TAG, "RuntimeEx while checking for updates", ex);
notificateDownloadError(ex.getMessage());
return false;
}
finally {
mWifiLock.release();
}
//Be sure to return false if the User canceled the Download
return !prepareForDownloadCancel && success;
}
private boolean downloadFile(UpdateInfo updateInfo) {
if (showDebugOutput) Log.d(TAG, "Called downloadFile");
HttpClient httpClient = new DefaultHttpClient();
HttpClient MD5httpClient = new DefaultHttpClient();
HttpUriRequest req, md5req;
HttpResponse response, md5response;
List<URI> updateMirrors = updateInfo.updateFileUris();
int size = updateMirrors.size();
int start = new Random().nextInt(size);
if (showDebugOutput) Log.d(TAG, "Mirrorcount: " + size);
URI updateURI;
File destinationFile;
File partialDestinationFile;
File destinationMD5File;
String downloadedMD5 = null;
//If directory not exists, create it
File directory = new File(fullUpdateFolderPath);
if (!directory.exists()) {
directory.mkdirs();
if (showDebugOutput) Log.d(TAG, "UpdateFolder created");
}
String fileName = updateInfo.getFileName();
if (null == fileName || fileName.length() < 1) {
fileName = "update.zip";
}
if (showDebugOutput) Log.d(TAG, "fileName: " + fileName);
//Set the Filename to update.zip.partial
partialDestinationFile = new File(fullUpdateFolderPath, fileName + ".partial");
destinationFile = new File(fullUpdateFolderPath, fileName);
if (partialDestinationFile.exists())
localFileSize = partialDestinationFile.length();
//For every Mirror
for (int i = 0; i < size; i++) {
if (!prepareForDownloadCancel) {
updateURI = updateMirrors.get((start + i) % size);
mMirrorName = updateURI.getHost();
if (showDebugOutput) Log.d(TAG, "Mirrorname: " + mMirrorName);
boolean md5Available = true;
mMirrorNameUpdated = false;
try {
req = new HttpGet(updateURI);
md5req = new HttpGet(updateURI + ".md5sum");
// Add no-cache Header, so the File gets downloaded each time
req.addHeader("Cache-Control", "no-cache");
md5req.addHeader("Cache-Control", "no-cache");
if (showDebugOutput) Log.d(TAG, "Trying to download md5sum file from " + md5req.getURI());
md5response = MD5httpClient.execute(md5req);
if (showDebugOutput) Log.d(TAG, "Trying to download update zip from " + req.getURI());
if (localFileSize > 0) {
if (showDebugOutput) Log.d(TAG, "localFileSize for Resume: " + localFileSize);
req.addHeader("Range", "bytes=" + localFileSize + "-");
}
response = httpClient.execute(req);
int serverResponse = response.getStatusLine().getStatusCode();
int md5serverResponse = md5response.getStatusLine().getStatusCode();
if (serverResponse == HttpStatus.SC_NOT_FOUND) {
if (showDebugOutput) Log.d(TAG, "File not found on Server. Trying next one.");
} else if (serverResponse != HttpStatus.SC_OK && serverResponse != HttpStatus.SC_PARTIAL_CONTENT) {
if (showDebugOutput)
Log.d(TAG, "Server returned status code " + serverResponse + " for update zip trying next mirror");
} else {
// server must support partial content for resume
if (localFileSize > 0 && serverResponse != HttpStatus.SC_PARTIAL_CONTENT) {
if (showDebugOutput) Log.d(TAG, "Resume not supported");
ToastHandler.sendMessage(ToastHandler.obtainMessage(0, R.string.download_resume_not_supported, 0));
//To get the UdpateProgressBar working correctly, when server does not support resume
localFileSize = 0;
} else if (localFileSize > 0 && serverResponse == HttpStatus.SC_PARTIAL_CONTENT) {
if (showDebugOutput) Log.d(TAG, "Resume supported");
ToastHandler.sendMessage(ToastHandler.obtainMessage(0, R.string.download_resume_download, 0));
}
if (md5serverResponse != HttpStatus.SC_OK) {
md5Available = false;
if (showDebugOutput)
Log.d(TAG, "Server returned status code " + md5serverResponse + " for update zip md5sum. Downloading without it");
}
if (md5Available) {
destinationMD5File = new File(fullUpdateFolderPath, fileName + ".md5sum");
if (destinationMD5File.exists()) destinationMD5File.delete();
try {
if (showDebugOutput) Log.d(TAG, "Trying to Read MD5 hash from response");
HttpEntity temp = md5response.getEntity();
InputStreamReader isr = new InputStreamReader(temp.getContent());
BufferedReader br = new BufferedReader(isr);
downloadedMD5 = br.readLine().split(" ")[0];
if (showDebugOutput) Log.d(TAG, "MD5: " + downloadedMD5);
br.close();
isr.close();
if (temp != null)
temp.consumeContent();
//Write the String in a .md5 File
if (downloadedMD5 != null && !downloadedMD5.equals("")) {
writeMD5(destinationMD5File, downloadedMD5);
}
}
catch (IOException e) {
Log.e(TAG, "Exception while reading MD5 response: ", e);
//TODO: Do not throw, continue with zipfile download
throw new IOException("MD5 Response cannot be read");
}
}
// Download Update ZIP if md5sum went ok
HttpEntity entity = response.getEntity();
dumpFile(entity, partialDestinationFile, destinationFile);
//Was the download canceled?
if (prepareForDownloadCancel) {
if (showDebugOutput) Log.d(TAG, "Download was canceled. Break the for loop");
break;
}
if (entity != null && !prepareForDownloadCancel) {
if (showDebugOutput) Log.d(TAG, "Consuming entity....");
entity.consumeContent();
if (showDebugOutput) Log.d(TAG, "Entity consumed");
} else {
if (showDebugOutput) Log.d(TAG, "Entity resetted to NULL");
entity = null;
}
if (showDebugOutput) Log.d(TAG, "Update download finished");
if (md5Available) {
if (showDebugOutput) Log.d(TAG, "Performing MD5 verification");
if (!MD5.checkMD5(downloadedMD5, destinationFile)) {
throw new IOException(res.getString(R.string.md5_verification_failed));
}
}
//If we reach here, download & MD5 check went fine :)
return true;
}
}
catch (IOException ex) {
ToastHandler.sendMessage(ToastHandler.obtainMessage(0, ex.getMessage()));
Log.e(TAG, "An error occured while downloading the update file. Trying next mirror", ex);
}
catch (NotEnoughSpaceException ex) {
ToastHandler.sendMessage(ToastHandler.obtainMessage(0, ex.getMessage()));
Log.e(TAG, "Not enough Space on SDCard to download the Update");
return false;
}
if (Thread.currentThread().isInterrupted() || !Thread.currentThread().isAlive())
break;
} else {
if (showDebugOutput) Log.d(TAG, "Not trying any more mirrors, download canceled");
break;
}
}
ToastHandler.sendMessage(ToastHandler.obtainMessage(0, R.string.unable_to_download_file, 0));
if (showDebugOutput) Log.d(TAG, "Unable to download the update file from any mirror");
return false;
}
private void dumpFile(HttpEntity entity, File partialDestinationFile, File destinationFile) throws IOException, NotEnoughSpaceException {
if (showDebugOutput) Log.d(TAG, "DumpFile Called");
if (!prepareForDownloadCancel) {
mcontentLength = (int) entity.getContentLength();
if (mcontentLength <= 0) {
if (showDebugOutput) Log.d(TAG, "unable to determine the update file size, Set ContentLength to 1024");
mcontentLength = 1024;
} else if (showDebugOutput) Log.d(TAG, "Update size: " + (mcontentLength / 1024) + "KB");
//Check if there is enough Space on SDCard for Downloading the Update
if (!SysUtils.EnoughSpaceOnSdCard(mcontentLength))
throw new NotEnoughSpaceException(res.getString(R.string.download_not_enough_space));
mStartTime = System.currentTimeMillis();
byte[] buff = new byte[64 * 1024];
int read;
RandomAccessFile out = new RandomAccessFile(partialDestinationFile, "rw");
out.seek(localFileSize);
InputStream is = entity.getContent();
TimerTask progressUpdateTimerTask = new TimerTask() {
@Override
public void run() {
onProgressUpdate();
}
};
Timer progressUpdateTimer = new Timer();
try {
//If File exists, set the Progress to it. Otherwise it will be initial 0
mtotalDownloaded = localFileSize;
progressUpdateTimer.scheduleAtFixedRate(progressUpdateTimerTask, 100, prefs.getProgressUpdateFreq());
while ((read = is.read(buff)) > 0 && !prepareForDownloadCancel) {
out.write(buff, 0, read);
mtotalDownloaded += read;
}
out.close();
is.close();
if (!prepareForDownloadCancel) {
partialDestinationFile.renameTo(destinationFile);
if (showDebugOutput) Log.d(TAG, "Download finished");
} else {
if (showDebugOutput) Log.d(TAG, "Download cancelled");
}
}
catch (IOException e) {
out.close();
try {
destinationFile.delete();
}
catch (SecurityException ex) {
Log.e(TAG, "Unable to delete downloaded File. Continue anyway.", ex);
}
}
finally {
progressUpdateTimer.cancel();
buff = null;
}
} else if (showDebugOutput) Log.d(TAG, "Download Cancel in Progress. Don't start Downloading");
}
private void writeMD5(File md5File, String md5) throws IOException {
if (showDebugOutput) Log.d(TAG, "Writing the calculated MD5 to disk");
FileWriter fw = new FileWriter(md5File);
try {
fw.write(md5);
fw.flush();
}
catch (IOException e) {
Log.e(TAG, "Exception while writing MD5 to disk", e);
}
finally {
fw.close();
}
}
private void onProgressUpdate() {
//Only update the Notification and DownloadLayout, when no downloadcancel is in progress, so the notification will not pop up again
if (!prepareForDownloadCancel) {
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Notification mNotification = new Notification(R.drawable.icon_notification, res.getString(R.string.notification_tickertext), System.currentTimeMillis());
mNotification.flags = Notification.FLAG_NO_CLEAR;
mNotification.flags = Notification.FLAG_ONGOING_EVENT;
RemoteViews mNotificationRemoteView = new RemoteViews(getPackageName(), R.layout.notification);
Intent mNotificationIntent = new Intent(this, DownloadActivity.class);
mNotificationIntent.putExtra(Constants.KEY_UPDATE_INFO, (Serializable) mCurrentUpdate);
PendingIntent mNotificationContentIntent = PendingIntent.getActivity(this, 0, mNotificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
mNotification.contentView = mNotificationRemoteView;
mNotification.contentIntent = mNotificationContentIntent;
//lcoalFileSize because the contentLength will only be the missing bytes and not the whole file
long contentLengthOfFullDownload = mcontentLength + localFileSize;
long speed = ((mtotalDownloaded - localFileSize) / (System.currentTimeMillis() - mStartTime));
speed = (speed > 0) ? speed : 1;
long remainingTime = ((contentLengthOfFullDownload - mtotalDownloaded) / speed);
String stringDownloaded = mtotalDownloaded / 1048576 + "/" + contentLengthOfFullDownload / 1048576 + " MB";
String stringSpeed = speed + " kB/s";
String stringRemainingTime = remainingTime / 60000 + " " + minutesString + " " + remainingTime % 60 + " " + secondsString;
String stringComplete = stringDownloaded + " " + stringSpeed + " " + stringRemainingTime;
mNotificationRemoteView.setTextViewText(R.id.notificationTextDownloadInfos, stringComplete);
mNotificationRemoteView.setProgressBar(R.id.notificationProgressBar, (int) contentLengthOfFullDownload, (int) mtotalDownloaded, false);
mNotificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_STATUS, mNotification);
if (!mMirrorNameUpdated) {
UpdateDownloadMirror(mMirrorName);
mMirrorNameUpdated = true;
}
//Update the DownloadProgress
UpdateDownloadProgress(mtotalDownloaded, (int) contentLengthOfFullDownload, stringDownloaded, stringSpeed, stringRemainingTime);
} else if (showDebugOutput)
Log.d(TAG, "Downloadcancel in Progress. Not updating the Notification and DownloadLayout");
}
private void notifyUser(UpdateInfo ui, boolean success) {
if (showDebugOutput) Log.d(TAG, "Called Notify User");
Intent i;
if (!success) {
if (showDebugOutput) Log.d(TAG, "Downloaded Update was NULL");
DeleteDownloadStatusNotification();
DownloadError();
stopSelf();
return;
}
i = new Intent(this, ApplyUpdateActivity.class);
i.putExtra(Constants.KEY_UPDATE_INFO, (Serializable) ui);
//Set the Notification to finished
DeleteDownloadStatusNotification();
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Notification mNotification = new Notification(R.drawable.icon, res.getString(R.string.notification_finished), System.currentTimeMillis());
mNotification.flags = Notification.FLAG_AUTO_CANCEL;
PendingIntent mNotificationContentIntent = PendingIntent.getActivity(this, 0, i, 0);
mNotification.setLatestEventInfo(this, res.getString(R.string.app_name), res.getString(R.string.notification_finished), mNotificationContentIntent);
Uri notificationRingtone = prefs.getConfiguredRingtone();
if (prefs.getVibrate())
mNotification.defaults = Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS;
else
mNotification.defaults = Notification.DEFAULT_LIGHTS;
if (notificationRingtone == null) {
mNotification.sound = null;
} else {
mNotification.sound = notificationRingtone;
}
mNotificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_FINISHED, mNotification);
DownloadFinished();
}
private void notificateDownloadError(String ExceptionText) {
mDownloading = false;
Intent i = new Intent(this, MainActivity.class);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, i,
PendingIntent.FLAG_ONE_SHOT);
Notification notification = new Notification(android.R.drawable.stat_notify_error,
res.getString(R.string.not_update_download_error_ticker),
System.currentTimeMillis());
notification.flags = Notification.FLAG_AUTO_CANCEL;
notification.setLatestEventInfo(
this,
res.getString(R.string.not_update_download_error_title),
ExceptionText,
contentIntent);
Uri notificationRingtone = prefs.getConfiguredRingtone();
if (prefs.getVibrate())
notification.defaults = Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS;
else
notification.defaults = Notification.DEFAULT_LIGHTS;
if (notificationRingtone == null) {
notification.sound = null;
} else {
notification.sound = notificationRingtone;
}
((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).notify(R.string.not_update_download_error_title, notification);
if (showDebugOutput) Log.d(TAG, "Download Error");
ToastHandler.sendMessage(ToastHandler.obtainMessage(0, R.string.exception_while_downloading, 0));
}
private void DeleteDownloadStatusNotification() {
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.cancel(Constants.NOTIFICATION_DOWNLOAD_STATUS);
if (showDebugOutput) Log.d(TAG, "Download Notification removed");
}
private void UpdateDownloadProgress(final long downloaded, final int total, final String downloadedText, final String speedText, final String remainingTimeText) {
final int N = mCallbacks.beginBroadcast();
for (int i = 0; i < N; i++) {
try {
mCallbacks.getBroadcastItem(i).updateDownloadProgress(downloaded, total, downloadedText, speedText, remainingTimeText);
}
catch (RemoteException e) {
// The RemoteCallbackList will take care of removing
// the dead object for us.
}
}
mCallbacks.finishBroadcast();
}
private void UpdateDownloadMirror(String mirrorName) {
final int M = mCallbacks.beginBroadcast();
for (int i = 0; i < M; i++) {
try {
mCallbacks.getBroadcastItem(i).UpdateDownloadMirror(mirrorName);
}
catch (RemoteException e) {
// The RemoteCallbackList will take care of removing
// the dead object for us.
}
}
mCallbacks.finishBroadcast();
}
private void DownloadFinished() {
final int M = mCallbacks.beginBroadcast();
for (int i = 0; i < M; i++) {
try {
mCallbacks.getBroadcastItem(i).DownloadFinished(mCurrentUpdate);
}
catch (RemoteException e) {
// The RemoteCallbackList will take care of removing
// the dead object for us.
}
}
mCallbacks.finishBroadcast();
}
private void DownloadError() {
final int M = mCallbacks.beginBroadcast();
for (int i = 0; i < M; i++) {
try {
mCallbacks.getBroadcastItem(i).DownloadError();
}
catch (RemoteException e) {
// The RemoteCallbackList will take care of removing
// the dead object for us.
}
}
mCallbacks.finishBroadcast();
}
private void cancelCurrentDownload() {
prepareForDownloadCancel = true;
if (showDebugOutput) Log.d(TAG, "Download Service CancelDownload was called");
DeleteDownloadStatusNotification();
File update = new File(fullUpdateFolderPath + "/" + mCurrentUpdate.getFileName());
File md5sum = new File(fullUpdateFolderPath + "/" + mCurrentUpdate.getFileName() + ".md5sum");
if (update.exists()) {
update.delete();
if (showDebugOutput) Log.d(TAG, update.getAbsolutePath() + " deleted");
}
if (md5sum.exists()) {
md5sum.delete();
if (showDebugOutput) Log.d(TAG, md5sum.getAbsolutePath() + " deleted");
}
mDownloading = false;
if (showDebugOutput) Log.d(TAG, "Download Cancel StopSelf was called");
stopSelf();
}
private void stopDownload() {
//TODO: Pause download
prepareForDownloadCancel = true;
mDownloading = false;
stopSelf();
}
private final Handler ToastHandler = new Handler() {
public void handleMessage(Message msg) {
if (msg.arg1 != 0)
Toast.makeText(DownloadService.this, msg.arg1, Toast.LENGTH_LONG).show();
else
Toast.makeText(DownloadService.this, (String) msg.obj, Toast.LENGTH_LONG).show();
}
};
}