1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/home/MostRecentPanel.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,383 @@ 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.Date; 1.12 +import java.util.EnumSet; 1.13 + 1.14 +import org.mozilla.gecko.R; 1.15 +import org.mozilla.gecko.Telemetry; 1.16 +import org.mozilla.gecko.TelemetryContract; 1.17 +import org.mozilla.gecko.db.BrowserContract.Combined; 1.18 +import org.mozilla.gecko.db.BrowserDB; 1.19 +import org.mozilla.gecko.db.BrowserDB.URLColumns; 1.20 +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; 1.21 + 1.22 +import android.app.Activity; 1.23 +import android.content.ContentResolver; 1.24 +import android.content.Context; 1.25 +import android.database.Cursor; 1.26 +import android.os.Bundle; 1.27 +import android.support.v4.app.LoaderManager.LoaderCallbacks; 1.28 +import android.support.v4.content.Loader; 1.29 +import android.util.SparseArray; 1.30 +import android.view.LayoutInflater; 1.31 +import android.view.View; 1.32 +import android.view.ViewGroup; 1.33 +import android.view.ViewStub; 1.34 +import android.widget.AdapterView; 1.35 +import android.widget.ImageView; 1.36 +import android.widget.TextView; 1.37 + 1.38 +/** 1.39 + * Fragment that displays recent history in a ListView. 1.40 + */ 1.41 +public class MostRecentPanel extends HomeFragment { 1.42 + // Logging tag name 1.43 + private static final String LOGTAG = "GeckoMostRecentPanel"; 1.44 + 1.45 + // Cursor loader ID for history query 1.46 + private static final int LOADER_ID_HISTORY = 0; 1.47 + 1.48 + // Adapter for the list of search results 1.49 + private MostRecentAdapter mAdapter; 1.50 + 1.51 + // The view shown by the fragment. 1.52 + private HomeListView mList; 1.53 + 1.54 + // Reference to the View to display when there are no results. 1.55 + private View mEmptyView; 1.56 + 1.57 + // Callbacks used for the search and favicon cursor loaders 1.58 + private CursorLoaderCallbacks mCursorLoaderCallbacks; 1.59 + 1.60 + // On URL open listener 1.61 + private OnUrlOpenListener mUrlOpenListener; 1.62 + 1.63 + public static MostRecentPanel newInstance() { 1.64 + return new MostRecentPanel(); 1.65 + } 1.66 + 1.67 + public MostRecentPanel() { 1.68 + mUrlOpenListener = null; 1.69 + } 1.70 + 1.71 + @Override 1.72 + public void onAttach(Activity activity) { 1.73 + super.onAttach(activity); 1.74 + 1.75 + try { 1.76 + mUrlOpenListener = (OnUrlOpenListener) activity; 1.77 + } catch (ClassCastException e) { 1.78 + throw new ClassCastException(activity.toString() 1.79 + + " must implement HomePager.OnUrlOpenListener"); 1.80 + } 1.81 + } 1.82 + 1.83 + @Override 1.84 + public void onDetach() { 1.85 + super.onDetach(); 1.86 + mUrlOpenListener = null; 1.87 + } 1.88 + 1.89 + @Override 1.90 + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 1.91 + return inflater.inflate(R.layout.home_most_recent_panel, container, false); 1.92 + } 1.93 + 1.94 + @Override 1.95 + public void onViewCreated(View view, Bundle savedInstanceState) { 1.96 + mList = (HomeListView) view.findViewById(R.id.list); 1.97 + mList.setTag(HomePager.LIST_TAG_MOST_RECENT); 1.98 + 1.99 + mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1.100 + @Override 1.101 + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1.102 + position -= mAdapter.getMostRecentSectionsCountBefore(position); 1.103 + final Cursor c = mAdapter.getCursor(position); 1.104 + final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); 1.105 + 1.106 + Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL); 1.107 + 1.108 + // This item is a TwoLinePageRow, so we allow switch-to-tab. 1.109 + mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); 1.110 + } 1.111 + }); 1.112 + 1.113 + mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() { 1.114 + @Override 1.115 + public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) { 1.116 + final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id); 1.117 + info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL)); 1.118 + info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE)); 1.119 + info.display = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.DISPLAY)); 1.120 + info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.HISTORY_ID)); 1.121 + final int bookmarkIdCol = cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID); 1.122 + if (cursor.isNull(bookmarkIdCol)) { 1.123 + // If this is a combined cursor, we may get a history item without a 1.124 + // bookmark, in which case the bookmarks ID column value will be null. 1.125 + info.bookmarkId = -1; 1.126 + } else { 1.127 + info.bookmarkId = cursor.getInt(bookmarkIdCol); 1.128 + } 1.129 + return info; 1.130 + } 1.131 + }); 1.132 + registerForContextMenu(mList); 1.133 + } 1.134 + 1.135 + @Override 1.136 + public void onDestroyView() { 1.137 + super.onDestroyView(); 1.138 + mList = null; 1.139 + mEmptyView = null; 1.140 + } 1.141 + 1.142 + @Override 1.143 + public void onActivityCreated(Bundle savedInstanceState) { 1.144 + super.onActivityCreated(savedInstanceState); 1.145 + 1.146 + // Intialize adapter 1.147 + mAdapter = new MostRecentAdapter(getActivity()); 1.148 + mList.setAdapter(mAdapter); 1.149 + 1.150 + // Create callbacks before the initial loader is started 1.151 + mCursorLoaderCallbacks = new CursorLoaderCallbacks(); 1.152 + loadIfVisible(); 1.153 + } 1.154 + 1.155 + @Override 1.156 + protected void load() { 1.157 + getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks); 1.158 + } 1.159 + 1.160 + private static class MostRecentCursorLoader extends SimpleCursorLoader { 1.161 + // Max number of history results 1.162 + private static final int HISTORY_LIMIT = 100; 1.163 + 1.164 + public MostRecentCursorLoader(Context context) { 1.165 + super(context); 1.166 + } 1.167 + 1.168 + @Override 1.169 + public Cursor loadCursor() { 1.170 + final ContentResolver cr = getContext().getContentResolver(); 1.171 + return BrowserDB.getRecentHistory(cr, HISTORY_LIMIT); 1.172 + } 1.173 + } 1.174 + 1.175 + private void updateUiFromCursor(Cursor c) { 1.176 + if (c != null && c.getCount() > 0) { 1.177 + return; 1.178 + } 1.179 + 1.180 + // Cursor is empty, so set the empty view if it hasn't been set already. 1.181 + if (mEmptyView == null) { 1.182 + // Set empty panel view. We delay this so that the empty view won't flash. 1.183 + final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub); 1.184 + mEmptyView = emptyViewStub.inflate(); 1.185 + 1.186 + final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image); 1.187 + emptyIcon.setImageResource(R.drawable.icon_most_recent_empty); 1.188 + 1.189 + final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text); 1.190 + emptyText.setText(R.string.home_most_recent_empty); 1.191 + 1.192 + mList.setEmptyView(mEmptyView); 1.193 + } 1.194 + } 1.195 + 1.196 + private static class MostRecentAdapter extends MultiTypeCursorAdapter { 1.197 + private static final int ROW_HEADER = 0; 1.198 + private static final int ROW_STANDARD = 1; 1.199 + 1.200 + private static final int[] VIEW_TYPES = new int[] { ROW_STANDARD, ROW_HEADER }; 1.201 + private static final int[] LAYOUT_TYPES = new int[] { R.layout.home_item_row, R.layout.home_header_row }; 1.202 + 1.203 + // For the time sections in history 1.204 + private static final long MS_PER_DAY = 86400000; 1.205 + private static final long MS_PER_WEEK = MS_PER_DAY * 7; 1.206 + 1.207 + // The time ranges for each section 1.208 + private static enum MostRecentSection { 1.209 + TODAY, 1.210 + YESTERDAY, 1.211 + WEEK, 1.212 + OLDER 1.213 + }; 1.214 + 1.215 + private final Context mContext; 1.216 + 1.217 + // Maps headers in the list with their respective sections 1.218 + private final SparseArray<MostRecentSection> mMostRecentSections; 1.219 + 1.220 + public MostRecentAdapter(Context context) { 1.221 + super(context, null, VIEW_TYPES, LAYOUT_TYPES); 1.222 + 1.223 + mContext = context; 1.224 + 1.225 + // Initialize map of history sections 1.226 + mMostRecentSections = new SparseArray<MostRecentSection>(); 1.227 + } 1.228 + 1.229 + @Override 1.230 + public Object getItem(int position) { 1.231 + final int type = getItemViewType(position); 1.232 + 1.233 + // Header items are not in the cursor 1.234 + if (type == ROW_HEADER) { 1.235 + return null; 1.236 + } 1.237 + 1.238 + return super.getItem(position - getMostRecentSectionsCountBefore(position)); 1.239 + } 1.240 + 1.241 + @Override 1.242 + public int getItemViewType(int position) { 1.243 + if (mMostRecentSections.get(position) != null) { 1.244 + return ROW_HEADER; 1.245 + } 1.246 + 1.247 + return ROW_STANDARD; 1.248 + } 1.249 + 1.250 + @Override 1.251 + public boolean isEnabled(int position) { 1.252 + return (getItemViewType(position) == ROW_STANDARD); 1.253 + } 1.254 + 1.255 + @Override 1.256 + public int getCount() { 1.257 + // Add the history section headers to the number of reported results. 1.258 + return super.getCount() + mMostRecentSections.size(); 1.259 + } 1.260 + 1.261 + @Override 1.262 + public Cursor swapCursor(Cursor cursor) { 1.263 + loadMostRecentSections(cursor); 1.264 + Cursor oldCursor = super.swapCursor(cursor); 1.265 + return oldCursor; 1.266 + } 1.267 + 1.268 + @Override 1.269 + public void bindView(View view, Context context, int position) { 1.270 + final int type = getItemViewType(position); 1.271 + 1.272 + if (type == ROW_HEADER) { 1.273 + final MostRecentSection section = mMostRecentSections.get(position); 1.274 + final TextView row = (TextView) view; 1.275 + row.setText(getMostRecentSectionTitle(section)); 1.276 + } else { 1.277 + // Account for the most recent section headers 1.278 + position -= getMostRecentSectionsCountBefore(position); 1.279 + final Cursor c = getCursor(position); 1.280 + final TwoLinePageRow row = (TwoLinePageRow) view; 1.281 + row.updateFromCursor(c); 1.282 + } 1.283 + } 1.284 + 1.285 + private String getMostRecentSectionTitle(MostRecentSection section) { 1.286 + switch (section) { 1.287 + case TODAY: 1.288 + return mContext.getString(R.string.history_today_section); 1.289 + case YESTERDAY: 1.290 + return mContext.getString(R.string.history_yesterday_section); 1.291 + case WEEK: 1.292 + return mContext.getString(R.string.history_week_section); 1.293 + case OLDER: 1.294 + return mContext.getString(R.string.history_older_section); 1.295 + } 1.296 + 1.297 + throw new IllegalStateException("Unrecognized history section"); 1.298 + } 1.299 + 1.300 + private int getMostRecentSectionsCountBefore(int position) { 1.301 + // Account for the number headers before the given position 1.302 + int sectionsBefore = 0; 1.303 + 1.304 + final int historySectionsCount = mMostRecentSections.size(); 1.305 + for (int i = 0; i < historySectionsCount; i++) { 1.306 + final int sectionPosition = mMostRecentSections.keyAt(i); 1.307 + if (sectionPosition > position) { 1.308 + break; 1.309 + } 1.310 + 1.311 + sectionsBefore++; 1.312 + } 1.313 + 1.314 + return sectionsBefore; 1.315 + } 1.316 + 1.317 + private static MostRecentSection getMostRecentSectionForTime(long from, long time) { 1.318 + long delta = from - time; 1.319 + 1.320 + if (delta < 0) { 1.321 + return MostRecentSection.TODAY; 1.322 + } 1.323 + 1.324 + if (delta < MS_PER_DAY) { 1.325 + return MostRecentSection.YESTERDAY; 1.326 + } 1.327 + 1.328 + if (delta < MS_PER_WEEK) { 1.329 + return MostRecentSection.WEEK; 1.330 + } 1.331 + 1.332 + return MostRecentSection.OLDER; 1.333 + } 1.334 + 1.335 + private void loadMostRecentSections(Cursor c) { 1.336 + // Clear any history sections that may have been loaded before. 1.337 + mMostRecentSections.clear(); 1.338 + 1.339 + if (c == null || !c.moveToFirst()) { 1.340 + return; 1.341 + } 1.342 + 1.343 + final Date now = new Date(); 1.344 + now.setHours(0); 1.345 + now.setMinutes(0); 1.346 + now.setSeconds(0); 1.347 + 1.348 + final long today = now.getTime(); 1.349 + MostRecentSection section = null; 1.350 + 1.351 + do { 1.352 + final int position = c.getPosition(); 1.353 + final long time = c.getLong(c.getColumnIndexOrThrow(URLColumns.DATE_LAST_VISITED)); 1.354 + final MostRecentSection itemSection = MostRecentAdapter.getMostRecentSectionForTime(today, time); 1.355 + 1.356 + if (section != itemSection) { 1.357 + section = itemSection; 1.358 + mMostRecentSections.append(position + mMostRecentSections.size(), section); 1.359 + } 1.360 + 1.361 + // Reached the last section, no need to continue 1.362 + if (section == MostRecentSection.OLDER) { 1.363 + break; 1.364 + } 1.365 + } while (c.moveToNext()); 1.366 + } 1.367 + } 1.368 + 1.369 + private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> { 1.370 + @Override 1.371 + public Loader<Cursor> onCreateLoader(int id, Bundle args) { 1.372 + return new MostRecentCursorLoader(getActivity()); 1.373 + } 1.374 + 1.375 + @Override 1.376 + public void onLoadFinished(Loader<Cursor> loader, Cursor c) { 1.377 + mAdapter.swapCursor(c); 1.378 + updateUiFromCursor(c); 1.379 + } 1.380 + 1.381 + @Override 1.382 + public void onLoaderReset(Loader<Cursor> loader) { 1.383 + mAdapter.swapCursor(null); 1.384 + } 1.385 + } 1.386 +}