Bug 882715 - Implement history page in new about:home (r=bnicholson)
authorLucas Rocha <lucasr@mozilla.com>
Mon, 08 Jul 2013 23:02:02 +0100
changeset 143385 af3a72f7b78e
parent 143384 84a315d4abda
child 143386 c4a0d7678b34
push id25130
push userlrocha@mozilla.com
push dateWed, 21 Aug 2013 09:41:27 +0000
treeherdermozilla-central@b2486721572e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbnicholson
bugs882715
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 882715 - Implement history page in new about:home (r=bnicholson)
mobile/android/base/Makefile.in
mobile/android/base/home/HistoryPage.java
mobile/android/base/resources/layout/home_header_row.xml
mobile/android/base/resources/layout/home_list_with_title.xml
mobile/android/base/resources/values-v16/styles.xml
mobile/android/base/resources/values/styles.xml
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -216,16 +216,17 @@ FENNEC_JAVA_FILES = \
   gfx/ViewTransform.java \
   gfx/VirtualLayer.java \
   home/BookmarksListAdapter.java \
   home/BookmarksListView.java \
   home/BookmarksPage.java \
   home/BookmarkFolderView.java \
   home/BookmarkThumbnailView.java \
   home/BrowserSearch.java \
+  home/HistoryPage.java \
   home/HomeFragment.java \
   home/HomeListView.java \
   home/HomePager.java \
   home/HomePagerTabStrip.java \
   home/FadedTextView.java \
   home/FaviconsLoader.java \
   home/ReadingListPage.java \
   home/SearchEngine.java \
@@ -461,16 +462,18 @@ RES_LAYOUT = \
   res/layout/datetime_picker.xml \
   res/layout/doorhanger.xml \
   res/layout/doorhanger_button.xml \
   res/layout/find_in_page_content.xml \
   res/layout/font_size_preference.xml \
   res/layout/gecko_app.xml \
   res/layout/home_bookmarks_page.xml \
   res/layout/home_item_row.xml \
