// 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.
#import "ios/chrome/browser/ui/reading_list/reading_list_mediator.h"
#import <algorithm>
#import "base/apple/foundation_util.h"
#import "base/location.h"
#import "base/memory/scoped_refptr.h"
#import "base/metrics/histogram_macros.h"
#import "base/strings/sys_string_conversions.h"
#import "components/reading_list/core/reading_list_model.h"
#import "components/reading_list/features/reading_list_switches.h"
#import "components/reading_list/ios/reading_list_model_bridge_observer.h"
#import "components/url_formatter/url_formatter.h"
#import "ios/chrome/browser/favicon/model/favicon_loader.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_favicon_loader_factory.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/sync/model/sync_observer_bridge.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_data_sink.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_list_item.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_list_item_factory.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_list_item_util.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_table_view_item.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_utils.h"
#import "ios/chrome/common/ui/favicon/favicon_constants.h"
#import "ios/chrome/common/ui/favicon/favicon_view.h"
namespace {
// Sorter function that orders ReadingListEntries by their update time.
bool EntrySorter(scoped_refptr<const ReadingListEntry> rhs,
scoped_refptr<const ReadingListEntry> lhs) {
return rhs->UpdateTime() > lhs->UpdateTime();
}
} // namespace
@interface ReadingListMediator () <ReadingListModelBridgeObserver,
SyncObserverModelBridge>
// The model passed on initialization.
@property(nonatomic, assign) ReadingListModel* model;
// Whether the consumer should be notified of model changes.
@property(nonatomic, assign) BOOL shouldMonitorModel;
// The ListItem factory passed on initialization.
@property(nonatomic, strong) ReadingListListItemFactory* itemFactory;
// Favicon Service used for UIRefresh Collections.
@property(nonatomic, assign, readonly) FaviconLoader* faviconLoader;
@end
@implementation ReadingListMediator {
std::unique_ptr<ReadingListModelBridge> _modelBridge;
std::unique_ptr<ReadingListModel::ScopedReadingListBatchUpdate> _batchToken;
// Observer to keep track of the syncing status.
std::unique_ptr<SyncObserverBridge> _syncObserver;
}
@synthesize dataSink = _dataSink;
#pragma mark - Public
- (instancetype)initWithModel:(ReadingListModel*)model
syncService:(nonnull syncer::SyncService*)syncService
faviconLoader:(nonnull FaviconLoader*)faviconLoader
listItemFactory:(ReadingListListItemFactory*)itemFactory {
self = [super init];
if (self) {
_model = model;
_itemFactory = itemFactory;
_shouldMonitorModel = YES;
_faviconLoader = faviconLoader;
_syncObserver = std::make_unique<SyncObserverBridge>(self, syncService);
// This triggers the callback method. Should be created last.
_modelBridge.reset(new ReadingListModelBridge(self, model));
}
return self;
}
- (scoped_refptr<const ReadingListEntry>)entryFromItem:
(id<ReadingListListItem>)item {
return self.model->GetEntryByURL(item.entryURL);
}
- (void)markEntryRead:(const GURL&)URL {
self.model->SetReadStatusIfExists(URL, true);
}
- (void)disconnect {
_dataSink = nil;
_model = nullptr;
_itemFactory = nil;
_faviconLoader = nullptr;
_modelBridge.reset();
_syncObserver.reset();
}
- (void)dealloc {
DCHECK(!_model);
}
#pragma mark - ReadingListDataSource
- (BOOL)isItemRead:(id<ReadingListListItem>)item {
scoped_refptr<const ReadingListEntry> readingListEntry =
self.model->GetEntryByURL(item.entryURL);
if (!readingListEntry) {
return NO;
}
return readingListEntry->IsRead();
}
- (void)dataSinkWillBeDismissed {
self.model->MarkAllSeen();
// Reset data sink to prevent further model update notifications.
self.dataSink = nil;
}
- (void)setReadStatus:(BOOL)read forItem:(id<ReadingListListItem>)item {
self.model->SetReadStatusIfExists(item.entryURL, read);
}
- (scoped_refptr<const ReadingListEntry>)entryWithURL:(const GURL&)URL {
return self.model->GetEntryByURL(URL);
}
- (void)removeEntryFromItem:(id<ReadingListListItem>)item {
[self logDeletionOfItem:item];
self.model->RemoveEntryByURL(item.entryURL, FROM_HERE);
}
- (void)fillReadItems:(NSMutableArray<id<ReadingListListItem>>*)readArray
unreadItems:(NSMutableArray<id<ReadingListListItem>>*)unreadArray {
std::vector<scoped_refptr<const ReadingListEntry>> readEntries;
std::vector<scoped_refptr<const ReadingListEntry>> unreadEntries;
for (const auto& url : self.model->GetKeys()) {
scoped_refptr<const ReadingListEntry> entry =
self.model->GetEntryByURL(url);
DCHECK(entry);
if (entry->IsRead()) {
readEntries.push_back(std::move(entry));
} else {
unreadEntries.push_back(std::move(entry));
}
}
std::sort(readEntries.begin(), readEntries.end(), EntrySorter);
std::sort(unreadEntries.begin(), unreadEntries.end(), EntrySorter);
for (scoped_refptr<const ReadingListEntry> entry : readEntries) {
bool needsExplicitUpload =
self.model->NeedsExplicitUploadToSyncServer(entry->URL());
ListItem<ReadingListListItem>* item =
[self.itemFactory cellItemForReadingListEntry:entry.get()
needsExplicitUpload:needsExplicitUpload];
[readArray addObject:item];
}
for (scoped_refptr<const ReadingListEntry> entry : unreadEntries) {
bool needsExplicitUpload =
self.model->NeedsExplicitUploadToSyncServer(entry->URL());
ListItem<ReadingListListItem>* item =
[self.itemFactory cellItemForReadingListEntry:entry.get()
needsExplicitUpload:needsExplicitUpload];
[unreadArray addObject:item];
}
DCHECK(self.model->GetKeys().size() ==
[readArray count] + [unreadArray count]);
}
- (void)fetchFaviconForItem:(id<ReadingListListItem>)item {
__weak id<ReadingListListItem> weakItem = item;
__weak ReadingListMediator* weakSelf = self;
void (^completionBlock)(FaviconAttributes* attributes) =
^(FaviconAttributes* attributes) {
id<ReadingListListItem> strongItem = weakItem;
ReadingListMediator* strongSelf = weakSelf;
if (!strongSelf || !strongItem) {
return;
}
strongItem.attributes = attributes;
[strongSelf.dataSink itemHasChangedAfterDelay:strongItem];
};
self.faviconLoader->FaviconForPageUrl(
item.faviconPageURL, kDesiredSmallFaviconSizePt, kMinFaviconSizePt,
/*fallback_to_google_server=*/false, completionBlock);
}
- (void)beginBatchUpdates {
self.shouldMonitorModel = NO;
_batchToken = self.model->BeginBatchUpdates();
}
- (void)endBatchUpdates {
_batchToken.reset();
self.shouldMonitorModel = YES;
}
#pragma mark - Properties
- (void)setDataSink:(id<ReadingListDataSink>)dataSink {
_dataSink = dataSink;
if (self.model->loaded()) {
[dataSink dataSourceReady:self];
}
}
- (BOOL)isReady {
return self.model->loaded();
}
- (BOOL)hasElements {
return self.model->size() > 0;
}
- (BOOL)hasReadElements {
return self.model->size() != self.model->unread_size();
}
#pragma mark - ReadingListModelBridgeObserver
- (void)readingListModelLoaded:(const ReadingListModel*)model {
UMA_HISTOGRAM_COUNTS_1000("ReadingList.Unread.Number", model->unread_size());
UMA_HISTOGRAM_COUNTS_1000("ReadingList.Read.Number",
model->size() - model->unread_size());
[self.dataSink dataSourceReady:self];
}
- (void)readingListModelDidApplyChanges:(const ReadingListModel*)model {
if (!self.shouldMonitorModel) {
return;
}
// Ignore single element updates when the data source is doing batch updates.
if (self.model->IsPerformingBatchUpdates()) {
return;
}
if ([self hasDataSourceChanged])
[self.dataSink dataSourceChanged];
}
- (void)readingListModelCompletedBatchUpdates:(const ReadingListModel*)model {
if (!self.shouldMonitorModel) {
return;
}
if ([self hasDataSourceChanged])
[self.dataSink dataSourceChanged];
}
#pragma mark - SyncObserverModelBridge
- (void)onSyncStateChanged {
// If the sync state, especially the account storage state changes, the UI
// including cloud icons on items needs to be updated.
if ([self hasDataSourceChanged]) {
[self.dataSink dataSourceChanged];
}
}
#pragma mark - Private
// Whether the data source has changed.
- (BOOL)hasDataSourceChanged {
NSMutableArray<id<ReadingListListItem>>* readArray = [NSMutableArray array];
NSMutableArray<id<ReadingListListItem>>* unreadArray = [NSMutableArray array];
[self fillReadItems:readArray unreadItems:unreadArray];
return [self currentSection:[self.dataSink readItems]
isDifferentOfArray:readArray] ||
[self currentSection:[self.dataSink unreadItems]
isDifferentOfArray:unreadArray];
}
// Returns whether there is a difference between the elements contained in the
// `sectionIdentifier` and those in the `array`. The comparison is done with the
// URL of the elements. If an element exist in both, the one in `currentSection`
// will be overwriten with the informations contained in the one from `array`.
- (BOOL)currentSection:(NSArray<id<ReadingListListItem>>*)currentSection
isDifferentOfArray:(NSArray<id<ReadingListListItem>>*)array {
if (currentSection.count != array.count)
return YES;
NSMutableArray<id<ReadingListListItem>>* itemsToReconfigure =
[NSMutableArray array];
NSInteger index = 0;
for (id<ReadingListListItem> newItem in array) {
id<ReadingListListItem> oldItem = currentSection[index];
if (oldItem.entryURL == newItem.entryURL) {
if (![oldItem isEqual:newItem]) {
[itemsToReconfigure addObject:oldItem];
oldItem.title = newItem.title;
oldItem.entryURL = newItem.entryURL;
oldItem.distillationState = newItem.distillationState;
oldItem.distillationDateText = newItem.distillationDateText;
oldItem.showCloudSlashIcon = newItem.showCloudSlashIcon;
}
if (oldItem.faviconPageURL != newItem.faviconPageURL) {
oldItem.faviconPageURL = newItem.faviconPageURL;
[self fetchFaviconForItem:oldItem];
}
}
if (![oldItem isEqual:newItem]) {
return YES;
}
index++;
}
[self.dataSink itemsHaveChanged:itemsToReconfigure];
return NO;
}
// Logs the deletions histograms for the entry associated with `item`.
- (void)logDeletionOfItem:(id<ReadingListListItem>)item {
scoped_refptr<const ReadingListEntry> entry = [self entryFromItem:item];
if (!entry)
return;
int64_t firstRead = entry->FirstReadTime();
if (firstRead > 0) {
// Log 0 if the entry has never been read.
firstRead = (base::Time::Now() - base::Time::UnixEpoch()).InMicroseconds() -
firstRead;
// Convert it to hours.
firstRead = firstRead / base::Time::kMicrosecondsPerHour;
}
UMA_HISTOGRAM_COUNTS_10000("ReadingList.FirstReadAgeOnDeletion", firstRead);
int64_t age = (base::Time::Now() - base::Time::UnixEpoch()).InMicroseconds() -
entry->CreationTime();
// Convert it to hours.
age = age / base::Time::kMicrosecondsPerHour;
if (entry->IsRead())
UMA_HISTOGRAM_COUNTS_10000("ReadingList.Read.AgeOnDeletion", age);
else
UMA_HISTOGRAM_COUNTS_10000("ReadingList.Unread.AgeOnDeletion", age);
}
@end