michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.home; michael@0: michael@0: import java.util.Date; michael@0: import java.util.EnumSet; michael@0: michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.Telemetry; michael@0: import org.mozilla.gecko.TelemetryContract; michael@0: import org.mozilla.gecko.db.BrowserContract.Combined; michael@0: import org.mozilla.gecko.db.BrowserDB; michael@0: import org.mozilla.gecko.db.BrowserDB.URLColumns; michael@0: import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; michael@0: michael@0: import android.app.Activity; michael@0: import android.content.ContentResolver; michael@0: import android.content.Context; michael@0: import android.database.Cursor; michael@0: import android.os.Bundle; michael@0: import android.support.v4.app.LoaderManager.LoaderCallbacks; michael@0: import android.support.v4.content.Loader; michael@0: import android.util.SparseArray; michael@0: import android.view.LayoutInflater; michael@0: import android.view.View; michael@0: import android.view.ViewGroup; michael@0: import android.view.ViewStub; michael@0: import android.widget.AdapterView; michael@0: import android.widget.ImageView; michael@0: import android.widget.TextView; michael@0: michael@0: /** michael@0: * Fragment that displays recent history in a ListView. michael@0: */ michael@0: public class MostRecentPanel extends HomeFragment { michael@0: // Logging tag name michael@0: private static final String LOGTAG = "GeckoMostRecentPanel"; michael@0: michael@0: // Cursor loader ID for history query michael@0: private static final int LOADER_ID_HISTORY = 0; michael@0: michael@0: // Adapter for the list of search results michael@0: private MostRecentAdapter mAdapter; michael@0: michael@0: // The view shown by the fragment. michael@0: private HomeListView mList; michael@0: michael@0: // Reference to the View to display when there are no results. michael@0: private View mEmptyView; michael@0: michael@0: // Callbacks used for the search and favicon cursor loaders michael@0: private CursorLoaderCallbacks mCursorLoaderCallbacks; michael@0: michael@0: // On URL open listener michael@0: private OnUrlOpenListener mUrlOpenListener; michael@0: michael@0: public static MostRecentPanel newInstance() { michael@0: return new MostRecentPanel(); michael@0: } michael@0: michael@0: public MostRecentPanel() { michael@0: mUrlOpenListener = null; michael@0: } michael@0: michael@0: @Override michael@0: public void onAttach(Activity activity) { michael@0: super.onAttach(activity); michael@0: michael@0: try { michael@0: mUrlOpenListener = (OnUrlOpenListener) activity; michael@0: } catch (ClassCastException e) { michael@0: throw new ClassCastException(activity.toString() michael@0: + " must implement HomePager.OnUrlOpenListener"); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onDetach() { michael@0: super.onDetach(); michael@0: mUrlOpenListener = null; michael@0: } michael@0: michael@0: @Override michael@0: public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { michael@0: return inflater.inflate(R.layout.home_most_recent_panel, container, false); michael@0: } michael@0: michael@0: @Override michael@0: public void onViewCreated(View view, Bundle savedInstanceState) { michael@0: mList = (HomeListView) view.findViewById(R.id.list); michael@0: mList.setTag(HomePager.LIST_TAG_MOST_RECENT); michael@0: michael@0: mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { michael@0: @Override michael@0: public void onItemClick(AdapterView parent, View view, int position, long id) { michael@0: position -= mAdapter.getMostRecentSectionsCountBefore(position); michael@0: final Cursor c = mAdapter.getCursor(position); michael@0: final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); michael@0: michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL); michael@0: michael@0: // This item is a TwoLinePageRow, so we allow switch-to-tab. michael@0: mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); michael@0: } michael@0: }); michael@0: michael@0: mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() { michael@0: @Override michael@0: public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) { michael@0: final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id); michael@0: info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL)); michael@0: info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE)); michael@0: info.display = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.DISPLAY)); michael@0: info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.HISTORY_ID)); michael@0: final int bookmarkIdCol = cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID); michael@0: if (cursor.isNull(bookmarkIdCol)) { michael@0: // If this is a combined cursor, we may get a history item without a michael@0: // bookmark, in which case the bookmarks ID column value will be null. michael@0: info.bookmarkId = -1; michael@0: } else { michael@0: info.bookmarkId = cursor.getInt(bookmarkIdCol); michael@0: } michael@0: return info; michael@0: } michael@0: }); michael@0: registerForContextMenu(mList); michael@0: } michael@0: michael@0: @Override michael@0: public void onDestroyView() { michael@0: super.onDestroyView(); michael@0: mList = null; michael@0: mEmptyView = null; michael@0: } michael@0: michael@0: @Override michael@0: public void onActivityCreated(Bundle savedInstanceState) { michael@0: super.onActivityCreated(savedInstanceState); michael@0: michael@0: // Intialize adapter michael@0: mAdapter = new MostRecentAdapter(getActivity()); michael@0: mList.setAdapter(mAdapter); michael@0: michael@0: // Create callbacks before the initial loader is started michael@0: mCursorLoaderCallbacks = new CursorLoaderCallbacks(); michael@0: loadIfVisible(); michael@0: } michael@0: michael@0: @Override michael@0: protected void load() { michael@0: getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks); michael@0: } michael@0: michael@0: private static class MostRecentCursorLoader extends SimpleCursorLoader { michael@0: // Max number of history results michael@0: private static final int HISTORY_LIMIT = 100; michael@0: michael@0: public MostRecentCursorLoader(Context context) { michael@0: super(context); michael@0: } michael@0: michael@0: @Override michael@0: public Cursor loadCursor() { michael@0: final ContentResolver cr = getContext().getContentResolver(); michael@0: return BrowserDB.getRecentHistory(cr, HISTORY_LIMIT); michael@0: } michael@0: } michael@0: michael@0: private void updateUiFromCursor(Cursor c) { michael@0: if (c != null && c.getCount() > 0) { michael@0: return; michael@0: } michael@0: michael@0: // Cursor is empty, so set the empty view if it hasn't been set already. michael@0: if (mEmptyView == null) { michael@0: // Set empty panel view. We delay this so that the empty view won't flash. michael@0: final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub); michael@0: mEmptyView = emptyViewStub.inflate(); michael@0: michael@0: final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image); michael@0: emptyIcon.setImageResource(R.drawable.icon_most_recent_empty); michael@0: michael@0: final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text); michael@0: emptyText.setText(R.string.home_most_recent_empty); michael@0: michael@0: mList.setEmptyView(mEmptyView); michael@0: } michael@0: } michael@0: michael@0: private static class MostRecentAdapter extends MultiTypeCursorAdapter { michael@0: private static final int ROW_HEADER = 0; michael@0: private static final int ROW_STANDARD = 1; michael@0: michael@0: private static final int[] VIEW_TYPES = new int[] { ROW_STANDARD, ROW_HEADER }; michael@0: private static final int[] LAYOUT_TYPES = new int[] { R.layout.home_item_row, R.layout.home_header_row }; michael@0: michael@0: // For the time sections in history michael@0: private static final long MS_PER_DAY = 86400000; michael@0: private static final long MS_PER_WEEK = MS_PER_DAY * 7; michael@0: michael@0: // The time ranges for each section michael@0: private static enum MostRecentSection { michael@0: TODAY, michael@0: YESTERDAY, michael@0: WEEK, michael@0: OLDER michael@0: }; michael@0: michael@0: private final Context mContext; michael@0: michael@0: // Maps headers in the list with their respective sections michael@0: private final SparseArray mMostRecentSections; michael@0: michael@0: public MostRecentAdapter(Context context) { michael@0: super(context, null, VIEW_TYPES, LAYOUT_TYPES); michael@0: michael@0: mContext = context; michael@0: michael@0: // Initialize map of history sections michael@0: mMostRecentSections = new SparseArray(); michael@0: } michael@0: michael@0: @Override michael@0: public Object getItem(int position) { michael@0: final int type = getItemViewType(position); michael@0: michael@0: // Header items are not in the cursor michael@0: if (type == ROW_HEADER) { michael@0: return null; michael@0: } michael@0: michael@0: return super.getItem(position - getMostRecentSectionsCountBefore(position)); michael@0: } michael@0: michael@0: @Override michael@0: public int getItemViewType(int position) { michael@0: if (mMostRecentSections.get(position) != null) { michael@0: return ROW_HEADER; michael@0: } michael@0: michael@0: return ROW_STANDARD; michael@0: } michael@0: michael@0: @Override michael@0: public boolean isEnabled(int position) { michael@0: return (getItemViewType(position) == ROW_STANDARD); michael@0: } michael@0: michael@0: @Override michael@0: public int getCount() { michael@0: // Add the history section headers to the number of reported results. michael@0: return super.getCount() + mMostRecentSections.size(); michael@0: } michael@0: michael@0: @Override michael@0: public Cursor swapCursor(Cursor cursor) { michael@0: loadMostRecentSections(cursor); michael@0: Cursor oldCursor = super.swapCursor(cursor); michael@0: return oldCursor; michael@0: } michael@0: michael@0: @Override michael@0: public void bindView(View view, Context context, int position) { michael@0: final int type = getItemViewType(position); michael@0: michael@0: if (type == ROW_HEADER) { michael@0: final MostRecentSection section = mMostRecentSections.get(position); michael@0: final TextView row = (TextView) view; michael@0: row.setText(getMostRecentSectionTitle(section)); michael@0: } else { michael@0: // Account for the most recent section headers michael@0: position -= getMostRecentSectionsCountBefore(position); michael@0: final Cursor c = getCursor(position); michael@0: final TwoLinePageRow row = (TwoLinePageRow) view; michael@0: row.updateFromCursor(c); michael@0: } michael@0: } michael@0: michael@0: private String getMostRecentSectionTitle(MostRecentSection section) { michael@0: switch (section) { michael@0: case TODAY: michael@0: return mContext.getString(R.string.history_today_section); michael@0: case YESTERDAY: michael@0: return mContext.getString(R.string.history_yesterday_section); michael@0: case WEEK: michael@0: return mContext.getString(R.string.history_week_section); michael@0: case OLDER: michael@0: return mContext.getString(R.string.history_older_section); michael@0: } michael@0: michael@0: throw new IllegalStateException("Unrecognized history section"); michael@0: } michael@0: michael@0: private int getMostRecentSectionsCountBefore(int position) { michael@0: // Account for the number headers before the given position michael@0: int sectionsBefore = 0; michael@0: michael@0: final int historySectionsCount = mMostRecentSections.size(); michael@0: for (int i = 0; i < historySectionsCount; i++) { michael@0: final int sectionPosition = mMostRecentSections.keyAt(i); michael@0: if (sectionPosition > position) { michael@0: break; michael@0: } michael@0: michael@0: sectionsBefore++; michael@0: } michael@0: michael@0: return sectionsBefore; michael@0: } michael@0: michael@0: private static MostRecentSection getMostRecentSectionForTime(long from, long time) { michael@0: long delta = from - time; michael@0: michael@0: if (delta < 0) { michael@0: return MostRecentSection.TODAY; michael@0: } michael@0: michael@0: if (delta < MS_PER_DAY) { michael@0: return MostRecentSection.YESTERDAY; michael@0: } michael@0: michael@0: if (delta < MS_PER_WEEK) { michael@0: return MostRecentSection.WEEK; michael@0: } michael@0: michael@0: return MostRecentSection.OLDER; michael@0: } michael@0: michael@0: private void loadMostRecentSections(Cursor c) { michael@0: // Clear any history sections that may have been loaded before. michael@0: mMostRecentSections.clear(); michael@0: michael@0: if (c == null || !c.moveToFirst()) { michael@0: return; michael@0: } michael@0: michael@0: final Date now = new Date(); michael@0: now.setHours(0); michael@0: now.setMinutes(0); michael@0: now.setSeconds(0); michael@0: michael@0: final long today = now.getTime(); michael@0: MostRecentSection section = null; michael@0: michael@0: do { michael@0: final int position = c.getPosition(); michael@0: final long time = c.getLong(c.getColumnIndexOrThrow(URLColumns.DATE_LAST_VISITED)); michael@0: final MostRecentSection itemSection = MostRecentAdapter.getMostRecentSectionForTime(today, time); michael@0: michael@0: if (section != itemSection) { michael@0: section = itemSection; michael@0: mMostRecentSections.append(position + mMostRecentSections.size(), section); michael@0: } michael@0: michael@0: // Reached the last section, no need to continue michael@0: if (section == MostRecentSection.OLDER) { michael@0: break; michael@0: } michael@0: } while (c.moveToNext()); michael@0: } michael@0: } michael@0: michael@0: private class CursorLoaderCallbacks implements LoaderCallbacks { michael@0: @Override michael@0: public Loader onCreateLoader(int id, Bundle args) { michael@0: return new MostRecentCursorLoader(getActivity()); michael@0: } michael@0: michael@0: @Override michael@0: public void onLoadFinished(Loader loader, Cursor c) { michael@0: mAdapter.swapCursor(c); michael@0: updateUiFromCursor(c); michael@0: } michael@0: michael@0: @Override michael@0: public void onLoaderReset(Loader loader) { michael@0: mAdapter.swapCursor(null); michael@0: } michael@0: } michael@0: }