+  res/layout/home_header_row.xml \
+  res/layout/home_list_with_title.xml \
   res/layout/home_search_item_row.xml \
   res/layout/web_app.xml \
   res/layout/launch_app_list.xml \
   res/layout/launch_app_listitem.xml \
   res/layout/menu_action_bar.xml \
   res/layout/menu_item_action_view.xml \
   res/layout/menu_popup.xml \
   res/layout/notification_icon_text.xml \
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/HistoryPage.java
@@ -0,0 +1,359 @@
+/* -*- 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.content.res.Resources;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+import java.util.Date;
+
+/**
+ * Fragment that displays recent history in a ListView.
+ */
+public class HistoryPage extends HomeFragment {
+    // Logging tag name
+    private static final String LOGTAG = "GeckoHistoryPage";
+
+    // Cursor loader ID for history query
+    private static final int HISTORY_LOADER_ID = 0;
+
+    // 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 HistorySection {
+        TODAY,
+        YESTERDAY,
+        WEEK,
+        OLDER
+    };
+
+    // Maps headers in the list with their respective sections
+    private SparseArray<HistorySection> mHistorySections;
+
+    // Adapter for the list of search results
+    private HistoryAdapter mAdapter;
+
+    // The view shown by the fragment.
+    private ListView mList;
+
+    // Callbacks used for the search and favicon cursor loaders
+    private CursorLoaderCallbacks mCursorLoaderCallbacks;
+
+    // Inflater used by the adapter
+    private LayoutInflater mInflater;
+
+    // On URL open listener
+    private OnUrlOpenListener mUrlOpenListener;
+
+    public static HistoryPage newInstance() {
+        return new HistoryPage();
+    }
+
+    public HistoryPage() {
+        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");
+        }
+
+        mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+
+        mHistorySections = null;
+        mInflater = null;
+        mUrlOpenListener = null;
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.home_list_with_title, container, false);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        final TextView title = (TextView) view.findViewById(R.id.title);
+        title.setText(R.string.history_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 -= getHistorySectionsCountBefore(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);
+            }
+        });
+
+        registerForContextMenu(mList);
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        mList = null;
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        // Initialize map of history sections
+        mHistorySections = new SparseArray<HistorySection>();
+
+        // Intialize adapter
+        mAdapter = new HistoryAdapter(getActivity());
+        mList.setAdapter(mAdapter);
+
+        // Create callbacks before the initial loader is started
+        mCursorLoaderCallbacks = new CursorLoaderCallbacks();
+
+        // Reconnect to the loader only if present
+        getLoaderManager().initLoader(HISTORY_LOADER_ID, null, mCursorLoaderCallbacks);
+    }
+
+    private String getHistorySectionTitle(HistorySection section) {
+        final Resources resources = getActivity().getResources();
+
+        switch (section) {
+        case TODAY:
+            return resources.getString(R.string.history_today_section);
+        case YESTERDAY:
+            return resources.getString(R.string.history_yesterday_section);
+        case WEEK:
+            return resources.getString(R.string.history_week_section);
+        case OLDER:
+            return resources.getString(R.string.history_older_section);
+        }
+
+        throw new IllegalStateException("Unrecognized history section");
+    }
+
+    private int getHistorySectionsCountBefore(int position) {
+        // Account for the number headers before the given position
+        int sectionsBefore = 0;
+
+        final int historySectionsCount = mHistorySections.size();
+        for (int i = 0; i < historySectionsCount; i++) {
+            final int sectionPosition = mHistorySections.keyAt(i);
+            if (sectionPosition > position) {
+                break;
+            }
+
+            sectionsBefore++;
+        }
+
+        return sectionsBefore;
+    }
+
+    private HistorySection getHistorySectionForTime(long from, long time) {
+        long delta = from - time;
+
+        if (delta < 0) {
+            return HistorySection.TODAY;
+        }
+
+        if (delta < MS_PER_DAY) {
+            return HistorySection.YESTERDAY;
+        }
+
+        if (delta < MS_PER_WEEK) {
+            return HistorySection.WEEK;
+        }
+
+        return HistorySection.OLDER;
+    }
+
+    private void loadHistorySections(Cursor c) {
+        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();
+        HistorySection section = null;
+
+        do {
+            final int position = c.getPosition();
+            final long time = c.getLong(c.getColumnIndexOrThrow(URLColumns.DATE_LAST_VISITED));
+            final HistorySection itemSection = getHistorySectionForTime(today, time);
+
+            if (section != itemSection) {
+                section = itemSection;
+                mHistorySections.append(position + mHistorySections.size(), section);
+            }
+
+            // Reached the last section, no need to continue
+            if (section == HistorySection.OLDER) {
+                break;
+            }
+        } while (c.moveToNext());
+    }
+
+    private static class HistoryCursorLoader extends SimpleCursorLoader {
+        // Max number of history results
+        private static final int HISTORY_LIMIT = 100;
+
+        public HistoryCursorLoader(Context context) {
+            super(context);
+        }
+
+        @Override
+        public Cursor loadCursor() {
+            final ContentResolver cr = getContext().getContentResolver();
+            return BrowserDB.getRecentHistory(cr, HISTORY_LIMIT);
+        }
+    }
+
+    private class HistoryAdapter extends SimpleCursorAdapter {
+        private static final int ROW_HEADER = 0;
+        private static final int ROW_STANDARD = 1;
+
+        private static final int ROW_TYPE_COUNT = 2;
+
+        public HistoryAdapter(Context context) {
+            super(context, -1, null, new String[] {}, new int[] {});
+        }
+
+        @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 - getHistorySectionsCountBefore(position));
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            if (mHistorySections.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() + mHistorySections.size();
+        }
+
+        @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) mInflater.inflate(R.layout.home_header_row, mList, false);
+                } else {
+                    row = (TextView) convertView;
+                }
+
+                final HistorySection section = mHistorySections.get(position);
+                row.setText(getHistorySectionTitle(section));
+
+                return row;
+            } else {
+                final TwoLinePageRow row;
+                if (convertView == null) {
+                    row = (TwoLinePageRow) mInflater.inflate(R.layout.home_item_row, mList, false);
+                } else {
+                    row = (TwoLinePageRow) convertView;
+                }
+
+                // Account for the search engines
+                position -= getHistorySectionsCountBefore(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 class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
+        @Override
+        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+            return new HistoryCursorLoader(getActivity());
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
+            loadHistorySections(c);
+            mAdapter.swapCursor(c);
+        }
+
+        @Override
+        public void onLoaderReset(Loader<Cursor> loader) {
+            mAdapter.swapCursor(null);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_header_row.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+          style="@style/AboutHome.HeaderItem"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_list_with_title.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="fill_parent"
+              android:layout_height="fill_parent"
+              android:orientation="vertical"
+              android:background="@android:color/white">
+
+    <TextView android:id="@+id/title"
+              style="@style/AboutHome.PageTitle"/>
+
+    <org.mozilla.gecko.home.HomeListView
+            android:id="@+id/list"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"/>
+
+</LinearLayout>
\ No newline at end of file
--- a/mobile/android/base/resources/values-v16/styles.xml
+++ b/mobile/android/base/resources/values-v16/styles.xml
@@ -7,9 +7,13 @@
 
     <style name="Widget.TwoLinePageRow.Title">
         <item name="android:textAppearance">@style/TextAppearance.Widget.TwoLinePageRow.Title</item>
         <item name="android:fontFamily">sans-serif-light</item>
         <item name="android:singleLine">true</item>
         <item name="android:ellipsize">none</item>
     </style>
 
+    <style name="AboutHome.TextAppearance.PageTitle" parent="TextAppearance.Medium">
+        <item name="android:fontFamily">sans-serif-light</item>
+    </style>
+
 </resources>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -455,41 +455,69 @@
     <style name="AboutHome.TextAppearance.Title" parent="TextAppearance">
         <item name="android:textColor">@color/abouthome_section_title</item>
     </style>
 
     <style name="AboutHome.TextAppearance.SubTitle" parent="TextAppearance.Micro">
         <item name="android:textColor">@color/abouthome_section_subtitle</item>
     </style>
 
+    <style name="AboutHome.TextAppearance.Header" parent="TextAppearance.Small">
+        <item name="android:textColor">#ff222222</item>
+    </style>
+
+    <style name="AboutHome.TextAppearance.PageTitle" parent="TextAppearance.Medium" />
+
     <style name="AboutHome.RowItem">
         <item name="android:background">@drawable/action_bar_button</item>
         <item name="android:focusable">true</item>
     </style>
 
     <style name="AboutHome.RowItem.TextRow">
         <item name="android:textAppearance">@style/AboutHome.TextAppearance.Title</item>
         <item name="android:ellipsize">middle</item>
         <item name="android:singleLine">true</item>
     </style>
 
+    <style name="AboutHome.HeaderItem">
+        <item name="android:layout_width">fill_parent</item>
+        <item name="android:layout_height">32dp</item>
+        <item name="android:textAppearance">@style/AboutHome.TextAppearance.Header</item>
+        <item name="android:background">#fff5f7f9</item>
+        <item name="android:focusable">false</item>
+        <item name="android:gravity">center|left</item>
+        <item name="android:paddingLeft">10dip</item>
+        <item name="android:paddingRight">10dip</item>
+    </style>
+
     <style name="AboutHome.LastTabRow" />
 
     <style name="AboutHome.LastTabRow.Title">
         <item name="android:textSize">16sp</item>
         <item name="android:singleLine">true</item>
         <item name="android:textColor">@color/abouthome_section_title</item>
     </style>
 
     <style name="AboutHome.LastTabRow.Url">
         <item name="android:textSize">12sp</item>
         <item name="android:singleLine">true</item>
         <item name="android:textColor">@color/abouthome_section_subtitle</item>
     </style>
 
+    <style name="AboutHome.PageTitle">
+        <item name="android:layout_width">fill_parent</item>
+        <item name="android:layout_height">32dp</item>
+        <item name="android:textAppearance">@style/AboutHome.TextAppearance.PageTitle</item>
+        <item name="android:background">@color/background_light</item>
+        <item name="android:focusable">false</item>
+        <item name="android:gravity">center|left</item>
+        <item name="android:paddingLeft">10dip</item>
+        <item name="android:paddingRight">10dip</item>
+    </style>
+
     <style name="GeckoDialogTitle">
         <item name="android:textAppearance">@android:style/TextAppearance.DialogWindowTitle</item>
     </style>
 
     <style name="GeckoDialogTitle.SubTitle" />
 
     <style name="CrashReporter" />