Commit 63512459 authored by Shakti Sahu's avatar Shakti Sahu Committed by Commit Bot

Separate EmptyViewCoordinator from the top model.

- Decouple the EmptyViewCoordinator from the underlying model.
- Add back the concept of loading to the View.

Change-Id: I568fcdc32af7aaf42908c56981538c02cc4f61ed
Reviewed-on: https://chromium-review.googlesource.com/1150840
Commit-Queue: Shakti Sahu <shaktisahu@chromium.org>
Reviewed-by: default avatarShakti Sahu <shaktisahu@chromium.org>
Cr-Commit-Position: refs/heads/master@{#579522}
parent b1121638
......@@ -9,12 +9,18 @@
android:layout_height="match_parent" >
<TextView
android:id="@+id/empty_view"
android:id="@+id/empty"
android:layout_marginTop="100dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center"
android:drawablePadding="3dp"
android:textAppearance="@style/BlackDisabledText1"/>
<org.chromium.chrome.browser.widget.LoadingView
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
......@@ -32,6 +32,9 @@ public class OfflineItemSource implements OfflineItemFilterSource, OfflineConten
private final Map<ContentId, OfflineItem> mItems = new HashMap<>();
private final ObserverList<OfflineItemFilterObserver> mObservers = new ObserverList<>();
/** Used to track whether or not the items have been loaded from {@code mProvider} or not. */
private boolean mItemsAvailable;
/**
* Used to track whether or not this is destroyed so we know whether or not to do additional
* work when outstanding callbacks return.
......@@ -48,7 +51,11 @@ public class OfflineItemSource implements OfflineItemFilterSource, OfflineConten
mProvider.addObserver(this);
mProvider.getAllItems(items -> {
if (!mDestroyed) onItemsAdded(items);
if (mDestroyed) return;
mItemsAvailable = true;
for (OfflineItemFilterObserver observer : mObservers) observer.onItemsAvailable();
onItemsAdded(items);
});
}
......@@ -69,6 +76,11 @@ public class OfflineItemSource implements OfflineItemFilterSource, OfflineConten
return mItems.values();
}
@Override
public boolean areItemsAvailable() {
return mItemsAvailable;
}
@Override
public void addObserver(OfflineItemFilterObserver observer) {
mObservers.addObserver(observer);
......
// Copyright 2018 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.home;
/** Helper class to expose the status of the offline prefetch feature. */
public class PrefetchStatusProvider {
/** @return Whether or not the offline prefetch feature is enabled. */
public boolean enabled() {
return true;
}
}
\ No newline at end of file
// Copyright 2018 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.home.empty;
import android.content.Context;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
import android.view.View;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.download.home.PrefetchStatusProvider;
import org.chromium.chrome.browser.download.home.empty.EmptyProperties.State;
import org.chromium.chrome.browser.download.home.filter.FilterCoordinator;
import org.chromium.chrome.browser.download.home.filter.Filters.FilterType;
import org.chromium.chrome.browser.download.home.filter.OfflineItemFilterObserver;
import org.chromium.chrome.browser.download.home.filter.OfflineItemFilterSource;
import org.chromium.chrome.browser.modelutil.PropertyModel;
import org.chromium.chrome.browser.modelutil.PropertyModelChangeProcessor;
import org.chromium.components.offline_items_collection.OfflineItem;
import java.util.Collection;
/** A class that determines whether an empty view should be shown and inserts into the model. */
public class EmptyCoordinator implements OfflineItemFilterObserver, FilterCoordinator.Observer {
private final PrefetchStatusProvider mPrefetchStatusProvider;
private final OfflineItemFilterSource mSource;
private final PropertyModel mModel;
private final EmptyView mView;
private boolean mShowingPrefetch = false;
/** Creates a {@link EmptyCoordinator} instance that monitors {@code source}. */
public EmptyCoordinator(Context context, PrefetchStatusProvider prefetchStatusProvider,
OfflineItemFilterSource source) {
mPrefetchStatusProvider = prefetchStatusProvider;
mSource = source;
mSource.addObserver(this);
mModel = new PropertyModel(EmptyProperties.ALL_KEYS);
mView = new EmptyView(context);
mModel.addObserver(
new PropertyModelChangeProcessor<>(mModel, mView, new EmptyViewBinder()));
calculateState();
}
/** @return The {@link View} that represents the empty screen. */
public View getView() {
return mView.getView();
}
// OfflineItemFilterObserver implementation.
@Override
public void onItemsAdded(Collection<OfflineItem> items) {
calculateState();
}
@Override
public void onItemsRemoved(Collection<OfflineItem> items) {
calculateState();
}
@Override
public void onItemUpdated(OfflineItem oldItem, OfflineItem item) {}
@Override
public void onItemsAvailable() {
calculateState();
}
// FilterCoordinator.Observer implementation.
@Override
public void onFilterChanged(@FilterType int selectedTab) {
mShowingPrefetch = selectedTab == FilterType.PREFETCHED;
calculateState();
}
private void calculateState() {
@State
int state;
if (!mSource.areItemsAvailable()) {
state = State.LOADING;
} else if (mSource.getItems().isEmpty()) {
state = State.EMPTY;
@StringRes
int textId;
@DrawableRes
int iconId;
if (mShowingPrefetch) {
iconId = R.drawable.ic_library_news_feed;
if (mPrefetchStatusProvider.enabled()) {
textId = R.string.download_manager_prefetch_tab_empty;
} else {
textId = R.string.download_manager_enable_prefetch_message;
}
} else {
iconId = R.drawable.downloads_big;
textId = R.string.download_manager_ui_empty;
}
mModel.setValue(EmptyProperties.EMPTY_TEXT_RES_ID, textId);
mModel.setValue(EmptyProperties.EMPTY_ICON_RES_ID, iconId);
} else {
state = State.GONE;
}
mModel.setValue(EmptyProperties.STATE, state);
}
}
// Copyright 2018 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.home.empty;
import android.support.annotation.IntDef;
import org.chromium.chrome.browser.modelutil.PropertyKey;
import org.chromium.chrome.browser.modelutil.PropertyModel.IntPropertyKey;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** The properties required to build a {@link EmptyView}. */
interface EmptyProperties {
@IntDef({State.LOADING, State.EMPTY, State.GONE})
@Retention(RetentionPolicy.SOURCE)
public @interface State {
int LOADING = 0;
int EMPTY = 1;
int GONE = 2;
}
/** The current state of the empty view. */
public static final IntPropertyKey STATE = new IntPropertyKey();
/** The current text resource to use for the empty view. */
public static final IntPropertyKey EMPTY_TEXT_RES_ID = new IntPropertyKey();
/** The current icon resource to use for the empty view. */
public static final IntPropertyKey EMPTY_ICON_RES_ID = new IntPropertyKey();
public static final PropertyKey[] ALL_KEYS =
new PropertyKey[] {STATE, EMPTY_TEXT_RES_ID, EMPTY_ICON_RES_ID};
}
\ No newline at end of file
// Copyright 2018 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.home.empty;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.download.home.empty.EmptyProperties.State;
import org.chromium.chrome.browser.widget.LoadingView;
/** A view that represents the visuals required for the empty state of the download home list. */
class EmptyView {
private final ViewGroup mView;
private final TextView mEmptyView;
private final LoadingView mLoadingView;
/** Creates a new {@link EmptyView} instance from {@code context}. */
public EmptyView(Context context) {
mView = (ViewGroup) LayoutInflater.from(context).inflate(
R.layout.downloads_empty_view, null);
mEmptyView = (TextView) mView.findViewById(R.id.empty);
mLoadingView = (LoadingView) mView.findViewById(R.id.loading);
}
/** The Android {@link View} representing the empty view. */
public View getView() {
return mView;
}
/** Sets the internal UI based on {@code state}. */
public void setState(@State int state) {
mEmptyView.setVisibility(state == State.EMPTY ? View.VISIBLE : View.INVISIBLE);
if (state == State.LOADING) {
mLoadingView.showLoadingUI();
} else {
mLoadingView.hideLoadingUI();
}
}
/** Sets the text resource to use for the empty view. */
public void setEmptyText(@StringRes int textId) {
mEmptyView.setText(textId);
}
/** Sets the icon resource to use for the empty view. */
public void setEmptyIcon(@DrawableRes int iconId) {
Drawable drawable = VectorDrawableCompat.create(
mView.getResources(), iconId, mView.getContext().getTheme());
mEmptyView.setCompoundDrawablesWithIntrinsicBounds(null, drawable, null, null);
}
}
\ No newline at end of file
// Copyright 2018 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.home.empty;
import org.chromium.chrome.browser.modelutil.PropertyKey;
import org.chromium.chrome.browser.modelutil.PropertyModel;
import org.chromium.chrome.browser.modelutil.PropertyModelChangeProcessor.ViewBinder;
/**
* A helper {@link ViewBinder} responsible for gluing {@link EmptyProperties} to
* {@link EmptyView}.
*/
class EmptyViewBinder implements ViewBinder<PropertyModel, EmptyView, PropertyKey> {
@Override
public void bind(PropertyModel model, EmptyView view, PropertyKey propertyKey) {
if (propertyKey == EmptyProperties.STATE) {
view.setState(model.getValue(EmptyProperties.STATE));
} else if (propertyKey == EmptyProperties.EMPTY_TEXT_RES_ID) {
view.setEmptyText(model.getValue(EmptyProperties.EMPTY_TEXT_RES_ID));
} else if (propertyKey == EmptyProperties.EMPTY_ICON_RES_ID) {
view.setEmptyIcon(model.getValue(EmptyProperties.EMPTY_ICON_RES_ID));
}
}
}
\ No newline at end of file
......@@ -9,9 +9,9 @@ import android.support.annotation.IntDef;
import android.view.View;
import org.chromium.base.ObserverList;
import org.chromium.chrome.browser.download.home.PrefetchStatusProvider;
import org.chromium.chrome.browser.download.home.filter.Filters.FilterType;
import org.chromium.chrome.browser.download.home.filter.chips.ChipsCoordinator;
import org.chromium.chrome.browser.modelutil.PropertyKey;
import org.chromium.chrome.browser.modelutil.PropertyModel;
import org.chromium.chrome.browser.modelutil.PropertyModelChangeProcessor;
......@@ -33,6 +33,7 @@ public class FilterCoordinator {
void onFilterChanged(@FilterType int selectedTab);
}
private final PrefetchStatusProvider mPrefetchStatusProvider;
private final ObserverList<Observer> mObserverList = new ObserverList<>();
private final PropertyModel mModel;
private final FilterViewBinder mViewBinder;
......@@ -45,22 +46,22 @@ public class FilterCoordinator {
* Builds a new FilterCoordinator.
* @param context The context to build the views and pull parameters from.
*/
public FilterCoordinator(Context context, OfflineItemFilterSource chipFilterSource) {
public FilterCoordinator(Context context, PrefetchStatusProvider prefetchStatusProvider,
OfflineItemFilterSource chipFilterSource) {
mPrefetchStatusProvider = prefetchStatusProvider;
mChipsProvider = new FilterChipsProvider(type -> handleChipSelected(), chipFilterSource);
mChipsCoordinator = new ChipsCoordinator(context, mChipsProvider);
mModel = new PropertyModel(FilterProperties.ALL_KEYS);
mViewBinder = new FilterViewBinder();
mView = new FilterView(context);
mModel.addObserver(new PropertyModelChangeProcessor<PropertyModel, FilterView, PropertyKey>(
mModel, mView, mViewBinder));
mModel.addObserver(new PropertyModelChangeProcessor<>(mModel, mView, mViewBinder));
mModel.setValue(
FilterProperties.CHANGE_LISTENER, selectedTab -> handleTabSelected(selectedTab));
selectTab(TabType.FILES);
// TODO(shaktisahu): Check if prefetch UI is enabled.
mModel.setValue(FilterProperties.SHOW_TABS, true);
mModel.setValue(FilterProperties.SHOW_TABS, mPrefetchStatusProvider.enabled());
}
/** @return The {@link View} representing this widget. */
......
......@@ -70,12 +70,17 @@ public abstract class OfflineItemFilter
addItems(mSource.getItems());
}
// OfflineItemSource implementation.
// OfflineItemFilterSource implementation.
@Override
public Set<OfflineItem> getItems() {
return mItems;
}
@Override
public boolean areItemsAvailable() {
return mSource.areItemsAvailable();
}
@Override
public void addObserver(OfflineItemFilterObserver observer) {
mObservers.addObserver(observer);
......@@ -112,6 +117,11 @@ public abstract class OfflineItemFilter
for (OfflineItemFilterObserver obs : mObservers) obs.onItemUpdated(oldItem, item);
}
@Override
public void onItemsAvailable() {
for (OfflineItemFilterObserver obs : mObservers) obs.onItemsAvailable();
}
// Helper method to help incorporate a collection of items into this filtered version.
private void addItems(Collection<OfflineItem> items) {
Set<OfflineItem> added = new HashSet<>();
......
......@@ -32,4 +32,11 @@ public interface OfflineItemFilterObserver {
* @param item The new {@link OfflineItem} after the update.
*/
void onItemUpdated(OfflineItem oldItem, OfflineItem item);
/**
* Called when the underlying {@link OfflineItem}s are available. This is meant to help detect
* a difference between an empty set and a set that is not loaded yet.
*/
default void
onItemsAvailable() {}
}
\ No newline at end of file
......@@ -19,6 +19,12 @@ public interface OfflineItemFilterSource {
*/
Collection<OfflineItem> getItems();
/**
* @return Whether or not the items are available, which is meant to help determine the
* difference between an empty set and a set that hasn't loaded yet.
*/
boolean areItemsAvailable();
/**
* Registers {@code observer} to be notified of changes to the item collection managed by this
* source.
......
......@@ -8,6 +8,8 @@ import android.content.Context;
import android.view.View;
import org.chromium.base.Callback;
import org.chromium.chrome.browser.download.home.PrefetchStatusProvider;
import org.chromium.chrome.browser.download.home.empty.EmptyCoordinator;
import org.chromium.chrome.browser.download.home.filter.FilterCoordinator;
import org.chromium.chrome.browser.download.home.filter.Filters.FilterType;
import org.chromium.chrome.browser.download.home.list.ListItem.ViewListItem;
......@@ -43,7 +45,7 @@ public class DateOrderedListCoordinator {
}
private final FilterCoordinator mFilterCoordinator;
private final EmptyViewCoordinator mEmptyViewCoordinator;
private final EmptyCoordinator mEmptyCoordinator;
private final DateOrderedListMediator mMediator;
private final DateOrderedListView mView;
......@@ -61,20 +63,24 @@ public class DateOrderedListCoordinator {
OfflineContentProvider provider, DeleteController deleteController,
SelectionDelegate<ListItem> selectionDelegate,
FilterCoordinator.Observer filterObserver) {
// TODO(shaktisahu): Use a real provider/have this provider query the real data source.
PrefetchStatusProvider prefetchProvider = new PrefetchStatusProvider();
ListItemModel model = new ListItemModel();
DecoratedListItemModel decoratedModel = new DecoratedListItemModel(model);
mView = new DateOrderedListView(context, decoratedModel);
mMediator = new DateOrderedListMediator(
offTheRecord, provider, deleteController, selectionDelegate, model);
// Hook up the FilterCoordinator with our mediator.
mFilterCoordinator = new FilterCoordinator(context, mMediator.getFilterSource());
mEmptyCoordinator =
new EmptyCoordinator(context, prefetchProvider, mMediator.getEmptySource());
mFilterCoordinator =
new FilterCoordinator(context, prefetchProvider, mMediator.getFilterSource());
mFilterCoordinator.addObserver(mMediator::onFilterTypeSelected);
mFilterCoordinator.addObserver(filterObserver);
mFilterCoordinator.addObserver(mEmptyCoordinator);
mEmptyViewCoordinator =
new EmptyViewCoordinator(context, decoratedModel, mMediator.getFilterSource());
mFilterCoordinator.addObserver(mEmptyViewCoordinator::onFilterTypeSelected);
decoratedModel.setHeader(new ViewListItem(Long.MAX_VALUE, mFilterCoordinator.getView()));
}
......
......@@ -138,6 +138,14 @@ class DateOrderedListMediator {
return mDeleteUndoFilter;
}
/**
* @return The {@link OfflineItemFilterSource} that should be used to determine whether there
* are no items and empty view should be shown.
*/
public OfflineItemFilterSource getEmptySource() {
return mTypeFilter;
}
private void onDeleteItem(OfflineItem item) {
onDeleteItems(CollectionUtil.newArrayList(item));
}
......
// Copyright 2018 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.home.list;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.download.home.filter.Filters;
import org.chromium.chrome.browser.download.home.filter.OfflineItemFilterObserver;
import org.chromium.chrome.browser.download.home.filter.OfflineItemFilterSource;
import org.chromium.chrome.browser.download.home.list.ListItem.ViewListItem;
import org.chromium.components.offline_items_collection.OfflineItem;
import java.util.Collection;
/**
* A class that determines whether an empty view should be shown and inserts into the model.
*/
class EmptyViewCoordinator implements OfflineItemFilterObserver {
private final Context mContext;
private final DecoratedListItemModel mDecoratedModel;
private final OfflineItemFilterSource mSource;
private final Handler mHandler = new Handler();
private ViewListItem mEmptyViewItem;
private @Filters.FilterType int mCurrentFilter;
/** Creates a {@link EmptyViewCoordinator} instance that wraos {@code model}. */
public EmptyViewCoordinator(Context context, DecoratedListItemModel decoratedModel,
OfflineItemFilterSource source) {
mContext = context;
mDecoratedModel = decoratedModel;
mSource = source;
mSource.addObserver(this);
mHandler.post(() -> onFilterTypeSelected(mCurrentFilter));
}
public void onFilterTypeSelected(@Filters.FilterType int filter) {
boolean hasEmptyView = mEmptyViewItem != null;
if (filter == mCurrentFilter && hasEmptyView == isEmpty()) return;
mCurrentFilter = filter;
updateForEmptyView();
}
private boolean isEmpty() {
boolean showingPrefetch = mCurrentFilter == Filters.FilterType.PREFETCHED;
for (OfflineItem item : mSource.getItems()) {
if (showingPrefetch && item.isSuggested) return false;
if (!showingPrefetch && !item.isSuggested) return false;
}
return true;
}
private void updateForEmptyView() {
mEmptyViewItem = isEmpty() ? createEmptyView() : null;
mDecoratedModel.setEmptyView(mEmptyViewItem);
}
private ViewListItem createEmptyView() {
boolean showingPrefetch = mCurrentFilter == Filters.FilterType.PREFETCHED;
// TODO(shaktisahu): Supply correct value for prefetch settings.
boolean prefetchEnabled = true;
View emptyView = LayoutInflater.from(mContext).inflate(R.layout.downloads_empty_view, null);
Drawable emptyDrawable = VectorDrawableCompat.create(mContext.getResources(),
showingPrefetch ? R.drawable.ic_library_news_feed : R.drawable.downloads_big,
mContext.getTheme());
TextView emptyTextView = emptyView.findViewById(R.id.empty_view);
emptyTextView.setText(showingPrefetch
? (prefetchEnabled ? R.string.download_manager_prefetch_tab_empty
: R.string.download_manager_enable_prefetch_message)
: R.string.download_manager_ui_empty);
emptyTextView.setCompoundDrawablesWithIntrinsicBounds(null, emptyDrawable, null, null);
return new ViewListItem(Long.MAX_VALUE - 1, emptyView);
}
// OfflineItemFilterObserver implementation.
@Override
public void onItemsAdded(Collection<OfflineItem> items) {
mHandler.post(() -> onFilterTypeSelected(mCurrentFilter));
}
@Override
public void onItemsRemoved(Collection<OfflineItem> items) {
mHandler.post(() -> onFilterTypeSelected(mCurrentFilter));
}
@Override
public void onItemUpdated(OfflineItem oldItem, OfflineItem item) {
mHandler.post(() -> onFilterTypeSelected(mCurrentFilter));
}
}
......@@ -444,6 +444,11 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/download/home/DownloadManagerCoordinatorFactory.java",
"java/src/org/chromium/chrome/browser/download/home/DownloadManagerCoordinatorImpl.java",
"java/src/org/chromium/chrome/browser/download/home/OfflineItemSource.java",
"java/src/org/chromium/chrome/browser/download/home/PrefetchStatusProvider.java",
"java/src/org/chromium/chrome/browser/download/home/empty/EmptyCoordinator.java",
"java/src/org/chromium/chrome/browser/download/home/empty/EmptyProperties.java",
"java/src/org/chromium/chrome/browser/download/home/empty/EmptyView.java",
"java/src/org/chromium/chrome/browser/download/home/empty/EmptyViewBinder.java",
"java/src/org/chromium/chrome/browser/download/home/filter/DeleteUndoOfflineItemFilter.java",
"java/src/org/chromium/chrome/browser/download/home/filter/Filters.java",
"java/src/org/chromium/chrome/browser/download/home/filter/FilterChipsProvider.java",
......@@ -474,7 +479,6 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/download/home/list/DateOrderedListView.java",
"java/src/org/chromium/chrome/browser/download/home/list/DateOrderedListViewAdapter.java",
"java/src/org/chromium/chrome/browser/download/home/list/DecoratedListItemModel.java",
"java/src/org/chromium/chrome/browser/download/home/list/EmptyViewCoordinator.java",
"java/src/org/chromium/chrome/browser/download/home/list/ItemUtils.java",
"java/src/org/chromium/chrome/browser/download/home/list/ListItem.java",
"java/src/org/chromium/chrome/browser/download/home/list/ListItemModel.java",
......
......@@ -104,6 +104,23 @@ public class OfflineItemFilterTest {
Assert.assertEquals(items, filter.getItems());
}
@Test
public void testItemsAvailable() {
when(mSource.areItemsAvailable()).thenReturn(false);
OfflineItemFilterImpl filter = new OfflineItemFilterImpl(mSource);
filter.addObserver(mObserver);
verify(mSource, times(1)).addObserver(filter);
verify(mSource, times(1)).getItems();
Assert.assertFalse(filter.areItemsAvailable());
when(mSource.areItemsAvailable()).thenReturn(true);
filter.onItemsAvailable();
verify(mObserver, times(1)).onItemsAvailable();
Assert.assertTrue(filter.areItemsAvailable());
}
@Test
public void testFiltering() {
OfflineItem item1 = new OfflineItem();
......
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