mobile/android/base/home/TopSitesPanel.java

changeset 0
6474c204b198
     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 +}

mercurial