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 151369 af3a72f7b78e
parent 151368 84a315d4abda
child 151370 c4a0d7678b34
push idunknown
push userunknown
push dateunknown
reviewersbnicholson
bugs882715
milestone25.0a1
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" />