michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.home; michael@0: michael@0: import java.util.ArrayList; michael@0: import java.util.EnumSet; michael@0: import java.util.HashMap; michael@0: import java.util.Map; michael@0: michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.Telemetry; michael@0: import org.mozilla.gecko.TelemetryContract; michael@0: import org.mozilla.gecko.db.BrowserContract.Combined; michael@0: import org.mozilla.gecko.db.BrowserContract.Thumbnails; michael@0: import org.mozilla.gecko.db.BrowserDB; michael@0: import org.mozilla.gecko.db.BrowserDB.URLColumns; michael@0: import org.mozilla.gecko.db.TopSitesCursorWrapper; michael@0: import org.mozilla.gecko.favicons.Favicons; michael@0: import org.mozilla.gecko.favicons.OnFaviconLoadedListener; michael@0: import org.mozilla.gecko.gfx.BitmapUtils; michael@0: import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; michael@0: import org.mozilla.gecko.home.PinSiteDialog.OnSiteSelectedListener; michael@0: import org.mozilla.gecko.home.TopSitesGridView.OnEditPinnedSiteListener; michael@0: import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: michael@0: import android.app.Activity; michael@0: import android.content.ContentResolver; michael@0: import android.content.Context; michael@0: import android.content.res.Configuration; michael@0: import android.database.Cursor; michael@0: import android.graphics.Bitmap; michael@0: import android.net.Uri; michael@0: import android.os.Bundle; michael@0: import android.support.v4.app.FragmentManager; michael@0: import android.support.v4.app.LoaderManager.LoaderCallbacks; michael@0: import android.support.v4.content.AsyncTaskLoader; michael@0: import android.support.v4.content.Loader; michael@0: import android.support.v4.widget.CursorAdapter; michael@0: import android.text.TextUtils; michael@0: import android.util.Log; michael@0: import android.view.ContextMenu; michael@0: import android.view.ContextMenu.ContextMenuInfo; michael@0: import android.view.LayoutInflater; michael@0: import android.view.MenuInflater; michael@0: import android.view.MenuItem; michael@0: import android.view.View; michael@0: import android.view.ViewGroup; michael@0: import android.widget.AdapterView; michael@0: import android.widget.ListView; michael@0: michael@0: /** michael@0: * Fragment that displays frecency search results in a ListView. michael@0: */ michael@0: public class TopSitesPanel extends HomeFragment { michael@0: // Logging tag name michael@0: private static final String LOGTAG = "GeckoTopSitesPanel"; michael@0: michael@0: // Cursor loader ID for the top sites michael@0: private static final int LOADER_ID_TOP_SITES = 0; michael@0: michael@0: // Loader ID for thumbnails michael@0: private static final int LOADER_ID_THUMBNAILS = 1; michael@0: michael@0: // Key for thumbnail urls michael@0: private static final String THUMBNAILS_URLS_KEY = "urls"; michael@0: michael@0: // Adapter for the list of top sites michael@0: private VisitedAdapter mListAdapter; michael@0: michael@0: // Adapter for the grid of top sites michael@0: private TopSitesGridAdapter mGridAdapter; michael@0: michael@0: // List of top sites michael@0: private HomeListView mList; michael@0: michael@0: // Grid of top sites michael@0: private TopSitesGridView mGrid; michael@0: michael@0: // Callbacks used for the search and favicon cursor loaders michael@0: private CursorLoaderCallbacks mCursorLoaderCallbacks; michael@0: michael@0: // Callback for thumbnail loader michael@0: private ThumbnailsLoaderCallbacks mThumbnailsLoaderCallbacks; michael@0: michael@0: // Listener for editing pinned sites. michael@0: private EditPinnedSiteListener mEditPinnedSiteListener; michael@0: michael@0: // On URL open listener michael@0: private OnUrlOpenListener mUrlOpenListener; michael@0: michael@0: // Max number of entries shown in the grid from the cursor. michael@0: private int mMaxGridEntries; michael@0: michael@0: // Time in ms until the Gecko thread is reset to normal priority. michael@0: private static final long PRIORITY_RESET_TIMEOUT = 10000; michael@0: michael@0: public static TopSitesPanel newInstance() { michael@0: return new TopSitesPanel(); michael@0: } michael@0: michael@0: public TopSitesPanel() { michael@0: mUrlOpenListener = null; michael@0: } michael@0: michael@0: private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); michael@0: private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); michael@0: michael@0: private static void debug(final String message) { michael@0: if (logDebug) { michael@0: Log.d(LOGTAG, message); michael@0: } michael@0: } michael@0: michael@0: private static void trace(final String message) { michael@0: if (logVerbose) { michael@0: Log.v(LOGTAG, message); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onAttach(Activity activity) { michael@0: super.onAttach(activity); michael@0: michael@0: mMaxGridEntries = activity.getResources().getInteger(R.integer.number_of_top_sites); michael@0: michael@0: try { michael@0: mUrlOpenListener = (OnUrlOpenListener) activity; michael@0: } catch (ClassCastException e) { michael@0: throw new ClassCastException(activity.toString() michael@0: + " must implement HomePager.OnUrlOpenListener"); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onDetach() { michael@0: super.onDetach(); michael@0: michael@0: mUrlOpenListener = null; michael@0: } michael@0: michael@0: @Override michael@0: public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { michael@0: final View view = inflater.inflate(R.layout.home_top_sites_panel, container, false); michael@0: michael@0: mList = (HomeListView) view.findViewById(R.id.list); michael@0: michael@0: mGrid = new TopSitesGridView(getActivity()); michael@0: mList.addHeaderView(mGrid); michael@0: michael@0: return view; michael@0: } michael@0: michael@0: @Override michael@0: public void onViewCreated(View view, Bundle savedInstanceState) { michael@0: mEditPinnedSiteListener = new EditPinnedSiteListener(); michael@0: michael@0: mList.setTag(HomePager.LIST_TAG_TOP_SITES); michael@0: mList.setHeaderDividersEnabled(false); michael@0: michael@0: mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { michael@0: @Override michael@0: public void onItemClick(AdapterView parent, View view, int position, long id) { michael@0: final ListView list = (ListView) parent; michael@0: final int headerCount = list.getHeaderViewsCount(); michael@0: if (position < headerCount) { michael@0: // The click is on a header, don't do anything. michael@0: return; michael@0: } michael@0: michael@0: // Absolute position for the adapter. michael@0: position += (mGridAdapter.getCount() - headerCount); michael@0: michael@0: final Cursor c = mListAdapter.getCursor(); michael@0: if (c == null || !c.moveToPosition(position)) { michael@0: return; michael@0: } michael@0: michael@0: final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); michael@0: michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM); michael@0: michael@0: // This item is a TwoLinePageRow, so we allow switch-to-tab. michael@0: mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); michael@0: } michael@0: }); michael@0: michael@0: mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() { michael@0: @Override michael@0: public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) { michael@0: final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id); michael@0: info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL)); michael@0: info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE)); michael@0: info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.HISTORY_ID)); michael@0: final int bookmarkIdCol = cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID); michael@0: if (cursor.isNull(bookmarkIdCol)) { michael@0: // If this is a combined cursor, we may get a history item without a michael@0: // bookmark, in which case the bookmarks ID column value will be null. michael@0: info.bookmarkId = -1; michael@0: } else { michael@0: info.bookmarkId = cursor.getInt(bookmarkIdCol); michael@0: } michael@0: return info; michael@0: } michael@0: }); michael@0: michael@0: mGrid.setOnUrlOpenListener(mUrlOpenListener); michael@0: mGrid.setOnEditPinnedSiteListener(mEditPinnedSiteListener); michael@0: michael@0: registerForContextMenu(mList); michael@0: registerForContextMenu(mGrid); michael@0: } michael@0: michael@0: @Override michael@0: public void onDestroyView() { michael@0: super.onDestroyView(); michael@0: michael@0: // Discard any additional item clicks on the list michael@0: // as the panel is getting destroyed (see bug 930160). michael@0: mList.setOnItemClickListener(null); michael@0: mList = null; michael@0: michael@0: mGrid = null; michael@0: mListAdapter = null; michael@0: mGridAdapter = null; michael@0: } michael@0: michael@0: @Override michael@0: public void onConfigurationChanged(Configuration newConfig) { michael@0: super.onConfigurationChanged(newConfig); michael@0: michael@0: // Detach and reattach the fragment as the layout changes. michael@0: if (isVisible()) { michael@0: getFragmentManager().beginTransaction() michael@0: .detach(this) michael@0: .attach(this) michael@0: .commitAllowingStateLoss(); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onActivityCreated(Bundle savedInstanceState) { michael@0: super.onActivityCreated(savedInstanceState); michael@0: michael@0: final Activity activity = getActivity(); michael@0: michael@0: // Setup the top sites grid adapter. michael@0: mGridAdapter = new TopSitesGridAdapter(activity, null); michael@0: mGrid.setAdapter(mGridAdapter); michael@0: michael@0: // Setup the top sites list adapter. michael@0: mListAdapter = new VisitedAdapter(activity, null); michael@0: mList.setAdapter(mListAdapter); michael@0: michael@0: // Create callbacks before the initial loader is started michael@0: mCursorLoaderCallbacks = new CursorLoaderCallbacks(); michael@0: mThumbnailsLoaderCallbacks = new ThumbnailsLoaderCallbacks(); michael@0: loadIfVisible(); michael@0: } michael@0: michael@0: @Override michael@0: public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { michael@0: if (menuInfo == null) { michael@0: return; michael@0: } michael@0: michael@0: if (!(menuInfo instanceof TopSitesGridContextMenuInfo)) { michael@0: // Long pressed item was not a Top Sites GridView item. Superclass michael@0: // can handle this. michael@0: super.onCreateContextMenu(menu, view, menuInfo); michael@0: return; michael@0: } michael@0: michael@0: // Long pressed item was a Top Sites GridView item, handle it. michael@0: MenuInflater inflater = new MenuInflater(view.getContext()); michael@0: inflater.inflate(R.menu.home_contextmenu, menu); michael@0: michael@0: // Hide ununsed menu items. michael@0: menu.findItem(R.id.home_open_in_reader).setVisible(false); michael@0: menu.findItem(R.id.home_edit_bookmark).setVisible(false); michael@0: menu.findItem(R.id.home_remove).setVisible(false); michael@0: michael@0: TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo; michael@0: menu.setHeaderTitle(info.getDisplayTitle()); michael@0: michael@0: if (!TextUtils.isEmpty(info.url)) { michael@0: if (info.isPinned) { michael@0: menu.findItem(R.id.top_sites_pin).setVisible(false); michael@0: } else { michael@0: menu.findItem(R.id.top_sites_unpin).setVisible(false); michael@0: } michael@0: } else { michael@0: menu.findItem(R.id.home_open_new_tab).setVisible(false); michael@0: menu.findItem(R.id.home_open_private_tab).setVisible(false); michael@0: menu.findItem(R.id.top_sites_pin).setVisible(false); michael@0: menu.findItem(R.id.top_sites_unpin).setVisible(false); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public boolean onContextItemSelected(MenuItem item) { michael@0: if (super.onContextItemSelected(item)) { michael@0: // HomeFragment was able to handle to selected item. michael@0: return true; michael@0: } michael@0: michael@0: ContextMenuInfo menuInfo = item.getMenuInfo(); michael@0: michael@0: if (menuInfo == null || !(menuInfo instanceof TopSitesGridContextMenuInfo)) { michael@0: return false; michael@0: } michael@0: michael@0: TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo; michael@0: final Activity activity = getActivity(); michael@0: michael@0: final int itemId = item.getItemId(); michael@0: michael@0: if (itemId == R.id.top_sites_pin) { michael@0: final String url = info.url; michael@0: final String title = info.title; michael@0: final int position = info.position; michael@0: final Context context = getActivity().getApplicationContext(); michael@0: michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: BrowserDB.pinSite(context.getContentResolver(), url, title, position); michael@0: } michael@0: }); michael@0: michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.TOP_SITES_PIN); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.top_sites_unpin) { michael@0: final int position = info.position; michael@0: final Context context = getActivity().getApplicationContext(); michael@0: michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: BrowserDB.unpinSite(context.getContentResolver(), position); michael@0: } michael@0: }); michael@0: michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.TOP_SITES_UNPIN); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.top_sites_edit) { michael@0: // Decode "user-entered" URLs before showing them. michael@0: mEditPinnedSiteListener.onEditPinnedSite(info.position, decodeUserEnteredUrl(info.url)); michael@0: michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.TOP_SITES_EDIT); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: @Override michael@0: protected void load() { michael@0: getLoaderManager().initLoader(LOADER_ID_TOP_SITES, null, mCursorLoaderCallbacks); michael@0: michael@0: // Since this is the primary fragment that loads whenever about:home is michael@0: // visited, we want to load it as quickly as possible. Heavy load on michael@0: // the Gecko thread can slow down the time it takes for thumbnails to michael@0: // appear, especially during startup (bug 897162). By minimizing the michael@0: // Gecko thread priority, we ensure that the UI appears quickly. The michael@0: // priority is reset to normal once thumbnails are loaded. michael@0: ThreadUtils.reduceGeckoPriority(PRIORITY_RESET_TIMEOUT); michael@0: } michael@0: michael@0: static String encodeUserEnteredUrl(String url) { michael@0: return Uri.fromParts("user-entered", url, null).toString(); michael@0: } michael@0: michael@0: /** michael@0: * Listener for editing pinned sites. michael@0: */ michael@0: private class EditPinnedSiteListener implements OnEditPinnedSiteListener, michael@0: OnSiteSelectedListener { michael@0: // Tag for the PinSiteDialog fragment. michael@0: private static final String TAG_PIN_SITE = "pin_site"; michael@0: michael@0: // Position of the pin. michael@0: private int mPosition; michael@0: michael@0: @Override michael@0: public void onEditPinnedSite(int position, String searchTerm) { michael@0: mPosition = position; michael@0: michael@0: final FragmentManager manager = getChildFragmentManager(); michael@0: PinSiteDialog dialog = (PinSiteDialog) manager.findFragmentByTag(TAG_PIN_SITE); michael@0: if (dialog == null) { michael@0: dialog = PinSiteDialog.newInstance(); michael@0: } michael@0: michael@0: dialog.setOnSiteSelectedListener(this); michael@0: dialog.setSearchTerm(searchTerm); michael@0: dialog.show(manager, TAG_PIN_SITE); michael@0: } michael@0: michael@0: @Override michael@0: public void onSiteSelected(final String url, final String title) { michael@0: final int position = mPosition; michael@0: final Context context = getActivity().getApplicationContext(); michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: BrowserDB.pinSite(context.getContentResolver(), url, title, position); michael@0: } michael@0: }); michael@0: } michael@0: } michael@0: michael@0: private void updateUiFromCursor(Cursor c) { michael@0: mList.setHeaderDividersEnabled(c != null && c.getCount() > mMaxGridEntries); michael@0: } michael@0: michael@0: private static class TopSitesLoader extends SimpleCursorLoader { michael@0: // Max number of search results michael@0: private static final int SEARCH_LIMIT = 30; michael@0: private int mMaxGridEntries; michael@0: michael@0: public TopSitesLoader(Context context) { michael@0: super(context); michael@0: mMaxGridEntries = context.getResources().getInteger(R.integer.number_of_top_sites); michael@0: } michael@0: michael@0: @Override michael@0: public Cursor loadCursor() { michael@0: trace("TopSitesLoader.loadCursor()"); michael@0: return BrowserDB.getTopSites(getContext().getContentResolver(), mMaxGridEntries, SEARCH_LIMIT); michael@0: } michael@0: } michael@0: michael@0: private class VisitedAdapter extends CursorAdapter { michael@0: public VisitedAdapter(Context context, Cursor cursor) { michael@0: super(context, cursor, 0); michael@0: } michael@0: michael@0: @Override michael@0: public int getCount() { michael@0: return Math.max(0, super.getCount() - mMaxGridEntries); michael@0: } michael@0: michael@0: @Override michael@0: public Object getItem(int position) { michael@0: return super.getItem(position + mMaxGridEntries); michael@0: } michael@0: michael@0: @Override michael@0: public void bindView(View view, Context context, Cursor cursor) { michael@0: final int position = cursor.getPosition(); michael@0: cursor.moveToPosition(position + mMaxGridEntries); michael@0: michael@0: final TwoLinePageRow row = (TwoLinePageRow) view; michael@0: row.updateFromCursor(cursor); michael@0: } michael@0: michael@0: @Override michael@0: public View newView(Context context, Cursor cursor, ViewGroup parent) { michael@0: return LayoutInflater.from(context).inflate(R.layout.bookmark_item_row, parent, false); michael@0: } michael@0: } michael@0: michael@0: public class TopSitesGridAdapter extends CursorAdapter { michael@0: // Cache to store the thumbnails. michael@0: // Ensure that this is only accessed from the UI thread. michael@0: private Map mThumbnails; michael@0: michael@0: public TopSitesGridAdapter(Context context, Cursor cursor) { michael@0: super(context, cursor, 0); michael@0: } michael@0: michael@0: @Override michael@0: public int getCount() { michael@0: return Math.min(mMaxGridEntries, super.getCount()); michael@0: } michael@0: michael@0: @Override michael@0: protected void onContentChanged() { michael@0: // Don't do anything. We don't want to regenerate every time michael@0: // our database is updated. michael@0: return; michael@0: } michael@0: michael@0: /** michael@0: * Update the thumbnails returned by the db. michael@0: * michael@0: * @param thumbnails A map of urls and their thumbnail bitmaps. michael@0: */ michael@0: public void updateThumbnails(Map thumbnails) { michael@0: mThumbnails = thumbnails; michael@0: michael@0: final int count = mGrid.getChildCount(); michael@0: for (int i = 0; i < count; i++) { michael@0: TopSitesGridItemView gridItem = (TopSitesGridItemView) mGrid.getChildAt(i); michael@0: michael@0: // All the views have already got their initial state at this point. michael@0: // This will force each view to load favicons for the missing michael@0: // thumbnails if necessary. michael@0: gridItem.markAsDirty(); michael@0: } michael@0: michael@0: notifyDataSetChanged(); michael@0: } michael@0: michael@0: @Override michael@0: public void bindView(View bindView, Context context, Cursor cursor) { michael@0: String url = ""; michael@0: String title = ""; michael@0: boolean pinned = false; michael@0: michael@0: // Cursor is already moved to required position. michael@0: if (!cursor.isAfterLast()) { michael@0: url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); michael@0: title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE)); michael@0: pinned = ((TopSitesCursorWrapper) cursor).isPinned(); michael@0: } michael@0: michael@0: final TopSitesGridItemView view = (TopSitesGridItemView) bindView; michael@0: michael@0: // If there is no url, then show "add bookmark". michael@0: if (TextUtils.isEmpty(url)) { michael@0: // Wait until thumbnails are loaded before showing anything. michael@0: if (mThumbnails != null) { michael@0: view.blankOut(); michael@0: } michael@0: michael@0: return; michael@0: } michael@0: michael@0: // Show the thumbnail, if any. michael@0: Bitmap thumbnail = (mThumbnails != null ? mThumbnails.get(url) : null); michael@0: michael@0: // Debounce bindView calls to avoid redundant redraws and favicon michael@0: // fetches. michael@0: final boolean updated = view.updateState(title, url, pinned, thumbnail); michael@0: michael@0: // If thumbnails are still being loaded, don't try to load favicons michael@0: // just yet. If we sent in a thumbnail, we're done now. michael@0: if (mThumbnails == null || thumbnail != null) { michael@0: return; michael@0: } michael@0: michael@0: // Thumbnails are delivered late, so we can't short-circuit any michael@0: // sooner than this. But we can avoid a duplicate favicon michael@0: // fetch... michael@0: if (!updated) { michael@0: debug("bindView called twice for same values; short-circuiting."); michael@0: return; michael@0: } michael@0: michael@0: // If we have no thumbnail, attempt to show a Favicon instead. michael@0: LoadIDAwareFaviconLoadedListener listener = new LoadIDAwareFaviconLoadedListener(view); michael@0: final int loadId = Favicons.getSizedFaviconForPageFromLocal(url, listener); michael@0: if (loadId == Favicons.LOADED) { michael@0: // Great! michael@0: return; michael@0: } michael@0: michael@0: // Otherwise, do this until the async lookup returns. michael@0: view.displayThumbnail(R.drawable.favicon); michael@0: michael@0: // Give each side enough information to shake hands later. michael@0: listener.setLoadId(loadId); michael@0: view.setLoadId(loadId); michael@0: } michael@0: michael@0: @Override michael@0: public View newView(Context context, Cursor cursor, ViewGroup parent) { michael@0: return new TopSitesGridItemView(context); michael@0: } michael@0: } michael@0: michael@0: private static class LoadIDAwareFaviconLoadedListener implements OnFaviconLoadedListener { michael@0: private volatile int loadId = Favicons.NOT_LOADING; michael@0: private final TopSitesGridItemView view; michael@0: public LoadIDAwareFaviconLoadedListener(TopSitesGridItemView view) { michael@0: this.view = view; michael@0: } michael@0: michael@0: public void setLoadId(int id) { michael@0: this.loadId = id; michael@0: } michael@0: michael@0: @Override michael@0: public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) { michael@0: if (TextUtils.equals(this.view.getUrl(), url)) { michael@0: this.view.displayFavicon(favicon, faviconURL, this.loadId); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private class CursorLoaderCallbacks implements LoaderCallbacks { michael@0: @Override michael@0: public Loader onCreateLoader(int id, Bundle args) { michael@0: trace("Creating TopSitesLoader: " + id); michael@0: return new TopSitesLoader(getActivity()); michael@0: } michael@0: michael@0: /** michael@0: * This method is called *twice* in some circumstances. michael@0: * michael@0: * If you try to avoid that through some kind of boolean flag, michael@0: * sometimes (e.g., returning to the activity) you'll *not* be called michael@0: * twice, and thus you'll never draw thumbnails. michael@0: * michael@0: * The root cause is TopSitesLoader.loadCursor being called twice. michael@0: * Why that is... dunno. michael@0: */ michael@0: @Override michael@0: public void onLoadFinished(Loader loader, Cursor c) { michael@0: debug("onLoadFinished: " + c.getCount() + " rows."); michael@0: michael@0: mListAdapter.swapCursor(c); michael@0: mGridAdapter.swapCursor(c); michael@0: updateUiFromCursor(c); michael@0: michael@0: final int col = c.getColumnIndexOrThrow(URLColumns.URL); michael@0: michael@0: // Load the thumbnails. michael@0: // Even though the cursor we're given is supposed to be fresh, michael@0: // we get a bad first value unless we reset its position. michael@0: // Using move(-1) and moveToNext() doesn't work correctly under michael@0: // rotation, so we use moveToFirst. michael@0: if (!c.moveToFirst()) { michael@0: return; michael@0: } michael@0: michael@0: final ArrayList urls = new ArrayList(); michael@0: int i = 1; michael@0: do { michael@0: urls.add(c.getString(col)); michael@0: } while (i++ < mMaxGridEntries && c.moveToNext()); michael@0: michael@0: if (urls.isEmpty()) { michael@0: return; michael@0: } michael@0: michael@0: Bundle bundle = new Bundle(); michael@0: bundle.putStringArrayList(THUMBNAILS_URLS_KEY, urls); michael@0: getLoaderManager().restartLoader(LOADER_ID_THUMBNAILS, bundle, mThumbnailsLoaderCallbacks); michael@0: } michael@0: michael@0: @Override michael@0: public void onLoaderReset(Loader loader) { michael@0: if (mListAdapter != null) { michael@0: mListAdapter.swapCursor(null); michael@0: } michael@0: michael@0: if (mGridAdapter != null) { michael@0: mGridAdapter.swapCursor(null); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * An AsyncTaskLoader to load the thumbnails from a cursor. michael@0: */ michael@0: private static class ThumbnailsLoader extends AsyncTaskLoader> { michael@0: private Map mThumbnails; michael@0: private ArrayList mUrls; michael@0: michael@0: public ThumbnailsLoader(Context context, ArrayList urls) { michael@0: super(context); michael@0: mUrls = urls; michael@0: } michael@0: michael@0: @Override michael@0: public Map loadInBackground() { michael@0: if (mUrls == null || mUrls.size() == 0) { michael@0: return null; michael@0: } michael@0: michael@0: // Query the DB for thumbnails. michael@0: final ContentResolver cr = getContext().getContentResolver(); michael@0: final Cursor cursor = BrowserDB.getThumbnailsForUrls(cr, mUrls); michael@0: michael@0: if (cursor == null) { michael@0: return null; michael@0: } michael@0: michael@0: final Map thumbnails = new HashMap(); michael@0: michael@0: try { michael@0: final int urlIndex = cursor.getColumnIndexOrThrow(Thumbnails.URL); michael@0: final int dataIndex = cursor.getColumnIndexOrThrow(Thumbnails.DATA); michael@0: michael@0: while (cursor.moveToNext()) { michael@0: String url = cursor.getString(urlIndex); michael@0: michael@0: // This should never be null, but if it is... michael@0: final byte[] b = cursor.getBlob(dataIndex); michael@0: if (b == null) { michael@0: continue; michael@0: } michael@0: michael@0: final Bitmap bitmap = BitmapUtils.decodeByteArray(b); michael@0: michael@0: // Our thumbnails are never null, so if we get a null decoded michael@0: // bitmap, it's because we hit an OOM or some other disaster. michael@0: // Give up immediately rather than hammering on. michael@0: if (bitmap == null) { michael@0: Log.w(LOGTAG, "Aborting thumbnail load; decode failed."); michael@0: break; michael@0: } michael@0: michael@0: thumbnails.put(url, bitmap); michael@0: } michael@0: } finally { michael@0: cursor.close(); michael@0: } michael@0: michael@0: return thumbnails; michael@0: } michael@0: michael@0: @Override michael@0: public void deliverResult(Map thumbnails) { michael@0: if (isReset()) { michael@0: mThumbnails = null; michael@0: return; michael@0: } michael@0: michael@0: mThumbnails = thumbnails; michael@0: michael@0: if (isStarted()) { michael@0: super.deliverResult(thumbnails); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: protected void onStartLoading() { michael@0: if (mThumbnails != null) { michael@0: deliverResult(mThumbnails); michael@0: } michael@0: michael@0: if (takeContentChanged() || mThumbnails == null) { michael@0: forceLoad(); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: protected void onStopLoading() { michael@0: cancelLoad(); michael@0: } michael@0: michael@0: @Override michael@0: public void onCanceled(Map thumbnails) { michael@0: mThumbnails = null; michael@0: } michael@0: michael@0: @Override michael@0: protected void onReset() { michael@0: super.onReset(); michael@0: michael@0: // Ensure the loader is stopped. michael@0: onStopLoading(); michael@0: michael@0: mThumbnails = null; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Loader callbacks for the thumbnails on TopSitesGridView. michael@0: */ michael@0: private class ThumbnailsLoaderCallbacks implements LoaderCallbacks> { michael@0: @Override michael@0: public Loader> onCreateLoader(int id, Bundle args) { michael@0: return new ThumbnailsLoader(getActivity(), args.getStringArrayList(THUMBNAILS_URLS_KEY)); michael@0: } michael@0: michael@0: @Override michael@0: public void onLoadFinished(Loader> loader, Map thumbnails) { michael@0: if (mGridAdapter != null) { michael@0: mGridAdapter.updateThumbnails(thumbnails); michael@0: } michael@0: michael@0: // Once thumbnails have finished loading, the UI is ready. Reset michael@0: // Gecko to normal priority. michael@0: ThreadUtils.resetGeckoPriority(); michael@0: } michael@0: michael@0: @Override michael@0: public void onLoaderReset(Loader> loader) { michael@0: if (mGridAdapter != null) { michael@0: mGridAdapter.updateThumbnails(null); michael@0: } michael@0: } michael@0: } michael@0: }