OLD | NEW |
1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 package org.chromium.chrome.browser.ntp.cards; | 5 package org.chromium.chrome.browser.ntp.cards; |
6 | 6 |
7 import android.annotation.SuppressLint; | 7 import android.annotation.SuppressLint; |
8 import android.graphics.Canvas; | 8 import android.graphics.Canvas; |
9 import android.support.v7.widget.RecyclerView; | 9 import android.support.v7.widget.RecyclerView; |
10 import android.support.v7.widget.RecyclerView.Adapter; | 10 import android.support.v7.widget.RecyclerView.Adapter; |
11 import android.support.v7.widget.RecyclerView.ViewHolder; | 11 import android.support.v7.widget.RecyclerView.ViewHolder; |
12 import android.support.v7.widget.helper.ItemTouchHelper; | 12 import android.support.v7.widget.helper.ItemTouchHelper; |
13 import android.view.View; | 13 import android.view.View; |
14 import android.view.ViewGroup; | 14 import android.view.ViewGroup; |
15 | 15 |
16 import org.chromium.base.Log; | 16 import org.chromium.base.Log; |
17 import org.chromium.base.VisibleForTesting; | 17 import org.chromium.base.VisibleForTesting; |
18 import org.chromium.chrome.R; | 18 import org.chromium.chrome.R; |
19 import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager; | 19 import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager; |
20 import org.chromium.chrome.browser.ntp.UiConfig; | 20 import org.chromium.chrome.browser.ntp.UiConfig; |
21 import org.chromium.chrome.browser.ntp.snippets.CategoryInt; | 21 import org.chromium.chrome.browser.ntp.snippets.CategoryInt; |
22 import org.chromium.chrome.browser.ntp.snippets.CategoryStatus; | 22 import org.chromium.chrome.browser.ntp.snippets.CategoryStatus; |
23 import org.chromium.chrome.browser.ntp.snippets.CategoryStatus.CategoryStatusEnu
m; | 23 import org.chromium.chrome.browser.ntp.snippets.CategoryStatus.CategoryStatusEnu
m; |
24 import org.chromium.chrome.browser.ntp.snippets.SectionHeader; | |
25 import org.chromium.chrome.browser.ntp.snippets.SectionHeaderViewHolder; | 24 import org.chromium.chrome.browser.ntp.snippets.SectionHeaderViewHolder; |
26 import org.chromium.chrome.browser.ntp.snippets.SnippetArticle; | 25 import org.chromium.chrome.browser.ntp.snippets.SnippetArticle; |
27 import org.chromium.chrome.browser.ntp.snippets.SnippetArticleViewHolder; | 26 import org.chromium.chrome.browser.ntp.snippets.SnippetArticleViewHolder; |
28 import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge; | 27 import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge; |
29 import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource; | 28 import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource; |
30 import org.chromium.chrome.browser.signin.SigninManager.SignInStateObserver; | 29 import org.chromium.chrome.browser.signin.SigninManager.SignInStateObserver; |
31 | 30 |
32 import java.util.ArrayList; | 31 import java.util.ArrayList; |
33 import java.util.Collections; | 32 import java.util.Collections; |
34 import java.util.LinkedHashMap; | 33 import java.util.LinkedHashMap; |
35 import java.util.List; | 34 import java.util.List; |
36 import java.util.Map; | 35 import java.util.Map; |
37 | 36 |
38 /** | 37 /** |
39 * A class that handles merging above the fold elements and below the fold cards
into an adapter | 38 * A class that handles merging above the fold elements and below the fold cards
into an adapter |
40 * that will be used to back the NTP RecyclerView. The first element in the adap
ter should always be | 39 * that will be used to back the NTP RecyclerView. The first element in the adap
ter should always be |
41 * the above-the-fold view (containing the logo, search box, and most visited ti
les) and subsequent | 40 * the above-the-fold view (containing the logo, search box, and most visited ti
les) and subsequent |
42 * elements will be the cards shown to the user | 41 * elements will be the cards shown to the user |
43 */ | 42 */ |
44 public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> | 43 public class NewTabPageAdapter |
45 implements SuggestionsSource.Observer, ItemGroup.Observer { | 44 extends Adapter<NewTabPageViewHolder> implements SuggestionsSource.Obser
ver, NodeParent { |
46 private static final String TAG = "Ntp"; | 45 private static final String TAG = "Ntp"; |
47 | 46 |
48 private final NewTabPageManager mNewTabPageManager; | 47 private final NewTabPageManager mNewTabPageManager; |
49 private final View mAboveTheFoldView; | 48 private final View mAboveTheFoldView; |
50 private final UiConfig mUiConfig; | 49 private final UiConfig mUiConfig; |
51 private final ItemTouchCallbacks mItemTouchCallbacks = new ItemTouchCallback
s(); | 50 private final ItemTouchCallbacks mItemTouchCallbacks = new ItemTouchCallback
s(); |
52 private NewTabPageRecyclerView mRecyclerView; | 51 private NewTabPageRecyclerView mRecyclerView; |
53 | 52 |
54 /** | 53 /** |
55 * List of all item groups (which can themselves contain multiple items. Whe
n flattened, this | 54 * List of all item groups (which can themselves contain multiple items. Whe
n flattened, this |
56 * will be a list of all items the adapter exposes. | 55 * will be a list of all items the adapter exposes. |
57 */ | 56 */ |
58 private final List<ItemGroup> mGroups = new ArrayList<>(); | 57 private final List<TreeNode> mGroups = new ArrayList<>(); |
59 private final AboveTheFoldItem mAboveTheFold = new AboveTheFoldItem(); | 58 private final AboveTheFoldItem mAboveTheFold = new AboveTheFoldItem(); |
60 private final SignInPromo mSigninPromo = new SignInPromo(); | 59 private final SignInPromo mSigninPromo; |
61 private final AllDismissedItem mAllDismissed = new AllDismissedItem(); | 60 private final AllDismissedItem mAllDismissed = new AllDismissedItem(); |
62 private final Footer mFooter = new Footer(); | 61 private final Footer mFooter = new Footer(); |
63 private final SpacingItem mBottomSpacer = new SpacingItem(); | 62 private final SpacingItem mBottomSpacer = new SpacingItem(); |
| 63 private final InnerNode mRoot; |
64 | 64 |
65 /** Maps suggestion categories to sections, with stable iteration ordering.
*/ | 65 /** Maps suggestion categories to sections, with stable iteration ordering.
*/ |
66 private final Map<Integer, SuggestionsSection> mSections = new LinkedHashMap
<>(); | 66 private final Map<Integer, SuggestionsSection> mSections = new LinkedHashMap
<>(); |
67 | 67 |
68 private class ItemTouchCallbacks extends ItemTouchHelper.Callback { | 68 private class ItemTouchCallbacks extends ItemTouchHelper.Callback { |
69 @Override | 69 @Override |
70 public void onSwiped(ViewHolder viewHolder, int direction) { | 70 public void onSwiped(ViewHolder viewHolder, int direction) { |
71 mRecyclerView.onItemDismissStarted(viewHolder); | 71 mRecyclerView.onItemDismissStarted(viewHolder); |
72 NewTabPageAdapter.this.dismissItem(viewHolder.getAdapterPosition()); | 72 NewTabPageAdapter.this.dismissItem(viewHolder.getAdapterPosition()); |
73 } | 73 } |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
127 * | 127 * |
128 * @param manager the NewTabPageManager to use to interact with the rest of
the system. | 128 * @param manager the NewTabPageManager to use to interact with the rest of
the system. |
129 * @param aboveTheFoldView the layout encapsulating all the above-the-fold e
lements | 129 * @param aboveTheFoldView the layout encapsulating all the above-the-fold e
lements |
130 * (logo, search box, most visited tiles) | 130 * (logo, search box, most visited tiles) |
131 * @param uiConfig the NTP UI configuration, to be passed to created views. | 131 * @param uiConfig the NTP UI configuration, to be passed to created views. |
132 */ | 132 */ |
133 public NewTabPageAdapter(NewTabPageManager manager, View aboveTheFoldView, U
iConfig uiConfig) { | 133 public NewTabPageAdapter(NewTabPageManager manager, View aboveTheFoldView, U
iConfig uiConfig) { |
134 mNewTabPageManager = manager; | 134 mNewTabPageManager = manager; |
135 mAboveTheFoldView = aboveTheFoldView; | 135 mAboveTheFoldView = aboveTheFoldView; |
136 mUiConfig = uiConfig; | 136 mUiConfig = uiConfig; |
137 mSigninPromo.setObserver(this); | 137 mRoot = new InnerNode(this) { |
| 138 @Override |
| 139 protected List<TreeNode> getChildren() { |
| 140 return mGroups; |
| 141 } |
| 142 }; |
| 143 |
| 144 mSigninPromo = new SignInPromo(mRoot); |
138 resetSections(/*alwaysAllowEmptySections=*/false); | 145 resetSections(/*alwaysAllowEmptySections=*/false); |
139 mNewTabPageManager.getSuggestionsSource().setObserver(this); | 146 mNewTabPageManager.getSuggestionsSource().setObserver(this); |
140 | 147 |
141 mNewTabPageManager.registerSignInStateObserver(new SignInStateObserver()
{ | 148 mNewTabPageManager.registerSignInStateObserver(new SignInStateObserver()
{ |
142 @Override | 149 @Override |
143 public void onSignedIn() { | 150 public void onSignedIn() { |
144 mSigninPromo.hide(); | 151 mSigninPromo.hide(); |
145 resetSections(/*alwaysAllowEmptySections=*/false); | 152 resetSections(/*alwaysAllowEmptySections=*/false); |
146 } | 153 } |
147 | 154 |
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
198 | 205 |
199 // Do not show an empty section if not allowed. | 206 // Do not show an empty section if not allowed. |
200 if (suggestions.isEmpty() && !info.showIfEmpty() && !alwaysAllowEmptySec
tions) { | 207 if (suggestions.isEmpty() && !info.showIfEmpty() && !alwaysAllowEmptySec
tions) { |
201 mSections.remove(category); | 208 mSections.remove(category); |
202 return 0; | 209 return 0; |
203 } | 210 } |
204 | 211 |
205 // Create the section if needed. | 212 // Create the section if needed. |
206 SuggestionsSection section = mSections.get(category); | 213 SuggestionsSection section = mSections.get(category); |
207 if (section == null) { | 214 if (section == null) { |
208 section = new SuggestionsSection(info, this); | 215 section = new SuggestionsSection(mRoot, info); |
209 mSections.put(category, section); | 216 mSections.put(category, section); |
210 } | 217 } |
211 | 218 |
212 // Add the new suggestions. | 219 // Add the new suggestions. |
213 setSuggestions(category, suggestions, categoryStatus); | 220 setSuggestions(category, suggestions, categoryStatus); |
214 | 221 |
215 return suggestions.size(); | 222 return suggestions.size(); |
216 } | 223 } |
217 | 224 |
218 /** Returns callbacks to configure the interactions with the RecyclerView's
items. */ | 225 /** Returns callbacks to configure the interactions with the RecyclerView's
items. */ |
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
278 } | 285 } |
279 } | 286 } |
280 | 287 |
281 @Override | 288 @Override |
282 public void onSuggestionInvalidated(@CategoryInt int category, String idWith
inCategory) { | 289 public void onSuggestionInvalidated(@CategoryInt int category, String idWith
inCategory) { |
283 if (!mSections.containsKey(category)) return; | 290 if (!mSections.containsKey(category)) return; |
284 mSections.get(category).removeSuggestionById(idWithinCategory); | 291 mSections.get(category).removeSuggestionById(idWithinCategory); |
285 } | 292 } |
286 | 293 |
287 @Override | 294 @Override |
288 @NewTabPageItem.ViewType | 295 @ItemViewType |
289 public int getItemViewType(int position) { | 296 public int getItemViewType(int position) { |
290 return getItems().get(position).getType(); | 297 return mRoot.getItemViewType(position); |
291 } | 298 } |
292 | 299 |
293 @Override | 300 @Override |
294 public NewTabPageViewHolder onCreateViewHolder(ViewGroup parent, int viewTyp
e) { | 301 public NewTabPageViewHolder onCreateViewHolder(ViewGroup parent, int viewTyp
e) { |
295 assert parent == mRecyclerView; | 302 assert parent == mRecyclerView; |
296 | 303 |
297 if (viewType == NewTabPageItem.VIEW_TYPE_ABOVE_THE_FOLD) { | 304 switch (viewType) { |
298 return new NewTabPageViewHolder(mAboveTheFoldView); | 305 case ItemViewType.ABOVE_THE_FOLD: |
| 306 return new NewTabPageViewHolder(mAboveTheFoldView); |
| 307 |
| 308 case ItemViewType.HEADER: |
| 309 return new SectionHeaderViewHolder(mRecyclerView, mUiConfig); |
| 310 |
| 311 case ItemViewType.SNIPPET: |
| 312 return new SnippetArticleViewHolder(mRecyclerView, mNewTabPageMa
nager, mUiConfig); |
| 313 |
| 314 case ItemViewType.SPACING: |
| 315 return new NewTabPageViewHolder(SpacingItem.createView(parent)); |
| 316 |
| 317 case ItemViewType.STATUS: |
| 318 return new StatusCardViewHolder(mRecyclerView, mUiConfig); |
| 319 |
| 320 case ItemViewType.PROGRESS: |
| 321 return new ProgressViewHolder(mRecyclerView); |
| 322 |
| 323 case ItemViewType.ACTION: |
| 324 return new ActionItem.ViewHolder(mRecyclerView, mNewTabPageManag
er, mUiConfig); |
| 325 |
| 326 case ItemViewType.PROMO: |
| 327 return new SignInPromo.ViewHolder(mRecyclerView, mUiConfig); |
| 328 |
| 329 case ItemViewType.FOOTER: |
| 330 return new Footer.ViewHolder(mRecyclerView, mNewTabPageManager); |
| 331 |
| 332 case ItemViewType.ALL_DISMISSED: |
| 333 return new AllDismissedItem.ViewHolder(mRecyclerView, mNewTabPag
eManager, this); |
299 } | 334 } |
300 | 335 |
301 if (viewType == NewTabPageItem.VIEW_TYPE_HEADER) { | 336 assert false : viewType; |
302 return new SectionHeaderViewHolder(mRecyclerView, mUiConfig); | |
303 } | |
304 | |
305 if (viewType == NewTabPageItem.VIEW_TYPE_SNIPPET) { | |
306 return new SnippetArticleViewHolder(mRecyclerView, mNewTabPageManage
r, mUiConfig); | |
307 } | |
308 | |
309 if (viewType == NewTabPageItem.VIEW_TYPE_SPACING) { | |
310 return new NewTabPageViewHolder(SpacingItem.createView(parent)); | |
311 } | |
312 | |
313 if (viewType == NewTabPageItem.VIEW_TYPE_STATUS) { | |
314 return new StatusCardViewHolder(mRecyclerView, mUiConfig); | |
315 } | |
316 | |
317 if (viewType == NewTabPageItem.VIEW_TYPE_PROGRESS) { | |
318 return new ProgressViewHolder(mRecyclerView); | |
319 } | |
320 | |
321 if (viewType == NewTabPageItem.VIEW_TYPE_ACTION) { | |
322 return new ActionItem.ViewHolder(mRecyclerView, mNewTabPageManager,
mUiConfig); | |
323 } | |
324 | |
325 if (viewType == NewTabPageItem.VIEW_TYPE_PROMO) { | |
326 return new SignInPromo.ViewHolder(mRecyclerView, mUiConfig); | |
327 } | |
328 | |
329 if (viewType == NewTabPageItem.VIEW_TYPE_FOOTER) { | |
330 return new Footer.ViewHolder(mRecyclerView, mNewTabPageManager); | |
331 } | |
332 | |
333 if (viewType == NewTabPageItem.VIEW_TYPE_ALL_DISMISSED) { | |
334 return new AllDismissedItem.ViewHolder(mRecyclerView, mNewTabPageMan
ager, this); | |
335 } | |
336 | |
337 return null; | 337 return null; |
338 } | 338 } |
339 | 339 |
340 @Override | 340 @Override |
341 public void onBindViewHolder(NewTabPageViewHolder holder, final int position
) { | 341 public void onBindViewHolder(NewTabPageViewHolder holder, final int position
) { |
342 getItems().get(position).onBindViewHolder(holder); | 342 mRoot.onBindViewHolder(holder, position); |
343 } | 343 } |
344 | 344 |
345 @Override | 345 @Override |
346 public int getItemCount() { | 346 public int getItemCount() { |
347 return getItems().size(); | 347 return mRoot.getItemCount(); |
348 } | 348 } |
349 | 349 |
350 public int getAboveTheFoldPosition() { | 350 public int getAboveTheFoldPosition() { |
351 return getGroupPositionOffset(mAboveTheFold); | 351 return getGroupPositionOffset(mAboveTheFold); |
352 } | 352 } |
353 | 353 |
354 public int getFirstHeaderPosition() { | 354 public int getFirstHeaderPosition() { |
355 List<NewTabPageItem> items = getItems(); | 355 int count = getItemCount(); |
356 for (int i = 0; i < items.size(); i++) { | 356 for (int i = 0; i < count; i++) { |
357 if (items.get(i) instanceof SectionHeader) return i; | 357 if (getItemViewType(i) == ItemViewType.HEADER) return i; |
358 } | 358 } |
359 return RecyclerView.NO_POSITION; | 359 return RecyclerView.NO_POSITION; |
360 } | 360 } |
361 | 361 |
362 public int getFirstCardPosition() { | 362 public int getFirstCardPosition() { |
363 for (int i = 0; i < getItemCount(); ++i) { | 363 for (int i = 0; i < getItemCount(); ++i) { |
364 if (CardViewHolder.isCard(getItemViewType(i))) return i; | 364 if (CardViewHolder.isCard(getItemViewType(i))) return i; |
365 } | 365 } |
366 return RecyclerView.NO_POSITION; | 366 return RecyclerView.NO_POSITION; |
367 } | 367 } |
368 | 368 |
369 public int getFooterPosition() { | 369 public int getFooterPosition() { |
370 return getGroupPositionOffset(mFooter); | 370 return getGroupPositionOffset(mFooter); |
371 } | 371 } |
372 | 372 |
373 public int getBottomSpacerPosition() { | 373 public int getBottomSpacerPosition() { |
374 return getGroupPositionOffset(mBottomSpacer); | 374 return getGroupPositionOffset(mBottomSpacer); |
375 } | 375 } |
376 | 376 |
377 public int getLastContentItemPosition() { | 377 public int getLastContentItemPosition() { |
378 return getGroupPositionOffset(hasAllBeenDismissed() ? mAllDismissed : mF
ooter); | 378 return getGroupPositionOffset(hasAllBeenDismissed() ? mAllDismissed : mF
ooter); |
379 } | 379 } |
380 | 380 |
381 public int getSuggestionPosition(SnippetArticle article) { | 381 public int getSuggestionPosition(SnippetArticle article) { |
382 List<NewTabPageItem> items = getItems(); | 382 for (int i = 0; i < mRoot.getItemCount(); i++) { |
383 for (int i = 0; i < items.size(); i++) { | 383 SnippetArticle articleToCheck = mRoot.getSuggestionAt(i); |
384 NewTabPageItem item = items.get(i); | 384 if (articleToCheck != null && articleToCheck.equals(article)) return
i; |
385 if (article.equals(item)) return i; | |
386 } | 385 } |
387 return RecyclerView.NO_POSITION; | 386 return RecyclerView.NO_POSITION; |
388 } | 387 } |
389 | 388 |
390 /** Start a request for new snippets. */ | 389 /** Start a request for new snippets. */ |
391 public void reloadSnippets() { | 390 public void reloadSnippets() { |
392 SnippetsBridge.fetchSnippets(/*forceRequest=*/true); | 391 SnippetsBridge.fetchSnippets(/*forceRequest=*/true); |
393 } | 392 } |
394 | 393 |
395 private void setSuggestions(@CategoryInt int category, List<SnippetArticle>
suggestions, | 394 private void setSuggestions(@CategoryInt int category, List<SnippetArticle>
suggestions, |
(...skipping 22 matching lines...) Expand all Loading... |
418 | 417 |
419 // TODO(mvanouwerkerk): Notify about the subset of changed items. At lea
st |mAboveTheFold| | 418 // TODO(mvanouwerkerk): Notify about the subset of changed items. At lea
st |mAboveTheFold| |
420 // has not changed when refreshing from the all dismissed state. | 419 // has not changed when refreshing from the all dismissed state. |
421 notifyDataSetChanged(); | 420 notifyDataSetChanged(); |
422 } | 421 } |
423 | 422 |
424 private void removeSection(SuggestionsSection section) { | 423 private void removeSection(SuggestionsSection section) { |
425 mSections.remove(section.getCategory()); | 424 mSections.remove(section.getCategory()); |
426 int startPos = getGroupPositionOffset(section); | 425 int startPos = getGroupPositionOffset(section); |
427 mGroups.remove(section); | 426 mGroups.remove(section); |
428 notifyItemRangeRemoved(startPos, section.getItems().size()); | 427 notifyItemRangeRemoved(startPos, section.getItemCount()); |
429 | 428 |
430 if (hasAllBeenDismissed()) { | 429 if (hasAllBeenDismissed()) { |
431 int footerPosition = getFooterPosition(); | 430 int footerPosition = getFooterPosition(); |
432 mGroups.set(mGroups.indexOf(mFooter), mAllDismissed); | 431 mGroups.set(mGroups.indexOf(mFooter), mAllDismissed); |
433 notifyItemChanged(footerPosition); | 432 notifyItemChanged(footerPosition); |
434 } | 433 } |
435 | 434 |
436 notifyItemChanged(getBottomSpacerPosition()); | 435 notifyItemChanged(getBottomSpacerPosition()); |
437 } | 436 } |
438 | 437 |
439 @Override | 438 @Override |
440 public void onItemRangeChanged(ItemGroup group, int itemPosition, int itemCo
unt) { | 439 public void onItemRangeChanged(TreeNode child, int itemPosition, int itemCou
nt) { |
| 440 assert child == mRoot; |
441 if (mGroups.isEmpty()) return; // The sections have not been initialised
yet. | 441 if (mGroups.isEmpty()) return; // The sections have not been initialised
yet. |
442 notifyItemRangeChanged(getGroupPositionOffset(group) + itemPosition, ite
mCount); | 442 notifyItemRangeChanged(itemPosition, itemCount); |
443 } | 443 } |
444 | 444 |
445 @Override | 445 @Override |
446 public void onItemRangeInserted(ItemGroup group, int itemPosition, int itemC
ount) { | 446 public void onItemRangeInserted(TreeNode child, int itemPosition, int itemCo
unt) { |
| 447 assert child == mRoot; |
447 if (mGroups.isEmpty()) return; // The sections have not been initialised
yet. | 448 if (mGroups.isEmpty()) return; // The sections have not been initialised
yet. |
448 notifyItemRangeInserted(getGroupPositionOffset(group) + itemPosition, it
emCount); | 449 notifyItemRangeInserted(itemPosition, itemCount); |
449 notifyItemChanged(getItems().size() - 1); // Refresh the spacer too. | 450 notifyItemChanged(getItemCount() - 1); // Refresh the spacer too. |
450 } | 451 } |
451 | 452 |
452 @Override | 453 @Override |
453 public void onItemRangeRemoved(ItemGroup group, int itemPosition, int itemCo
unt) { | 454 public void onItemRangeRemoved(TreeNode child, int itemPosition, int itemCou
nt) { |
| 455 assert child == mRoot; |
454 if (mGroups.isEmpty()) return; // The sections have not been initialised
yet. | 456 if (mGroups.isEmpty()) return; // The sections have not been initialised
yet. |
455 notifyItemRangeRemoved(getGroupPositionOffset(group) + itemPosition, ite
mCount); | 457 notifyItemRangeRemoved(itemPosition, itemCount); |
456 notifyItemChanged(getItems().size() - 1); // Refresh the spacer too. | 458 notifyItemChanged(getItemCount() - 1); // Refresh the spacer too. |
457 } | 459 } |
458 | 460 |
459 @Override | 461 @Override |
460 public void onAttachedToRecyclerView(RecyclerView recyclerView) { | 462 public void onAttachedToRecyclerView(RecyclerView recyclerView) { |
461 super.onAttachedToRecyclerView(recyclerView); | 463 super.onAttachedToRecyclerView(recyclerView); |
462 | 464 |
463 // We are assuming for now that the adapter is used with a single Recycl
erView. | 465 // We are assuming for now that the adapter is used with a single Recycl
erView. |
464 // Getting the reference as we are doing here is going to be broken if t
hat changes. | 466 // Getting the reference as we are doing here is going to be broken if t
hat changes. |
465 assert mRecyclerView == null; | 467 assert mRecyclerView == null; |
466 | 468 |
467 // FindBugs chokes on the cast below when not checked, raising BC_UNCONF
IRMED_CAST | 469 // FindBugs chokes on the cast below when not checked, raising BC_UNCONF
IRMED_CAST |
468 assert recyclerView instanceof NewTabPageRecyclerView; | 470 assert recyclerView instanceof NewTabPageRecyclerView; |
469 | 471 |
470 mRecyclerView = (NewTabPageRecyclerView) recyclerView; | 472 mRecyclerView = (NewTabPageRecyclerView) recyclerView; |
471 } | 473 } |
472 | 474 |
473 /** | 475 /** |
474 * Dismisses the item at the provided adapter position. Can also cause the d
ismissal of other | 476 * Dismisses the item at the provided adapter position. Can also cause the d
ismissal of other |
475 * items or even entire sections. | 477 * items or even entire sections. |
476 */ | 478 */ |
477 // TODO(crbug.com/635567): Fix this properly. | 479 // TODO(crbug.com/635567): Fix this properly. |
478 @SuppressLint("SwitchIntDef") | 480 @SuppressLint("SwitchIntDef") |
479 public void dismissItem(int position) { | 481 public void dismissItem(int position) { |
480 int itemViewType = getItemViewType(position); | 482 int itemViewType = getItemViewType(position); |
481 | 483 |
482 // TODO(dgn): Polymorphism is supposed to allow to avoid that kind of st
uff. | 484 // TODO(dgn): Polymorphism is supposed to allow to avoid that kind of st
uff. |
483 switch (itemViewType) { | 485 switch (itemViewType) { |
484 case NewTabPageItem.VIEW_TYPE_STATUS: | 486 case ItemViewType.STATUS: |
485 case NewTabPageItem.VIEW_TYPE_ACTION: | 487 case ItemViewType.ACTION: |
486 dismissSection((SuggestionsSection) getGroup(position)); | 488 dismissSection(getSuggestionsSection(position)); |
487 return; | 489 return; |
488 | 490 |
489 case NewTabPageItem.VIEW_TYPE_SNIPPET: | 491 case ItemViewType.SNIPPET: |
490 dismissSuggestion(position); | 492 dismissSuggestion(position); |
491 return; | 493 return; |
492 | 494 |
493 case NewTabPageItem.VIEW_TYPE_PROMO: | 495 case ItemViewType.PROMO: |
494 dismissPromo(); | 496 dismissPromo(); |
495 return; | 497 return; |
496 | 498 |
497 default: | 499 default: |
498 Log.wtf(TAG, "Unsupported dismissal of item of type %d", itemVie
wType); | 500 Log.wtf(TAG, "Unsupported dismissal of item of type %d", itemVie
wType); |
499 return; | 501 return; |
500 } | 502 } |
501 } | 503 } |
502 | 504 |
503 private void dismissSection(SuggestionsSection section) { | 505 private void dismissSection(SuggestionsSection section) { |
504 mNewTabPageManager.getSuggestionsSource().dismissCategory(section.getCat
egory()); | 506 mNewTabPageManager.getSuggestionsSource().dismissCategory(section.getCat
egory()); |
505 removeSection(section); | 507 removeSection(section); |
506 } | 508 } |
507 | 509 |
508 private void dismissSuggestion(int position) { | 510 private void dismissSuggestion(int position) { |
509 SnippetArticle suggestion = (SnippetArticle) getItems().get(position); | 511 SnippetArticle suggestion = mRoot.getSuggestionAt(position); |
510 | |
511 SuggestionsSource suggestionsSource = mNewTabPageManager.getSuggestionsS
ource(); | 512 SuggestionsSource suggestionsSource = mNewTabPageManager.getSuggestionsS
ource(); |
512 if (suggestionsSource == null) { | 513 if (suggestionsSource == null) { |
513 // It is possible for this method to be called after the NewTabPage
has had destroy() | 514 // It is possible for this method to be called after the NewTabPage
has had destroy() |
514 // called. This can happen when NewTabPageRecyclerView.dismissWithAn
imation() is called | 515 // called. This can happen when NewTabPageRecyclerView.dismissWithAn
imation() is called |
515 // and the animation ends after the user has navigated away. In this
case we cannot | 516 // and the animation ends after the user has navigated away. In this
case we cannot |
516 // inform the native side that the snippet has been dismissed (http:
//crbug.com/649299). | 517 // inform the native side that the snippet has been dismissed (http:
//crbug.com/649299). |
517 return; | 518 return; |
518 } | 519 } |
519 | 520 |
520 announceItemRemoved(suggestion.mTitle); | 521 announceItemRemoved(suggestion.mTitle); |
521 | 522 |
522 suggestionsSource.dismissSuggestion(suggestion); | 523 suggestionsSource.dismissSuggestion(suggestion); |
523 SuggestionsSection section = (SuggestionsSection) getGroup(position); | 524 SuggestionsSection section = getSuggestionsSection(position); |
524 section.removeSuggestion(suggestion); | 525 section.removeSuggestion(suggestion); |
525 } | 526 } |
526 | 527 |
527 private void dismissPromo() { | 528 private void dismissPromo() { |
528 // TODO(dgn): accessibility announcement. | 529 // TODO(dgn): accessibility announcement. |
529 mSigninPromo.dismiss(); | 530 mSigninPromo.dismiss(); |
530 | 531 |
531 if (hasAllBeenDismissed()) { | 532 if (hasAllBeenDismissed()) { |
532 int footerPosition = getFooterPosition(); | 533 int footerPosition = getFooterPosition(); |
533 mGroups.set(mGroups.indexOf(mFooter), mAllDismissed); | 534 mGroups.set(mGroups.indexOf(mFooter), mAllDismissed); |
534 notifyItemChanged(footerPosition); | 535 notifyItemChanged(footerPosition); |
535 } | 536 } |
536 } | 537 } |
537 | 538 |
538 /** | 539 /** |
539 * Returns an unmodifiable list containing all items in the adapter. | |
540 */ | |
541 private List<NewTabPageItem> getItems() { | |
542 List<NewTabPageItem> items = new ArrayList<>(); | |
543 for (ItemGroup group : mGroups) { | |
544 items.addAll(group.getItems()); | |
545 } | |
546 return Collections.unmodifiableList(items); | |
547 } | |
548 | |
549 /** | |
550 * Returns another view holder that should be dismissed at the same time as
the provided one. | 540 * Returns another view holder that should be dismissed at the same time as
the provided one. |
551 */ | 541 */ |
552 public ViewHolder getDismissSibling(ViewHolder viewHolder) { | 542 public ViewHolder getDismissSibling(ViewHolder viewHolder) { |
553 int swipePos = viewHolder.getAdapterPosition(); | 543 int swipePos = viewHolder.getAdapterPosition(); |
554 ItemGroup group = getGroup(swipePos); | 544 SuggestionsSection section = getSuggestionsSection(swipePos); |
| 545 if (section == null) return null; |
555 | 546 |
556 if (!(group instanceof SuggestionsSection)) return null; | 547 int siblingPosDelta = |
557 | 548 section.getDismissSiblingPosDelta(swipePos - getGroupPositionOff
set(section)); |
558 SuggestionsSection section = (SuggestionsSection) group; | |
559 int siblingPosDelta = section.getDismissSiblingPosDelta(getItems().get(s
wipePos)); | |
560 if (siblingPosDelta == 0) return null; | 549 if (siblingPosDelta == 0) return null; |
561 | 550 |
562 return mRecyclerView.findViewHolderForAdapterPosition(siblingPosDelta +
swipePos); | 551 return mRecyclerView.findViewHolderForAdapterPosition(siblingPosDelta +
swipePos); |
563 } | 552 } |
564 | 553 |
565 private boolean hasAllBeenDismissed() { | 554 private boolean hasAllBeenDismissed() { |
566 return mSections.isEmpty() && !mSigninPromo.isShown(); | 555 return mSections.isEmpty() && !mSigninPromo.isShown(); |
567 } | 556 } |
568 | 557 |
| 558 /** |
| 559 * @param itemPosition The position of an item in the adapter. |
| 560 * @return Returns the {@link SuggestionsSection} that contains the item at |
| 561 * {@code itemPosition}, or null if the item is not part of one. |
| 562 */ |
569 @VisibleForTesting | 563 @VisibleForTesting |
570 ItemGroup getGroup(int itemPosition) { | 564 SuggestionsSection getSuggestionsSection(int itemPosition) { |
571 int itemsSkipped = 0; | 565 TreeNode child = mGroups.get(mRoot.getChildIndexForPosition(itemPosition
)); |
572 for (ItemGroup group : mGroups) { | 566 if (!(child instanceof SuggestionsSection)) return null; |
573 List<NewTabPageItem> items = group.getItems(); | 567 return (SuggestionsSection) child; |
574 itemsSkipped += items.size(); | |
575 if (itemPosition < itemsSkipped) return group; | |
576 } | |
577 return null; | |
578 } | 568 } |
579 | 569 |
580 @VisibleForTesting | 570 @VisibleForTesting |
581 List<ItemGroup> getGroups() { | 571 List<TreeNode> getGroups() { |
582 return Collections.unmodifiableList(mGroups); | 572 return Collections.unmodifiableList(mGroups); |
583 } | 573 } |
584 | 574 |
585 @VisibleForTesting | 575 @VisibleForTesting |
586 int getGroupPositionOffset(ItemGroup group) { | 576 int getGroupPositionOffset(TreeNode group) { |
587 int positionOffset = 0; | 577 return mRoot.getStartingOffsetForChild(group); |
588 for (ItemGroup candidateGroup : mGroups) { | |
589 if (candidateGroup == group) return positionOffset; | |
590 positionOffset += candidateGroup.getItems().size(); | |
591 } | |
592 Log.d(TAG, "Group not found: %s", group); | |
593 return RecyclerView.NO_POSITION; | |
594 } | 578 } |
595 | 579 |
596 @VisibleForTesting | 580 @VisibleForTesting |
597 SnippetArticle getSuggestionAt(int position) { | 581 SnippetArticle getSuggestionAt(int position) { |
598 return (SnippetArticle) getItems().get(position); | 582 return mRoot.getSuggestionAt(position); |
599 } | 583 } |
600 | 584 |
601 private void announceItemRemoved(String suggestionTitle) { | 585 private void announceItemRemoved(String suggestionTitle) { |
602 // In tests the RecyclerView can be null. | 586 // In tests the RecyclerView can be null. |
603 if (mRecyclerView == null) return; | 587 if (mRecyclerView == null) return; |
604 | 588 |
605 mRecyclerView.announceForAccessibility(mRecyclerView.getResources().getS
tring( | 589 mRecyclerView.announceForAccessibility(mRecyclerView.getResources().getS
tring( |
606 R.string.ntp_accessibility_item_removed, suggestionTitle)); | 590 R.string.ntp_accessibility_item_removed, suggestionTitle)); |
607 } | 591 } |
608 } | 592 } |
OLD | NEW |