/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.home; import org.mozilla.gecko.R; import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.db.BrowserDB.URLColumns; import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; import org.mozilla.gecko.home.TwoLinePageRow; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.os.Bundle; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.support.v4.widget.SimpleCursorAdapter; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import android.view.LayoutInflater; import android.widget.AdapterView; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import java.util.Date; /** * Fragment that displays recent history in a ListView. */ public class MostRecentPage extends HomeFragment { // Logging tag name private static final String LOGTAG = "GeckoMostRecentPage"; // Cursor loader ID for history query private static final int LOADER_ID_HISTORY = 0; // Adapter for the list of search results private MostRecentAdapter mAdapter; // The view shown by the fragment. private ListView mList; // Callbacks used for the search and favicon cursor loaders private CursorLoaderCallbacks mCursorLoaderCallbacks; // On URL open listener private OnUrlOpenListener mUrlOpenListener; public static MostRecentPage newInstance() { return new MostRecentPage(); } public MostRecentPage() { mUrlOpenListener = null; } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mUrlOpenListener = (OnUrlOpenListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement HomePager.OnUrlOpenListener"); } } @Override public void onDetach() { super.onDetach(); mUrlOpenListener = null; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.home_most_recent_page, container, false); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { final TextView title = (TextView) view.findViewById(R.id.title); title.setText(R.string.home_most_recent_title); mList = (ListView) view.findViewById(R.id.list); mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { position -= mAdapter.getMostRecentSectionsCountBefore(position); final Cursor c = mAdapter.getCursor(); if (c == null || !c.moveToPosition(position)) { return; } final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); mUrlOpenListener.onUrlOpen(url); } }); // Set empty page view. final View emptyView = view.findViewById(R.id.home_empty_view); ((ImageView) emptyView.findViewById(R.id.home_empty_image)).setImageResource(R.drawable.icon_most_recent_empty); ((TextView) emptyView.findViewById(R.id.home_empty_text)).setText(R.string.home_most_recent_empty); mList.setEmptyView(emptyView); registerForContextMenu(mList); } @Override public void onDestroyView() { super.onDestroyView(); mList = null; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); final Activity activity = getActivity(); // Intialize adapter mAdapter = new MostRecentAdapter(activity); mList.setAdapter(mAdapter); // Create callbacks before the initial loader is started mCursorLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager()); loadIfVisible(); } @Override protected void load() { getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks); } private static class MostRecentCursorLoader extends SimpleCursorLoader { // Max number of history results private static final int HISTORY_LIMIT = 100; public MostRecentCursorLoader(Context context) { super(context); } @Override public Cursor loadCursor() { final ContentResolver cr = getContext().getContentResolver(); return BrowserDB.getRecentHistory(cr, HISTORY_LIMIT); } } private static class MostRecentAdapter extends SimpleCursorAdapter { private static final int ROW_HEADER = 0; private static final int ROW_STANDARD = 1; private static final int ROW_TYPE_COUNT = 2; // For the time sections in history private static final long MS_PER_DAY = 86400000; private static final long MS_PER_WEEK = MS_PER_DAY * 7; // The time ranges for each section private static enum MostRecentSection { TODAY, YESTERDAY, WEEK, OLDER }; private final Context mContext; // Maps headers in the list with their respective sections private final SparseArray mMostRecentSections; public MostRecentAdapter(Context context) { super(context, -1, null, new String[] {}, new int[] {}); mContext = context; // Initialize map of history sections mMostRecentSections = new SparseArray(); } @Override public Object getItem(int position) { final int type = getItemViewType(position); // Header items are not in the cursor if (type == ROW_HEADER) { return null; } return super.getItem(position - getMostRecentSectionsCountBefore(position)); } @Override public int getItemViewType(int position) { if (mMostRecentSections.get(position) != null) { return ROW_HEADER; } return ROW_STANDARD; } @Override public int getViewTypeCount() { // view can be either a standard page row, or a header row return ROW_TYPE_COUNT; } @Override public boolean isEnabled(int position) { return (getItemViewType(position) == ROW_STANDARD); } @Override public int getCount() { // Add the history section headers to the number of reported results. return super.getCount() + mMostRecentSections.size(); } @Override public Cursor swapCursor(Cursor cursor) { Cursor oldCursor = super.swapCursor(cursor); loadMostRecentSections(cursor); return oldCursor; } @Override public View getView(int position, View convertView, ViewGroup parent) { final int type = getItemViewType(position); if (type == ROW_HEADER) { final TextView row; if (convertView == null) { row = (TextView) LayoutInflater.from(mContext).inflate(R.layout.home_header_row, parent, false); } else { row = (TextView) convertView; } final MostRecentSection section = mMostRecentSections.get(position); row.setText(getMostRecentSectionTitle(section)); return row; } else { final TwoLinePageRow row; if (convertView == null) { row = (TwoLinePageRow) LayoutInflater.from(mContext).inflate(R.layout.home_item_row, parent, false); } else { row = (TwoLinePageRow) convertView; } // Account for the search engines position -= getMostRecentSectionsCountBefore(position); final Cursor c = getCursor(); if (!c.moveToPosition(position)) { throw new IllegalStateException("Couldn't move cursor to position " + position); } row.updateFromCursor(c); return row; } } private String getMostRecentSectionTitle(MostRecentSection section) { switch (section) { case TODAY: return mContext.getString(R.string.history_today_section); case YESTERDAY: return mContext.getString(R.string.history_yesterday_section); case WEEK: return mContext.getString(R.string.history_week_section); case OLDER: return mContext.getString(R.string.history_older_section); } throw new IllegalStateException("Unrecognized history section"); } private int getMostRecentSectionsCountBefore(int position) { // Account for the number headers before the given position int sectionsBefore = 0; final int historySectionsCount = mMostRecentSections.size(); for (int i = 0; i < historySectionsCount; i++) { final int sectionPosition = mMostRecentSections.keyAt(i); if (sectionPosition > position) { break; } sectionsBefore++; } return sectionsBefore; } private static MostRecentSection getMostRecentSectionForTime(long from, long time) { long delta = from - time; if (delta < 0) { return MostRecentSection.TODAY; } if (delta < MS_PER_DAY) { return MostRecentSection.YESTERDAY; } if (delta < MS_PER_WEEK) { return MostRecentSection.WEEK; } return MostRecentSection.OLDER; } private void loadMostRecentSections(Cursor c) { if (c == null || !c.moveToFirst()) { return; } // Clear any history sections that may have been loaded before. mMostRecentSections.clear(); final Date now = new Date(); now.setHours(0); now.setMinutes(0); now.setSeconds(0); final long today = now.getTime(); MostRecentSection section = null; do { final int position = c.getPosition(); final long time = c.getLong(c.getColumnIndexOrThrow(URLColumns.DATE_LAST_VISITED)); final MostRecentSection itemSection = MostRecentAdapter.getMostRecentSectionForTime(today, time); if (section != itemSection) { section = itemSection; mMostRecentSections.append(position + mMostRecentSections.size(), section); } // Reached the last section, no need to continue if (section == MostRecentSection.OLDER) { break; } } while (c.moveToNext()); } } private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks { public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) { super(context, loaderManager); } @Override public Loader onCreateLoader(int id, Bundle args) { if (id == LOADER_ID_HISTORY) { return new MostRecentCursorLoader(getActivity()); } else { return super.onCreateLoader(id, args); } } @Override public void onLoadFinished(Loader loader, Cursor c) { if (loader.getId() == LOADER_ID_HISTORY) { mAdapter.swapCursor(c); loadFavicons(c); } else { super.onLoadFinished(loader, c); } } @Override public void onLoaderReset(Loader loader) { if (loader.getId() == LOADER_ID_HISTORY) { mAdapter.swapCursor(null); } else { super.onLoaderReset(loader); } } @Override public void onFaviconsLoaded() { mAdapter.notifyDataSetChanged(); } } }