// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.download; import android.app.ActivityManager; import android.app.DownloadManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.shapes.OvalShape; import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.support.v4.app.NotificationCompat; import android.text.TextUtils; import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.ContextUtils; import org.chromium.base.Log; import org.chromium.base.VisibleForTesting; import org.chromium.base.library_loader.ProcessInitException; import org.chromium.chrome.R; import org.chromium.chrome.browser.ChromeApplication; import org.chromium.chrome.browser.init.BrowserParts; import org.chromium.chrome.browser.init.ChromeBrowserInitializer; import org.chromium.chrome.browser.init.EmptyBrowserParts; import org.chromium.chrome.browser.offlinepages.downloads.OfflinePageDownloadBridge; import org.chromium.chrome.browser.util.IntentUtils; import java.text.NumberFormat; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Set; /** * Service responsible for creating and updating download notifications even after * Chrome gets killed. */ public class DownloadNotificationService extends Service { static final String EXTRA_DOWNLOAD_NOTIFICATION_ID = "DownloadNotificationId"; static final String EXTRA_DOWNLOAD_GUID = "DownloadGuid"; static final String EXTRA_DOWNLOAD_FILE_NAME = "DownloadFileName"; static final String EXTRA_DOWNLOAD_FILE_PATH = "DownloadFilePath"; static final String EXTRA_NOTIFICATION_DISMISSED = "NotificationDismissed"; static final String EXTRA_DOWNLOAD_IS_OFF_THE_RECORD = "DownloadIsOffTheRecord"; static final String EXTRA_DOWNLOAD_IS_OFFLINE_PAGE = "DownloadIsOfflinePage"; static final String EXTRA_IS_SUPPORTED_MIME_TYPE = "IsSupportedMimeType"; static final String ACTION_DOWNLOAD_CANCEL = "org.chromium.chrome.browser.download.DOWNLOAD_CANCEL"; static final String ACTION_DOWNLOAD_PAUSE = "org.chromium.chrome.browser.download.DOWNLOAD_PAUSE"; static final String ACTION_DOWNLOAD_RESUME = "org.chromium.chrome.browser.download.DOWNLOAD_RESUME"; public static final String ACTION_DOWNLOAD_RESUME_ALL = "org.chromium.chrome.browser.download.DOWNLOAD_RESUME_ALL"; public static final String ACTION_DOWNLOAD_OPEN = "org.chromium.chrome.browser.download.DOWNLOAD_OPEN"; static final int INVALID_DOWNLOAD_PERCENTAGE = -1; @VisibleForTesting static final String PENDING_DOWNLOAD_NOTIFICATIONS = "PendingDownloadNotifications"; static final String NOTIFICATION_NAMESPACE = "DownloadNotificationService"; private static final String TAG = "DownloadNotification"; private static final String NEXT_DOWNLOAD_NOTIFICATION_ID = "NextDownloadNotificationId"; // Notification Id starting value, to avoid conflicts from IDs used in prior versions. private static final int STARTING_NOTIFICATION_ID = 1000000; private static final String AUTO_RESUMPTION_ATTEMPT_LEFT = "ResumptionAttemptLeft"; private static final int MAX_RESUMPTION_ATTEMPT_LEFT = 5; @VisibleForTesting static final int SECONDS_PER_MINUTE = 60; @VisibleForTesting static final int SECONDS_PER_HOUR = 60 * 60; @VisibleForTesting static final int SECONDS_PER_DAY = 24 * 60 * 60; private final IBinder mBinder = new LocalBinder(); private final List<DownloadSharedPreferenceEntry> mDownloadSharedPreferenceEntries = new ArrayList<DownloadSharedPreferenceEntry>(); private final List<String> mDownloadsInProgress = new ArrayList<String>(); private NotificationManager mNotificationManager; private SharedPreferences mSharedPrefs; private Context mContext; private int mNextNotificationId; private int mNumAutoResumptionAttemptLeft; private boolean mStopPostingProgressNotifications; private Bitmap mDownloadSuccessLargeIcon; /** * Class for clients to access. */ public class LocalBinder extends Binder { DownloadNotificationService getService() { return DownloadNotificationService.this; } } @Override public void onTaskRemoved(Intent rootIntent) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { ActivityManager am = (ActivityManager) mContext.getSystemService( Context.ACTIVITY_SERVICE); List<ActivityManager.AppTask> tasks = am.getAppTasks(); // In multi-window case, there could be multiple tasks. Only swiping away the last // activity should be pause the notification. if (tasks.size() > 0) return; } mStopPostingProgressNotifications = true; // This funcion is called when Chrome is swiped away from the recent apps // drawer. So it doesn't catch all scenarios that chrome can get killed. // This will only help Android 4.4.2. onBrowserKilled(); } @Override public void onCreate() { mStopPostingProgressNotifications = false; mContext = getApplicationContext(); mNotificationManager = (NotificationManager) mContext.getSystemService( Context.NOTIFICATION_SERVICE); mSharedPrefs = ContextUtils.getAppSharedPreferences(); parseDownloadSharedPrefs(); // Because this service is a started service and returns START_STICKY in // onStartCommand(), it will be restarted as soon as resources are available // after it is killed. As a result, onCreate() may be called after Chrome // gets killed and before user restarts chrome. In that case, // DownloadManagerService.hasDownloadManagerService() will return false as // there are no calls to initialize DownloadManagerService. Pause all the // download notifications as download will not progress without chrome. if (!DownloadManagerService.hasDownloadManagerService()) { onBrowserKilled(); } mNextNotificationId = mSharedPrefs.getInt( NEXT_DOWNLOAD_NOTIFICATION_ID, STARTING_NOTIFICATION_ID); } /** * Called when browser is killed. Schedule a resumption task and pause all the download * notifications. */ private void onBrowserKilled() { cancelOffTheRecordNotifications(); pauseAllDownloads(); if (!mDownloadSharedPreferenceEntries.isEmpty()) { boolean allowMeteredConnection = false; for (int i = 0; i < mDownloadSharedPreferenceEntries.size(); ++i) { if (mDownloadSharedPreferenceEntries.get(i).canDownloadWhileMetered) { allowMeteredConnection = true; } } if (mNumAutoResumptionAttemptLeft > 0) { DownloadResumptionScheduler.getDownloadResumptionScheduler(mContext).schedule( allowMeteredConnection); } } stopSelf(); } @Override public int onStartCommand(final Intent intent, int flags, int startId) { if (isDownloadOperationIntent(intent)) { handleDownloadOperation(intent); DownloadResumptionScheduler.getDownloadResumptionScheduler(mContext).cancelTask(); // Limit the number of auto resumption attempts in case Chrome falls into a vicious // cycle. if (intent.getAction() == ACTION_DOWNLOAD_RESUME_ALL) { if (mNumAutoResumptionAttemptLeft > 0) { mNumAutoResumptionAttemptLeft--; updateResumptionAttemptLeft(); } } else { // Reset number of attempts left if the action is triggered by user. mNumAutoResumptionAttemptLeft = MAX_RESUMPTION_ATTEMPT_LEFT; clearResumptionAttemptLeft(); } } // This should restart the service after Chrome gets killed. However, this // doesn't work on Android 4.4.2. return START_STICKY; } @Override public IBinder onBind(Intent intent) { return mBinder; } /** * Helper method to update the remaining number of background resumption attempts left. * @param attamptLeft Number of attempt left. */ private void updateResumptionAttemptLeft() { SharedPreferences.Editor editor = mSharedPrefs.edit(); editor.putInt(AUTO_RESUMPTION_ATTEMPT_LEFT, mNumAutoResumptionAttemptLeft); editor.apply(); } /** * Helper method to clear the remaining number of background resumption attempts left. */ static void clearResumptionAttemptLeft() { SharedPreferences SharedPrefs = ContextUtils.getAppSharedPreferences(); SharedPreferences.Editor editor = SharedPrefs.edit(); editor.remove(AUTO_RESUMPTION_ATTEMPT_LEFT); editor.apply(); } /** * Add a in-progress download notification. * @param downloadGuid GUID of the download. * @param fileName File name of the download. * @param percentage Percentage completed. Value should be between 0 to 100 if * the percentage can be determined, or -1 if it is unknown. * @param timeRemainingInMillis Remaining download time in milliseconds. * @param startTime Time when download started. * @param isOffTheRecord Whether the download is off the record. * @param canDownloadWhileMetered Whether the download can happen in metered network. */ public void notifyDownloadProgress(String downloadGuid, String fileName, int percentage, long timeRemainingInMillis, long startTime, boolean isOffTheRecord, boolean canDownloadWhileMetered, boolean isOfflinePage) { if (mStopPostingProgressNotifications) return; boolean indeterminate = percentage == INVALID_DOWNLOAD_PERCENTAGE; NotificationCompat.Builder builder = buildNotification( android.R.drawable.stat_sys_download, fileName, null); builder.setOngoing(true).setProgress(100, percentage, indeterminate); builder.setPriority(Notification.PRIORITY_HIGH); if (!indeterminate) { NumberFormat formatter = NumberFormat.getPercentInstance(Locale.getDefault()); String percentText = formatter.format(percentage / 100.0); String duration = formatRemainingTime(mContext, timeRemainingInMillis); builder.setContentText(duration); if (Build.VERSION.CODENAME.equals("N") || Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { builder.setSubText(percentText); } else { builder.setContentInfo(percentText); } } int notificationId = getNotificationId(downloadGuid); int itemType = isOfflinePage ? DownloadSharedPreferenceEntry.ITEM_TYPE_OFFLINE_PAGE : DownloadSharedPreferenceEntry.ITEM_TYPE_DOWNLOAD; addOrReplaceSharedPreferenceEntry(new DownloadSharedPreferenceEntry(notificationId, isOffTheRecord, canDownloadWhileMetered, downloadGuid, fileName, itemType)); if (startTime > 0) builder.setWhen(startTime); Intent cancelIntent = buildActionIntent( ACTION_DOWNLOAD_CANCEL, notificationId, downloadGuid, fileName, isOfflinePage); builder.addAction(R.drawable.btn_close_white, mContext.getResources().getString(R.string.download_notification_cancel_button), buildPendingIntent(cancelIntent, notificationId)); Intent pauseIntent = buildActionIntent( ACTION_DOWNLOAD_PAUSE, notificationId, downloadGuid, fileName, isOfflinePage); builder.addAction(R.drawable.ic_vidcontrol_pause, mContext.getResources().getString(R.string.download_notification_pause_button), buildPendingIntent(pauseIntent, notificationId)); updateNotification(notificationId, builder.build()); if (!mDownloadsInProgress.contains(downloadGuid)) { mDownloadsInProgress.add(downloadGuid); } } /** * Cancel a download notification. * @notificationId Notification ID of the download * @param downloadGuid GUID of the download. */ @VisibleForTesting void cancelNotification(int notificaitonId, String downloadGuid) { mNotificationManager.cancel(NOTIFICATION_NAMESPACE, notificaitonId); removeSharedPreferenceEntry(downloadGuid); mDownloadsInProgress.remove(downloadGuid); } /** * Called when a download is canceled. * @param downloadGuid GUID of the download. */ public void notifyDownloadCanceled(String downloadGuid) { DownloadSharedPreferenceEntry entry = getDownloadSharedPreferenceEntry(downloadGuid); if (entry == null) return; cancelNotification(entry.notificationId, downloadGuid); } /** * Change a download notification to paused state. * @param downloadGuid GUID of the download. * @param isResumable Whether download can be resumed. * @param isAutoResumable whether download is can be resumed automatically. */ public void notifyDownloadPaused(String downloadGuid, boolean isResumable, boolean isAutoResumable) { DownloadSharedPreferenceEntry entry = getDownloadSharedPreferenceEntry(downloadGuid); if (entry == null) return; if (!isResumable) { notifyDownloadFailed(downloadGuid, entry.fileName); return; } NotificationCompat.Builder builder = buildNotification( android.R.drawable.ic_media_pause, entry.fileName, mContext.getResources().getString(R.string.download_notification_paused)); Intent cancelIntent = buildActionIntent( ACTION_DOWNLOAD_CANCEL, entry.notificationId, entry.downloadGuid, entry.fileName, entry.isOfflinePage()); Intent dismissIntent = new Intent(cancelIntent); dismissIntent.putExtra(EXTRA_NOTIFICATION_DISMISSED, true); builder.setDeleteIntent(buildPendingIntent(dismissIntent, entry.notificationId)); builder.addAction(R.drawable.btn_close_white, mContext.getResources().getString(R.string.download_notification_cancel_button), buildPendingIntent(cancelIntent, entry.notificationId)); Intent resumeIntent = buildActionIntent( ACTION_DOWNLOAD_RESUME, entry.notificationId, entry.downloadGuid, entry.fileName, entry.isOfflinePage()); resumeIntent.putExtra(EXTRA_DOWNLOAD_IS_OFF_THE_RECORD, entry.isOffTheRecord); builder.addAction(R.drawable.ic_get_app_white_24dp, mContext.getResources().getString(R.string.download_notification_resume_button), buildPendingIntent(resumeIntent, entry.notificationId)); updateNotification(entry.notificationId, builder.build()); // If download is not auto resumable, there is no need to keep it in SharedPreferences. // Keep off the record downloads in SharedPreferences so we can cancel it when browser is // killed. if (!isAutoResumable && !entry.isOffTheRecord) { removeSharedPreferenceEntry(downloadGuid); } mDownloadsInProgress.remove(downloadGuid); } /** * Add a download successful notification. * @param downloadGuid GUID of the download. * @param filePath Full path to the download. * @param fileName Filename of the download. * @param systemDownloadId Download ID assigned by system DownloadManager. * @param isOfflinePage Whether the download is for offline page. * @param isSupportedMimeType Whether the MIME type can be viewed inside browser. * @return ID of the successful download notification. Used for removing the notification when * user click on the snackbar. */ public int notifyDownloadSuccessful( String downloadGuid, String filePath, String fileName, long systemDownloadId, boolean isOfflinePage, boolean isSupportedMimeType) { int notificationId = getNotificationId(downloadGuid); NotificationCompat.Builder builder = buildNotification( R.drawable.offline_pin, fileName, mContext.getResources().getString(R.string.download_notification_completed)); ComponentName component = new ComponentName( mContext.getPackageName(), DownloadBroadcastReceiver.class.getName()); Intent intent; if (isOfflinePage) { intent = buildActionIntent(ACTION_DOWNLOAD_OPEN, notificationId, downloadGuid, fileName, isOfflinePage); } else { intent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED); long[] idArray = {systemDownloadId}; intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS, idArray); intent.putExtra(EXTRA_DOWNLOAD_FILE_PATH, filePath); intent.putExtra(EXTRA_IS_SUPPORTED_MIME_TYPE, isSupportedMimeType); } intent.setComponent(component); builder.setContentIntent(PendingIntent.getBroadcast( mContext, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)); if (mDownloadSuccessLargeIcon == null) { Bitmap bitmap = BitmapFactory.decodeResource( mContext.getResources(), R.drawable.offline_pin); mDownloadSuccessLargeIcon = getLargeNotificationIcon(bitmap); } builder.setLargeIcon(mDownloadSuccessLargeIcon); updateNotification(notificationId, builder.build()); removeSharedPreferenceEntry(downloadGuid); mDownloadsInProgress.remove(downloadGuid); return notificationId; } /** * Add a download failed notification. * @param downloadGuid GUID of the download. * @param fileName GUID of the download. */ public void notifyDownloadFailed(String downloadGuid, String fileName) { // If the download is not in history db, fileName could be empty. Get it from // SharedPreferences. if (TextUtils.isEmpty(fileName)) { DownloadSharedPreferenceEntry entry = getDownloadSharedPreferenceEntry(downloadGuid); if (entry == null) return; fileName = entry.fileName; } int notificationId = getNotificationId(downloadGuid); NotificationCompat.Builder builder = buildNotification( android.R.drawable.stat_sys_download_done, fileName, mContext.getResources().getString(R.string.download_notification_failed)); updateNotification(notificationId, builder.build()); removeSharedPreferenceEntry(downloadGuid); mDownloadsInProgress.remove(downloadGuid); } /** * Called to pause all the download notifications. */ @VisibleForTesting void pauseAllDownloads() { for (int i = mDownloadSharedPreferenceEntries.size() - 1; i >= 0; --i) { DownloadSharedPreferenceEntry entry = mDownloadSharedPreferenceEntries.get(i); notifyDownloadPaused(entry.downloadGuid, !entry.isOffTheRecord, true); } } /** * Cancels all off the record download notifications. */ void cancelOffTheRecordNotifications() { for (int i = mDownloadSharedPreferenceEntries.size() - 1; i >= 0; --i) { DownloadSharedPreferenceEntry entry = mDownloadSharedPreferenceEntries.get(i); if (entry.isOffTheRecord) { notifyDownloadCanceled(entry.downloadGuid); } } } /** * Helper method to build a PendingIntent from the provided intent. * @param intent Intent to broadcast. * @param notificationId ID of the notification. */ private PendingIntent buildPendingIntent(Intent intent, int notificationId) { return PendingIntent.getBroadcast( mContext, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); } /** * Helper method to build an download action Intent from the provided information. * @param action Download action to perform. * @param notificationId ID of the notification. * @param downloadGuid GUID of the download. * @param fileName Name of the download file. * @param isOfflinePage Whether the intent is for offline page download. */ private Intent buildActionIntent( String action, int notificationId, String downloadGuid, String fileName, boolean isOfflinePage) { ComponentName component = new ComponentName( mContext.getPackageName(), DownloadBroadcastReceiver.class.getName()); Intent intent = new Intent(action); intent.setComponent(component); intent.putExtra(EXTRA_DOWNLOAD_NOTIFICATION_ID, notificationId); intent.putExtra(EXTRA_DOWNLOAD_GUID, downloadGuid); intent.putExtra(EXTRA_DOWNLOAD_FILE_NAME, fileName); intent.putExtra(EXTRA_DOWNLOAD_IS_OFFLINE_PAGE, isOfflinePage); return intent; } /** * Builds a notification to be displayed. * @param iconId Id of the notification icon. * @param title Title of the notification. * @param contentText Notification content text to be displayed. * @return notification builder that builds the notification to be displayed */ private NotificationCompat.Builder buildNotification( int iconId, String title, String contentText) { NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext) .setContentTitle(title) .setSmallIcon(iconId) .setLocalOnly(true) .setAutoCancel(true) .setContentText(contentText); return builder; } private Bitmap getLargeNotificationIcon(Bitmap bitmap) { Resources resources = mContext.getResources(); int height = (int) resources.getDimension(android.R.dimen.notification_large_icon_height); int width = (int) resources.getDimension(android.R.dimen.notification_large_icon_width); final OvalShape circle = new OvalShape(); circle.resize(width, height); final Paint paint = new Paint(); paint.setColor(ApiCompatibilityUtils.getColor(resources, R.color.google_blue_grey_500)); final Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(result); circle.draw(canvas, paint); float leftOffset = (width - bitmap.getWidth()) / 2f; float topOffset = (height - bitmap.getHeight()) / 2f; if (leftOffset >= 0 && topOffset >= 0) { canvas.drawBitmap(bitmap, leftOffset, topOffset, null); } else { // Scale down the icon into the notification icon dimensions canvas.drawBitmap(bitmap, new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()), new Rect(0, 0, width, height), null); } return result; } /** * Retrives DownloadSharedPreferenceEntry from a download action intent. * @param intent Intent that contains the download action. */ private DownloadSharedPreferenceEntry getDownloadEntryFromIntent(Intent intent) { if (intent.getAction() == ACTION_DOWNLOAD_RESUME_ALL) return null; String guid = IntentUtils.safeGetStringExtra(intent, EXTRA_DOWNLOAD_GUID); DownloadSharedPreferenceEntry entry = getDownloadSharedPreferenceEntry(guid); if (entry != null) return entry; int notificationId = IntentUtils.safeGetIntExtra( intent, EXTRA_DOWNLOAD_NOTIFICATION_ID, -1); String fileName = IntentUtils.safeGetStringExtra(intent, EXTRA_DOWNLOAD_FILE_NAME); boolean metered = DownloadManagerService.isActiveNetworkMetered(mContext); boolean isOffTheRecord = IntentUtils.safeGetBooleanExtra( intent, EXTRA_DOWNLOAD_IS_OFF_THE_RECORD, false); boolean isOfflinePage = IntentUtils.safeGetBooleanExtra( intent, EXTRA_DOWNLOAD_IS_OFFLINE_PAGE, false); return new DownloadSharedPreferenceEntry(notificationId, isOffTheRecord, metered, guid, fileName, isOfflinePage ? DownloadSharedPreferenceEntry.ITEM_TYPE_OFFLINE_PAGE : DownloadSharedPreferenceEntry.ITEM_TYPE_DOWNLOAD); } /** * Helper method to launch the browser process and handle a download operation that is included * in the given intent. * @param intent Intent with the download operation. */ private void handleDownloadOperation(final Intent intent) { final DownloadSharedPreferenceEntry entry = getDownloadEntryFromIntent(intent); if (intent.getAction() == ACTION_DOWNLOAD_PAUSE) { // If browser process already goes away, the download should have already paused. Do // nothing in that case. if (!DownloadManagerService.hasDownloadManagerService()) { notifyDownloadPaused(entry.downloadGuid, !entry.isOffTheRecord, false); return; } } else if (intent.getAction() == ACTION_DOWNLOAD_RESUME) { boolean metered = DownloadManagerService.isActiveNetworkMetered(mContext); if (!entry.canDownloadWhileMetered) { // If user manually resumes a download, update the network type if it // is not metered previously. entry.canDownloadWhileMetered = metered; } // Update the SharedPreference entry. addOrReplaceSharedPreferenceEntry(entry); } else if (intent.getAction() == ACTION_DOWNLOAD_RESUME_ALL && (mDownloadSharedPreferenceEntries.isEmpty() || DownloadManagerService.hasDownloadManagerService())) { return; } else if (intent.getAction() == ACTION_DOWNLOAD_OPEN) { // TODO(fgorski): Do we even need to do anything special here, before we launch Chrome? } BrowserParts parts = new EmptyBrowserParts() { @Override public boolean shouldStartGpuProcess() { return false; } @Override public void finishNativeInitialization() { int itemType = entry != null ? entry.itemType : (intent.getAction() == ACTION_DOWNLOAD_OPEN ? DownloadSharedPreferenceEntry.ITEM_TYPE_OFFLINE_PAGE : DownloadSharedPreferenceEntry.ITEM_TYPE_DOWNLOAD); DownloadServiceDelegate downloadServiceDelegate = intent.getAction() == ACTION_DOWNLOAD_OPEN ? null : getServiceDelegate(itemType); switch (intent.getAction()) { case ACTION_DOWNLOAD_CANCEL: // TODO(qinmin): Alternatively, we can delete the downloaded content on // SD card, and remove the download ID from the SharedPreferences so we // don't need to restart the browser process. http://crbug.com/579643. cancelNotification(entry.notificationId, entry.downloadGuid); downloadServiceDelegate.cancelDownload(entry.downloadGuid, entry.isOffTheRecord, IntentUtils.safeGetBooleanExtra( intent, EXTRA_NOTIFICATION_DISMISSED, false)); break; case ACTION_DOWNLOAD_PAUSE: downloadServiceDelegate.pauseDownload(entry.downloadGuid, entry.isOffTheRecord); break; case ACTION_DOWNLOAD_RESUME: notifyDownloadProgress(entry.downloadGuid, entry.fileName, INVALID_DOWNLOAD_PERCENTAGE, 0, 0, entry.isOffTheRecord, entry.canDownloadWhileMetered, entry.isOfflinePage()); downloadServiceDelegate.resumeDownload(entry.buildDownloadItem(), true); break; case ACTION_DOWNLOAD_RESUME_ALL: assert entry == null; resumeAllPendingDownloads(); break; case ACTION_DOWNLOAD_OPEN: OfflinePageDownloadBridge.openDownloadedPage( IntentUtils.safeGetStringExtra(intent, EXTRA_DOWNLOAD_GUID)); break; default: Log.e(TAG, "Unrecognized intent action.", intent); break; } if (intent.getAction() != ACTION_DOWNLOAD_OPEN) { downloadServiceDelegate.destroyServiceDelegate(); } } }; try { ChromeBrowserInitializer.getInstance(mContext).handlePreNativeStartup(parts); ChromeBrowserInitializer.getInstance(mContext).handlePostNativeStartup(true, parts); } catch (ProcessInitException e) { Log.e(TAG, "Unable to load native library.", e); ChromeApplication.reportStartupErrorAndExit(e); } } /** * Gets appropriate download delegate that can handle interactions with download item referred * to by the entry. * @param forOfflinePage Whether the service should deal with offline pages or downloads. * @return delegate for interactions with the entry */ DownloadServiceDelegate getServiceDelegate(int downloadItemType) { if (downloadItemType == DownloadSharedPreferenceEntry.ITEM_TYPE_OFFLINE_PAGE) { return OfflinePageDownloadBridge.getDownloadServiceDelegate(); } if (downloadItemType != DownloadSharedPreferenceEntry.ITEM_TYPE_DOWNLOAD) { Log.e(TAG, "Unrecognized intent type.", downloadItemType); } return DownloadManagerService.getDownloadManagerService(getApplicationContext()); } /** * Update the notification with id. * @param id Id of the notification that has to be updated. * @param notification the notification object that needs to be updated. */ @VisibleForTesting void updateNotification(int id, Notification notification) { mNotificationManager.notify(NOTIFICATION_NAMESPACE, id, notification); } /** * Checks if an intent requires operations on a download. * @param intent An intent to validate. * @return true if the intent requires actions, or false otherwise. */ static boolean isDownloadOperationIntent(Intent intent) { if (intent == null) return false; if (ACTION_DOWNLOAD_RESUME_ALL.equals(intent.getAction())) return true; if (!ACTION_DOWNLOAD_CANCEL.equals(intent.getAction()) && !ACTION_DOWNLOAD_RESUME.equals(intent.getAction()) && !ACTION_DOWNLOAD_PAUSE.equals(intent.getAction()) && !ACTION_DOWNLOAD_OPEN.equals(intent.getAction())) { return false; } if (!intent.hasExtra(EXTRA_DOWNLOAD_NOTIFICATION_ID) || !intent.hasExtra(EXTRA_DOWNLOAD_FILE_NAME) || !intent.hasExtra(EXTRA_DOWNLOAD_GUID)) { return false; } final int notificationId = IntentUtils.safeGetIntExtra(intent, EXTRA_DOWNLOAD_NOTIFICATION_ID, -1); if (notificationId == -1) return false; final String fileName = IntentUtils.safeGetStringExtra(intent, EXTRA_DOWNLOAD_FILE_NAME); if (fileName == null) return false; final String guid = IntentUtils.safeGetStringExtra(intent, EXTRA_DOWNLOAD_GUID); if (!DownloadSharedPreferenceEntry.isValidGUID(guid)) return false; return true; } /** * Adds a DownloadSharedPreferenceEntry to SharedPrefs. If an entry with the GUID already exists * in SharedPrefs, update it if it has changed. * @param DownloadSharedPreferenceEntry A DownloadSharedPreferenceEntry to be added. */ private void addOrReplaceSharedPreferenceEntry(DownloadSharedPreferenceEntry pendingEntry) { Iterator<DownloadSharedPreferenceEntry> iterator = mDownloadSharedPreferenceEntries.iterator(); while (iterator.hasNext()) { DownloadSharedPreferenceEntry entry = iterator.next(); if (entry.downloadGuid.equals(pendingEntry.downloadGuid)) { if (entry.equals(pendingEntry)) return; iterator.remove(); break; } } mDownloadSharedPreferenceEntries.add(pendingEntry); storeDownloadSharedPreferenceEntries(); } /** * Removes a DownloadSharedPreferenceEntry from SharedPrefs given by the GUID. * @param guid Download GUID to be removed. */ private void removeSharedPreferenceEntry(String guid) { Iterator<DownloadSharedPreferenceEntry> iterator = mDownloadSharedPreferenceEntries.iterator(); boolean found = false; while (iterator.hasNext()) { DownloadSharedPreferenceEntry entry = iterator.next(); if (entry.downloadGuid.equals(guid)) { iterator.remove(); found = true; break; } } if (found) { storeDownloadSharedPreferenceEntries(); } } /** * Resumes all pending downloads from |mDownloadSharedPreferenceEntries|. If a download is * already in progress, do nothing. */ public void resumeAllPendingDownloads() { boolean isNetworkMetered = DownloadManagerService.isActiveNetworkMetered(mContext); if (!DownloadManagerService.hasDownloadManagerService()) return; for (int i = 0; i < mDownloadSharedPreferenceEntries.size(); ++i) { DownloadSharedPreferenceEntry entry = mDownloadSharedPreferenceEntries.get(i); if (mDownloadsInProgress.contains(entry.downloadGuid)) continue; if (!entry.canDownloadWhileMetered && isNetworkMetered) continue; notifyDownloadProgress(entry.downloadGuid, entry.fileName, INVALID_DOWNLOAD_PERCENTAGE, 0, 0, false, entry.canDownloadWhileMetered, entry.isOfflinePage()); DownloadServiceDelegate downloadServiceDelegate = getServiceDelegate(entry.itemType); downloadServiceDelegate.resumeDownload(entry.buildDownloadItem(), false); downloadServiceDelegate.destroyServiceDelegate(); } } /** * Parse a list of the DownloadSharedPreferenceEntry and the number of auto resumption attempt * left from the shared preference. */ void parseDownloadSharedPrefs() { mNumAutoResumptionAttemptLeft = mSharedPrefs.getInt(AUTO_RESUMPTION_ATTEMPT_LEFT, MAX_RESUMPTION_ATTEMPT_LEFT); if (!mSharedPrefs.contains(PENDING_DOWNLOAD_NOTIFICATIONS)) return; Set<String> entries = DownloadManagerService.getStoredDownloadInfo( mSharedPrefs, PENDING_DOWNLOAD_NOTIFICATIONS); for (String entryString : entries) { DownloadSharedPreferenceEntry entry = DownloadSharedPreferenceEntry.parseFromString(entryString); if (entry.notificationId > 0) { mDownloadSharedPreferenceEntries.add( DownloadSharedPreferenceEntry.parseFromString(entryString)); } } } /** * Gets a DownloadSharedPreferenceEntry that has the given GUID. * @param guid GUID to query. * @return a DownloadSharedPreferenceEntry that has the specified GUID. */ private DownloadSharedPreferenceEntry getDownloadSharedPreferenceEntry(String guid) { for (int i = 0; i < mDownloadSharedPreferenceEntries.size(); ++i) { if (mDownloadSharedPreferenceEntries.get(i).downloadGuid.equals(guid)) { return mDownloadSharedPreferenceEntries.get(i); } } return null; } /** * Helper method to store all the SharedPreferences entries. */ private void storeDownloadSharedPreferenceEntries() { Set<String> entries = new HashSet<String>(); for (int i = 0; i < mDownloadSharedPreferenceEntries.size(); ++i) { entries.add(mDownloadSharedPreferenceEntries.get(i).getSharedPreferenceString()); } DownloadManagerService.storeDownloadInfo( mSharedPrefs, PENDING_DOWNLOAD_NOTIFICATIONS, entries); } /** * Return the notification ID for the given download GUID. * @return notification ID to be used. */ private int getNotificationId(String downloadGuid) { DownloadSharedPreferenceEntry entry = getDownloadSharedPreferenceEntry(downloadGuid); if (entry != null) return entry.notificationId; int notificationId = mNextNotificationId; mNextNotificationId = mNextNotificationId == Integer.MAX_VALUE ? STARTING_NOTIFICATION_ID : mNextNotificationId + 1; SharedPreferences.Editor editor = mSharedPrefs.edit(); editor.putInt(NEXT_DOWNLOAD_NOTIFICATION_ID, mNextNotificationId); editor.apply(); return notificationId; } /** * Format remaining time for the given millis, in the following format: * 5 hours; will include 1 unit, can go down to seconds precision. * This is similar to what android.java.text.Formatter.formatShortElapsedTime() does. Don't use * ui::TimeFormat::Simple() as it is very expensive. * * @param context the application context. * @param millis the remaining time in milli seconds. * @return the formatted remaining time. */ public static String formatRemainingTime(Context context, long millis) { long secondsLong = millis / 1000; int days = 0; int hours = 0; int minutes = 0; if (secondsLong >= SECONDS_PER_DAY) { days = (int) (secondsLong / SECONDS_PER_DAY); secondsLong -= (long) days * SECONDS_PER_DAY; } if (secondsLong >= SECONDS_PER_HOUR) { hours = (int) (secondsLong / SECONDS_PER_HOUR); secondsLong -= (long) hours * SECONDS_PER_HOUR; } if (secondsLong >= SECONDS_PER_MINUTE) { minutes = (int) (secondsLong / SECONDS_PER_MINUTE); secondsLong -= (long) minutes * SECONDS_PER_MINUTE; } int seconds = (int) secondsLong; if (days >= 2) { days += (hours + 12) / 24; return context.getString(R.string.remaining_duration_days, days); } else if (days > 0) { return context.getString(R.string.remaining_duration_one_day); } else if (hours >= 2) { hours += (minutes + 30) / 60; return context.getString(R.string.remaining_duration_hours, hours); } else if (hours > 0) { return context.getString(R.string.remaining_duration_one_hour); } else if (minutes >= 2) { minutes += (seconds + 30) / 60; return context.getString(R.string.remaining_duration_minutes, minutes); } else if (minutes > 0) { return context.getString(R.string.remaining_duration_one_minute); } else if (seconds == 1) { return context.getString(R.string.remaining_duration_one_second); } else { return context.getString(R.string.remaining_duration_seconds, seconds); } } }