package net.rdrei.android.scdl2.receiver; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import net.rdrei.android.scdl2.ApplicationPreferences; import net.rdrei.android.scdl2.Config; import net.rdrei.android.scdl2.IOUtil; import net.rdrei.android.scdl2.R; import net.rdrei.android.scdl2.guice.TrackerProvider; import net.rdrei.android.scdl2.service.MediaScannerService; import net.rdrei.android.scdl2.ui.DownloadPreferencesActivity; import roboguice.receiver.RoboBroadcastReceiver; import roboguice.util.Ln; import roboguice.util.RoboAsyncTask; import android.app.DownloadManager; import android.app.DownloadManager.Query; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.database.Cursor; import com.google.android.gms.analytics.HitBuilders; import com.google.android.gms.analytics.StandardExceptionParser; import com.google.inject.Inject; /** * Broadcast receiver reacting on download finished events. * * @author pascal * */ public class DownloadCompleteReceiver extends RoboBroadcastReceiver { private static final int HTTP_ERROR_FORBIDDEN = 403; private static final String ANALYTICS_TAG = "DOWNLOAD_COMPLETED_RECEIVER"; @Inject private DownloadManager mDownloadManager; @Inject private NotificationManager mNotificationManager; @Inject private TrackerProvider mTrackerProvider; @Override public void handleReceive(final Context context, final Intent intent) { final long downloadId = intent.getLongExtra( DownloadManager.EXTRA_DOWNLOAD_ID, 0); final ResolveDownloadTask task = new ResolveDownloadTask(context, downloadId); task.execute(); } /** * @param context * @param title */ private void showSuccessNotification(final Context context, final String title) { final Intent downloadIntent = new Intent( DownloadManager.ACTION_VIEW_DOWNLOADS); @SuppressWarnings("deprecation") final Notification notification = new Notification.Builder(context) .setAutoCancel(true) .setContentTitle( context.getString(R.string.notification_download_finished)) .setContentText(title) .setTicker( context.getString( R.string.notification_download_finished_ticker, title)) .setSmallIcon(android.R.drawable.stat_sys_download_done) .setContentIntent( PendingIntent .getActivity(context, 0, downloadIntent, 0)) .getNotification(); mNotificationManager.notify(0, notification); } /** * Create a notification indicating a download error. * * @param context * Activity context. * @param reason * The error code provided by {@link DownloadManager} */ private void showErrorNotification(final Context context, final int reason, final String title) { final String errorMessage = getDownloadErrorMessage(context, reason); final Intent preferencesIntent = new Intent(context, DownloadPreferencesActivity.class); preferencesIntent.putExtra( DownloadPreferencesActivity.EXTRA_DOWNLOAD_ERROR, reason); @SuppressWarnings("deprecation") final Notification notification = new Notification.Builder(context) .setAutoCancel(true) .setContentTitle( context.getString( R.string.notification_download_failed, title)) .setContentText(errorMessage) .setTicker( context.getString( R.string.notification_download_failed_ticker, title)) .setSmallIcon(android.R.drawable.stat_notify_error) .setContentIntent( PendingIntent.getActivity(context, 0, preferencesIntent, 0)).getNotification(); mNotificationManager.notify(0, notification); mTrackerProvider.get().send(new HitBuilders.ExceptionBuilder().setFatal(false) .setDescription("Download Receiver Error") .set("CODE", String.valueOf(reason)) .build() ); } private String getDownloadErrorMessage(final Context context, final int reason) { final int messageId; switch (reason) { case DownloadManager.ERROR_DEVICE_NOT_FOUND: messageId = R.string.error_cant_write; break; case DownloadManager.ERROR_FILE_ALREADY_EXISTS: messageId = R.string.error_already_exists; break; case DownloadManager.ERROR_INSUFFICIENT_SPACE: messageId = R.string.error_insufficient_space; break; case HTTP_ERROR_FORBIDDEN: messageId = R.string.error_download_expired; break; default: messageId = R.string.error_unknown; } return context.getString(messageId, reason); } /** * Simple POJO for passing around download information. * * @author pascal * */ public static class Download { private String mTitle; private String mPath; private int mStatus; private int mReason; public String getTitle() { return mTitle; } public void setTitle(final String title) { mTitle = title; } /** * This is not used right now, but should be to customize the * notification in case of an error. * * @return */ public int getStatus() { return mStatus; } public void setStatus(final int status) { mStatus = status; } public String getPath() { return mPath; } public void setPath(final String path) { mPath = path; } public int getReason() { return mReason; } public void setReason(final int reason) { mReason = reason; } /** * Returns the normalized path to the downloaded file, i.e. without * leading protocol specifier. * * @return String */ public String getNormalizedPath() { if (mPath == null) { throw new NullPointerException( "Path wasn't set when requesting normalizedPath."); } try { return new File(new URI(mPath)).getAbsolutePath(); } catch (final URISyntaxException e) { throw new RuntimeException( "Parsing path URI from DownloadManager failed horribly.", e); } } } private class ResolveDownloadTask extends RoboAsyncTask<Download> { private final long mDownloadId; public ResolveDownloadTask(final Context context, final long downloadId) { super(context); mDownloadId = downloadId; } @Override public Download call() throws Exception { final Query query = new DownloadManager.Query(); query.setFilterById(mDownloadId); final Cursor cursor = mDownloadManager.query(query); try { if (!cursor.moveToFirst()) { // Download could not be found. Ln.d("Could not find download with id %d.", mDownloadId); return null; } final int descriptionIndex = cursor .getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); if (!cursor.getString(descriptionIndex).equals( context.getString(R.string.download_description))) { // Download doesn't belong to us. Weird way to check, but // way, way easier than keeping track of the IDs. Ln.d("Description did not match SCDL default description."); return null; } final int titleIndex = cursor .getColumnIndex(DownloadManager.COLUMN_TITLE); final String title = cursor.getString(titleIndex); final int statusIndex = cursor .getColumnIndex(DownloadManager.COLUMN_STATUS); final int status = cursor.getInt(statusIndex); final int localUriIndex = cursor .getColumnIndex(DownloadManager.COLUMN_LOCAL_URI); final String downloadUri = cursor.getString(localUriIndex); final int reasonIndex = cursor .getColumnIndex(DownloadManager.COLUMN_REASON); final int reason = cursor.getInt(reasonIndex); final Download download = new Download(); download.setTitle(title); download.setStatus(status); download.setPath(downloadUri); download.setReason(reason); return download; } finally { cursor.close(); } } @Override protected void onSuccess(final Download t) throws Exception { super.onSuccess(t); if (t == null) { mTrackerProvider.get().send(new HitBuilders.ExceptionBuilder() .setFatal(false) .setDescription("Null-pointer in DownloadCompleteReceiver.onSuccess()") .build() ); } if (t.getStatus() == DownloadManager.STATUS_FAILED) { Ln.e("Download of '%s' failed with reason %d", t.getTitle(), t.getReason()); mTrackerProvider.get().send(new HitBuilders.ExceptionBuilder().setFatal(false) .setDescription("Download Receiver Download Error") .set("CODE", String.valueOf(t.getReason())) .build() ); showErrorNotification(context, t.getReason(), t.getTitle()); return; } if (shouldMoveFileToLocal(t)) { Ln.d("Moving temporary file to local location."); moveFileToLocal(t); } final Intent scanIntent = new Intent(context, MediaScannerService.class); scanIntent.putExtra(MediaScannerService.EXTRA_PATH, t.getNormalizedPath()); context.startService(scanIntent); showSuccessNotification(context, t.getTitle()); } protected boolean shouldMoveFileToLocal(final Download download) { return download.getPath().endsWith(Config.TMP_DOWNLOAD_POSTFIX); } /** * Moves a download to a local location and removes the temporary path * suffix. * * @param download */ protected void moveFileToLocal(final Download download) { final File path = new File(download.getNormalizedPath()); final String filename = path.getName(); @SuppressWarnings("deprecation") final File newDir = context.getDir( ApplicationPreferences.DEFAULT_STORAGE_DIRECTORY, Context.MODE_WORLD_READABLE); final String newFileName = path.getName().substring(0, filename.length() - Config.TMP_DOWNLOAD_POSTFIX.length()); final File newPath = new File(newDir, newFileName); try { IOUtil.copyFile(path, newPath); } catch (final IOException err) { Ln.w(err, "Failed to rename download."); mTrackerProvider.get().send(new HitBuilders.ExceptionBuilder() .setFatal(false) .setDescription("Download Receiver Rename Error: " + new StandardExceptionParser(context, null) .getDescription(Thread.currentThread().getName(), err) ) .build() ); return; } Ln.d("Download moved to %s", newPath.toString()); path.delete(); download.setPath(newPath.getAbsolutePath()); } } }