package com.novoda.downloadmanager.lib; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.support.annotation.NonNull; import com.novoda.downloadmanager.lib.logger.LLog; import com.novoda.downloadmanager.notifications.NotificationDisplayer; import com.novoda.downloadmanager.notifications.NotificationVisibility; import static android.content.Intent.ACTION_BOOT_COMPLETED; import static android.content.Intent.ACTION_MEDIA_MOUNTED; import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; import static com.novoda.downloadmanager.lib.Constants.ACTION_CANCEL; import static com.novoda.downloadmanager.lib.Constants.ACTION_RETRY; import static com.novoda.downloadmanager.notifications.NotificationVisibility.ACTIVE_OR_COMPLETE; import static com.novoda.downloadmanager.notifications.NotificationVisibility.ONLY_WHEN_COMPLETE; /** * Receives system broadcasts (boot, network connectivity) */ public class DownloadReceiver extends BroadcastReceiver { // TODO split this into multiple receivers public static final String EXTRA_BATCH_ID = "com.novoda.downloadmanager.extra.BATCH_ID"; private static final String TAG = "DownloadReceiver"; private static Handler sAsyncHandler; static { final HandlerThread thread = new HandlerThread(TAG); thread.start(); sAsyncHandler = new Handler(thread.getLooper()); } private final DownloadsUriProvider downloadsUriProvider; private BatchRepository batchRepository; public DownloadReceiver() { downloadsUriProvider = DownloadsUriProvider.getInstance(); } @Override public void onReceive(@NonNull Context context, @NonNull Intent intent) { ContentResolver contentResolver = context.getContentResolver(); DownloadDeleter downloadDeleter = new DownloadDeleter(contentResolver); RealSystemFacade systemFacade = new RealSystemFacade(context, new Clock()); batchRepository = BatchRepository.from(contentResolver, downloadDeleter, downloadsUriProvider, systemFacade); switch (intent.getAction()) { case ACTION_BOOT_COMPLETED: case ACTION_MEDIA_MOUNTED: case ACTION_RETRY: startService(context); break; case CONNECTIVITY_ACTION: checkConnectivityToStartService(context); break; case NotificationDisplayer.ACTION_DOWNLOAD_CANCELLED_CLICK: case NotificationDisplayer.ACTION_DOWNLOAD_FAILED_CLICK: case NotificationDisplayer.ACTION_DOWNLOAD_SUCCESS_CLICK: case NotificationDisplayer.ACTION_DOWNLOAD_RUNNING_CLICK: case NotificationDisplayer.ACTION_DOWNLOAD_SUBMITTED_CLICK: case NotificationDisplayer.ACTION_DOWNLOAD_OTHER_CLICK: case NotificationDisplayer.ACTION_NOTIFICATION_DISMISSED: case ACTION_CANCEL: handleSystemNotificationAction(context, intent); break; default: // no need to handle any other cases break; } } private void checkConnectivityToStartService(Context context) { ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo info = connManager.getActiveNetworkInfo(); if (info != null && info.isConnected()) { startService(context); } } private void handleSystemNotificationAction(final Context context, final Intent intent) { final PendingResult result = goAsync(); if (result == null) { handleNotificationBroadcast(context, intent); } else { sAsyncHandler.post( new Runnable() { @Override public void run() { handleNotificationBroadcast(context, intent); result.finish(); } }); } } private void handleNotificationBroadcast(Context context, Intent intent) { long[] ids = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS); int[] statuses = intent.getIntArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_STATUSES); long batchId = getBatchId(intent); String action = intent.getAction(); switch (action) { case NotificationDisplayer.ACTION_DOWNLOAD_CANCELLED_CLICK: case NotificationDisplayer.ACTION_DOWNLOAD_FAILED_CLICK: case NotificationDisplayer.ACTION_DOWNLOAD_SUCCESS_CLICK: sendNotificationClickedIntent(context, ids, statuses); hideNotification(context, batchId); break; case NotificationDisplayer.ACTION_DOWNLOAD_RUNNING_CLICK: case NotificationDisplayer.ACTION_DOWNLOAD_SUBMITTED_CLICK: sendNotificationClickedIntent(context, ids, statuses); break; case NotificationDisplayer.ACTION_NOTIFICATION_DISMISSED: hideNotification(context, batchId); break; case ACTION_CANCEL: cancelBatchThroughDatabaseState(batchId); break; case NotificationDisplayer.ACTION_DOWNLOAD_OTHER_CLICK: // not sure - but seems sensible to just dismiss the notification hideNotification(context, batchId); break; default: // shouldn't happen - other cases should be handled in ACTION_DOWNLOAD_OTHER_CLICK break; } } /** * Notify the owner of a running download that its notification was clicked. */ private void sendNotificationClickedIntent(Context context, long[] ids, int[] statuses) { Intent appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED); appIntent.setPackage(context.getPackageName()); appIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS, ids); appIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_STATUSES, statuses); context.sendBroadcast(appIntent); } /** * Mark the given {@link DownloadManager#COLUMN_ID} as being acknowledged by * user so it's not renewed later. */ private void hideNotification(Context context, long batchId) { Uri uri = ContentUris.withAppendedId(downloadsUriProvider.getBatchesUri(), batchId); Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); try { if (cursor.moveToFirst()) { int status = getInt(cursor, DownloadContract.Batches.COLUMN_STATUS); @NotificationVisibility.Value int visibility = getInt(cursor, DownloadContract.Batches.COLUMN_VISIBILITY); if ((DownloadStatus.isCancelled(status) || DownloadStatus.isCompleted(status)) && (visibility == ACTIVE_OR_COMPLETE || visibility == ONLY_WHEN_COMPLETE)) { ContentValues values = new ContentValues(1); values.put(DownloadContract.Batches.COLUMN_VISIBILITY, NotificationVisibility.HIDDEN); context.getContentResolver().update(uri, values, null, null); } } else { LLog.w("Missing details for download " + batchId); } } finally { cursor.close(); } } /** * Mark the given batch as being cancelled by user so it will be cancelled by the running thread. */ private void cancelBatchThroughDatabaseState(long batchId) { batchRepository.cancelBatch(batchId); } private long getBatchId(Intent intent) { return intent.getLongExtra(EXTRA_BATCH_ID, -1); } private static int getInt(Cursor cursor, String col) { return cursor.getInt(cursor.getColumnIndexOrThrow(col)); } private void startService(Context context) { context.startService(new Intent(context, DownloadService.class)); } }