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