mobile/android/base/home/TopSitesPanel.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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/. */
     6 package org.mozilla.gecko.home;
     8 import java.util.ArrayList;
     9 import java.util.EnumSet;
    10 import java.util.HashMap;
    11 import java.util.Map;
    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;
    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;
    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";
    62     // Cursor loader ID for the top sites
    63     private static final int LOADER_ID_TOP_SITES = 0;
    65     // Loader ID for thumbnails
    66     private static final int LOADER_ID_THUMBNAILS = 1;
    68     // Key for thumbnail urls
    69     private static final String THUMBNAILS_URLS_KEY = "urls";
    71     // Adapter for the list of top sites
    72     private VisitedAdapter mListAdapter;
    74     // Adapter for the grid of top sites
    75     private TopSitesGridAdapter mGridAdapter;
    77     // List of top sites
    78     private HomeListView mList;
    80     // Grid of top sites
    81     private TopSitesGridView mGrid;
    83     // Callbacks used for the search and favicon cursor loaders
    84     private CursorLoaderCallbacks mCursorLoaderCallbacks;
    86     // Callback for thumbnail loader
    87     private ThumbnailsLoaderCallbacks mThumbnailsLoaderCallbacks;
    89     // Listener for editing pinned sites.
    90     private EditPinnedSiteListener mEditPinnedSiteListener;
    92     // On URL open listener
    93     private OnUrlOpenListener mUrlOpenListener;
    95     // Max number of entries shown in the grid from the cursor.
    96     private int mMaxGridEntries;
    98     // Time in ms until the Gecko thread is reset to normal priority.
    99     private static final long PRIORITY_RESET_TIMEOUT = 10000;
   101     public static TopSitesPanel newInstance() {
   102         return new TopSitesPanel();
   103     }
   105     public TopSitesPanel() {
   106         mUrlOpenListener = null;
   107     }
   109     private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
   110     private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
   112     private static void debug(final String message) {
   113         if (logDebug) {
   114             Log.d(LOGTAG, message);
   115         }
   116     }
   118     private static void trace(final String message) {
   119         if (logVerbose) {
   120             Log.v(LOGTAG, message);
   121         }
   122     }
   124     @Override
   125     public void onAttach(Activity activity) {
   126         super.onAttach(activity);
   128         mMaxGridEntries = activity.getResources().getInteger(R.integer.number_of_top_sites);
   130         try {
   131             mUrlOpenListener = (OnUrlOpenListener) activity;
   132         } catch (ClassCastException e) {
   133             throw new ClassCastException(activity.toString()
   134                     + " must implement HomePager.OnUrlOpenListener");
   135         }
   136     }
   138     @Override
   139     public void onDetach() {
   140         super.onDetach();
   142         mUrlOpenListener = null;
   143     }
   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);
   149         mList = (HomeListView) view.findViewById(R.id.list);
   151         mGrid = new TopSitesGridView(getActivity());
   152         mList.addHeaderView(mGrid);
   154         return view;
   155     }
   157     @Override
   158     public void onViewCreated(View view, Bundle savedInstanceState) {
   159         mEditPinnedSiteListener = new EditPinnedSiteListener();
   161         mList.setTag(HomePager.LIST_TAG_TOP_SITES);
   162         mList.setHeaderDividersEnabled(false);
   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                 }
   174                 // Absolute position for the adapter.
   175                 position += (mGridAdapter.getCount() - headerCount);
   177                 final Cursor c = mListAdapter.getCursor();
   178                 if (c == null || !c.moveToPosition(position)) {
   179                     return;
   180                 }
   182                 final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
   184                 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM);
   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         });
   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         });
   210         mGrid.setOnUrlOpenListener(mUrlOpenListener);
   211         mGrid.setOnEditPinnedSiteListener(mEditPinnedSiteListener);
   213         registerForContextMenu(mList);
   214         registerForContextMenu(mGrid);
   215     }
   217     @Override
   218     public void onDestroyView() {
   219         super.onDestroyView();
   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;
   226         mGrid = null;
   227         mListAdapter = null;
   228         mGridAdapter = null;
   229     }
   231     @Override
   232     public void onConfigurationChanged(Configuration newConfig) {
   233         super.onConfigurationChanged(newConfig);
   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     }
   244     @Override
   245     public void onActivityCreated(Bundle savedInstanceState) {
   246         super.onActivityCreated(savedInstanceState);
   248         final Activity activity = getActivity();
   250         // Setup the top sites grid adapter.
   251         mGridAdapter = new TopSitesGridAdapter(activity, null);
   252         mGrid.setAdapter(mGridAdapter);
   254         // Setup the top sites list adapter.
   255         mListAdapter = new VisitedAdapter(activity, null);
   256         mList.setAdapter(mListAdapter);
   258         // Create callbacks before the initial loader is started
   259         mCursorLoaderCallbacks = new CursorLoaderCallbacks();
   260         mThumbnailsLoaderCallbacks = new ThumbnailsLoaderCallbacks();
   261         loadIfVisible();
   262     }
   264     @Override
   265     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
   266         if (menuInfo == null) {
   267             return;
   268         }
   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         }
   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);
   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);
   286         TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo;
   287         menu.setHeaderTitle(info.getDisplayTitle());
   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     }
   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         }
   310         ContextMenuInfo menuInfo = item.getMenuInfo();
   312         if (menuInfo == null || !(menuInfo instanceof TopSitesGridContextMenuInfo)) {
   313             return false;
   314         }
   316         TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo;
   317         final Activity activity = getActivity();
   319         final int itemId = item.getItemId();
   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();
   327             ThreadUtils.postToBackgroundThread(new Runnable() {
   328                 @Override
   329                 public void run() {
   330                     BrowserDB.pinSite(context.getContentResolver(), url, title, position);
   331                 }
   332             });
   334             Telemetry.sendUIEvent(TelemetryContract.Event.TOP_SITES_PIN);
   335             return true;
   336         }
   338         if (itemId == R.id.top_sites_unpin) {
   339             final int position = info.position;
   340             final Context context = getActivity().getApplicationContext();
   342             ThreadUtils.postToBackgroundThread(new Runnable() {
   343                 @Override
   344                 public void run() {
   345                     BrowserDB.unpinSite(context.getContentResolver(), position);
   346                 }
   347             });
   349             Telemetry.sendUIEvent(TelemetryContract.Event.TOP_SITES_UNPIN);
   350             return true;
   351         }
   353         if (itemId == R.id.top_sites_edit) {
   354             // Decode "user-entered" URLs before showing them.
   355             mEditPinnedSiteListener.onEditPinnedSite(info.position, decodeUserEnteredUrl(info.url));
   357             Telemetry.sendUIEvent(TelemetryContract.Event.TOP_SITES_EDIT);
   358             return true;
   359         }
   361         return false;
   362     }
   364     @Override
   365     protected void load() {
   366         getLoaderManager().initLoader(LOADER_ID_TOP_SITES, null, mCursorLoaderCallbacks);
   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     }
   377     static String encodeUserEnteredUrl(String url) {
   378         return Uri.fromParts("user-entered", url, null).toString();
   379     }
   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";
   389         // Position of the pin.
   390         private int mPosition;
   392         @Override
   393         public void onEditPinnedSite(int position, String searchTerm) {
   394             mPosition = position;
   396             final FragmentManager manager = getChildFragmentManager();
   397             PinSiteDialog dialog = (PinSiteDialog) manager.findFragmentByTag(TAG_PIN_SITE);
   398             if (dialog == null) {
   399                 dialog = PinSiteDialog.newInstance();
   400             }
   402             dialog.setOnSiteSelectedListener(this);
   403             dialog.setSearchTerm(searchTerm);
   404             dialog.show(manager, TAG_PIN_SITE);
   405         }
   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     }
   420     private void updateUiFromCursor(Cursor c) {
   421         mList.setHeaderDividersEnabled(c != null && c.getCount() > mMaxGridEntries);
   422     }
   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;
   429         public TopSitesLoader(Context context) {
   430             super(context);
   431             mMaxGridEntries = context.getResources().getInteger(R.integer.number_of_top_sites);
   432         }
   434         @Override
   435         public Cursor loadCursor() {
   436             trace("TopSitesLoader.loadCursor()");
   437             return BrowserDB.getTopSites(getContext().getContentResolver(), mMaxGridEntries, SEARCH_LIMIT);
   438         }
   439     }
   441     private class VisitedAdapter extends CursorAdapter {
   442         public VisitedAdapter(Context context, Cursor cursor) {
   443             super(context, cursor, 0);
   444         }
   446         @Override
   447         public int getCount() {
   448             return Math.max(0, super.getCount() - mMaxGridEntries);
   449         }
   451         @Override
   452         public Object getItem(int position) {
   453             return super.getItem(position + mMaxGridEntries);
   454         }
   456         @Override
   457         public void bindView(View view, Context context, Cursor cursor) {
   458             final int position = cursor.getPosition();
   459             cursor.moveToPosition(position + mMaxGridEntries);
   461             final TwoLinePageRow row = (TwoLinePageRow) view;
   462             row.updateFromCursor(cursor);
   463         }
   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     }
   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;
   476         public TopSitesGridAdapter(Context context, Cursor cursor) {
   477             super(context, cursor, 0);
   478         }
   480         @Override
   481         public int getCount() {
   482             return Math.min(mMaxGridEntries, super.getCount());
   483         }
   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         }
   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;
   500             final int count = mGrid.getChildCount();
   501             for (int i = 0; i < count; i++) {
   502                 TopSitesGridItemView gridItem = (TopSitesGridItemView) mGrid.getChildAt(i);
   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             }
   510             notifyDataSetChanged();
   511         }
   513         @Override
   514         public void bindView(View bindView, Context context, Cursor cursor) {
   515             String url = "";
   516             String title = "";
   517             boolean pinned = false;
   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             }
   526             final TopSitesGridItemView view = (TopSitesGridItemView) bindView;
   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                 }
   535                 return;
   536             }
   538             // Show the thumbnail, if any.
   539             Bitmap thumbnail = (mThumbnails != null ? mThumbnails.get(url) : null);
   541             // Debounce bindView calls to avoid redundant redraws and favicon
   542             // fetches.
   543             final boolean updated = view.updateState(title, url, pinned, thumbnail);
   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             }
   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             }
   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             }
   567             // Otherwise, do this until the async lookup returns.
   568             view.displayThumbnail(R.drawable.favicon);
   570             // Give each side enough information to shake hands later.
   571             listener.setLoadId(loadId);
   572             view.setLoadId(loadId);
   573         }
   575         @Override
   576         public View newView(Context context, Cursor cursor, ViewGroup parent) {
   577             return new TopSitesGridItemView(context);
   578         }
   579     }
   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         }
   588         public void setLoadId(int id) {
   589             this.loadId = id;
   590         }
   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     }
   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         }
   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.");
   621             mListAdapter.swapCursor(c);
   622             mGridAdapter.swapCursor(c);
   623             updateUiFromCursor(c);
   625             final int col = c.getColumnIndexOrThrow(URLColumns.URL);
   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             }
   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());
   642             if (urls.isEmpty()) {
   643                 return;
   644             }
   646             Bundle bundle = new Bundle();
   647             bundle.putStringArrayList(THUMBNAILS_URLS_KEY, urls);
   648             getLoaderManager().restartLoader(LOADER_ID_THUMBNAILS, bundle, mThumbnailsLoaderCallbacks);
   649         }
   651         @Override
   652         public void onLoaderReset(Loader<Cursor> loader) {
   653             if (mListAdapter != null) {
   654                 mListAdapter.swapCursor(null);
   655             }
   657             if (mGridAdapter != null) {
   658                 mGridAdapter.swapCursor(null);
   659             }
   660         }
   661     }
   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;
   670         public ThumbnailsLoader(Context context, ArrayList<String> urls) {
   671             super(context);
   672             mUrls = urls;
   673         }
   675         @Override
   676         public Map<String, Bitmap> loadInBackground() {
   677             if (mUrls == null || mUrls.size() == 0) {
   678                 return null;
   679             }
   681             // Query the DB for thumbnails.
   682             final ContentResolver cr = getContext().getContentResolver();
   683             final Cursor cursor = BrowserDB.getThumbnailsForUrls(cr, mUrls);
   685             if (cursor == null) {
   686                 return null;
   687             }
   689             final Map<String, Bitmap> thumbnails = new HashMap<String, Bitmap>();
   691             try {
   692                 final int urlIndex = cursor.getColumnIndexOrThrow(Thumbnails.URL);
   693                 final int dataIndex = cursor.getColumnIndexOrThrow(Thumbnails.DATA);
   695                 while (cursor.moveToNext()) {
   696                     String url = cursor.getString(urlIndex);
   698                     // This should never be null, but if it is...
   699                     final byte[] b = cursor.getBlob(dataIndex);
   700                     if (b == null) {
   701                         continue;
   702                     }
   704                     final Bitmap bitmap = BitmapUtils.decodeByteArray(b);
   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                     }
   714                     thumbnails.put(url, bitmap);
   715                 }
   716             } finally {
   717                 cursor.close();
   718             }
   720             return thumbnails;
   721         }
   723         @Override
   724         public void deliverResult(Map<String, Bitmap> thumbnails) {
   725             if (isReset()) {
   726                 mThumbnails = null;
   727                 return;
   728             }
   730             mThumbnails = thumbnails;
   732             if (isStarted()) {
   733                 super.deliverResult(thumbnails);
   734             }
   735         }
   737         @Override
   738         protected void onStartLoading() {
   739             if (mThumbnails != null) {
   740                 deliverResult(mThumbnails);
   741             }
   743             if (takeContentChanged() || mThumbnails == null) {
   744                 forceLoad();
   745             }
   746         }
   748         @Override
   749         protected void onStopLoading() {
   750             cancelLoad();
   751         }
   753         @Override
   754         public void onCanceled(Map<String, Bitmap> thumbnails) {
   755             mThumbnails = null;
   756         }
   758         @Override
   759         protected void onReset() {
   760             super.onReset();
   762             // Ensure the loader is stopped.
   763             onStopLoading();
   765             mThumbnails = null;
   766         }
   767     }
   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         }
   778         @Override
   779         public void onLoadFinished(Loader<Map<String, Bitmap>> loader, Map<String, Bitmap> thumbnails) {
   780             if (mGridAdapter != null) {
   781                 mGridAdapter.updateThumbnails(thumbnails);
   782             }
   784             // Once thumbnails have finished loading, the UI is ready. Reset
   785             // Gecko to normal priority.
   786             ThreadUtils.resetGeckoPriority();
   787         }
   789         @Override
   790         public void onLoaderReset(Loader<Map<String, Bitmap>> loader) {
   791             if (mGridAdapter != null) {
   792                 mGridAdapter.updateThumbnails(null);
   793             }
   794         }
   795     }
   796 }

mercurial