Index: chrome/android/java/src/org/chromium/chrome/browser/download/items/OfflineContentAggregatorNotifier.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/download/items/OfflineContentAggregatorNotifier.java b/chrome/android/java/src/org/chromium/chrome/browser/download/items/OfflineContentAggregatorNotifier.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..a23b9904a0c427b096a92d72704f81e506fbfb9a |
--- /dev/null |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/download/items/OfflineContentAggregatorNotifier.java |
@@ -0,0 +1,198 @@ |
+// 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. |
+ |
+package org.chromium.chrome.browser.download.items; |
+ |
+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 java.util.ArrayList; |
+import java.util.HashMap; |
+import java.util.HashSet; |
+import java.util.Iterator; |
+import java.util.Map; |
+import java.util.Map.Entry; |
+import java.util.Set; |
+ |
+/** |
+ * A glue class that bridges an OfflineContentProvider with an Android notification UI layer. This |
+ * class assumes that the UI layer might not necessarily be ready to receive events (which is true |
+ * in the case of a {@link Service}), so it can queue updates until the UI is ready. |
+ */ |
+public class OfflineContentAggregatorNotifier implements OfflineContentProvider.Observer { |
+ /** |
+ * An interface that represents the Android notification UI surface that this class will post |
+ * events to. |
+ */ |
+ public interface NotifierUi { |
+ /** |
+ * Called when this {@link OfflineContentAggregatorNotifier} needs the UI to be available |
+ * because it has to send an update to it. |
+ * @param onReadyEvent A {@link Runnable} that should be run when the UI becomes available. |
+ * Should only be called if this method returns {@code false}. |
+ * @return Whether or not the UI is available. If not, {@code onReadyEvent} |
+ * will be notified once the UI is available. |
+ */ |
+ boolean onUiNeeded(Runnable onReadyEvent); |
+ |
+ /** |
+ * Called when this {@link OfflineContentAggregatorNotifier} no longer expects to send |
+ * updates to the UI in the short term and it can shut down or suspend itself. |
+ */ |
+ void onUiNotNeeded(); |
+ |
+ /** |
+ * Called when there is an update to {@code item} that needs to be propagated to the UI. |
+ * The item might be new or might be an update to an existing {@link OfflineItem}. |
+ * @param item The {@link OfflineItem} to show state for. |
+ */ |
+ void updateItem(OfflineItem item); |
+ |
+ /** |
+ * Called when {@code id} has been removed from the underlying data source and any UI should |
+ * be removed. |
+ * @param id The {@link ContentId} of the {@link OfflineItem} to remove from the UI. |
+ */ |
+ void removeItem(ContentId id); |
+ } |
+ |
+ /** Any updates from {@code mProvider} that have not been propagated to {@code mUi} yet. */ |
gone
2017/03/20 19:03:36
@link #mUi, etc
David Trainor- moved to gerrit
2017/03/25 03:31:13
Done.
|
+ private final Set<ContentId> mPendingDeadUpdates = new HashSet<>(); |
+ |
+ /** Any removals from {@code mProvider} that have not been propagated to {@code mUi} yet. */ |
+ private final Map<ContentId, OfflineItem> mPendingLiveUpdates = new HashMap<>(); |
+ |
+ /** |
+ * A list of 'active' {@link OfflineItem}'s as currently known by this class. 'Active' means |
gone
2017/03/20 19:03:36
no apostrophe before s
David Trainor- moved to gerrit
2017/03/25 03:31:13
Done.
|
+ * {@link OfflineItem#state} is {@link OfflineItemState#IN_PROGRESS} or |
+ * {@link OfflineItemState#PENDING}. |
+ */ |
+ private final Set<ContentId> mActiveItems = new HashSet<>(); |
+ |
+ private final OfflineContentProvider mProvider; |
+ private final NotifierUi mUi; |
+ |
+ /** A helper {@link Runnable} that will be called when {@code mUi} is initialized and ready. */ |
+ private final Runnable mUiReadyObserver = new Runnable() { |
+ @Override |
+ public void run() { |
+ flushPendingActions(); |
+ } |
+ }; |
+ |
+ /** |
+ * Creates an instance of {@link OfflineContentAggregatorNotifier} that will glue |
+ * {@code provider} to {@code ui}. |
+ * @param provider The {@link OfflineContentProvider} to expose to {@code ui}. |
+ * @param ui The {@link NotifierUi} that will visually represent {@code provider}. |
+ */ |
+ public OfflineContentAggregatorNotifier(OfflineContentProvider provider, NotifierUi ui) { |
+ mProvider = provider; |
+ mUi = ui; |
+ |
+ mProvider.addObserver(this); |
+ } |
+ |
+ /** |
+ * Destroys this {@link OfflineContentAggregatorNotifier}. This will detach from any internal |
+ * links to the glued objects specified in the constructor. |
+ */ |
+ public void destroy() { |
+ mProvider.removeObserver(this); |
+ } |
+ |
+ private void flushPendingActions() { |
+ for (Iterator<ContentId> it = mPendingDeadUpdates.iterator(); it.hasNext();) { |
+ ContentId id = it.next(); |
+ if (!mUi.onUiNeeded(mUiReadyObserver)) break; |
gone
2017/03/20 19:03:36
does this really need to happen for every single i
David Trainor- moved to gerrit
2017/03/25 03:31:13
I think the main worry is that the service could d
|
+ |
+ removeItemInternal(id); |
+ it.remove(); |
+ } |
+ |
+ for (Iterator<Entry<ContentId, OfflineItem>> it = mPendingLiveUpdates.entrySet().iterator(); |
+ it.hasNext();) { |
+ Entry<ContentId, OfflineItem> item = it.next(); |
+ if (!mUi.onUiNeeded(mUiReadyObserver)) break; |
+ |
+ updateItemInternal(item.getValue()); |
+ it.remove(); |
+ } |
+ |
+ if (mActiveItems.isEmpty()) mUi.onUiNotNeeded(); |
+ } |
+ |
+ private void processLiveItem(OfflineItem item) { |
+ if (mUi.onUiNeeded(mUiReadyObserver)) { |
+ updateItemInternal(item); |
+ if (mActiveItems.isEmpty()) mUi.onUiNotNeeded(); |
+ } else { |
+ mPendingDeadUpdates.remove(item.id); |
+ mPendingLiveUpdates.put(item.id, item); |
+ } |
+ } |
+ |
+ private void processDeadItem(ContentId id) { |
+ if (mUi.onUiNeeded(mUiReadyObserver)) { |
+ removeItemInternal(id); |
+ if (mActiveItems.isEmpty()) mUi.onUiNotNeeded(); |
+ } else { |
+ mPendingDeadUpdates.add(id); |
+ mPendingLiveUpdates.remove(id); |
+ } |
+ } |
+ |
+ private void updateItemInternal(OfflineItem item) { |
+ switch (item.state) { |
+ case OfflineItemState.IN_PROGRESS: |
+ mActiveItems.add(item.id); |
+ break; |
+ case OfflineItemState.PENDING: |
+ case OfflineItemState.COMPLETE: |
+ case OfflineItemState.CANCELLED: |
+ case OfflineItemState.INTERRUPTED: |
+ case OfflineItemState.FAILED: |
+ case OfflineItemState.PAUSED: |
+ mActiveItems.remove(item.id); |
+ break; |
+ } |
+ mUi.updateItem(item); |
+ } |
+ |
+ private void removeItemInternal(ContentId id) { |
+ mActiveItems.remove(id); |
+ mUi.removeItem(id); |
+ } |
+ |
+ // OfflineContentProvider.Observer implementation. |
+ @Override |
+ public void onItemsAvailable() { |
+ // TODO(dtrainor): Query all items and push the current state to notifications? |
+ } |
+ |
+ @Override |
+ public void onItemsAdded(ArrayList<OfflineItem> items) { |
+ for (int i = 0; i < items.size(); i++) { |
+ OfflineItem item = items.get(i); |
+ |
+ // Only update the UI for new OfflineItems that are in progress or pending. |
+ if (item.state == OfflineItemState.IN_PROGRESS |
+ || item.state == OfflineItemState.PENDING) { |
+ processLiveItem(item); |
+ } |
+ } |
+ } |
+ |
+ @Override |
+ public void onItemRemoved(ContentId id) { |
+ processDeadItem(id); |
+ } |
+ |
+ @Override |
+ public void onItemUpdated(OfflineItem item) { |
+ processLiveItem(item); |
+ } |
+} |