blob: 6fcff42b13c68ec55107c90bf90fd9e109da08cd [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.
#import "ios/chrome/browser/ui/reading_list/reading_list_mediator.h"
#include <algorithm>
#import "base/mac/foundation_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/sys_string_conversions.h"
#include "components/favicon/core/large_icon_service.h"
#include "components/reading_list/core/reading_list_model.h"
#import "components/reading_list/ios/reading_list_model_bridge_observer.h"
#include "components/url_formatter/url_formatter.h"
#include "ios/chrome/browser/experimental_flags.h"
#import "ios/chrome/browser/ui/favicon/favicon_attributes_provider.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_collection_view_item.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/browser/ui/uikit_ui_util.h"
#import "ios/chrome/common/favicon/favicon_view.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Sorter function that orders ReadingListEntries by their update time.
bool EntrySorter(const ReadingListEntry* rhs, const ReadingListEntry* lhs) {
return rhs->UpdateTime() > lhs->UpdateTime();
}
// Light gray color that matches the favicon background image color to eliminate
// setting a non-opaque background color.
const CGFloat kFallbackIconDefaultBackgroundColor = 0xf1f3f4;
} // namespace
@interface ReadingListMediator ()<ReadingListModelBridgeObserver> {
std::unique_ptr<ReadingListModelBridge> _modelBridge;
std::unique_ptr<ReadingListModel::ScopedReadingListBatchUpdate> _batchToken;
}
// 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;
// Lazily instantiated.
@property(nonatomic, strong, readonly)
FaviconAttributesProvider* attributesProvider;
@property(nonatomic, assign, readonly)
favicon::LargeIconService* largeIconService;
@end
@implementation ReadingListMediator
@synthesize dataSink = _dataSink;
@synthesize model = _model;
@synthesize shouldMonitorModel = _shouldMonitorModel;
@synthesize itemFactory = _itemFactory;
@synthesize attributesProvider = _attributesProvider;
@synthesize largeIconService = _largeIconService;
#pragma mark - Public
- (instancetype)initWithModel:(ReadingListModel*)model
largeIconService:(favicon::LargeIconService*)largeIconService
listItemFactory:(ReadingListListItemFactory*)itemFactory {
self = [super init];
if (self) {
_model = model;
_largeIconService = largeIconService;
_itemFactory = itemFactory;
_shouldMonitorModel = YES;
// This triggers the callback method. Should be created last.
_modelBridge.reset(new ReadingListModelBridge(self, model));
}
return self;
}
- (const ReadingListEntry*)entryFromItem:(id<ReadingListListItem>)item {
return self.model->GetEntryByURL(item.entryURL);
}
- (void)markEntryRead:(const GURL&)URL {
self.model->SetReadStatus(URL, true);
}
#pragma mark - ReadingListDataSource
- (BOOL)isItemRead:(id<ReadingListListItem>)item {
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->SetReadStatus(item.entryURL, read);
}
- (const ReadingListEntry*)entryWithURL:(const GURL&)URL {
return self.model->GetEntryByURL(URL);
}
- (void)removeEntryFromItem:(id<ReadingListListItem>)item {
[self logDeletionOfItem:item];
self.model->RemoveEntryByURL(item.entryURL);
}
- (void)fillReadItems:(NSMutableArray<id<ReadingListListItem>>*)readArray
unreadItems:(NSMutableArray<id<ReadingListListItem>>*)unreadArray {
std::vector<const ReadingListEntry*> readEntries;
std::vector<const ReadingListEntry*> unreadEntries;
for (const auto& url : self.model->Keys()) {
const ReadingListEntry* entry = self.model->GetEntryByURL(url);
DCHECK(entry);
if (entry->IsRead()) {
readEntries.push_back(entry);
} else {
unreadEntries.push_back(entry);
}
}
std::sort(readEntries.begin(), readEntries.end(), EntrySorter);
std::sort(unreadEntries.begin(), unreadEntries.end(), EntrySorter);
for (const ReadingListEntry* entry : readEntries) {
[readArray addObject:[self.itemFactory cellItemForReadingListEntry:entry]];
}
for (const ReadingListEntry* entry : unreadEntries) {
[unreadArray
addObject:[self.itemFactory cellItemForReadingListEntry:entry]];
}
DCHECK(self.model->Keys().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;
}
if (attributes.monogramString &&
experimental_flags::IsCollectionsUIRebootEnabled()) {
UIColor* textColor = [UIColor colorWithWhite:0 alpha:0.33];
UIColor* backgroundColor =
UIColorFromRGB(kFallbackIconDefaultBackgroundColor);
attributes = [FaviconAttributes
attributesWithMonogram:attributes.monogramString
textColor:textColor
backgroundColor:backgroundColor
defaultBackgroundColor:NO];
}
strongItem.attributes = attributes;
[strongSelf.dataSink itemHasChangedAfterDelay:strongItem];
};
[self.attributesProvider fetchFaviconAttributesForURL:item.faviconPageURL
completion:completionBlock];
}
- (void)beginBatchUpdates {
self.shouldMonitorModel = NO;
_batchToken = self.model->BeginBatchUpdates();
}
- (void)endBatchUpdates {
_batchToken.reset();
self.shouldMonitorModel = YES;
}
#pragma mark - Properties
- (FaviconAttributesProvider*)attributesProvider {
if (_attributesProvider) {
return _attributesProvider;
}
// Accept any favicon even the smallest ones (16x16) as it is better than the
// fallback icon.
// Pass 1 as minimum size to avoid empty favicons.
_attributesProvider = [[FaviconAttributesProvider alloc]
initWithFaviconSize:kFaviconPreferredSize
minFaviconSize:1
largeIconService:self.largeIconService];
return _attributesProvider;
}
- (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 - 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.distillationSizeText = newItem.distillationSizeText;
}
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 {
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