// 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.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import org.chromium.base.Log; import org.chromium.base.ThreadUtils; import org.chromium.base.VisibleForTesting; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.Nullable; /** * DownloadNotifier implementation that creates and updates download notifications. * This class creates the {@link DownloadNotificationService} when needed, and binds * to the latter to issue calls to show and update notifications. */ public class SystemDownloadNotifier implements DownloadNotifier { private static final String TAG = "DownloadNotifier"; private static final int DOWNLOAD_NOTIFICATION_TYPE_PROGRESS = 0; private static final int DOWNLOAD_NOTIFICATION_TYPE_SUCCESS = 1; private static final int DOWNLOAD_NOTIFICATION_TYPE_FAILURE = 2; private static final int DOWNLOAD_NOTIFICATION_TYPE_CANCEL = 3; private static final int DOWNLOAD_NOTIFICATION_TYPE_RESUME_ALL = 4; private static final int DOWNLOAD_NOTIFICATION_TYPE_PAUSE = 5; private static final int DOWNLOAD_NOTIFICATION_TYPE_INTERRUPT = 6; private final Context mApplicationContext; private final Object mLock = new Object(); @Nullable private DownloadNotificationService mBoundService; private boolean mServiceStarted; private Set<String> mActiveDownloads = new HashSet<String>(); private List<PendingNotificationInfo> mPendingNotifications = new ArrayList<PendingNotificationInfo>(); /** * Pending download notifications to be posted. */ static class PendingNotificationInfo { // Pending download notifications to be posted. public final int type; public final DownloadInfo downloadInfo; public long startTime; public boolean isAutoResumable; public boolean canDownloadWhileMetered; public boolean canResolve; public long systemDownloadId; public boolean isSupportedMimeType; public PendingNotificationInfo(int type, DownloadInfo downloadInfo) { this.type = type; this.downloadInfo = downloadInfo; } } /** * Constructor. * @param context Application context. */ public SystemDownloadNotifier(Context context) { mApplicationContext = context.getApplicationContext(); } /** * Object to receive information as the service is started and stopped. */ private final ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { synchronized (mLock) { if (!(service instanceof DownloadNotificationService.LocalBinder)) { Log.w(TAG, "Not from DownloadNotificationService, do not connect." + " Component name: " + className); assert false; return; } mBoundService = ((DownloadNotificationService.LocalBinder) service).getService(); // updateDownloadNotification() may leave some outstanding notifications // before the service is connected, handle them now. handlePendingNotifications(); } } @Override public void onServiceDisconnected(ComponentName className) { synchronized (mLock) { mBoundService = null; mServiceStarted = false; } } }; /** * For tests only: sets the DownloadNotificationService. * @param service An instance of DownloadNotificationService. */ @VisibleForTesting void setDownloadNotificationService(DownloadNotificationService service) { synchronized (mLock) { mBoundService = service; } } /** * Handles all the pending notifications that hasn't been processed. */ @VisibleForTesting void handlePendingNotifications() { synchronized (mLock) { if (mPendingNotifications.isEmpty()) return; for (PendingNotificationInfo info : mPendingNotifications) { updateDownloadNotification(info); } mPendingNotifications.clear(); } } /** * Starts and binds to the download notification service if needed. */ private void startAndBindToServiceIfNeeded() { assert Thread.holdsLock(mLock); if (mServiceStarted) return; startService(); mServiceStarted = true; } /** * Stops the download notification service if there are no download in progress. */ private void stopServiceIfNeeded() { assert Thread.holdsLock(mLock); if (mActiveDownloads.isEmpty() && mServiceStarted) { stopService(); mServiceStarted = false; } } /** * Starts and binds to the download notification service. */ @VisibleForTesting void startService() { assert Thread.holdsLock(mLock); mApplicationContext.startService( new Intent(mApplicationContext, DownloadNotificationService.class)); mApplicationContext.bindService(new Intent(mApplicationContext, DownloadNotificationService.class), mConnection, Context.BIND_AUTO_CREATE); } /** * Stops the download notification service. */ @VisibleForTesting void stopService() { assert Thread.holdsLock(mLock); mApplicationContext.stopService( new Intent(mApplicationContext, DownloadNotificationService.class)); } @Override public void notifyDownloadCanceled(String downloadGuid) { DownloadInfo downloadInfo = new DownloadInfo.Builder() .setDownloadGuid(downloadGuid) .build(); updateDownloadNotification( new PendingNotificationInfo(DOWNLOAD_NOTIFICATION_TYPE_CANCEL, downloadInfo)); } @Override public void notifyDownloadSuccessful(DownloadInfo downloadInfo, long systemDownloadId, boolean canResolve, boolean isSupportedMimeType) { PendingNotificationInfo info = new PendingNotificationInfo(DOWNLOAD_NOTIFICATION_TYPE_SUCCESS, downloadInfo); info.canResolve = canResolve; info.systemDownloadId = systemDownloadId; info.isSupportedMimeType = isSupportedMimeType; updateDownloadNotification(info); } @Override public void notifyDownloadFailed(DownloadInfo downloadInfo) { updateDownloadNotification( new PendingNotificationInfo(DOWNLOAD_NOTIFICATION_TYPE_FAILURE, downloadInfo)); } @Override public void notifyDownloadProgress( DownloadInfo downloadInfo, long startTime, boolean canDownloadWhileMetered) { PendingNotificationInfo info = new PendingNotificationInfo(DOWNLOAD_NOTIFICATION_TYPE_PROGRESS, downloadInfo); info.startTime = startTime; info.canDownloadWhileMetered = canDownloadWhileMetered; updateDownloadNotification(info); } @Override public void notifyDownloadPaused(DownloadInfo downloadInfo) { PendingNotificationInfo info = new PendingNotificationInfo(DOWNLOAD_NOTIFICATION_TYPE_PAUSE, downloadInfo); updateDownloadNotification(info); } @Override public void notifyDownloadInterrupted(DownloadInfo downloadInfo, boolean isAutoResumable) { PendingNotificationInfo info = new PendingNotificationInfo(DOWNLOAD_NOTIFICATION_TYPE_INTERRUPT, downloadInfo); info.isAutoResumable = isAutoResumable; updateDownloadNotification(info); } @Override public void resumePendingDownloads() { updateDownloadNotification( new PendingNotificationInfo(DOWNLOAD_NOTIFICATION_TYPE_RESUME_ALL, null)); } /** * Called when a successful notification is shown. * @param info Pending notification information to be handled. * @param notificationId ID of the notification. */ @VisibleForTesting void onSuccessNotificationShown( final PendingNotificationInfo notificationInfo, final int notificationId) { ThreadUtils.postOnUiThread(new Runnable() { @Override public void run() { DownloadManagerService.getDownloadManagerService( mApplicationContext).onSuccessNotificationShown( notificationInfo.downloadInfo, notificationInfo.canResolve, notificationId, notificationInfo.systemDownloadId); } }); } /** * Updates the download notification if the notification service is started. Otherwise, * wait for the notification service to become ready. * @param info Pending notification information to be handled. */ private void updateDownloadNotification(final PendingNotificationInfo notificationInfo) { synchronized (mLock) { startAndBindToServiceIfNeeded(); final DownloadInfo info = notificationInfo.downloadInfo; if (notificationInfo.type == DOWNLOAD_NOTIFICATION_TYPE_PROGRESS) { mActiveDownloads.add(info.getDownloadGuid()); } else if (notificationInfo.type != DOWNLOAD_NOTIFICATION_TYPE_RESUME_ALL) { mActiveDownloads.remove(info.getDownloadGuid()); } if (mBoundService == null) { // We need to wait for the service to connect before we can handle // the notification. Put the notification in the pending notifications // list. mPendingNotifications.add(notificationInfo); } else { switch (notificationInfo.type) { case DOWNLOAD_NOTIFICATION_TYPE_PROGRESS: mBoundService.notifyDownloadProgress(info.getDownloadGuid(), info.getFileName(), info.getPercentCompleted(), info.getTimeRemainingInMillis(), notificationInfo.startTime, info.isOffTheRecord(), notificationInfo.canDownloadWhileMetered, info.isOfflinePage()); break; case DOWNLOAD_NOTIFICATION_TYPE_PAUSE: mBoundService.notifyDownloadPaused(info.getDownloadGuid(), true, false); break; case DOWNLOAD_NOTIFICATION_TYPE_INTERRUPT: mBoundService.notifyDownloadPaused( info.getDownloadGuid(), info.isResumable(), notificationInfo.isAutoResumable); break; case DOWNLOAD_NOTIFICATION_TYPE_SUCCESS: final int notificationId = mBoundService.notifyDownloadSuccessful( info.getDownloadGuid(), info.getFilePath(), info.getFileName(), notificationInfo.systemDownloadId, info.isOfflinePage(), notificationInfo.isSupportedMimeType); onSuccessNotificationShown(notificationInfo, notificationId); stopServiceIfNeeded(); break; case DOWNLOAD_NOTIFICATION_TYPE_FAILURE: mBoundService.notifyDownloadFailed( info.getDownloadGuid(), info.getFileName()); stopServiceIfNeeded(); break; case DOWNLOAD_NOTIFICATION_TYPE_CANCEL: mBoundService.notifyDownloadCanceled(info.getDownloadGuid()); stopServiceIfNeeded(); break; case DOWNLOAD_NOTIFICATION_TYPE_RESUME_ALL: mBoundService.resumeAllPendingDownloads(); stopServiceIfNeeded(); break; default: assert false; } } } } }