Commit 261b3795 authored by Xing Liu's avatar Xing Liu Committed by Commit Bot

Download notification: Fix a crash on O+.

When startForeground called with a cancelled notification update,
startForeground() will be bypassed in DownloadForegroundServiceManager.
startOrUpdateForegroundService(). This leads to an Android crash.

Instead we need to create an dummy notification and still call
startForeground() with it to avoid the crash. This dummy notification
will be dropped immediately in DownloadNotificationService.

Bug: 1121096
Change-Id: I0f68bff47403b7478f504af6ee03f154c727c82f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2436511
Commit-Queue: Xing Liu <xingliu@chromium.org>
Reviewed-by: default avatarDavid Trainor <dtrainor@chromium.org>
Cr-Commit-Position: refs/heads/master@{#811871}
parent da3537d6
...@@ -11,6 +11,8 @@ import android.content.ComponentName; ...@@ -11,6 +11,8 @@ import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Handler; import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
...@@ -20,6 +22,11 @@ import androidx.annotation.VisibleForTesting; ...@@ -20,6 +22,11 @@ import androidx.annotation.VisibleForTesting;
import org.chromium.base.ContextUtils; import org.chromium.base.ContextUtils;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.chrome.browser.download.DownloadNotificationService.DownloadStatus; import org.chromium.chrome.browser.download.DownloadNotificationService.DownloadStatus;
import org.chromium.chrome.browser.notifications.NotificationUmaTracker;
import org.chromium.chrome.browser.notifications.NotificationWrapperBuilderFactory;
import org.chromium.chrome.browser.notifications.channels.ChromeChannelDefinitions;
import org.chromium.components.browser_ui.notifications.NotificationMetadata;
import org.chromium.components.browser_ui.notifications.NotificationWrapperBuilder;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
...@@ -29,14 +36,14 @@ import java.util.Map; ...@@ -29,14 +36,14 @@ import java.util.Map;
* Manager to stop and start the foreground service associated with downloads. * Manager to stop and start the foreground service associated with downloads.
*/ */
public class DownloadForegroundServiceManager { public class DownloadForegroundServiceManager {
private static class DownloadUpdate { protected static class DownloadUpdate {
int mNotificationId; int mNotificationId;
Notification mNotification; Notification mNotification;
@DownloadNotificationService.DownloadStatus @DownloadNotificationService.DownloadStatus
int mDownloadStatus; int mDownloadStatus;
Context mContext; Context mContext;
DownloadUpdate(int notificationId, Notification notification, DownloadUpdate(int notificationId, @Nullable Notification notification,
@DownloadNotificationService.DownloadStatus int downloadStatus, Context context) { @DownloadNotificationService.DownloadStatus int downloadStatus, Context context) {
mNotificationId = notificationId; mNotificationId = notificationId;
mNotification = notification; mNotification = notification;
...@@ -67,6 +74,11 @@ public class DownloadForegroundServiceManager { ...@@ -67,6 +74,11 @@ public class DownloadForegroundServiceManager {
// This is true when context.bindService has been called and before context.unbindService. // This is true when context.bindService has been called and before context.unbindService.
private boolean mIsServiceBound; private boolean mIsServiceBound;
// Whether startForeground() is called. startForeground() must be called in 5 seconds after the
// service is started.
private boolean mStartForegroundCalled;
// This is non-null when onServiceConnected has been called (aka service is active). // This is non-null when onServiceConnected has been called (aka service is active).
private DownloadForegroundService mBoundService; private DownloadForegroundService mBoundService;
...@@ -81,9 +93,19 @@ public class DownloadForegroundServiceManager { ...@@ -81,9 +93,19 @@ public class DownloadForegroundServiceManager {
public DownloadForegroundServiceManager() {} public DownloadForegroundServiceManager() {}
/**
* Updates download notification status. In progress notification will have a foreground service
* associated. If all notifications are not in progress, foreground service will stop.
* @param context Android {@link Context}.
* @param downloadStatus Download status. In progress notification will have a foreground
* service.
* @param notificationId The notification id.
* @param notification The notification associated with the id. Can be null if {@link
* DownloadNotificationService} tries to cancel a notification.
*/
public void updateDownloadStatus(Context context, public void updateDownloadStatus(Context context,
@DownloadNotificationService.DownloadStatus int downloadStatus, int notificationId, @DownloadNotificationService.DownloadStatus int downloadStatus, int notificationId,
Notification notification) { @Nullable Notification notification) {
if (downloadStatus != DownloadNotificationService.DownloadStatus.IN_PROGRESS) { if (downloadStatus != DownloadNotificationService.DownloadStatus.IN_PROGRESS) {
Log.w(TAG, Log.w(TAG,
"updateDownloadStatus status: " + downloadStatus + ", id: " + notificationId); "updateDownloadStatus status: " + downloadStatus + ", id: " + notificationId);
...@@ -123,8 +145,7 @@ public class DownloadForegroundServiceManager { ...@@ -123,8 +145,7 @@ public class DownloadForegroundServiceManager {
// In the pending case, start foreground with specific notificationId and notification. // In the pending case, start foreground with specific notificationId and notification.
if (isProcessingPending) { if (isProcessingPending) {
Log.w(TAG, "Starting service with type " + downloadUpdate.mDownloadStatus); Log.w(TAG, "Starting service with type " + downloadUpdate.mDownloadStatus);
startOrUpdateForegroundService( startOrUpdateForegroundService(downloadUpdate);
downloadUpdate.mNotificationId, downloadUpdate.mNotification);
// Post a delayed task to eventually check to see if service needs to be stopped. // Post a delayed task to eventually check to see if service needs to be stopped.
postMaybeStopServiceRunnable(); postMaybeStopServiceRunnable();
...@@ -147,8 +168,7 @@ public class DownloadForegroundServiceManager { ...@@ -147,8 +168,7 @@ public class DownloadForegroundServiceManager {
// Make sure the pinned notification is still active, if not, update. // Make sure the pinned notification is still active, if not, update.
if (mDownloadUpdateQueue.get(mPinnedNotificationId) == null if (mDownloadUpdateQueue.get(mPinnedNotificationId) == null
|| !isActive(mDownloadUpdateQueue.get(mPinnedNotificationId).mDownloadStatus)) { || !isActive(mDownloadUpdateQueue.get(mPinnedNotificationId).mDownloadStatus)) {
startOrUpdateForegroundService( startOrUpdateForegroundService(downloadUpdate);
downloadUpdate.mNotificationId, downloadUpdate.mNotification);
} }
// Clear out inactive download updates in queue if there is at least one active download. // Clear out inactive download updates in queue if there is at least one active download.
...@@ -195,6 +215,7 @@ public class DownloadForegroundServiceManager { ...@@ -195,6 +215,7 @@ public class DownloadForegroundServiceManager {
void startAndBindService(Context context) { void startAndBindService(Context context) {
Log.w(TAG, "startAndBindService"); Log.w(TAG, "startAndBindService");
mIsServiceBound = true; mIsServiceBound = true;
mStartForegroundCalled = false;
startAndBindServiceInternal(context); startAndBindServiceInternal(context);
} }
...@@ -231,8 +252,21 @@ public class DownloadForegroundServiceManager { ...@@ -231,8 +252,21 @@ public class DownloadForegroundServiceManager {
/** Helper code to start or update foreground service. */ /** Helper code to start or update foreground service. */
@VisibleForTesting @VisibleForTesting
void startOrUpdateForegroundService(int notificationId, Notification notification) { void startOrUpdateForegroundService(DownloadUpdate update) {
Log.w(TAG, "startOrUpdateForegroundService id: " + notificationId); Log.w(TAG, "startOrUpdateForegroundService id: " + update.mNotificationId);
int notificationId = update.mNotificationId;
Notification notification = update.mNotification;
// On O+, we must call startForeground or Android will crash. If the last update
// is DownloadStatus.CANCELLED, then create an empty notification. See crbug.com/1121096.
// Notices the empty notification will be cancelled immediately in
// DownloadNotificationService afterward.
if (VERSION.SDK_INT >= VERSION_CODES.O && notification == null && !mStartForegroundCalled) {
assert update.mDownloadStatus == DownloadStatus.CANCELLED;
notification = createEmptyNotification(notificationId, update.mContext);
}
if (mBoundService != null && notificationId != INVALID_NOTIFICATION_ID if (mBoundService != null && notificationId != INVALID_NOTIFICATION_ID
&& notification != null) { && notification != null) {
// If there was an originally pinned notification, get its id and notification. // If there was an originally pinned notification, get its id and notification.
...@@ -246,12 +280,25 @@ public class DownloadForegroundServiceManager { ...@@ -246,12 +280,25 @@ public class DownloadForegroundServiceManager {
// Start service and handle notifications. // Start service and handle notifications.
mBoundService.startOrUpdateForegroundService(notificationId, notification, mBoundService.startOrUpdateForegroundService(notificationId, notification,
mPinnedNotificationId, oldNotification, killOldNotification); mPinnedNotificationId, oldNotification, killOldNotification);
mStartForegroundCalled = true;
// After the service has been started and the notification handled, change stored id. // After the service has been started and the notification handled, change stored id.
mPinnedNotificationId = notificationId; mPinnedNotificationId = notificationId;
} }
} }
// Creates an empty notification to feed to startForeground().
private Notification createEmptyNotification(int notificationId, Context context) {
NotificationWrapperBuilder builder =
NotificationWrapperBuilderFactory.createNotificationWrapperBuilder(
true /* preferCompat */, ChromeChannelDefinitions.ChannelId.DOWNLOADS,
null /* remoteAppPackageName */,
new NotificationMetadata(
NotificationUmaTracker.SystemNotificationType.DOWNLOAD_FILES, null,
notificationId));
return builder.build();
}
/** Helper code to stop and unbind service. */ /** Helper code to stop and unbind service. */
@VisibleForTesting @VisibleForTesting
......
...@@ -286,9 +286,9 @@ public class DownloadNotificationService { ...@@ -286,9 +286,9 @@ public class DownloadNotificationService {
mDownloadSharedPreferenceHelper.getDownloadSharedPreferenceEntry(id); mDownloadSharedPreferenceHelper.getDownloadSharedPreferenceEntry(id);
if (entry == null) return; if (entry == null) return;
cancelNotification(entry.notificationId, id);
mDownloadForegroundServiceManager.updateDownloadStatus(ContextUtils.getApplicationContext(), mDownloadForegroundServiceManager.updateDownloadStatus(ContextUtils.getApplicationContext(),
DownloadStatus.CANCELLED, entry.notificationId, null); DownloadStatus.CANCELLED, entry.notificationId, null);
cancelNotification(entry.notificationId, id);
} }
/** /**
......
...@@ -79,9 +79,9 @@ public final class DownloadForegroundServiceManagerTest { ...@@ -79,9 +79,9 @@ public final class DownloadForegroundServiceManagerTest {
} }
@Override @Override
void startOrUpdateForegroundService(int notificationId, Notification notification) { void startOrUpdateForegroundService(DownloadUpdate downloadUpdate) {
mUpdatedNotificationId = notificationId; mUpdatedNotificationId = downloadUpdate.mNotificationId;
super.startOrUpdateForegroundService(notificationId, notification); super.startOrUpdateForegroundService(downloadUpdate);
} }
// Skip waiting for delayed runnable in tests. // Skip waiting for delayed runnable in tests.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment