diff --git a/mobile/android/base/db/BrowserDB.java b/mobile/android/base/db/BrowserDB.java index 26881c76850f..d2c5c720b450 100644 --- a/mobile/android/base/db/BrowserDB.java +++ b/mobile/android/base/db/BrowserDB.java @@ -97,6 +97,8 @@ public interface BrowserDB { */ public abstract Cursor getRecentHistory(ContentResolver cr, int limit); + public abstract Cursor getRecentHistoryBetweenTime(ContentResolver cr, int historyLimit, long start, long end); + public abstract void expireHistory(ContentResolver cr, ExpirePriority priority); public abstract void removeHistoryEntry(ContentResolver cr, String url); diff --git a/mobile/android/base/db/LocalBrowserDB.java b/mobile/android/base/db/LocalBrowserDB.java index bef479ecd89b..451065fa2f3c 100644 --- a/mobile/android/base/db/LocalBrowserDB.java +++ b/mobile/android/base/db/LocalBrowserDB.java @@ -719,6 +719,21 @@ public class LocalBrowserDB implements BrowserDB { History.DATE_LAST_VISITED + " DESC"); } + @Override + public Cursor getRecentHistoryBetweenTime(ContentResolver cr, int limit, long start, long end) { + return cr.query(combinedUriWithLimit(limit), + new String[] { Combined._ID, + Combined.BOOKMARK_ID, + Combined.HISTORY_ID, + Combined.URL, + Combined.TITLE, + Combined.DATE_LAST_VISITED, + Combined.VISITS }, + History.DATE_LAST_VISITED + " >= " + start + " AND " + History.DATE_LAST_VISITED + " < " + end, + null, + History.DATE_LAST_VISITED + " DESC"); + } + @Override public void expireHistory(ContentResolver cr, ExpirePriority priority) { Uri url = mHistoryExpireUriWithProfile; diff --git a/mobile/android/base/db/StubBrowserDB.java b/mobile/android/base/db/StubBrowserDB.java index 8cf55767c63d..8cefa4a98981 100644 --- a/mobile/android/base/db/StubBrowserDB.java +++ b/mobile/android/base/db/StubBrowserDB.java @@ -224,6 +224,11 @@ public class StubBrowserDB implements BrowserDB { return null; } + @Override + public Cursor getRecentHistoryBetweenTime(ContentResolver cr, int limit, long time, long end) { + return null; + } + public void expireHistory(ContentResolver cr, BrowserContract.ExpirePriority priority) { } diff --git a/mobile/android/base/home/HistoryHeaderListCursorAdapter.java b/mobile/android/base/home/HistoryHeaderListCursorAdapter.java new file mode 100644 index 000000000000..36bf97abb9f8 --- /dev/null +++ b/mobile/android/base/home/HistoryHeaderListCursorAdapter.java @@ -0,0 +1,143 @@ +/* -*- 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 android.content.Context; +import android.database.Cursor; +import android.util.SparseArray; +import android.view.View; +import android.widget.TextView; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.db.BrowserContract; + +/** + * CursorAdapter for HistoryPanel to partition history items by MostRecentSection range headers. + */ +public class HistoryHeaderListCursorAdapter extends MultiTypeCursorAdapter implements HistoryPanel.HistoryUrlProvider { + private static final int ROW_HEADER = 0; + private static final int ROW_STANDARD = 1; + + private static final int[] VIEW_TYPES = new int[] { ROW_STANDARD, ROW_HEADER }; + private static final int[] LAYOUT_TYPES = new int[] { R.layout.home_item_row, R.layout.home_header_row }; + + // Maps headers in the list with their respective sections + private final SparseArray mMostRecentSections; + + public HistoryHeaderListCursorAdapter(Context context) { + super(context, null, VIEW_TYPES, LAYOUT_TYPES); + + // 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 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) { + loadMostRecentSections(cursor); + Cursor oldCursor = super.swapCursor(cursor); + return oldCursor; + } + + @Override + public void bindView(View view, Context context, int position) { + final int type = getItemViewType(position); + + if (type == ROW_HEADER) { + final HistoryPanel.MostRecentSection section = mMostRecentSections.get(position); + final TextView row = (TextView) view; + row.setText(HistoryPanel.getMostRecentSectionTitle(section)); + } else { + // Account for the most recent section headers + position -= getMostRecentSectionsCountBefore(position); + final Cursor c = getCursor(position); + final TwoLinePageRow row = (TwoLinePageRow) view; + row.updateFromCursor(c); + } + } + + 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 void loadMostRecentSections(Cursor c) { + // Clear any history sections that may have been loaded before. + mMostRecentSections.clear(); + + if (c == null || !c.moveToFirst()) { + return; + } + + HistoryPanel.MostRecentSection section = null; + + do { + final int position = c.getPosition(); + final long time = c.getLong(c.getColumnIndexOrThrow(BrowserContract.History.DATE_LAST_VISITED)); + final HistoryPanel.MostRecentSection itemSection = HistoryPanel.getMostRecentSectionForTime(time); + + if (section != itemSection) { + section = itemSection; + mMostRecentSections.append(position + mMostRecentSections.size(), section); + } + + // Reached the last section, no need to continue + if (section == HistoryPanel.MostRecentSection.OLDER_THAN_SIX_MONTHS) { + break; + } + } while (c.moveToNext()); + } + + @Override + public String getURL(int position) { + position -= getMostRecentSectionsCountBefore(position); + final Cursor c = getCursor(position); + return c.getString(c.getColumnIndexOrThrow(BrowserContract.History.URL)); + } +} diff --git a/mobile/android/base/home/HistoryPanel.java b/mobile/android/base/home/HistoryPanel.java index e0453b8f3c40..f4ac7f58a308 100644 --- a/mobile/android/base/home/HistoryPanel.java +++ b/mobile/android/base/home/HistoryPanel.java @@ -5,8 +5,11 @@ package org.mozilla.gecko.home; -import java.util.Date; +import java.util.ArrayList; +import java.util.Calendar; import java.util.EnumSet; +import java.util.List; +import java.util.Locale; import org.json.JSONException; import org.json.JSONObject; @@ -20,7 +23,6 @@ import org.mozilla.gecko.RestrictedProfiles; import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.db.BrowserContract.Combined; -import org.mozilla.gecko.db.BrowserContract.History; import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType; import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; @@ -34,6 +36,7 @@ import android.database.Cursor; import android.graphics.Typeface; import android.os.Bundle; import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; import android.text.SpannableStringBuilder; import android.text.TextPaint; import android.text.method.LinkMovementMethod; @@ -41,7 +44,6 @@ import android.text.style.ClickableSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import android.util.Log; -import android.util.SparseArray; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.View; @@ -58,6 +60,11 @@ public class HistoryPanel extends HomeFragment { // Logging tag name private static final String LOGTAG = "GeckoHistoryPanel"; + // 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; + private static final List recentSectionTimeOffsetList = new ArrayList<>(MostRecentSection.values().length); + // Cursor loader ID for history query private static final int LOADER_ID_HISTORY = 0; @@ -65,8 +72,12 @@ public class HistoryPanel extends HomeFragment { private final static String FORMAT_S1 = "%1$s"; private final static String FORMAT_S2 = "%2$s"; + // Maintain selected range state. + // Only accessed from the UI thread. + private static MostRecentSection selected; + // Adapter for the list of recent history entries. - private HistoryAdapter mAdapter; + private CursorAdapter mAdapter; // The view shown by the fragment. private HomeListView mList; @@ -80,6 +91,24 @@ public class HistoryPanel extends HomeFragment { // Callbacks used for the search and favicon cursor loaders private CursorLoaderCallbacks mCursorLoaderCallbacks; + // The time ranges for each section + public enum MostRecentSection { + TODAY, + YESTERDAY, + WEEK, + THIS_MONTH, + MONTH_AGO, + TWO_MONTHS_AGO, + THREE_MONTHS_AGO, + FOUR_MONTHS_AGO, + FIVE_MONTHS_AGO, + MostRecentSection, OLDER_THAN_SIX_MONTHS + }; + + protected interface HistoryUrlProvider { + public String getURL(int position); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.home_history_panel, container, false); @@ -93,9 +122,7 @@ public class HistoryPanel extends HomeFragment { 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(position); - final String url = c.getString(c.getColumnIndexOrThrow(History.URL)); + final String url = ((HistoryUrlProvider) mAdapter).getURL(position); Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM); @@ -186,8 +213,11 @@ public class HistoryPanel extends HomeFragment { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - // Intialize adapter - mAdapter = new HistoryAdapter(getActivity()); + // Reset selection. + selected = MostRecentSection.THIS_MONTH; + + // Initialize adapter + mAdapter = new HistoryHeaderListCursorAdapter(getActivity()); mList.setAdapter(mAdapter); // Create callbacks before the initial loader is started @@ -200,23 +230,6 @@ public class HistoryPanel extends HomeFragment { getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks); } - private static class HistoryCursorLoader extends SimpleCursorLoader { - // Max number of history results - private static final int HISTORY_LIMIT = 100; - private final BrowserDB mDB; - - public HistoryCursorLoader(Context context) { - super(context); - mDB = GeckoProfile.get(context).getDB(); - } - - @Override - public Cursor loadCursor() { - final ContentResolver cr = getContext().getContentResolver(); - return mDB.getRecentHistory(cr, HISTORY_LIMIT); - } - } - private void updateUiFromCursor(Cursor c) { if (c != null && c.getCount() > 0) { if (RestrictedProfiles.isAllowed(getActivity(), Restriction.DISALLOW_CLEAR_HISTORY)) { @@ -314,176 +327,80 @@ public class HistoryPanel extends HomeFragment { return ssb; } - private static class HistoryAdapter extends MultiTypeCursorAdapter { - private static final int ROW_HEADER = 0; - private static final int ROW_STANDARD = 1; + private static void updateRecentSectionOffset(final Context context) { + final long now = System.currentTimeMillis(); + final Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 1); - private static final int[] VIEW_TYPES = new int[] { ROW_STANDARD, ROW_HEADER }; - private static final int[] LAYOUT_TYPES = new int[] { R.layout.home_item_row, R.layout.home_header_row }; + // Calculate the start, end time and display text for the MostRecentSection range. + recentSectionTimeOffsetList.add(MostRecentSection.TODAY.ordinal(), + new MostRecentSectionRange(cal.getTimeInMillis(), now, context.getString(R.string.history_today_section))); + recentSectionTimeOffsetList.add(MostRecentSection.YESTERDAY.ordinal(), + new MostRecentSectionRange(cal.getTimeInMillis() - MS_PER_DAY, cal.getTimeInMillis(), context.getString(R.string.history_yesterday_section))); + recentSectionTimeOffsetList.add(MostRecentSection.WEEK.ordinal(), + new MostRecentSectionRange(cal.getTimeInMillis() - MS_PER_WEEK, now, context.getString(R.string.history_week_section))); - // 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; + // Update the calendar to start of next month. + cal.add(Calendar.MONTH, 1); + cal.set(Calendar.DAY_OF_MONTH, cal.getMinimum(Calendar.DAY_OF_MONTH)); - // The time ranges for each section - private static enum MostRecentSection { - TODAY, - YESTERDAY, - WEEK, - OLDER - }; + // Iterate over the remaining MostRecentSections, to find the start, end and display text. + for (int i = MostRecentSection.THIS_MONTH.ordinal(); i <= MostRecentSection.OLDER_THAN_SIX_MONTHS.ordinal(); i++) { + final long end = cal.getTimeInMillis(); + cal.add(Calendar.MONTH, -1); + final long start = cal.getTimeInMillis(); + final String displayName = (i != MostRecentSection.OLDER_THAN_SIX_MONTHS.ordinal()) + ? cal.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()) + : context.getString(R.string.history_older_section); + recentSectionTimeOffsetList.add(i, new MostRecentSectionRange(start, end, displayName)); + } + } - private final Context mContext; + private static class HistoryCursorLoader extends SimpleCursorLoader { + // Max number of history results + private static final int HISTORY_LIMIT = 100; + private final BrowserDB mDB; - // Maps headers in the list with their respective sections - private final SparseArray mMostRecentSections; - - public HistoryAdapter(Context context) { - super(context, null, VIEW_TYPES, LAYOUT_TYPES); - - mContext = context; - - // Initialize map of history sections - mMostRecentSections = new SparseArray(); + public HistoryCursorLoader(Context context) { + super(context); + mDB = GeckoProfile.get(context).getDB(); } @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)); + public Cursor loadCursor() { + final ContentResolver cr = getContext().getContentResolver(); + updateRecentSectionOffset(getContext()); + MostRecentSectionRange mostRecentSectionRange = recentSectionTimeOffsetList.get(selected.ordinal()); + return mDB.getRecentHistoryBetweenTime(cr, HISTORY_LIMIT, mostRecentSectionRange.start, mostRecentSectionRange.end); } + } - @Override - public int getItemViewType(int position) { - if (mMostRecentSections.get(position) != null) { - return ROW_HEADER; - } + protected static String getMostRecentSectionTitle(MostRecentSection section) { + return recentSectionTimeOffsetList.get(section.ordinal()).displayName; + } - return ROW_STANDARD; - } - - @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) { - loadMostRecentSections(cursor); - Cursor oldCursor = super.swapCursor(cursor); - return oldCursor; - } - - @Override - public void bindView(View view, Context context, int position) { - final int type = getItemViewType(position); - - if (type == ROW_HEADER) { - final MostRecentSection section = mMostRecentSections.get(position); - final TextView row = (TextView) view; - row.setText(getMostRecentSectionTitle(section)); - } else { - // Account for the most recent section headers - position -= getMostRecentSectionsCountBefore(position); - final Cursor c = getCursor(position); - final TwoLinePageRow row = (TwoLinePageRow) view; - row.updateFromCursor(c); + protected static MostRecentSection getMostRecentSectionForTime(long time) { + for (int i = 0; i < MostRecentSection.OLDER_THAN_SIX_MONTHS.ordinal(); i++) { + if (time > recentSectionTimeOffsetList.get(i).start) { + return MostRecentSection.values()[i]; } } - 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); - } + return MostRecentSection.OLDER_THAN_SIX_MONTHS; + } - throw new IllegalStateException("Unrecognized history section"); - } + private static class MostRecentSectionRange { + private final long start; + private final long end; + private final String displayName; - 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) { - // Clear any history sections that may have been loaded before. - mMostRecentSections.clear(); - - if (c == null || !c.moveToFirst()) { - return; - } - - 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(History.DATE_LAST_VISITED)); - final MostRecentSection itemSection = HistoryAdapter.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 MostRecentSectionRange(long start, long end, String displayName) { + this.start = start; + this.end = end; + this.displayName = displayName; } } diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd index f58764daab61..2e2a9c66ee0e 100644 --- a/mobile/android/base/locales/en-US/android_strings.dtd +++ b/mobile/android/base/locales/en-US/android_strings.dtd @@ -55,8 +55,9 @@ - - + + + diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 1a0d17bab3c7..79d2e539bc3f 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -313,6 +313,7 @@ gbjar.sources += [ 'home/BrowserSearch.java', 'home/DynamicPanel.java', 'home/FramePanelLayout.java', + 'home/HistoryHeaderListCursorAdapter.java', 'home/HistoryPanel.java', 'home/HomeAdapter.java', 'home/HomeBanner.java', diff --git a/mobile/android/base/strings.xml.in b/mobile/android/base/strings.xml.in index 05d730d96ef1..37d57e27fc01 100644 --- a/mobile/android/base/strings.xml.in +++ b/mobile/android/base/strings.xml.in @@ -87,8 +87,9 @@ &history_today_section; &history_yesterday_section; - &history_week_section2; - &history_older_section2; + &history_week_section3; + &history_this_month_section; + &history_older_section3; &share; &share_title;