org.chromium.chrome.browser.download.DownloadNotificationService.java Source code

Java tutorial

Introduction

Here is the source code for org.chromium.chrome.browser.download.DownloadNotificationService.java

Source

// 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);
        }
    }
}