mobile/android/base/home/TopSitesPanel.java

branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
equal deleted inserted replaced
-1:000000000000 0:bd1de80159bc
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6 package org.mozilla.gecko.home;
7
8 import java.util.ArrayList;
9 import java.util.EnumSet;
10 import java.util.HashMap;
11 import java.util.Map;
12
13 import org.mozilla.gecko.R;
14 import org.mozilla.gecko.Telemetry;
15 import org.mozilla.gecko.TelemetryContract;
16 import org.mozilla.gecko.db.BrowserContract.Combined;
17 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
18 import org.mozilla.gecko.db.BrowserDB;
19 import org.mozilla.gecko.db.BrowserDB.URLColumns;
20 import org.mozilla.gecko.db.TopSitesCursorWrapper;
21 import org.mozilla.gecko.favicons.Favicons;
22 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
23 import org.mozilla.gecko.gfx.BitmapUtils;
24 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
25 import org.mozilla.gecko.home.PinSiteDialog.OnSiteSelectedListener;
26 import org.mozilla.gecko.home.TopSitesGridView.OnEditPinnedSiteListener;
27 import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
28 import org.mozilla.gecko.util.ThreadUtils;
29
30 import android.app.Activity;
31 import android.content.ContentResolver;
32 import android.content.Context;
33 import android.content.res.Configuration;
34 import android.database.Cursor;
35 import android.graphics.Bitmap;
36 import android.net.Uri;
37 import android.os.Bundle;
38 import android.support.v4.app.FragmentManager;
39 import android.support.v4.app.LoaderManager.LoaderCallbacks;
40 import android.support.v4.content.AsyncTaskLoader;
41 import android.support.v4.content.Loader;
42 import android.support.v4.widget.CursorAdapter;
43 import android.text.TextUtils;
44 import android.util.Log;
45 import android.view.ContextMenu;
46 import android.view.ContextMenu.ContextMenuInfo;
47 import android.view.LayoutInflater;
48 import android.view.MenuInflater;
49 import android.view.MenuItem;
50 import android.view.View;
51 import android.view.ViewGroup;
52 import android.widget.AdapterView;
53 import android.widget.ListView;
54
55 /**
56 * Fragment that displays frecency search results in a ListView.
57 */
58 public class TopSitesPanel extends HomeFragment {
59 // Logging tag name
60 private static final String LOGTAG = "GeckoTopSitesPanel";
61
62 // Cursor loader ID for the top sites
63 private static final int LOADER_ID_TOP_SITES = 0;
64
65 // Loader ID for thumbnails
66 private static final int LOADER_ID_THUMBNAILS = 1;
67
68 // Key for thumbnail urls
69 private static final String THUMBNAILS_URLS_KEY = "urls";
70
71 // Adapter for the list of top sites
72 private VisitedAdapter mListAdapter;
73
74 // Adapter for the grid of top sites
75 private TopSitesGridAdapter mGridAdapter;
76
77 // List of top sites
78 private HomeListView mList;
79
80 // Grid of top sites
81 private TopSitesGridView mGrid;
82
83 // Callbacks used for the search and favicon cursor loaders
84 private CursorLoaderCallbacks mCursorLoaderCallbacks;
85
86 // Callback for thumbnail loader
87 private ThumbnailsLoaderCallbacks mThumbnailsLoaderCallbacks;
88
89 // Listener for editing pinned sites.
90 private EditPinnedSiteListener mEditPinnedSiteListener;
91
92 // On URL open listener
93 private OnUrlOpenListener mUrlOpenListener;
94
95 // Max number of entries shown in the grid from the cursor.
96 private int mMaxGridEntries;
97
98 // Time in ms until the Gecko thread is reset to normal priority.
99 private static final long PRIORITY_RESET_TIMEOUT = 10000;
100
101 public static TopSitesPanel newInstance() {
102 return new TopSitesPanel();
103 }
104
105 public TopSitesPanel() {
106 mUrlOpenListener = null;
107 }
108
109 private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
110 private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
111
112 private static void debug(final String message) {
113 if (logDebug) {
114 Log.d(LOGTAG, message);
115 }
116 }
117
118 private static void trace(final String message) {
119 if (logVerbose) {
120 Log.v(LOGTAG, message);
121 }
122 }
123
124 @Override
125 public void onAttach(Activity activity) {
126 super.onAttach(activity);
127
128 mMaxGridEntries = activity.getResources().getInteger(R.integer.number_of_top_sites);
129
130 try {
131 mUrlOpenListener = (OnUrlOpenListener) activity;
132 } catch (ClassCastException e) {
133 throw new ClassCastException(activity.toString()
134 + " must implement HomePager.OnUrlOpenListener");
135 }
136 }
137
138 @Override
139 public void onDetach() {
140 super.onDetach();
141
142 mUrlOpenListener = null;
143 }
144
145 @Override
146 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
147 final View view = inflater.inflate(R.layout.home_top_sites_panel, container, false);
148
149 mList = (HomeListView) view.findViewById(R.id.list);
150
151 mGrid = new TopSitesGridView(getActivity());
152 mList.addHeaderView(mGrid);
153
154 return view;
155 }
156
157 @Override
158 public void onViewCreated(View view, Bundle savedInstanceState) {
159 mEditPinnedSiteListener = new EditPinnedSiteListener();
160
161 mList.setTag(HomePager.LIST_TAG_TOP_SITES);
162 mList.setHeaderDividersEnabled(false);
163
164 mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
165 @Override
166 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
167 final ListView list = (ListView) parent;
168 final int headerCount = list.getHeaderViewsCount();
169 if (position < headerCount) {
170 // The click is on a header, don't do anything.
171 return;
172 }
173
174 // Absolute position for the adapter.
175 position += (mGridAdapter.getCount() - headerCount);
176
177 final Cursor c = mListAdapter.getCursor();
178 if (c == null || !c.moveToPosition(position)) {
179 return;
180 }
181
182 final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
183
184 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM);
185
186 // This item is a TwoLinePageRow, so we allow switch-to-tab.
187 mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
188 }
189 });
190
191 mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
192 @Override
193 public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
194 final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
195 info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL));
196 info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE));
197 info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.HISTORY_ID));
198 final int bookmarkIdCol = cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID);
199 if (cursor.isNull(bookmarkIdCol)) {
200 // If this is a combined cursor, we may get a history item without a
201 // bookmark, in which case the bookmarks ID column value will be null.
202 info.bookmarkId = -1;
203 } else {
204 info.bookmarkId = cursor.getInt(bookmarkIdCol);
205 }
206 return info;
207 }
208 });
209
210 mGrid.setOnUrlOpenListener(mUrlOpenListener);
211 mGrid.setOnEditPinnedSiteListener(mEditPinnedSiteListener);
212
213 registerForContextMenu(mList);
214 registerForContextMenu(mGrid);
215 }
216
217 @Override
218 public void onDestroyView() {
219 super.onDestroyView();
220
221 // Discard any additional item clicks on the list
222 // as the panel is getting destroyed (see bug 930160).
223 mList.setOnItemClickListener(null);
224 mList = null;
225
226 mGrid = null;
227 mListAdapter = null;
228 mGridAdapter = null;
229 }
230
231 @Override
232 public void onConfigurationChanged(Configuration newConfig) {
233 super.onConfigurationChanged(newConfig);
234
235 // Detach and reattach the fragment as the layout changes.
236 if (isVisible()) {
237 getFragmentManager().beginTransaction()
238 .detach(this)
239 .attach(this)
240 .commitAllowingStateLoss();
241 }
242 }
243
244 @Override
245 public void onActivityCreated(Bundle savedInstanceState) {
246 super.onActivityCreated(savedInstanceState);
247
248 final Activity activity = getActivity();
249
250 // Setup the top sites grid adapter.
251 mGridAdapter = new TopSitesGridAdapter(activity, null);
252 mGrid.setAdapter(mGridAdapter);
253
254 // Setup the top sites list adapter.
255 mListAdapter = new VisitedAdapter(activity, null);
256 mList.setAdapter(mListAdapter);
257
258 // Create callbacks before the initial loader is started
259 mCursorLoaderCallbacks = new CursorLoaderCallbacks();
260 mThumbnailsLoaderCallbacks = new ThumbnailsLoaderCallbacks();
261 loadIfVisible();
262 }
263
264 @Override
265 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
266 if (menuInfo == null) {
267 return;
268 }
269
270 if (!(menuInfo instanceof TopSitesGridContextMenuInfo)) {
271 // Long pressed item was not a Top Sites GridView item. Superclass
272 // can handle this.
273 super.onCreateContextMenu(menu, view, menuInfo);
274 return;
275 }
276
277 // Long pressed item was a Top Sites GridView item, handle it.
278 MenuInflater inflater = new MenuInflater(view.getContext());
279 inflater.inflate(R.menu.home_contextmenu, menu);
280
281 // Hide ununsed menu items.
282 menu.findItem(R.id.home_open_in_reader).setVisible(false);
283 menu.findItem(R.id.home_edit_bookmark).setVisible(false);
284 menu.findItem(R.id.home_remove).setVisible(false);
285
286 TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo;
287 menu.setHeaderTitle(info.getDisplayTitle());
288
289 if (!TextUtils.isEmpty(info.url)) {
290 if (info.isPinned) {
291 menu.findItem(R.id.top_sites_pin).setVisible(false);
292 } else {
293 menu.findItem(R.id.top_sites_unpin).setVisible(false);
294 }
295 } else {
296 menu.findItem(R.id.home_open_new_tab).setVisible(false);
297 menu.findItem(R.id.home_open_private_tab).setVisible(false);
298 menu.findItem(R.id.top_sites_pin).setVisible(false);
299 menu.findItem(R.id.top_sites_unpin).setVisible(false);
300 }
301 }
302
303 @Override
304 public boolean onContextItemSelected(MenuItem item) {
305 if (super.onContextItemSelected(item)) {
306 // HomeFragment was able to handle to selected item.
307 return true;
308 }
309
310 ContextMenuInfo menuInfo = item.getMenuInfo();
311
312 if (menuInfo == null || !(menuInfo instanceof TopSitesGridContextMenuInfo)) {
313 return false;
314 }
315
316 TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo;
317 final Activity activity = getActivity();
318
319 final int itemId = item.getItemId();
320
321 if (itemId == R.id.top_sites_pin) {
322 final String url = info.url;
323 final String title = info.title;
324 final int position = info.position;
325 final Context context = getActivity().getApplicationContext();
326
327 ThreadUtils.postToBackgroundThread(new Runnable() {
328 @Override
329 public void run() {
330 BrowserDB.pinSite(context.getContentResolver(), url, title, position);
331 }
332 });
333
334 Telemetry.sendUIEvent(TelemetryContract.Event.TOP_SITES_PIN);
335 return true;
336 }
337
338 if (itemId == R.id.top_sites_unpin) {
339 final int position = info.position;
340 final Context context = getActivity().getApplicationContext();
341
342 ThreadUtils.postToBackgroundThread(new Runnable() {
343 @Override
344 public void run() {
345 BrowserDB.unpinSite(context.getContentResolver(), position);
346 }
347 });
348
349 Telemetry.sendUIEvent(TelemetryContract.Event.TOP_SITES_UNPIN);
350 return true;
351 }
352
353 if (itemId == R.id.top_sites_edit) {
354 // Decode "user-entered" URLs before showing them.
355 mEditPinnedSiteListener.onEditPinnedSite(info.position, decodeUserEnteredUrl(info.url));
356
357 Telemetry.sendUIEvent(TelemetryContract.Event.TOP_SITES_EDIT);
358 return true;
359 }
360
361 return false;
362 }
363
364 @Override
365 protected void load() {
366 getLoaderManager().initLoader(LOADER_ID_TOP_SITES, null, mCursorLoaderCallbacks);
367
368 // Since this is the primary fragment that loads whenever about:home is
369 // visited, we want to load it as quickly as possible. Heavy load on
370 // the Gecko thread can slow down the time it takes for thumbnails to
371 // appear, especially during startup (bug 897162). By minimizing the
372 // Gecko thread priority, we ensure that the UI appears quickly. The
373 // priority is reset to normal once thumbnails are loaded.
374 ThreadUtils.reduceGeckoPriority(PRIORITY_RESET_TIMEOUT);
375 }
376
377 static String encodeUserEnteredUrl(String url) {
378 return Uri.fromParts("user-entered", url, null).toString();
379 }
380
381 /**
382 * Listener for editing pinned sites.
383 */
384 private class EditPinnedSiteListener implements OnEditPinnedSiteListener,
385 OnSiteSelectedListener {
386 // Tag for the PinSiteDialog fragment.
387 private static final String TAG_PIN_SITE = "pin_site";
388
389 // Position of the pin.
390 private int mPosition;
391
392 @Override
393 public void onEditPinnedSite(int position, String searchTerm) {
394 mPosition = position;
395
396 final FragmentManager manager = getChildFragmentManager();
397 PinSiteDialog dialog = (PinSiteDialog) manager.findFragmentByTag(TAG_PIN_SITE);
398 if (dialog == null) {
399 dialog = PinSiteDialog.newInstance();
400 }
401
402 dialog.setOnSiteSelectedListener(this);
403 dialog.setSearchTerm(searchTerm);
404 dialog.show(manager, TAG_PIN_SITE);
405 }
406
407 @Override
408 public void onSiteSelected(final String url, final String title) {
409 final int position = mPosition;
410 final Context context = getActivity().getApplicationContext();
411 ThreadUtils.postToBackgroundThread(new Runnable() {
412 @Override
413 public void run() {
414 BrowserDB.pinSite(context.getContentResolver(), url, title, position);
415 }
416 });
417 }
418 }
419
420 private void updateUiFromCursor(Cursor c) {
421 mList.setHeaderDividersEnabled(c != null && c.getCount() > mMaxGridEntries);
422 }
423
424 private static class TopSitesLoader extends SimpleCursorLoader {
425 // Max number of search results
426 private static final int SEARCH_LIMIT = 30;
427 private int mMaxGridEntries;
428
429 public TopSitesLoader(Context context) {
430 super(context);
431 mMaxGridEntries = context.getResources().getInteger(R.integer.number_of_top_sites);
432 }
433
434 @Override
435 public Cursor loadCursor() {
436 trace("TopSitesLoader.loadCursor()");
437 return BrowserDB.getTopSites(getContext().getContentResolver(), mMaxGridEntries, SEARCH_LIMIT);
438 }
439 }
440
441 private class VisitedAdapter extends CursorAdapter {
442 public VisitedAdapter(Context context, Cursor cursor) {
443 super(context, cursor, 0);
444 }
445
446 @Override
447 public int getCount() {
448 return Math.max(0, super.getCount() - mMaxGridEntries);
449 }
450
451 @Override
452 public Object getItem(int position) {
453 return super.getItem(position + mMaxGridEntries);
454 }
455
456 @Override
457 public void bindView(View view, Context context, Cursor cursor) {
458 final int position = cursor.getPosition();
459 cursor.moveToPosition(position + mMaxGridEntries);
460
461 final TwoLinePageRow row = (TwoLinePageRow) view;
462 row.updateFromCursor(cursor);
463 }
464
465 @Override
466 public View newView(Context context, Cursor cursor, ViewGroup parent) {
467 return LayoutInflater.from(context).inflate(R.layout.bookmark_item_row, parent, false);
468 }
469 }
470
471 public class TopSitesGridAdapter extends CursorAdapter {
472 // Cache to store the thumbnails.
473 // Ensure that this is only accessed from the UI thread.
474 private Map<String, Bitmap> mThumbnails;
475
476 public TopSitesGridAdapter(Context context, Cursor cursor) {
477 super(context, cursor, 0);
478 }
479
480 @Override
481 public int getCount() {
482 return Math.min(mMaxGridEntries, super.getCount());
483 }
484
485 @Override
486 protected void onContentChanged() {
487 // Don't do anything. We don't want to regenerate every time
488 // our database is updated.
489 return;
490 }
491
492 /**
493 * Update the thumbnails returned by the db.
494 *
495 * @param thumbnails A map of urls and their thumbnail bitmaps.
496 */
497 public void updateThumbnails(Map<String, Bitmap> thumbnails) {
498 mThumbnails = thumbnails;
499
500 final int count = mGrid.getChildCount();
501 for (int i = 0; i < count; i++) {
502 TopSitesGridItemView gridItem = (TopSitesGridItemView) mGrid.getChildAt(i);
503
504 // All the views have already got their initial state at this point.
505 // This will force each view to load favicons for the missing
506 // thumbnails if necessary.
507 gridItem.markAsDirty();
508 }
509
510 notifyDataSetChanged();
511 }
512
513 @Override
514 public void bindView(View bindView, Context context, Cursor cursor) {
515 String url = "";
516 String title = "";
517 boolean pinned = false;
518
519 // Cursor is already moved to required position.
520 if (!cursor.isAfterLast()) {
521 url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL));
522 title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE));
523 pinned = ((TopSitesCursorWrapper) cursor).isPinned();
524 }
525
526 final TopSitesGridItemView view = (TopSitesGridItemView) bindView;
527
528 // If there is no url, then show "add bookmark".
529 if (TextUtils.isEmpty(url)) {
530 // Wait until thumbnails are loaded before showing anything.
531 if (mThumbnails != null) {
532 view.blankOut();
533 }
534
535 return;
536 }
537
538 // Show the thumbnail, if any.
539 Bitmap thumbnail = (mThumbnails != null ? mThumbnails.get(url) : null);
540
541 // Debounce bindView calls to avoid redundant redraws and favicon
542 // fetches.
543 final boolean updated = view.updateState(title, url, pinned, thumbnail);
544
545 // If thumbnails are still being loaded, don't try to load favicons
546 // just yet. If we sent in a thumbnail, we're done now.
547 if (mThumbnails == null || thumbnail != null) {
548 return;
549 }
550
551 // Thumbnails are delivered late, so we can't short-circuit any
552 // sooner than this. But we can avoid a duplicate favicon
553 // fetch...
554 if (!updated) {
555 debug("bindView called twice for same values; short-circuiting.");
556 return;
557 }
558
559 // If we have no thumbnail, attempt to show a Favicon instead.
560 LoadIDAwareFaviconLoadedListener listener = new LoadIDAwareFaviconLoadedListener(view);
561 final int loadId = Favicons.getSizedFaviconForPageFromLocal(url, listener);
562 if (loadId == Favicons.LOADED) {
563 // Great!
564 return;
565 }
566
567 // Otherwise, do this until the async lookup returns.
568 view.displayThumbnail(R.drawable.favicon);
569
570 // Give each side enough information to shake hands later.
571 listener.setLoadId(loadId);
572 view.setLoadId(loadId);
573 }
574
575 @Override
576 public View newView(Context context, Cursor cursor, ViewGroup parent) {
577 return new TopSitesGridItemView(context);
578 }
579 }
580
581 private static class LoadIDAwareFaviconLoadedListener implements OnFaviconLoadedListener {
582 private volatile int loadId = Favicons.NOT_LOADING;
583 private final TopSitesGridItemView view;
584 public LoadIDAwareFaviconLoadedListener(TopSitesGridItemView view) {
585 this.view = view;
586 }
587
588 public void setLoadId(int id) {
589 this.loadId = id;
590 }
591
592 @Override
593 public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
594 if (TextUtils.equals(this.view.getUrl(), url)) {
595 this.view.displayFavicon(favicon, faviconURL, this.loadId);
596 }
597 }
598 }
599
600 private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
601 @Override
602 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
603 trace("Creating TopSitesLoader: " + id);
604 return new TopSitesLoader(getActivity());
605 }
606
607 /**
608 * This method is called *twice* in some circumstances.
609 *
610 * If you try to avoid that through some kind of boolean flag,
611 * sometimes (e.g., returning to the activity) you'll *not* be called
612 * twice, and thus you'll never draw thumbnails.
613 *
614 * The root cause is TopSitesLoader.loadCursor being called twice.
615 * Why that is... dunno.
616 */
617 @Override
618 public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
619 debug("onLoadFinished: " + c.getCount() + " rows.");
620
621 mListAdapter.swapCursor(c);
622 mGridAdapter.swapCursor(c);
623 updateUiFromCursor(c);
624
625 final int col = c.getColumnIndexOrThrow(URLColumns.URL);
626
627 // Load the thumbnails.
628 // Even though the cursor we're given is supposed to be fresh,
629 // we get a bad first value unless we reset its position.
630 // Using move(-1) and moveToNext() doesn't work correctly under
631 // rotation, so we use moveToFirst.
632 if (!c.moveToFirst()) {
633 return;
634 }
635
636 final ArrayList<String> urls = new ArrayList<String>();
637 int i = 1;
638 do {
639 urls.add(c.getString(col));
640 } while (i++ < mMaxGridEntries && c.moveToNext());
641
642 if (urls.isEmpty()) {
643 return;
644 }
645
646 Bundle bundle = new Bundle();
647 bundle.putStringArrayList(THUMBNAILS_URLS_KEY, urls);
648 getLoaderManager().restartLoader(LOADER_ID_THUMBNAILS, bundle, mThumbnailsLoaderCallbacks);
649 }
650
651 @Override
652 public void onLoaderReset(Loader<Cursor> loader) {
653 if (mListAdapter != null) {
654 mListAdapter.swapCursor(null);
655 }
656
657 if (mGridAdapter != null) {
658 mGridAdapter.swapCursor(null);
659 }
660 }
661 }
662
663 /**
664 * An AsyncTaskLoader to load the thumbnails from a cursor.
665 */
666 private static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, Bitmap>> {
667 private Map<String, Bitmap> mThumbnails;
668 private ArrayList<String> mUrls;
669
670 public ThumbnailsLoader(Context context, ArrayList<String> urls) {
671 super(context);
672 mUrls = urls;
673 }
674
675 @Override
676 public Map<String, Bitmap> loadInBackground() {
677 if (mUrls == null || mUrls.size() == 0) {
678 return null;
679 }
680
681 // Query the DB for thumbnails.
682 final ContentResolver cr = getContext().getContentResolver();
683 final Cursor cursor = BrowserDB.getThumbnailsForUrls(cr, mUrls);
684
685 if (cursor == null) {
686 return null;
687 }
688
689 final Map<String, Bitmap> thumbnails = new HashMap<String, Bitmap>();
690
691 try {
692 final int urlIndex = cursor.getColumnIndexOrThrow(Thumbnails.URL);
693 final int dataIndex = cursor.getColumnIndexOrThrow(Thumbnails.DATA);
694
695 while (cursor.moveToNext()) {
696 String url = cursor.getString(urlIndex);
697
698 // This should never be null, but if it is...
699 final byte[] b = cursor.getBlob(dataIndex);
700 if (b == null) {
701 continue;
702 }
703
704 final Bitmap bitmap = BitmapUtils.decodeByteArray(b);
705
706 // Our thumbnails are never null, so if we get a null decoded
707 // bitmap, it's because we hit an OOM or some other disaster.
708 // Give up immediately rather than hammering on.
709 if (bitmap == null) {
710 Log.w(LOGTAG, "Aborting thumbnail load; decode failed.");
711 break;
712 }
713
714 thumbnails.put(url, bitmap);
715 }
716 } finally {
717 cursor.close();
718 }
719
720 return thumbnails;
721 }
722
723 @Override
724 public void deliverResult(Map<String, Bitmap> thumbnails) {
725 if (isReset()) {
726 mThumbnails = null;
727 return;
728 }
729
730 mThumbnails = thumbnails;
731
732 if (isStarted()) {
733 super.deliverResult(thumbnails);
734 }
735 }
736
737 @Override
738 protected void onStartLoading() {
739 if (mThumbnails != null) {
740 deliverResult(mThumbnails);
741 }
742
743 if (takeContentChanged() || mThumbnails == null) {
744 forceLoad();
745 }
746 }
747
748 @Override
749 protected void onStopLoading() {
750 cancelLoad();
751 }
752
753 @Override
754 public void onCanceled(Map<String, Bitmap> thumbnails) {
755 mThumbnails = null;
756 }
757
758 @Override
759 protected void onReset() {
760 super.onReset();
761
762 // Ensure the loader is stopped.
763 onStopLoading();
764
765 mThumbnails = null;
766 }
767 }
768
769 /**
770 * Loader callbacks for the thumbnails on TopSitesGridView.
771 */
772 private class ThumbnailsLoaderCallbacks implements LoaderCallbacks<Map<String, Bitmap>> {
773 @Override
774 public Loader<Map<String, Bitmap>> onCreateLoader(int id, Bundle args) {
775 return new ThumbnailsLoader(getActivity(), args.getStringArrayList(THUMBNAILS_URLS_KEY));
776 }
777
778 @Override
779 public void onLoadFinished(Loader<Map<String, Bitmap>> loader, Map<String, Bitmap> thumbnails) {
780 if (mGridAdapter != null) {
781 mGridAdapter.updateThumbnails(thumbnails);
782 }
783
784 // Once thumbnails have finished loading, the UI is ready. Reset
785 // Gecko to normal priority.
786 ThreadUtils.resetGeckoPriority();
787 }
788
789 @Override
790 public void onLoaderReset(Loader<Map<String, Bitmap>> loader) {
791 if (mGridAdapter != null) {
792 mGridAdapter.updateThumbnails(null);
793 }
794 }
795 }
796 }

mercurial