blob: 2986804628daff94d0b876b5961b1971d940621f [file] [log] [blame]
// Copyright 2017 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 static android.app.DownloadManager.ACTION_NOTIFICATION_CLICKED;
import static org.chromium.chrome.browser.download.DownloadNotificationService.ACTION_DOWNLOAD_CANCEL;
import static org.chromium.chrome.browser.download.DownloadNotificationService.ACTION_DOWNLOAD_OPEN;
import static org.chromium.chrome.browser.download.DownloadNotificationService.ACTION_DOWNLOAD_PAUSE;
import static org.chromium.chrome.browser.download.DownloadNotificationService.ACTION_DOWNLOAD_RESUME;
import static org.chromium.chrome.browser.download.DownloadNotificationService.EXTRA_DOWNLOAD_CONTENTID_ID;
import static org.chromium.chrome.browser.download.DownloadNotificationService.EXTRA_DOWNLOAD_CONTENTID_NAMESPACE;
import static org.chromium.chrome.browser.download.DownloadNotificationService.EXTRA_DOWNLOAD_STATE_AT_CANCEL;
import static org.chromium.chrome.browser.download.DownloadNotificationService.EXTRA_IS_OFF_THE_RECORD;
import static org.chromium.chrome.browser.download.DownloadNotificationService.clearResumptionAttemptLeft;
import android.app.DownloadManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.support.annotation.Nullable;
import com.google.ipc.invalidation.util.Preconditions;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.chrome.browser.ChromeApplication;
import org.chromium.chrome.browser.download.DownloadNotificationUmaHelper.UmaDownloadResumption;
import org.chromium.chrome.browser.download.items.OfflineContentAggregatorNotificationBridgeUiFactory;
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.util.IntentUtils;
import org.chromium.components.offline_items_collection.ContentId;
import org.chromium.components.offline_items_collection.LegacyHelpers;
import org.chromium.components.offline_items_collection.PendingState;
import org.chromium.content_public.browser.BrowserStartupController;
/**
* Class that spins up native when an interaction with a notification happens and passes the
* relevant information on to native.
*/
public class DownloadBroadcastManager extends Service {
private static final String TAG = "DLBroadcastManager";
private static final int WAIT_TIME_MS = 5000;
private final DownloadSharedPreferenceHelper mDownloadSharedPreferenceHelper =
DownloadSharedPreferenceHelper.getInstance();
private final DownloadNotificationService mDownloadNotificationService;
private final Handler mHandler = new Handler();
private final Runnable mStopSelfRunnable = new Runnable() {
@Override
public void run() {
stopSelf();
}
};
public DownloadBroadcastManager() {
mDownloadNotificationService = DownloadNotificationService.getInstance();
}
// The service is only explicitly started in the resume case.
// TODO(dtrainor): Start DownloadBroadcastManager explicitly in resumption refactor.
public static void startDownloadBroadcastManager(Context context, Intent source) {
Intent intent = source != null ? new Intent(source) : new Intent();
intent.setComponent(new ComponentName(context, DownloadBroadcastManager.class));
context.startService(intent);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// Handle the download operation.
onNotificationInteraction(intent);
// If Chrome gets killed, do not restart the service.
return START_NOT_STICKY;
}
/**
* Passes down information about a notification interaction to native.
* @param intent with information about the notification interaction (action, contentId, etc).
*/
public void onNotificationInteraction(final Intent intent) {
if (!isActionHandled(intent)) return;
// Remove delayed stop of service until after native library is loaded.
mHandler.removeCallbacks(mStopSelfRunnable);
// Since there is a user interaction, resumption is not needed, so clear any queued.
cancelQueuedResumptions();
// Update notification appearance immediately in case it takes a while for native to load.
updateNotification(intent);
// Handle the intent and propagate it through the native library.
loadNativeAndPropagateInteraction(intent);
}
/**
* Cancel any download resumption tasks and reset the number of resumption attempts available.
*/
void cancelQueuedResumptions() {
DownloadResumptionScheduler.getDownloadResumptionScheduler().cancel();
// Reset number of attempts left if the action is triggered by user.
clearResumptionAttemptLeft();
}
/**
* Immediately update notification appearance without changing stored notification state.
* @param intent with information about the notification.
*/
void updateNotification(Intent intent) {
String action = intent.getAction();
if (!immediateNotificationUpdateNeeded(action)) return;
final DownloadSharedPreferenceEntry entry = getDownloadEntryFromIntent(intent);
if (entry == null) return;
switch (action) {
case ACTION_DOWNLOAD_PAUSE:
mDownloadNotificationService.notifyDownloadPaused(entry.id, entry.fileName, true,
false, entry.isOffTheRecord, entry.isTransient, null, null, false, true,
false, PendingState.NOT_PENDING);
break;
case ACTION_DOWNLOAD_CANCEL:
mDownloadNotificationService.notifyDownloadCanceled(entry.id, true);
break;
case ACTION_DOWNLOAD_RESUME:
// If user manually resumes a download, update the network type if it
// is not metered previously.
boolean canDownloadWhileMetered = entry.canDownloadWhileMetered
|| DownloadManagerService.isActiveNetworkMetered(
ContextUtils.getApplicationContext());
// Update the SharedPreference entry.
mDownloadSharedPreferenceHelper.addOrReplaceSharedPreferenceEntry(
new DownloadSharedPreferenceEntry(entry.id, entry.notificationId,
entry.isOffTheRecord, canDownloadWhileMetered, entry.fileName, true,
entry.isTransient));
mDownloadNotificationService.notifyDownloadPending(entry.id, entry.fileName,
entry.isOffTheRecord, entry.canDownloadWhileMetered, entry.isTransient,
null, null, false, true, PendingState.PENDING_NETWORK);
break;
default:
// No-op.
break;
}
}
boolean immediateNotificationUpdateNeeded(String action) {
return ACTION_DOWNLOAD_PAUSE.equals(action) || ACTION_DOWNLOAD_CANCEL.equals(action)
|| ACTION_DOWNLOAD_RESUME.equals(action);
}
/**
* Helper function that loads the native and runs given runnable.
* @param intent that is propagated when the native is loaded.
*/
@VisibleForTesting
void loadNativeAndPropagateInteraction(final Intent intent) {
final boolean browserStarted =
BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER)
.isStartupSuccessfullyCompleted();
final ContentId id = getContentIdFromIntent(intent);
final BrowserParts parts = new EmptyBrowserParts() {
@Override
public void finishNativeInitialization() {
// Delay the stop of the service by WAIT_TIME_MS after native library is loaded.
mHandler.postDelayed(mStopSelfRunnable, WAIT_TIME_MS);
if (ACTION_DOWNLOAD_RESUME.equals(intent.getAction())
&& LegacyHelpers.isLegacyDownload(id)) {
DownloadNotificationUmaHelper.recordDownloadResumptionHistogram(browserStarted
? UmaDownloadResumption.BROWSER_RUNNING
: UmaDownloadResumption.BROWSER_NOT_RUNNING);
if (!browserStarted) {
DownloadManagerService.getDownloadManagerService()
.onBackgroundDownloadStarted(id.id);
}
}
propagateInteraction(intent);
}
@Override
public boolean startServiceManagerOnly() {
if (!LegacyHelpers.isLegacyDownload(id)) return false;
return DownloadUtils.shouldStartServiceManagerOnly()
&& !ACTION_DOWNLOAD_OPEN.equals(intent.getAction());
}
};
try {
ChromeBrowserInitializer.getInstance().handlePreNativeStartup(parts);
ChromeBrowserInitializer.getInstance().handlePostNativeStartup(true, parts);
} catch (ProcessInitException e) {
Log.e(TAG, "Unable to load native library.", e);
ChromeApplication.reportStartupErrorAndExit(e);
}
}
@VisibleForTesting
void propagateInteraction(Intent intent) {
String action = intent.getAction();
DownloadNotificationUmaHelper.recordNotificationInteractionHistogram(action);
final ContentId id = getContentIdFromIntent(intent);
// Handle actions that do not require a specific entry or service delegate.
switch (action) {
case ACTION_NOTIFICATION_CLICKED:
openDownload(ContextUtils.getApplicationContext(), intent, id);
return;
case ACTION_DOWNLOAD_OPEN:
if (id != null) {
OfflineContentAggregatorNotificationBridgeUiFactory.instance().openItem(id);
}
return;
}
final DownloadSharedPreferenceEntry entry = getDownloadEntryFromIntent(intent);
boolean isOffTheRecord = entry == null
? IntentUtils.safeGetBooleanExtra(intent, EXTRA_IS_OFF_THE_RECORD, false)
: entry.isOffTheRecord;
DownloadServiceDelegate downloadServiceDelegate = getServiceDelegate(id);
Preconditions.checkNotNull(downloadServiceDelegate);
Preconditions.checkNotNull(id);
// Handle all remaining actions.
switch (action) {
case ACTION_DOWNLOAD_CANCEL:
DownloadNotificationUmaHelper.recordStateAtCancelHistogram(
LegacyHelpers.isLegacyDownload(id),
intent.getIntExtra(EXTRA_DOWNLOAD_STATE_AT_CANCEL, -1));
downloadServiceDelegate.cancelDownload(id, isOffTheRecord);
break;
case ACTION_DOWNLOAD_PAUSE:
downloadServiceDelegate.pauseDownload(id, isOffTheRecord);
break;
case ACTION_DOWNLOAD_RESUME:
DownloadItem item = (entry != null)
? entry.buildDownloadItem()
: new DownloadItem(false,
new DownloadInfo.Builder()
.setDownloadGuid(id.id)
.setIsOffTheRecord(isOffTheRecord)
.build());
downloadServiceDelegate.resumeDownload(id, item, true /* hasUserGesture */);
break;
default:
// No-op.
break;
}
downloadServiceDelegate.destroyServiceDelegate();
}
static boolean isActionHandled(Intent intent) {
if (intent == null) return false;
String action = intent.getAction();
return ACTION_DOWNLOAD_CANCEL.equals(action) || ACTION_DOWNLOAD_PAUSE.equals(action)
|| ACTION_DOWNLOAD_RESUME.equals(action) || ACTION_DOWNLOAD_OPEN.equals(action)
|| ACTION_NOTIFICATION_CLICKED.equals(action);
}
/**
* Retrieves DownloadSharedPreferenceEntry from a download action intent.
* TODO(crbug.com/691805): Instead of getting entire entry, pass only id/isOffTheRecord, after
* consolidating all downloads-related objects.
* @param intent Intent that contains the download action.
*/
private DownloadSharedPreferenceEntry getDownloadEntryFromIntent(Intent intent) {
return mDownloadSharedPreferenceHelper.getDownloadSharedPreferenceEntry(
getContentIdFromIntent(intent));
}
/**
* @param intent The {@link Intent} to pull from and build a {@link ContentId}.
* @return A {@link ContentId} built by pulling extras from {@code intent}. This will be
* {@code null} if {@code intent} is missing any required extras.
*/
private static ContentId getContentIdFromIntent(Intent intent) {
if (!intent.hasExtra(EXTRA_DOWNLOAD_CONTENTID_ID)
|| !intent.hasExtra(EXTRA_DOWNLOAD_CONTENTID_NAMESPACE)) {
return null;
}
return new ContentId(
IntentUtils.safeGetStringExtra(intent, EXTRA_DOWNLOAD_CONTENTID_NAMESPACE),
IntentUtils.safeGetStringExtra(intent, EXTRA_DOWNLOAD_CONTENTID_ID));
}
/**
* Gets appropriate download delegate that can handle interactions with download item referred
* to by the entry.
* @param id The {@link ContentId} to grab the delegate for.
* @return delegate for interactions with the entry
*/
static DownloadServiceDelegate getServiceDelegate(ContentId id) {
if (LegacyHelpers.isLegacyDownload(id)) {
return DownloadManagerService.getDownloadManagerService();
}
return OfflineContentAggregatorNotificationBridgeUiFactory.instance();
}
/**
* Called to open a particular download item. Falls back to opening Download Home.
* @param context Context of the receiver.
* @param intent Intent from the android DownloadManager.
*/
private void openDownload(Context context, Intent intent, ContentId contentId) {
long ids[] =
intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
if (ids == null || ids.length == 0) {
DownloadManagerService.openDownloadsPage(context);
return;
}
long id = ids[0];
DownloadManagerBridge.queryDownloadResult(id, result -> {
if (result.contentUri == null) {
DownloadManagerService.openDownloadsPage(context);
return;
}
String downloadFilename = IntentUtils.safeGetStringExtra(
intent, DownloadNotificationService.EXTRA_DOWNLOAD_FILE_PATH);
boolean isSupportedMimeType = IntentUtils.safeGetBooleanExtra(
intent, DownloadNotificationService.EXTRA_IS_SUPPORTED_MIME_TYPE, false);
boolean isOffTheRecord = IntentUtils.safeGetBooleanExtra(
intent, DownloadNotificationService.EXTRA_IS_OFF_THE_RECORD, false);
String originalUrl =
IntentUtils.safeGetStringExtra(intent, Intent.EXTRA_ORIGINATING_URI);
String referrer = IntentUtils.safeGetStringExtra(intent, Intent.EXTRA_REFERRER);
DownloadManagerService.openDownloadedContent(context, downloadFilename,
isSupportedMimeType, isOffTheRecord, contentId.id, id, originalUrl, referrer,
DownloadMetrics.DownloadOpenSource.NOTIFICATION);
});
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
// Since this service does not need to be bound, just return null.
return null;
}
}