1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/home/TopSitesPanel.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,796 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.home; 1.10 + 1.11 +import java.util.ArrayList; 1.12 +import java.util.EnumSet; 1.13 +import java.util.HashMap; 1.14 +import java.util.Map; 1.15 + 1.16 +import org.mozilla.gecko.R; 1.17 +import org.mozilla.gecko.Telemetry; 1.18 +import org.mozilla.gecko.TelemetryContract; 1.19 +import org.mozilla.gecko.db.BrowserContract.Combined; 1.20 +import org.mozilla.gecko.db.BrowserContract.Thumbnails; 1.21 +import org.mozilla.gecko.db.BrowserDB; 1.22 +import org.mozilla.gecko.db.BrowserDB.URLColumns; 1.23 +import org.mozilla.gecko.db.TopSitesCursorWrapper; 1.24 +import org.mozilla.gecko.favicons.Favicons; 1.25 +import org.mozilla.gecko.favicons.OnFaviconLoadedListener; 1.26 +import org.mozilla.gecko.gfx.BitmapUtils; 1.27 +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; 1.28 +import org.mozilla.gecko.home.PinSiteDialog.OnSiteSelectedListener; 1.29 +import org.mozilla.gecko.home.TopSitesGridView.OnEditPinnedSiteListener; 1.30 +import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo; 1.31 +import org.mozilla.gecko.util.ThreadUtils; 1.32 + 1.33 +import android.app.Activity; 1.34 +import android.content.ContentResolver; 1.35 +import android.content.Context; 1.36 +import android.content.res.Configuration; 1.37 +import android.database.Cursor; 1.38 +import android.graphics.Bitmap; 1.39 +import android.net.Uri; 1.40 +import android.os.Bundle; 1.41 +import android.support.v4.app.FragmentManager; 1.42 +import android.support.v4.app.LoaderManager.LoaderCallbacks; 1.43 +import android.support.v4.content.AsyncTaskLoader; 1.44 +import android.support.v4.content.Loader; 1.45 +import android.support.v4.widget.CursorAdapter; 1.46 +import android.text.TextUtils; 1.47 +import android.util.Log; 1.48 +import android.view.ContextMenu; 1.49 +import android.view.ContextMenu.ContextMenuInfo; 1.50 +import android.view.LayoutInflater; 1.51 +import android.view.MenuInflater; 1.52 +import android.view.MenuItem; 1.53 +import android.view.View; 1.54 +import android.view.ViewGroup; 1.55 +import android.widget.AdapterView; 1.56 +import android.widget.ListView; 1.57 + 1.58 +/** 1.59 + * Fragment that displays frecency search results in a ListView. 1.60 + */ 1.61 +public class TopSitesPanel extends HomeFragment { 1.62 + // Logging tag name 1.63 + private static final String LOGTAG = "GeckoTopSitesPanel"; 1.64 + 1.65 + // Cursor loader ID for the top sites 1.66 + private static final int LOADER_ID_TOP_SITES = 0; 1.67 + 1.68 + // Loader ID for thumbnails 1.69 + private static final int LOADER_ID_THUMBNAILS = 1; 1.70 + 1.71 + // Key for thumbnail urls 1.72 + private static final String THUMBNAILS_URLS_KEY = "urls"; 1.73 + 1.74 + // Adapter for the list of top sites 1.75 + private VisitedAdapter mListAdapter; 1.76 + 1.77 + // Adapter for the grid of top sites 1.78 + private TopSitesGridAdapter mGridAdapter; 1.79 + 1.80 + // List of top sites 1.81 + private HomeListView mList; 1.82 + 1.83 + // Grid of top sites 1.84 + private TopSitesGridView mGrid; 1.85 + 1.86 + // Callbacks used for the search and favicon cursor loaders 1.87 + private CursorLoaderCallbacks mCursorLoaderCallbacks; 1.88 + 1.89 + // Callback for thumbnail loader 1.90 + private ThumbnailsLoaderCallbacks mThumbnailsLoaderCallbacks; 1.91 + 1.92 + // Listener for editing pinned sites. 1.93 + private EditPinnedSiteListener mEditPinnedSiteListener; 1.94 + 1.95 + // On URL open listener 1.96 + private OnUrlOpenListener mUrlOpenListener; 1.97 + 1.98 + // Max number of entries shown in the grid from the cursor. 1.99 + private int mMaxGridEntries; 1.100 + 1.101 + // Time in ms until the Gecko thread is reset to normal priority. 1.102 + private static final long PRIORITY_RESET_TIMEOUT = 10000; 1.103 + 1.104 + public static TopSitesPanel newInstance() { 1.105 + return new TopSitesPanel(); 1.106 + } 1.107 + 1.108 + public TopSitesPanel() { 1.109 + mUrlOpenListener = null; 1.110 + } 1.111 + 1.112 + private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); 1.113 + private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); 1.114 + 1.115 + private static void debug(final String message) { 1.116 + if (logDebug) { 1.117 + Log.d(LOGTAG, message); 1.118 + } 1.119 + } 1.120 + 1.121 + private static void trace(final String message) { 1.122 + if (logVerbose) { 1.123 + Log.v(LOGTAG, message); 1.124 + } 1.125 + } 1.126 + 1.127 + @Override 1.128 + public void onAttach(Activity activity) { 1.129 + super.onAttach(activity); 1.130 + 1.131 + mMaxGridEntries = activity.getResources().getInteger(R.integer.number_of_top_sites); 1.132 + 1.133 + try { 1.134 + mUrlOpenListener = (OnUrlOpenListener) activity; 1.135 + } catch (ClassCastException e) { 1.136 + throw new ClassCastException(activity.toString() 1.137 + + " must implement HomePager.OnUrlOpenListener"); 1.138 + } 1.139 + } 1.140 + 1.141 + @Override 1.142 + public void onDetach() { 1.143 + super.onDetach(); 1.144 + 1.145 + mUrlOpenListener = null; 1.146 + } 1.147 + 1.148 + @Override 1.149 + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 1.150 + final View view = inflater.inflate(R.layout.home_top_sites_panel, container, false); 1.151 + 1.152 + mList = (HomeListView) view.findViewById(R.id.list); 1.153 + 1.154 + mGrid = new TopSitesGridView(getActivity()); 1.155 + mList.addHeaderView(mGrid); 1.156 + 1.157 + return view; 1.158 + } 1.159 + 1.160 + @Override 1.161 + public void onViewCreated(View view, Bundle savedInstanceState) { 1.162 + mEditPinnedSiteListener = new EditPinnedSiteListener(); 1.163 + 1.164 + mList.setTag(HomePager.LIST_TAG_TOP_SITES); 1.165 + mList.setHeaderDividersEnabled(false); 1.166 + 1.167 + mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1.168 + @Override 1.169 + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1.170 + final ListView list = (ListView) parent; 1.171 + final int headerCount = list.getHeaderViewsCount(); 1.172 + if (position < headerCount) { 1.173 + // The click is on a header, don't do anything. 1.174 + return; 1.175 + } 1.176 + 1.177 + // Absolute position for the adapter. 1.178 + position += (mGridAdapter.getCount() - headerCount); 1.179 + 1.180 + final Cursor c = mListAdapter.getCursor(); 1.181 + if (c == null || !c.moveToPosition(position)) { 1.182 + return; 1.183 + } 1.184 + 1.185 + final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); 1.186 + 1.187 + Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM); 1.188 + 1.189 + // This item is a TwoLinePageRow, so we allow switch-to-tab. 1.190 + mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); 1.191 + } 1.192 + }); 1.193 + 1.194 + mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() { 1.195 + @Override 1.196 + public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) { 1.197 + final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id); 1.198 + info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL)); 1.199 + info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE)); 1.200 + info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.HISTORY_ID)); 1.201 + final int bookmarkIdCol = cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID); 1.202 + if (cursor.isNull(bookmarkIdCol)) { 1.203 + // If this is a combined cursor, we may get a history item without a 1.204 + // bookmark, in which case the bookmarks ID column value will be null. 1.205 + info.bookmarkId = -1; 1.206 + } else { 1.207 + info.bookmarkId = cursor.getInt(bookmarkIdCol); 1.208 + } 1.209 + return info; 1.210 + } 1.211 + }); 1.212 + 1.213 + mGrid.setOnUrlOpenListener(mUrlOpenListener); 1.214 + mGrid.setOnEditPinnedSiteListener(mEditPinnedSiteListener); 1.215 + 1.216 + registerForContextMenu(mList); 1.217 + registerForContextMenu(mGrid); 1.218 + } 1.219 + 1.220 + @Override 1.221 + public void onDestroyView() { 1.222 + super.onDestroyView(); 1.223 + 1.224 + // Discard any additional item clicks on the list 1.225 + // as the panel is getting destroyed (see bug 930160). 1.226 + mList.setOnItemClickListener(null); 1.227 + mList = null; 1.228 + 1.229 + mGrid = null; 1.230 + mListAdapter = null; 1.231 + mGridAdapter = null; 1.232 + } 1.233 + 1.234 + @Override 1.235 + public void onConfigurationChanged(Configuration newConfig) { 1.236 + super.onConfigurationChanged(newConfig); 1.237 + 1.238 + // Detach and reattach the fragment as the layout changes. 1.239 + if (isVisible()) { 1.240 + getFragmentManager().beginTransaction() 1.241 + .detach(this) 1.242 + .attach(this) 1.243 + .commitAllowingStateLoss(); 1.244 + } 1.245 + } 1.246 + 1.247 + @Override 1.248 + public void onActivityCreated(Bundle savedInstanceState) { 1.249 + super.onActivityCreated(savedInstanceState); 1.250 + 1.251 + final Activity activity = getActivity(); 1.252 + 1.253 + // Setup the top sites grid adapter. 1.254 + mGridAdapter = new TopSitesGridAdapter(activity, null); 1.255 + mGrid.setAdapter(mGridAdapter); 1.256 + 1.257 + // Setup the top sites list adapter. 1.258 + mListAdapter = new VisitedAdapter(activity, null); 1.259 + mList.setAdapter(mListAdapter); 1.260 + 1.261 + // Create callbacks before the initial loader is started 1.262 + mCursorLoaderCallbacks = new CursorLoaderCallbacks(); 1.263 + mThumbnailsLoaderCallbacks = new ThumbnailsLoaderCallbacks(); 1.264 + loadIfVisible(); 1.265 + } 1.266 + 1.267 + @Override 1.268 + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 1.269 + if (menuInfo == null) { 1.270 + return; 1.271 + } 1.272 + 1.273 + if (!(menuInfo instanceof TopSitesGridContextMenuInfo)) { 1.274 + // Long pressed item was not a Top Sites GridView item. Superclass 1.275 + // can handle this. 1.276 + super.onCreateContextMenu(menu, view, menuInfo); 1.277 + return; 1.278 + } 1.279 + 1.280 + // Long pressed item was a Top Sites GridView item, handle it. 1.281 + MenuInflater inflater = new MenuInflater(view.getContext()); 1.282 + inflater.inflate(R.menu.home_contextmenu, menu); 1.283 + 1.284 + // Hide ununsed menu items. 1.285 + menu.findItem(R.id.home_open_in_reader).setVisible(false); 1.286 + menu.findItem(R.id.home_edit_bookmark).setVisible(false); 1.287 + menu.findItem(R.id.home_remove).setVisible(false); 1.288 + 1.289 + TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo; 1.290 + menu.setHeaderTitle(info.getDisplayTitle()); 1.291 + 1.292 + if (!TextUtils.isEmpty(info.url)) { 1.293 + if (info.isPinned) { 1.294 + menu.findItem(R.id.top_sites_pin).setVisible(false); 1.295 + } else { 1.296 + menu.findItem(R.id.top_sites_unpin).setVisible(false); 1.297 + } 1.298 + } else { 1.299 + menu.findItem(R.id.home_open_new_tab).setVisible(false); 1.300 + menu.findItem(R.id.home_open_private_tab).setVisible(false); 1.301 + menu.findItem(R.id.top_sites_pin).setVisible(false); 1.302 + menu.findItem(R.id.top_sites_unpin).setVisible(false); 1.303 + } 1.304 + } 1.305 + 1.306 + @Override 1.307 + public boolean onContextItemSelected(MenuItem item) { 1.308 + if (super.onContextItemSelected(item)) { 1.309 + // HomeFragment was able to handle to selected item. 1.310 + return true; 1.311 + } 1.312 + 1.313 + ContextMenuInfo menuInfo = item.getMenuInfo(); 1.314 + 1.315 + if (menuInfo == null || !(menuInfo instanceof TopSitesGridContextMenuInfo)) { 1.316 + return false; 1.317 + } 1.318 + 1.319 + TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo; 1.320 + final Activity activity = getActivity(); 1.321 + 1.322 + final int itemId = item.getItemId(); 1.323 + 1.324 + if (itemId == R.id.top_sites_pin) { 1.325 + final String url = info.url; 1.326 + final String title = info.title; 1.327 + final int position = info.position; 1.328 + final Context context = getActivity().getApplicationContext(); 1.329 + 1.330 + ThreadUtils.postToBackgroundThread(new Runnable() { 1.331 + @Override 1.332 + public void run() { 1.333 + BrowserDB.pinSite(context.getContentResolver(), url, title, position); 1.334 + } 1.335 + }); 1.336 + 1.337 + Telemetry.sendUIEvent(TelemetryContract.Event.TOP_SITES_PIN); 1.338 + return true; 1.339 + } 1.340 + 1.341 + if (itemId == R.id.top_sites_unpin) { 1.342 + final int position = info.position; 1.343 + final Context context = getActivity().getApplicationContext(); 1.344 + 1.345 + ThreadUtils.postToBackgroundThread(new Runnable() { 1.346 + @Override 1.347 + public void run() { 1.348 + BrowserDB.unpinSite(context.getContentResolver(), position); 1.349 + } 1.350 + }); 1.351 + 1.352 + Telemetry.sendUIEvent(TelemetryContract.Event.TOP_SITES_UNPIN); 1.353 + return true; 1.354 + } 1.355 + 1.356 + if (itemId == R.id.top_sites_edit) { 1.357 + // Decode "user-entered" URLs before showing them. 1.358 + mEditPinnedSiteListener.onEditPinnedSite(info.position, decodeUserEnteredUrl(info.url)); 1.359 + 1.360 + Telemetry.sendUIEvent(TelemetryContract.Event.TOP_SITES_EDIT); 1.361 + return true; 1.362 + } 1.363 + 1.364 + return false; 1.365 + } 1.366 + 1.367 + @Override 1.368 + protected void load() { 1.369 + getLoaderManager().initLoader(LOADER_ID_TOP_SITES, null, mCursorLoaderCallbacks); 1.370 + 1.371 + // Since this is the primary fragment that loads whenever about:home is 1.372 + // visited, we want to load it as quickly as possible. Heavy load on 1.373 + // the Gecko thread can slow down the time it takes for thumbnails to 1.374 + // appear, especially during startup (bug 897162). By minimizing the 1.375 + // Gecko thread priority, we ensure that the UI appears quickly. The 1.376 + // priority is reset to normal once thumbnails are loaded. 1.377 + ThreadUtils.reduceGeckoPriority(PRIORITY_RESET_TIMEOUT); 1.378 + } 1.379 + 1.380 + static String encodeUserEnteredUrl(String url) { 1.381 + return Uri.fromParts("user-entered", url, null).toString(); 1.382 + } 1.383 + 1.384 + /** 1.385 + * Listener for editing pinned sites. 1.386 + */ 1.387 + private class EditPinnedSiteListener implements OnEditPinnedSiteListener, 1.388 + OnSiteSelectedListener { 1.389 + // Tag for the PinSiteDialog fragment. 1.390 + private static final String TAG_PIN_SITE = "pin_site"; 1.391 + 1.392 + // Position of the pin. 1.393 + private int mPosition; 1.394 + 1.395 + @Override 1.396 + public void onEditPinnedSite(int position, String searchTerm) { 1.397 + mPosition = position; 1.398 + 1.399 + final FragmentManager manager = getChildFragmentManager(); 1.400 + PinSiteDialog dialog = (PinSiteDialog) manager.findFragmentByTag(TAG_PIN_SITE); 1.401 + if (dialog == null) { 1.402 + dialog = PinSiteDialog.newInstance(); 1.403 + } 1.404 + 1.405 + dialog.setOnSiteSelectedListener(this); 1.406 + dialog.setSearchTerm(searchTerm); 1.407 + dialog.show(manager, TAG_PIN_SITE); 1.408 + } 1.409 + 1.410 + @Override 1.411 + public void onSiteSelected(final String url, final String title) { 1.412 + final int position = mPosition; 1.413 + final Context context = getActivity().getApplicationContext(); 1.414 + ThreadUtils.postToBackgroundThread(new Runnable() { 1.415 + @Override 1.416 + public void run() { 1.417 + BrowserDB.pinSite(context.getContentResolver(), url, title, position); 1.418 + } 1.419 + }); 1.420 + } 1.421 + } 1.422 + 1.423 + private void updateUiFromCursor(Cursor c) { 1.424 + mList.setHeaderDividersEnabled(c != null && c.getCount() > mMaxGridEntries); 1.425 + } 1.426 + 1.427 + private static class TopSitesLoader extends SimpleCursorLoader { 1.428 + // Max number of search results 1.429 + private static final int SEARCH_LIMIT = 30; 1.430 + private int mMaxGridEntries; 1.431 + 1.432 + public TopSitesLoader(Context context) { 1.433 + super(context); 1.434 + mMaxGridEntries = context.getResources().getInteger(R.integer.number_of_top_sites); 1.435 + } 1.436 + 1.437 + @Override 1.438 + public Cursor loadCursor() { 1.439 + trace("TopSitesLoader.loadCursor()"); 1.440 + return BrowserDB.getTopSites(getContext().getContentResolver(), mMaxGridEntries, SEARCH_LIMIT); 1.441 + } 1.442 + } 1.443 + 1.444 + private class VisitedAdapter extends CursorAdapter { 1.445 + public VisitedAdapter(Context context, Cursor cursor) { 1.446 + super(context, cursor, 0); 1.447 + } 1.448 + 1.449 + @Override 1.450 + public int getCount() { 1.451 + return Math.max(0, super.getCount() - mMaxGridEntries); 1.452 + } 1.453 + 1.454 + @Override 1.455 + public Object getItem(int position) { 1.456 + return super.getItem(position + mMaxGridEntries); 1.457 + } 1.458 + 1.459 + @Override 1.460 + public void bindView(View view, Context context, Cursor cursor) { 1.461 + final int position = cursor.getPosition(); 1.462 + cursor.moveToPosition(position + mMaxGridEntries); 1.463 + 1.464 + final TwoLinePageRow row = (TwoLinePageRow) view; 1.465 + row.updateFromCursor(cursor); 1.466 + } 1.467 + 1.468 + @Override 1.469 + public View newView(Context context, Cursor cursor, ViewGroup parent) { 1.470 + return LayoutInflater.from(context).inflate(R.layout.bookmark_item_row, parent, false); 1.471 + } 1.472 + } 1.473 + 1.474 + public class TopSitesGridAdapter extends CursorAdapter { 1.475 + // Cache to store the thumbnails. 1.476 + // Ensure that this is only accessed from the UI thread. 1.477 + private Map<String, Bitmap> mThumbnails; 1.478 + 1.479 + public TopSitesGridAdapter(Context context, Cursor cursor) { 1.480 + super(context, cursor, 0); 1.481 + } 1.482 + 1.483 + @Override 1.484 + public int getCount() { 1.485 + return Math.min(mMaxGridEntries, super.getCount()); 1.486 + } 1.487 + 1.488 + @Override 1.489 + protected void onContentChanged() { 1.490 + // Don't do anything. We don't want to regenerate every time 1.491 + // our database is updated. 1.492 + return; 1.493 + } 1.494 + 1.495 + /** 1.496 + * Update the thumbnails returned by the db. 1.497 + * 1.498 + * @param thumbnails A map of urls and their thumbnail bitmaps. 1.499 + */ 1.500 + public void updateThumbnails(Map<String, Bitmap> thumbnails) { 1.501 + mThumbnails = thumbnails; 1.502 + 1.503 + final int count = mGrid.getChildCount(); 1.504 + for (int i = 0; i < count; i++) { 1.505 + TopSitesGridItemView gridItem = (TopSitesGridItemView) mGrid.getChildAt(i); 1.506 + 1.507 + // All the views have already got their initial state at this point. 1.508 + // This will force each view to load favicons for the missing 1.509 + // thumbnails if necessary. 1.510 + gridItem.markAsDirty(); 1.511 + } 1.512 + 1.513 + notifyDataSetChanged(); 1.514 + } 1.515 + 1.516 + @Override 1.517 + public void bindView(View bindView, Context context, Cursor cursor) { 1.518 + String url = ""; 1.519 + String title = ""; 1.520 + boolean pinned = false; 1.521 + 1.522 + // Cursor is already moved to required position. 1.523 + if (!cursor.isAfterLast()) { 1.524 + url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); 1.525 + title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE)); 1.526 + pinned = ((TopSitesCursorWrapper) cursor).isPinned(); 1.527 + } 1.528 + 1.529 + final TopSitesGridItemView view = (TopSitesGridItemView) bindView; 1.530 + 1.531 + // If there is no url, then show "add bookmark". 1.532 + if (TextUtils.isEmpty(url)) { 1.533 + // Wait until thumbnails are loaded before showing anything. 1.534 + if (mThumbnails != null) { 1.535 + view.blankOut(); 1.536 + } 1.537 + 1.538 + return; 1.539 + } 1.540 + 1.541 + // Show the thumbnail, if any. 1.542 + Bitmap thumbnail = (mThumbnails != null ? mThumbnails.get(url) : null); 1.543 + 1.544 + // Debounce bindView calls to avoid redundant redraws and favicon 1.545 + // fetches. 1.546 + final boolean updated = view.updateState(title, url, pinned, thumbnail); 1.547 + 1.548 + // If thumbnails are still being loaded, don't try to load favicons 1.549 + // just yet. If we sent in a thumbnail, we're done now. 1.550 + if (mThumbnails == null || thumbnail != null) { 1.551 + return; 1.552 + } 1.553 + 1.554 + // Thumbnails are delivered late, so we can't short-circuit any 1.555 + // sooner than this. But we can avoid a duplicate favicon 1.556 + // fetch... 1.557 + if (!updated) { 1.558 + debug("bindView called twice for same values; short-circuiting."); 1.559 + return; 1.560 + } 1.561 + 1.562 + // If we have no thumbnail, attempt to show a Favicon instead. 1.563 + LoadIDAwareFaviconLoadedListener listener = new LoadIDAwareFaviconLoadedListener(view); 1.564 + final int loadId = Favicons.getSizedFaviconForPageFromLocal(url, listener); 1.565 + if (loadId == Favicons.LOADED) { 1.566 + // Great! 1.567 + return; 1.568 + } 1.569 + 1.570 + // Otherwise, do this until the async lookup returns. 1.571 + view.displayThumbnail(R.drawable.favicon); 1.572 + 1.573 + // Give each side enough information to shake hands later. 1.574 + listener.setLoadId(loadId); 1.575 + view.setLoadId(loadId); 1.576 + } 1.577 + 1.578 + @Override 1.579 + public View newView(Context context, Cursor cursor, ViewGroup parent) { 1.580 + return new TopSitesGridItemView(context); 1.581 + } 1.582 + } 1.583 + 1.584 + private static class LoadIDAwareFaviconLoadedListener implements OnFaviconLoadedListener { 1.585 + private volatile int loadId = Favicons.NOT_LOADING; 1.586 + private final TopSitesGridItemView view; 1.587 + public LoadIDAwareFaviconLoadedListener(TopSitesGridItemView view) { 1.588 + this.view = view; 1.589 + } 1.590 + 1.591 + public void setLoadId(int id) { 1.592 + this.loadId = id; 1.593 + } 1.594 + 1.595 + @Override 1.596 + public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) { 1.597 + if (TextUtils.equals(this.view.getUrl(), url)) { 1.598 + this.view.displayFavicon(favicon, faviconURL, this.loadId); 1.599 + } 1.600 + } 1.601 + } 1.602 + 1.603 + private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> { 1.604 + @Override 1.605 + public Loader<Cursor> onCreateLoader(int id, Bundle args) { 1.606 + trace("Creating TopSitesLoader: " + id); 1.607 + return new TopSitesLoader(getActivity()); 1.608 + } 1.609 + 1.610 + /** 1.611 + * This method is called *twice* in some circumstances. 1.612 + * 1.613 + * If you try to avoid that through some kind of boolean flag, 1.614 + * sometimes (e.g., returning to the activity) you'll *not* be called 1.615 + * twice, and thus you'll never draw thumbnails. 1.616 + * 1.617 + * The root cause is TopSitesLoader.loadCursor being called twice. 1.618 + * Why that is... dunno. 1.619 + */ 1.620 + @Override 1.621 + public void onLoadFinished(Loader<Cursor> loader, Cursor c) { 1.622 + debug("onLoadFinished: " + c.getCount() + " rows."); 1.623 + 1.624 + mListAdapter.swapCursor(c); 1.625 + mGridAdapter.swapCursor(c); 1.626 + updateUiFromCursor(c); 1.627 + 1.628 + final int col = c.getColumnIndexOrThrow(URLColumns.URL); 1.629 + 1.630 + // Load the thumbnails. 1.631 + // Even though the cursor we're given is supposed to be fresh, 1.632 + // we get a bad first value unless we reset its position. 1.633 + // Using move(-1) and moveToNext() doesn't work correctly under 1.634 + // rotation, so we use moveToFirst. 1.635 + if (!c.moveToFirst()) { 1.636 + return; 1.637 + } 1.638 + 1.639 + final ArrayList<String> urls = new ArrayList<String>(); 1.640 + int i = 1; 1.641 + do { 1.642 + urls.add(c.getString(col)); 1.643 + } while (i++ < mMaxGridEntries && c.moveToNext()); 1.644 + 1.645 + if (urls.isEmpty()) { 1.646 + return; 1.647 + } 1.648 + 1.649 + Bundle bundle = new Bundle(); 1.650 + bundle.putStringArrayList(THUMBNAILS_URLS_KEY, urls); 1.651 + getLoaderManager().restartLoader(LOADER_ID_THUMBNAILS, bundle, mThumbnailsLoaderCallbacks); 1.652 + } 1.653 + 1.654 + @Override 1.655 + public void onLoaderReset(Loader<Cursor> loader) { 1.656 + if (mListAdapter != null) { 1.657 + mListAdapter.swapCursor(null); 1.658 + } 1.659 + 1.660 + if (mGridAdapter != null) { 1.661 + mGridAdapter.swapCursor(null); 1.662 + } 1.663 + } 1.664 + } 1.665 + 1.666 + /** 1.667 + * An AsyncTaskLoader to load the thumbnails from a cursor. 1.668 + */ 1.669 + private static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, Bitmap>> { 1.670 + private Map<String, Bitmap> mThumbnails; 1.671 + private ArrayList<String> mUrls; 1.672 + 1.673 + public ThumbnailsLoader(Context context, ArrayList<String> urls) { 1.674 + super(context); 1.675 + mUrls = urls; 1.676 + } 1.677 + 1.678 + @Override 1.679 + public Map<String, Bitmap> loadInBackground() { 1.680 + if (mUrls == null || mUrls.size() == 0) { 1.681 + return null; 1.682 + } 1.683 + 1.684 + // Query the DB for thumbnails. 1.685 + final ContentResolver cr = getContext().getContentResolver(); 1.686 + final Cursor cursor = BrowserDB.getThumbnailsForUrls(cr, mUrls); 1.687 + 1.688 + if (cursor == null) { 1.689 + return null; 1.690 + } 1.691 + 1.692 + final Map<String, Bitmap> thumbnails = new HashMap<String, Bitmap>(); 1.693 + 1.694 + try { 1.695 + final int urlIndex = cursor.getColumnIndexOrThrow(Thumbnails.URL); 1.696 + final int dataIndex = cursor.getColumnIndexOrThrow(Thumbnails.DATA); 1.697 + 1.698 + while (cursor.moveToNext()) { 1.699 + String url = cursor.getString(urlIndex); 1.700 + 1.701 + // This should never be null, but if it is... 1.702 + final byte[] b = cursor.getBlob(dataIndex); 1.703 + if (b == null) { 1.704 + continue; 1.705 + } 1.706 + 1.707 + final Bitmap bitmap = BitmapUtils.decodeByteArray(b); 1.708 + 1.709 + // Our thumbnails are never null, so if we get a null decoded 1.710 + // bitmap, it's because we hit an OOM or some other disaster. 1.711 + // Give up immediately rather than hammering on. 1.712 + if (bitmap == null) { 1.713 + Log.w(LOGTAG, "Aborting thumbnail load; decode failed."); 1.714 + break; 1.715 + } 1.716 + 1.717 + thumbnails.put(url, bitmap); 1.718 + } 1.719 + } finally { 1.720 + cursor.close(); 1.721 + } 1.722 + 1.723 + return thumbnails; 1.724 + } 1.725 + 1.726 + @Override 1.727 + public void deliverResult(Map<String, Bitmap> thumbnails) { 1.728 + if (isReset()) { 1.729 + mThumbnails = null; 1.730 + return; 1.731 + } 1.732 + 1.733 + mThumbnails = thumbnails; 1.734 + 1.735 + if (isStarted()) { 1.736 + super.deliverResult(thumbnails); 1.737 + } 1.738 + } 1.739 + 1.740 + @Override 1.741 + protected void onStartLoading() { 1.742 + if (mThumbnails != null) { 1.743 + deliverResult(mThumbnails); 1.744 + } 1.745 + 1.746 + if (takeContentChanged() || mThumbnails == null) { 1.747 + forceLoad(); 1.748 + } 1.749 + } 1.750 + 1.751 + @Override 1.752 + protected void onStopLoading() { 1.753 + cancelLoad(); 1.754 + } 1.755 + 1.756 + @Override 1.757 + public void onCanceled(Map<String, Bitmap> thumbnails) { 1.758 + mThumbnails = null; 1.759 + } 1.760 + 1.761 + @Override 1.762 + protected void onReset() { 1.763 + super.onReset(); 1.764 + 1.765 + // Ensure the loader is stopped. 1.766 + onStopLoading(); 1.767 + 1.768 + mThumbnails = null; 1.769 + } 1.770 + } 1.771 + 1.772 + /** 1.773 + * Loader callbacks for the thumbnails on TopSitesGridView. 1.774 + */ 1.775 + private class ThumbnailsLoaderCallbacks implements LoaderCallbacks<Map<String, Bitmap>> { 1.776 + @Override 1.777 + public Loader<Map<String, Bitmap>> onCreateLoader(int id, Bundle args) { 1.778 + return new ThumbnailsLoader(getActivity(), args.getStringArrayList(THUMBNAILS_URLS_KEY)); 1.779 + } 1.780 + 1.781 + @Override 1.782 + public void onLoadFinished(Loader<Map<String, Bitmap>> loader, Map<String, Bitmap> thumbnails) { 1.783 + if (mGridAdapter != null) { 1.784 + mGridAdapter.updateThumbnails(thumbnails); 1.785 + } 1.786 + 1.787 + // Once thumbnails have finished loading, the UI is ready. Reset 1.788 + // Gecko to normal priority. 1.789 + ThreadUtils.resetGeckoPriority(); 1.790 + } 1.791 + 1.792 + @Override 1.793 + public void onLoaderReset(Loader<Map<String, Bitmap>> loader) { 1.794 + if (mGridAdapter != null) { 1.795 + mGridAdapter.updateThumbnails(null); 1.796 + } 1.797 + } 1.798 + } 1.799 +}