Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
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/. */
6 package org.mozilla.gecko.home;
8 import java.util.Date;
9 import java.util.EnumSet;
11 import org.mozilla.gecko.R;
12 import org.mozilla.gecko.Telemetry;
13 import org.mozilla.gecko.TelemetryContract;
14 import org.mozilla.gecko.db.BrowserContract.Combined;
15 import org.mozilla.gecko.db.BrowserDB;
16 import org.mozilla.gecko.db.BrowserDB.URLColumns;
17 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
19 import android.app.Activity;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.database.Cursor;
23 import android.os.Bundle;
24 import android.support.v4.app.LoaderManager.LoaderCallbacks;
25 import android.support.v4.content.Loader;
26 import android.util.SparseArray;
27 import android.view.LayoutInflater;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.view.ViewStub;
31 import android.widget.AdapterView;
32 import android.widget.ImageView;
33 import android.widget.TextView;
35 /**
36 * Fragment that displays recent history in a ListView.
37 */
38 public class MostRecentPanel extends HomeFragment {
39 // Logging tag name
40 private static final String LOGTAG = "GeckoMostRecentPanel";
42 // Cursor loader ID for history query
43 private static final int LOADER_ID_HISTORY = 0;
45 // Adapter for the list of search results
46 private MostRecentAdapter mAdapter;
48 // The view shown by the fragment.
49 private HomeListView mList;
51 // Reference to the View to display when there are no results.
52 private View mEmptyView;
54 // Callbacks used for the search and favicon cursor loaders
55 private CursorLoaderCallbacks mCursorLoaderCallbacks;
57 // On URL open listener
58 private OnUrlOpenListener mUrlOpenListener;
60 public static MostRecentPanel newInstance() {
61 return new MostRecentPanel();
62 }
64 public MostRecentPanel() {
65 mUrlOpenListener = null;
66 }
68 @Override
69 public void onAttach(Activity activity) {
70 super.onAttach(activity);
72 try {
73 mUrlOpenListener = (OnUrlOpenListener) activity;
74 } catch (ClassCastException e) {
75 throw new ClassCastException(activity.toString()
76 + " must implement HomePager.OnUrlOpenListener");
77 }
78 }
80 @Override
81 public void onDetach() {
82 super.onDetach();
83 mUrlOpenListener = null;
84 }
86 @Override
87 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
88 return inflater.inflate(R.layout.home_most_recent_panel, container, false);
89 }
91 @Override
92 public void onViewCreated(View view, Bundle savedInstanceState) {
93 mList = (HomeListView) view.findViewById(R.id.list);
94 mList.setTag(HomePager.LIST_TAG_MOST_RECENT);
96 mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
97 @Override
98 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
99 position -= mAdapter.getMostRecentSectionsCountBefore(position);
100 final Cursor c = mAdapter.getCursor(position);
101 final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
103 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL);
105 // This item is a TwoLinePageRow, so we allow switch-to-tab.
106 mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
107 }
108 });
110 mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
111 @Override
112 public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
113 final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
114 info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL));
115 info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE));
116 info.display = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.DISPLAY));
117 info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.HISTORY_ID));
118 final int bookmarkIdCol = cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID);
119 if (cursor.isNull(bookmarkIdCol)) {
120 // If this is a combined cursor, we may get a history item without a
121 // bookmark, in which case the bookmarks ID column value will be null.
122 info.bookmarkId = -1;
123 } else {
124 info.bookmarkId = cursor.getInt(bookmarkIdCol);
125 }
126 return info;
127 }
128 });
129 registerForContextMenu(mList);
130 }
132 @Override
133 public void onDestroyView() {
134 super.onDestroyView();
135 mList = null;
136 mEmptyView = null;
137 }
139 @Override
140 public void onActivityCreated(Bundle savedInstanceState) {
141 super.onActivityCreated(savedInstanceState);
143 // Intialize adapter
144 mAdapter = new MostRecentAdapter(getActivity());
145 mList.setAdapter(mAdapter);
147 // Create callbacks before the initial loader is started
148 mCursorLoaderCallbacks = new CursorLoaderCallbacks();
149 loadIfVisible();
150 }
152 @Override
153 protected void load() {
154 getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks);
155 }
157 private static class MostRecentCursorLoader extends SimpleCursorLoader {
158 // Max number of history results
159 private static final int HISTORY_LIMIT = 100;
161 public MostRecentCursorLoader(Context context) {
162 super(context);
163 }
165 @Override
166 public Cursor loadCursor() {
167 final ContentResolver cr = getContext().getContentResolver();
168 return BrowserDB.getRecentHistory(cr, HISTORY_LIMIT);
169 }
170 }
172 private void updateUiFromCursor(Cursor c) {
173 if (c != null && c.getCount() > 0) {
174 return;
175 }
177 // Cursor is empty, so set the empty view if it hasn't been set already.
178 if (mEmptyView == null) {
179 // Set empty panel view. We delay this so that the empty view won't flash.
180 final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub);
181 mEmptyView = emptyViewStub.inflate();
183 final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image);
184 emptyIcon.setImageResource(R.drawable.icon_most_recent_empty);
186 final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text);
187 emptyText.setText(R.string.home_most_recent_empty);
189 mList.setEmptyView(mEmptyView);
190 }
191 }
193 private static class MostRecentAdapter extends MultiTypeCursorAdapter {
194 private static final int ROW_HEADER = 0;
195 private static final int ROW_STANDARD = 1;
197 private static final int[] VIEW_TYPES = new int[] { ROW_STANDARD, ROW_HEADER };
198 private static final int[] LAYOUT_TYPES = new int[] { R.layout.home_item_row, R.layout.home_header_row };
200 // For the time sections in history
201 private static final long MS_PER_DAY = 86400000;
202 private static final long MS_PER_WEEK = MS_PER_DAY * 7;
204 // The time ranges for each section
205 private static enum MostRecentSection {
206 TODAY,
207 YESTERDAY,
208 WEEK,
209 OLDER
210 };
212 private final Context mContext;
214 // Maps headers in the list with their respective sections
215 private final SparseArray<MostRecentSection> mMostRecentSections;
217 public MostRecentAdapter(Context context) {
218 super(context, null, VIEW_TYPES, LAYOUT_TYPES);
220 mContext = context;
222 // Initialize map of history sections
223 mMostRecentSections = new SparseArray<MostRecentSection>();
224 }
226 @Override
227 public Object getItem(int position) {
228 final int type = getItemViewType(position);
230 // Header items are not in the cursor
231 if (type == ROW_HEADER) {
232 return null;
233 }
235 return super.getItem(position - getMostRecentSectionsCountBefore(position));
236 }
238 @Override
239 public int getItemViewType(int position) {
240 if (mMostRecentSections.get(position) != null) {
241 return ROW_HEADER;
242 }
244 return ROW_STANDARD;
245 }
247 @Override
248 public boolean isEnabled(int position) {
249 return (getItemViewType(position) == ROW_STANDARD);
250 }
252 @Override
253 public int getCount() {
254 // Add the history section headers to the number of reported results.
255 return super.getCount() + mMostRecentSections.size();
256 }
258 @Override
259 public Cursor swapCursor(Cursor cursor) {
260 loadMostRecentSections(cursor);
261 Cursor oldCursor = super.swapCursor(cursor);
262 return oldCursor;
263 }
265 @Override
266 public void bindView(View view, Context context, int position) {
267 final int type = getItemViewType(position);
269 if (type == ROW_HEADER) {
270 final MostRecentSection section = mMostRecentSections.get(position);
271 final TextView row = (TextView) view;
272 row.setText(getMostRecentSectionTitle(section));
273 } else {
274 // Account for the most recent section headers
275 position -= getMostRecentSectionsCountBefore(position);
276 final Cursor c = getCursor(position);
277 final TwoLinePageRow row = (TwoLinePageRow) view;
278 row.updateFromCursor(c);
279 }
280 }
282 private String getMostRecentSectionTitle(MostRecentSection section) {
283 switch (section) {
284 case TODAY:
285 return mContext.getString(R.string.history_today_section);
286 case YESTERDAY:
287 return mContext.getString(R.string.history_yesterday_section);
288 case WEEK:
289 return mContext.getString(R.string.history_week_section);
290 case OLDER:
291 return mContext.getString(R.string.history_older_section);
292 }
294 throw new IllegalStateException("Unrecognized history section");
295 }
297 private int getMostRecentSectionsCountBefore(int position) {
298 // Account for the number headers before the given position
299 int sectionsBefore = 0;
301 final int historySectionsCount = mMostRecentSections.size();
302 for (int i = 0; i < historySectionsCount; i++) {
303 final int sectionPosition = mMostRecentSections.keyAt(i);
304 if (sectionPosition > position) {
305 break;
306 }
308 sectionsBefore++;
309 }
311 return sectionsBefore;
312 }
314 private static MostRecentSection getMostRecentSectionForTime(long from, long time) {
315 long delta = from - time;
317 if (delta < 0) {
318 return MostRecentSection.TODAY;
319 }
321 if (delta < MS_PER_DAY) {
322 return MostRecentSection.YESTERDAY;
323 }
325 if (delta < MS_PER_WEEK) {
326 return MostRecentSection.WEEK;
327 }
329 return MostRecentSection.OLDER;
330 }
332 private void loadMostRecentSections(Cursor c) {
333 // Clear any history sections that may have been loaded before.
334 mMostRecentSections.clear();
336 if (c == null || !c.moveToFirst()) {
337 return;
338 }
340 final Date now = new Date();
341 now.setHours(0);
342 now.setMinutes(0);
343 now.setSeconds(0);
345 final long today = now.getTime();
346 MostRecentSection section = null;
348 do {
349 final int position = c.getPosition();
350 final long time = c.getLong(c.getColumnIndexOrThrow(URLColumns.DATE_LAST_VISITED));
351 final MostRecentSection itemSection = MostRecentAdapter.getMostRecentSectionForTime(today, time);
353 if (section != itemSection) {
354 section = itemSection;
355 mMostRecentSections.append(position + mMostRecentSections.size(), section);
356 }
358 // Reached the last section, no need to continue
359 if (section == MostRecentSection.OLDER) {
360 break;
361 }
362 } while (c.moveToNext());
363 }
364 }
366 private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
367 @Override
368 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
369 return new MostRecentCursorLoader(getActivity());
370 }
372 @Override
373 public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
374 mAdapter.swapCursor(c);
375 updateUiFromCursor(c);
376 }
378 @Override
379 public void onLoaderReset(Loader<Cursor> loader) {
380 mAdapter.swapCursor(null);
381 }
382 }
383 }