// Copyright 2017 The Chromium Authors
// 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.items;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.graphics.Bitmap;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.ArgumentMatchers;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.download.DownloadInfo;
import org.chromium.chrome.browser.download.DownloadNotifier;
import org.chromium.components.offline_items_collection.ContentId;
import org.chromium.components.offline_items_collection.OfflineContentProvider;
import org.chromium.components.offline_items_collection.OfflineItem;
import org.chromium.components.offline_items_collection.OfflineItemState;
import org.chromium.components.offline_items_collection.OfflineItemVisuals;
import org.chromium.components.offline_items_collection.PendingState;
import org.chromium.ui.permissions.ContextualNotificationPermissionRequester;
import java.util.List;
/**
* Unit tests for {@link OfflineContentAggregatorNotifierBridgeUi}. Validate that it interacts with
* both the {@link DownloadNotifier} and the {@link OfflineContentProvider} in expected ways.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class OfflineContentAggregatorNotificationBridgeUiTest {
/** Helper class to validate that a DownloadInfo has the right ContentId. */
static class DownloadInfoIdMatcher implements ArgumentMatcher<DownloadInfo> {
private final ContentId mExpectedId;
public DownloadInfoIdMatcher(ContentId expected) {
mExpectedId = expected;
}
@Override
public boolean matches(DownloadInfo argument) {
return ((DownloadInfo) argument).getContentId().equals(mExpectedId);
}
@Override
public String toString() {
return mExpectedId == null ? null : mExpectedId.toString();
}
}
@Mock private OfflineContentProvider mProvider;
@Mock private DownloadNotifier mNotifier;
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
private static OfflineItem buildOfflineItem(ContentId id, @OfflineItemState int state) {
OfflineItem item = new OfflineItem();
item.id = id;
item.state = state;
return item;
}
@Before
public void setUp() {
ContextualNotificationPermissionRequester.setInstance(
new ContextualNotificationPermissionRequester() {
@Override
public void requestPermissionIfNeeded() {}
@Override
public boolean doesAppLevelSettingsAllowSiteNotifications() {
return false;
}
});
}
@Test
public void testAddedItemsGetSentToTheUi() {
OfflineContentAggregatorNotificationBridgeUi bridge =
new OfflineContentAggregatorNotificationBridgeUi(mProvider, mNotifier);
verify(mProvider, times(1)).addObserver(bridge);
List<OfflineItem> items =
List.of(
buildOfflineItem(new ContentId("1", "A"), OfflineItemState.IN_PROGRESS),
buildOfflineItem(new ContentId("2", "B"), OfflineItemState.PENDING),
buildOfflineItem(new ContentId("3", "C"), OfflineItemState.COMPLETE),
buildOfflineItem(new ContentId("4", "D"), OfflineItemState.CANCELLED),
buildOfflineItem(new ContentId("5", "E"), OfflineItemState.INTERRUPTED),
buildOfflineItem(new ContentId("6", "F"), OfflineItemState.FAILED),
buildOfflineItem(new ContentId("7", "G"), OfflineItemState.PAUSED));
bridge.onItemsAdded(items);
verify(mProvider, times(1)).getVisualsForItem(items.get(0).id, bridge);
verify(mProvider, times(1)).getVisualsForItem(items.get(1).id, bridge);
verify(mProvider, times(1)).getVisualsForItem(items.get(2).id, bridge);
verify(mProvider, never()).getVisualsForItem(items.get(3).id, bridge);
verify(mProvider, times(1)).getVisualsForItem(items.get(4).id, bridge);
verify(mProvider, times(1)).getVisualsForItem(items.get(5).id, bridge);
verify(mProvider, times(1)).getVisualsForItem(items.get(6).id, bridge);
for (int i = 0; i < items.size(); i++) {
bridge.onVisualsAvailable(items.get(i).id, new OfflineItemVisuals());
}
verify(mNotifier, times(1))
.notifyDownloadProgress(
argThat(new DownloadInfoIdMatcher(items.get(0).id)),
ArgumentMatchers.anyLong(),
ArgumentMatchers.anyBoolean());
verify(mNotifier, times(1))
.notifyDownloadSuccessful(
argThat(new DownloadInfoIdMatcher(items.get(2).id)),
ArgumentMatchers.anyLong(),
ArgumentMatchers.anyBoolean(),
ArgumentMatchers.anyBoolean());
verify(mNotifier, times(1))
.notifyDownloadCanceled(items.get(3).id /* OfflineItemState.CANCELLED */);
verify(mNotifier, times(1))
.notifyDownloadInterrupted(
argThat(new DownloadInfoIdMatcher(items.get(4).id)),
ArgumentMatchers.anyBoolean(),
eq(PendingState.NOT_PENDING));
verify(mNotifier, times(1))
.notifyDownloadFailed(argThat(new DownloadInfoIdMatcher(items.get(5).id)));
verify(mNotifier, times(1))
.notifyDownloadPaused(argThat(new DownloadInfoIdMatcher(items.get(6).id)));
bridge.destroy();
verify(mProvider, times(1)).removeObserver(bridge);
}
@Test
public void testItemUpdatesGetSentToTheUi() {
OfflineContentAggregatorNotificationBridgeUi bridge =
new OfflineContentAggregatorNotificationBridgeUi(mProvider, mNotifier);
verify(mProvider, times(1)).addObserver(bridge);
List<OfflineItem> items =
List.of(
buildOfflineItem(new ContentId("1", "A"), OfflineItemState.IN_PROGRESS),
buildOfflineItem(new ContentId("2", "B"), OfflineItemState.PENDING),
buildOfflineItem(new ContentId("3", "C"), OfflineItemState.COMPLETE),
buildOfflineItem(new ContentId("4", "D"), OfflineItemState.CANCELLED),
buildOfflineItem(new ContentId("5", "E"), OfflineItemState.INTERRUPTED),
buildOfflineItem(new ContentId("6", "F"), OfflineItemState.FAILED),
buildOfflineItem(new ContentId("7", "G"), OfflineItemState.PAUSED));
for (int i = 0; i < items.size(); i++) bridge.onItemUpdated(items.get(i), null);
verify(mProvider, times(1)).getVisualsForItem(items.get(0).id, bridge);
verify(mProvider, times(1)).getVisualsForItem(items.get(1).id, bridge);
verify(mProvider, times(1)).getVisualsForItem(items.get(2).id, bridge);
verify(mProvider, never()).getVisualsForItem(items.get(3).id, bridge);
verify(mProvider, times(1)).getVisualsForItem(items.get(4).id, bridge);
verify(mProvider, times(1)).getVisualsForItem(items.get(5).id, bridge);
verify(mProvider, times(1)).getVisualsForItem(items.get(6).id, bridge);
for (int i = 0; i < items.size(); i++) {
bridge.onVisualsAvailable(items.get(i).id, new OfflineItemVisuals());
}
verify(mNotifier, times(1))
.notifyDownloadProgress(
argThat(new DownloadInfoIdMatcher(items.get(0).id)),
ArgumentMatchers.anyLong(),
ArgumentMatchers.anyBoolean());
verify(mNotifier, times(1))
.notifyDownloadSuccessful(
argThat(new DownloadInfoIdMatcher(items.get(2).id)),
ArgumentMatchers.anyLong(),
ArgumentMatchers.anyBoolean(),
ArgumentMatchers.anyBoolean());
verify(mNotifier, times(1))
.notifyDownloadCanceled(items.get(3).id /* OfflineItemState.CANCELLED */);
verify(mNotifier, times(1))
.notifyDownloadInterrupted(
argThat(new DownloadInfoIdMatcher(items.get(4).id)),
ArgumentMatchers.anyBoolean(),
eq(PendingState.NOT_PENDING));
verify(mNotifier, times(1))
.notifyDownloadFailed(argThat(new DownloadInfoIdMatcher(items.get(5).id)));
verify(mNotifier, times(1))
.notifyDownloadPaused(argThat(new DownloadInfoIdMatcher(items.get(6).id)));
bridge.destroy();
verify(mProvider, times(1)).removeObserver(bridge);
}
@Test
public void testNullVisuals() {
OfflineContentAggregatorNotificationBridgeUi bridge =
new OfflineContentAggregatorNotificationBridgeUi(mProvider, mNotifier);
verify(mProvider, times(1)).addObserver(bridge);
OfflineItem item1 = buildOfflineItem(new ContentId("1", "A"), OfflineItemState.IN_PROGRESS);
OfflineItem item2 = buildOfflineItem(new ContentId("2", "B"), OfflineItemState.IN_PROGRESS);
OfflineItemVisuals visuals1 = new OfflineItemVisuals();
visuals1.icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
bridge.onItemUpdated(item1, null);
bridge.onItemUpdated(item2, null);
verify(mProvider, times(1)).getVisualsForItem(item1.id, bridge);
verify(mProvider, times(1)).getVisualsForItem(item2.id, bridge);
ArgumentCaptor<DownloadInfo> captor = ArgumentCaptor.forClass(DownloadInfo.class);
bridge.onVisualsAvailable(item1.id, visuals1);
bridge.onVisualsAvailable(item2.id, null);
verify(mNotifier, times(2))
.notifyDownloadProgress(
captor.capture(),
ArgumentMatchers.anyLong(),
ArgumentMatchers.anyBoolean());
List<DownloadInfo> capturedInfo = captor.getAllValues();
Assert.assertEquals(item1.id, capturedInfo.get(0).getContentId());
Assert.assertEquals(visuals1.icon, capturedInfo.get(0).getIcon());
Assert.assertEquals(item2.id, capturedInfo.get(1).getContentId());
Assert.assertEquals(null, capturedInfo.get(1).getIcon());
bridge.destroy();
verify(mProvider, times(1)).removeObserver(bridge);
}
@Test
public void testRemovedItemsGetRemovedFromTheUi() {
OfflineContentAggregatorNotificationBridgeUi bridge =
new OfflineContentAggregatorNotificationBridgeUi(mProvider, mNotifier);
verify(mProvider, times(1)).addObserver(bridge);
ContentId id = new ContentId("1", "A");
bridge.onItemRemoved(id);
verify(mNotifier, times(1)).notifyDownloadCanceled(id);
bridge.destroy();
verify(mProvider, times(1)).removeObserver(bridge);
}
@Test
public void testRemovedItemsIgnoreVisualsCallback() {
OfflineContentAggregatorNotificationBridgeUi bridge =
new OfflineContentAggregatorNotificationBridgeUi(mProvider, mNotifier);
verify(mProvider, times(1)).addObserver(bridge);
OfflineItem item = buildOfflineItem(new ContentId("1", "A"), OfflineItemState.IN_PROGRESS);
bridge.onItemUpdated(item, null);
verify(mProvider, times(1)).getVisualsForItem(item.id, bridge);
bridge.onItemRemoved(item.id);
bridge.onVisualsAvailable(item.id, new OfflineItemVisuals());
InOrder order = inOrder(mNotifier);
order.verify(mNotifier, times(1)).notifyDownloadCanceled(item.id);
order.verifyNoMoreInteractions();
bridge.destroy();
verify(mProvider, times(1)).removeObserver(bridge);
}
@Test
public void testOnlyRequestsVisualsOnceForMultipleUpdates() {
OfflineContentAggregatorNotificationBridgeUi bridge =
new OfflineContentAggregatorNotificationBridgeUi(mProvider, mNotifier);
verify(mProvider, times(1)).addObserver(bridge);
OfflineItem item = buildOfflineItem(new ContentId("1", "A"), OfflineItemState.IN_PROGRESS);
bridge.onItemUpdated(item, null);
bridge.onItemUpdated(item, null);
verify(mProvider, times(1)).getVisualsForItem(item.id, bridge);
bridge.destroy();
verify(mProvider, times(1)).removeObserver(bridge);
}
@Test
public void testVisualsAreCachedForInterestingItems() {
OfflineContentAggregatorNotificationBridgeUi bridge =
new OfflineContentAggregatorNotificationBridgeUi(mProvider, mNotifier);
verify(mProvider, times(1)).addObserver(bridge);
List<OfflineItem> interestingItems =
List.of(
buildOfflineItem(new ContentId("1", "A"), OfflineItemState.IN_PROGRESS),
buildOfflineItem(new ContentId("2", "B"), OfflineItemState.PENDING),
buildOfflineItem(new ContentId("3", "C"), OfflineItemState.COMPLETE),
buildOfflineItem(new ContentId("5", "E"), OfflineItemState.INTERRUPTED),
buildOfflineItem(new ContentId("7", "G"), OfflineItemState.PAUSED));
List<OfflineItem> uninterestingItems =
List.of(buildOfflineItem(new ContentId("6", "F"), OfflineItemState.FAILED));
for (int i = 0; i < interestingItems.size(); i++) {
OfflineItem item = interestingItems.get(i);
bridge.onItemUpdated(item, null);
bridge.onVisualsAvailable(item.id, null);
bridge.onItemUpdated(item, null);
verify(mProvider, times(1)).getVisualsForItem(item.id, bridge);
verify(mNotifier, times(2))
.notifyDownloadProgress(
ArgumentMatchers.any(),
ArgumentMatchers.anyLong(),
ArgumentMatchers.anyBoolean());
}
for (int i = 0; i < uninterestingItems.size(); i++) {
OfflineItem item = uninterestingItems.get(i);
bridge.onItemUpdated(item, null);
bridge.onVisualsAvailable(item.id, null);
bridge.onItemUpdated(item, null);
verify(mProvider, times(2)).getVisualsForItem(item.id, bridge);
}
bridge.destroy();
verify(mProvider, times(1)).removeObserver(bridge);
}
@Test
public void testVisualsGetClearedForUninterestingItems() {
OfflineContentAggregatorNotificationBridgeUi bridge =
new OfflineContentAggregatorNotificationBridgeUi(mProvider, mNotifier);
verify(mProvider, times(1)).addObserver(bridge);
ContentId id = new ContentId("1", "A");
OfflineItem item1 = buildOfflineItem(id, OfflineItemState.IN_PROGRESS);
OfflineItem item2 = buildOfflineItem(id, OfflineItemState.FAILED);
OfflineItem item3 = buildOfflineItem(id, OfflineItemState.IN_PROGRESS);
bridge.onItemUpdated(item1, null);
bridge.onVisualsAvailable(item1.id, new OfflineItemVisuals());
bridge.onItemUpdated(item2, null);
bridge.onItemUpdated(item3, null);
bridge.onVisualsAvailable(item1.id, new OfflineItemVisuals());
verify(mProvider, times(2)).getVisualsForItem(id, bridge);
bridge.destroy();
verify(mProvider, times(1)).removeObserver(bridge);
}
}