mobile/android/base/home/TopSitesPanel.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial