Bug 1084062 - Support read/unread state in reading list UI. r=mhaigh
authorSebastian Kaspari <s.kaspari@gmail.com>
Fri, 14 Aug 2015 10:17:40 +0200
changeset 292713 c6ebcbe6189ebca7d33243da8fb0b8a6c9440e63
parent 292712 2d31021f762633b37bf6faaca36322e1616d9da4
child 292714 5b3f31a4970588f2d1b62c6822d570069be2cec4
push id962
push userjlund@mozilla.com
push dateFri, 04 Dec 2015 23:28:54 +0000
treeherdermozilla-release@23a2d286e80f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmhaigh
bugs1084062
milestone43.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 1084062 - Support read/unread state in reading list UI. r=mhaigh
mobile/android/base/db/BrowserContract.java
mobile/android/base/db/LocalReadingListAccessor.java
mobile/android/base/db/ReadingListAccessor.java
mobile/android/base/db/StubBrowserDB.java
mobile/android/base/home/HomeContextMenuInfo.java
mobile/android/base/home/HomeFragment.java
mobile/android/base/home/ReadingListPanel.java
mobile/android/base/home/ReadingListRow.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/resources/drawable/reading_list_indicator_read.xml
mobile/android/base/resources/drawable/reading_list_indicator_unread.xml
mobile/android/base/resources/layout/reading_list_row_view.xml
mobile/android/base/resources/menu/home_contextmenu.xml
mobile/android/base/resources/values/dimens.xml
mobile/android/base/resources/values/styles.xml
mobile/android/base/strings.xml.in
--- a/mobile/android/base/db/BrowserContract.java
+++ b/mobile/android/base/db/BrowserContract.java
@@ -410,17 +410,17 @@ public class BrowserContract {
         // other states (to avoid having to query to pre-fill a ContentValues), but should be ignored.
         public static final int SYNC_CHANGE_NONE = 0;
         public static final int SYNC_CHANGE_UNREAD_CHANGED   = 1 << 0;    // => marked_read_{on,by}, is_unread
         public static final int SYNC_CHANGE_FAVORITE_CHANGED = 1 << 1;    // => is_favorite
         public static final int SYNC_CHANGE_RESOLVED = 1 << 2;            // => is_article, resolved_{url,title}, excerpt, word_count
 
 
         public static final String DEFAULT_SORT_ORDER = CLIENT_LAST_MODIFIED + " DESC";
-        public static final String[] DEFAULT_PROJECTION = new String[] { _ID, URL, TITLE, EXCERPT, WORD_COUNT };
+        public static final String[] DEFAULT_PROJECTION = new String[] { _ID, URL, TITLE, EXCERPT, WORD_COUNT, IS_UNREAD };
 
         // Minimum fields required to create a reading list item.
         public static final String[] REQUIRED_FIELDS = { ReadingListItems.URL, ReadingListItems.TITLE };
 
         // All fields that might be mapped from the DB into a record object.
         public static final String[] ALL_FIELDS = {
                 CommonColumns._ID,
                 URLColumns.URL,
--- a/mobile/android/base/db/LocalReadingListAccessor.java
+++ b/mobile/android/base/db/LocalReadingListAccessor.java
@@ -185,16 +185,24 @@ public class LocalReadingListAccessor im
         values.put(ReadingListItems.MARKED_READ_ON, System.currentTimeMillis());
         values.put(ReadingListItems.IS_UNREAD, 0);
 
         // The ContentProvider will take care of updating the sync metadata.
         cr.update(mReadingListUriWithProfile, values, ReadingListItems._ID + " = " + itemID, null);
     }
 
     @Override
+    public void markAsUnread(ContentResolver cr, long itemID) {
+        final ContentValues values = new ContentValues();
+        values.put(ReadingListItems.IS_UNREAD, 1);
+
+        cr.update(mReadingListUriWithProfile, values, ReadingListItems._ID + " = " + itemID, null);
+    }
+
+    @Override
     public void updateContent(ContentResolver cr, long itemID, String resolvedTitle, String resolvedURL, String excerpt) {
         final ContentValues values = new ContentValues();
         values.put(ReadingListItems.CONTENT_STATUS, ReadingListItems.STATUS_FETCHED_ARTICLE);
         values.put(ReadingListItems.RESOLVED_URL, resolvedURL);
         values.put(ReadingListItems.RESOLVED_TITLE, resolvedTitle);
         values.put(ReadingListItems.EXCERPT, excerpt);
 
         // The ContentProvider will take care of updating the sync metadata.
--- a/mobile/android/base/db/ReadingListAccessor.java
+++ b/mobile/android/base/db/ReadingListAccessor.java
@@ -32,11 +32,12 @@ public interface ReadingListAccessor {
 
     void updateReadingListItem(ContentResolver cr, ContentValues values);
 
     void removeReadingListItemWithURL(ContentResolver cr, String uri);
 
     void registerContentObserver(Context context, ContentObserver observer);
 
     void markAsRead(ContentResolver cr, long itemID);
+    void markAsUnread(ContentResolver cr, long itemID);
     void updateContent(ContentResolver cr, long itemID, String resolvedTitle, String resolvedURL, String excerpt);
     void deleteItem(ContentResolver cr, long itemID);
 }
--- a/mobile/android/base/db/StubBrowserDB.java
+++ b/mobile/android/base/db/StubBrowserDB.java
@@ -69,16 +69,20 @@ class StubReadingListAccessor implements
     public void registerContentObserver(Context context, ContentObserver observer) {
     }
 
     @Override
     public void markAsRead(ContentResolver cr, long itemID) {
     }
 
     @Override
+    public void markAsUnread(ContentResolver cr, long itemID) {
+    }
+
+    @Override
     public void updateContent(ContentResolver cr, long itemID, String resolvedTitle, String resolvedURL, String excerpt) {
     }
 
     @Override
     public void deleteItem(ContentResolver cr, long itemID) {
 
     }
 }
--- a/mobile/android/base/home/HomeContextMenuInfo.java
+++ b/mobile/android/base/home/HomeContextMenuInfo.java
@@ -20,16 +20,17 @@ import android.widget.ListAdapter;
 public class HomeContextMenuInfo extends AdapterContextMenuInfo {
 
     public String url;
     public String title;
     public boolean isFolder;
     public int historyId = -1;
     public int bookmarkId = -1;
     public int readingListItemId = -1;
+    public boolean isUnread;
     public RemoveItemType itemType = null;
 
     // Item type to be handled with "Remove" selection.
     public static enum RemoveItemType {
         BOOKMARKS, HISTORY, READING_LIST
     }
 
     public HomeContextMenuInfo(View targetView, int position, long id) {
--- a/mobile/android/base/home/HomeFragment.java
+++ b/mobile/android/base/home/HomeFragment.java
@@ -150,16 +150,19 @@ public abstract class HomeFragment exten
 
         if (!StringUtils.isShareableUrl(info.url) || GeckoProfile.get(getActivity()).inGuestMode()) {
             menu.findItem(R.id.home_share).setVisible(false);
         }
 
         if (!RestrictedProfiles.isAllowed(view.getContext(), Restriction.DISALLOW_PRIVATE_BROWSING)) {
             menu.findItem(R.id.home_open_private_tab).setVisible(false);
         }
+
+        menu.findItem(R.id.mark_read).setVisible(info.isInReadingList() && info.isUnread);
+        menu.findItem(R.id.mark_unread).setVisible(info.isInReadingList() && !info.isUnread);
     }
 
     @Override
     public boolean onContextItemSelected(MenuItem item) {
         // onContextItemSelected() is first dispatched to the activity and
         // then dispatched to its fragments. Since fragments cannot "override"
         // menu item selection handling, it's better to avoid menu id collisions
         // between the activity and its fragments.
@@ -248,16 +251,32 @@ public abstract class HomeFragment exten
         if (itemId == R.id.home_remove) {
             // For Top Sites grid items, position is required in case item is Pinned.
             final int position = info instanceof TopSitesGridContextMenuInfo ? info.position : -1;
 
             (new RemoveItemByUrlTask(context, info.url, info.itemType, position)).execute();
             return true;
         }
 
+        if (itemId == R.id.mark_read) {
+            GeckoProfile
+                    .get(context)
+                    .getDB()
+                    .getReadingListAccessor()
+                    .markAsRead(context.getContentResolver(), info.id);
+        }
+
+        if (itemId == R.id.mark_unread) {
+            GeckoProfile
+                    .get(context)
+                    .getDB()
+                    .getReadingListAccessor()
+                    .markAsUnread(context.getContentResolver(), info.id);
+        }
+
         return false;
     }
 
     @Override
     public void setUserVisibleHint (boolean isVisibleToUser) {
         if (isVisibleToUser == getUserVisibleHint()) {
             return;
         }
--- a/mobile/android/base/home/ReadingListPanel.java
+++ b/mobile/android/base/home/ReadingListPanel.java
@@ -85,33 +85,45 @@ public class ReadingListPanel extends Ho
 
                 String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
                 url = ReaderModeUtils.getAboutReaderForUrl(url);
 
                 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM);
 
                 // This item is a TwoLinePageRow, so we allow switch-to-tab.
                 mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
+
+                markAsRead(id);
             }
         });
 
         mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
             @Override
             public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
                 final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
                 info.url = cursor.getString(cursor.getColumnIndexOrThrow(ReadingListItems.URL));
                 info.title = cursor.getString(cursor.getColumnIndexOrThrow(ReadingListItems.TITLE));
                 info.readingListItemId = cursor.getInt(cursor.getColumnIndexOrThrow(ReadingListItems._ID));
+                info.isUnread = cursor.getInt(cursor.getColumnIndexOrThrow(ReadingListItems.IS_UNREAD)) == 1;
                 info.itemType = RemoveItemType.READING_LIST;
                 return info;
             }
         });
         registerForContextMenu(mList);
     }
 
+    private void markAsRead(long id) {
+        final Context context = getActivity();
+
+        GeckoProfile.get(context).getDB().getReadingListAccessor().markAsRead(
+            context.getContentResolver(),
+            id
+        );
+    }
+
     @Override
     public void onDestroyView() {
         super.onDestroyView();
         mList = null;
         mTopView = null;
         mEmptyView = null;
     }
 
--- a/mobile/android/base/home/ReadingListRow.java
+++ b/mobile/android/base/home/ReadingListRow.java
@@ -13,26 +13,28 @@ import org.mozilla.gecko.home.TwoLinePag
 import org.mozilla.gecko.util.StringUtils;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
+import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
 public class ReadingListRow extends LinearLayout {
 
     private final Resources resources;
 
     private final TextView title;
     private final TextView excerpt;
     private final TextView readTime;
+    private final ImageView indicator;
 
     // Average reading speed in words per minute.
     private static final int AVERAGE_READING_SPEED = 250;
 
     // Length of average word.
     private static final float AVERAGE_WORD_LENGTH = 5.1f;
 
 
@@ -45,30 +47,37 @@ public class ReadingListRow extends Line
 
         LayoutInflater.from(context).inflate(R.layout.reading_list_row_view, this);
 
         resources = context.getResources();
 
         title = (TextView) findViewById(R.id.title);
         excerpt = (TextView) findViewById(R.id.excerpt);
         readTime = (TextView) findViewById(R.id.read_time);
+        indicator = (ImageView) findViewById(R.id.indicator);
     }
 
     public void updateFromCursor(Cursor cursor) {
         if (cursor == null) {
             return;
         }
 
+        final boolean isUnread = cursor.getInt(cursor.getColumnIndexOrThrow(ReadingListItems.IS_UNREAD)) == 1;
+
         final String url = cursor.getString(cursor.getColumnIndexOrThrow(ReadingListItems.URL));
 
         final String titleText = cursor.getString(cursor.getColumnIndexOrThrow(ReadingListItems.TITLE));
         title.setText(TextUtils.isEmpty(titleText) ? StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url)) : titleText);
+        title.setTextAppearance(getContext(), isUnread ? R.style.Widget_ReadingListRow_Title_Unread : R.style.Widget_ReadingListRow_Title_Read);
 
         final String excerptText = cursor.getString(cursor.getColumnIndexOrThrow(ReadingListItems.EXCERPT));
         excerpt.setText(TextUtils.isEmpty(excerptText) ? url : excerptText);
+        excerpt.setTextAppearance(getContext(), isUnread ? R.style.Widget_ReadingListRow_Title_Unread : R.style.Widget_ReadingListRow_Title_Read);
+
+        indicator.setImageResource(isUnread ? R.drawable.reading_list_indicator_unread : R.drawable.reading_list_indicator_read);
 
         /* Disabled until UX issues are fixed (see bug 1110461).
         final int lengthIndex = cursor.getColumnIndexOrThrow(ReadingListItems.LENGTH);
         final int minutes = getEstimatedReadTime(cursor.getInt(lengthIndex));
         if (minutes <= 60) {
             readTime.setText(resources.getString(R.string.reading_list_time_minutes, minutes));
         } else {
             readTime.setText(resources.getString(R.string.reading_list_time_over_an_hour));
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -386,16 +386,18 @@ size. -->
 <!ENTITY contextmenu_copyurl "Copy Address">
 <!ENTITY contextmenu_edit_bookmark "Edit">
 <!ENTITY contextmenu_subscribe "Subscribe to Page">
 <!ENTITY contextmenu_site_settings "Edit Site Settings">
 <!ENTITY contextmenu_top_sites_edit "Edit">
 <!ENTITY contextmenu_top_sites_pin "Pin Site">
 <!ENTITY contextmenu_top_sites_unpin "Unpin Site">
 <!ENTITY contextmenu_add_search_engine "Add a Search Engine">
+<!ENTITY contextmenu_mark_read "Mark as read">
+<!ENTITY contextmenu_mark_unread "Mark as unread">
 
 <!-- Localization note (doorhanger_login_no_username): This string is used in the save-login doorhanger
      where normally a username would be displayed. In this case, no username was found, and this placeholder
      contains brackets to indicate this is not actually a username, but rather a placeholder -->
 <!ENTITY doorhanger_login_no_username "[No username]">
 <!ENTITY doorhanger_login_edit_title "Edit login">
 <!ENTITY doorhanger_login_edit_username_hint "Username">
 <!ENTITY doorhanger_login_edit_password_hint "Password">
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/reading_list_indicator_read.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/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval"
+    android:useLevel="false">
+
+    <stroke
+        android:width="1dp"
+        android:color="@color/disabled_grey" />
+
+    <solid
+        android:color="@android:color/white" />
+
+    <size
+        android:width="16dp"
+        android:height="16dp" />
+</shape>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/reading_list_indicator_unread.xml
@@ -0,0 +1,14 @@
+<?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/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+
+    <solid android:color="@color/fennec_ui_orange"/>
+
+    <size
+        android:width="16dp"
+        android:height="16dp"/>
+</shape>
--- a/mobile/android/base/resources/layout/reading_list_row_view.xml
+++ b/mobile/android/base/resources/layout/reading_list_row_view.xml
@@ -1,20 +1,25 @@
 <?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/. -->
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
+    <ImageView
+        android:id="@+id/indicator"
+        android:layout_width="64dp"
+        android:layout_height="match_parent"
+        android:scaleType="center" />
+
     <LinearLayout
         android:layout_width="0dip"
         android:layout_height="match_parent"
         android:layout_weight="1"
-        android:paddingLeft="@dimen/reading_list_row_padding_left"
         android:paddingRight="@dimen/reading_list_row_padding_right"
         android:orientation="vertical"
         android:gravity="center_vertical">
 
         <TextView
             android:id="@+id/title"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
--- a/mobile/android/base/resources/menu/home_contextmenu.xml
+++ b/mobile/android/base/resources/menu/home_contextmenu.xml
@@ -24,15 +24,21 @@
           android:title="@string/contextmenu_top_sites_pin"/>
 
     <item android:id="@+id/top_sites_unpin"
           android:title="@string/contextmenu_top_sites_unpin"/>
 
     <item android:id="@+id/home_edit_bookmark"
           android:title="@string/contextmenu_edit_bookmark"/>
 
+    <item android:id="@+id/mark_read"
+          android:title="@string/contextmenu_mark_read" />
+
+    <item android:id="@+id/mark_unread"
+        android:title="@string/contextmenu_mark_unread" />
+
     <item android:id="@+id/home_remove"
           android:title="@string/contextmenu_remove"/>
 
     <item android:id="@+id/home_add_to_launcher"
           android:title="@string/contextmenu_add_to_launcher"/>
 
 </menu>
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -81,17 +81,16 @@
     <!-- Regular page row on about:home -->
     <dimen name="page_row_height">64dp</dimen>
 
     <!-- Group/heading page row on about:home -->
     <dimen name="page_group_height">56dp</dimen>
 
     <!-- Reading list row on about:home -->
     <dimen name="reading_list_row_height">128dp</dimen>
-    <dimen name="reading_list_row_padding_left">15dp</dimen>
     <dimen name="reading_list_row_padding_right">10dp</dimen>
 
     <!-- Remote Tabs static view top padding. Less in landscape on phones. -->
     <dimen name="home_remote_tabs_top_padding">48dp</dimen>
 
     <!-- Remote Tabs Hidden devices row height -->
     <dimen name="home_remote_tabs_hidden_footer_height">40dp</dimen>
 
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -132,23 +132,39 @@
     <style name="Widget.ReadingListRow" />
 
     <style name="Widget.ReadingListRow.Title">
         <item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemTitle</item>
         <item name="android:maxLines">2</item>
         <item name="android:ellipsize">end</item>
     </style>
 
+    <style name="Widget.ReadingListRow.Title.Read" parent="Widget.ReadingListRow.Title">
+        <item name="android:textColor">@color/disabled_grey</item>
+    </style>
+
+    <style name="Widget.ReadingListRow.Title.Unread" parent="Widget.ReadingListRow.Title">
+        <item name="android:textColor">@color/text_and_tabs_tray_grey</item>
+    </style>
+
     <style name="Widget.ReadingListRow.Description">
         <item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemDescription</item>
         <item name="android:maxLines">3</item>
         <item name="android:ellipsize">end</item>
         <item name="android:lineSpacingMultiplier">1.3</item>
     </style>
 
+    <style name="Widget.ReadingListRow.Description.Read" parent="Widget.ReadingListRow.Description">
+        <item name="android:textColor">@color/disabled_grey</item>
+    </style>
+
+    <style name="Widget.ReadingListRow.Description.Unread" parent="Widget.ReadingListRow.Description">
+        <item name="android:textColor">@color/text_and_tabs_tray_grey</item>
+    </style>
+
     <style name="Widget.ReadingListRow.ReadTime">
         <item name="android:textColor">@color/fennec_ui_orange</item>
     </style>
 
     <style name="Widget.BookmarkFolderView" parent="Widget.TwoLinePageRow.Title">
         <item name="android:singleLine">true</item>
         <item name="android:ellipsize">none</item>
         <item name="android:paddingLeft">10dip</item>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -355,16 +355,18 @@
   <string name="contextmenu_copyurl">&contextmenu_copyurl;</string>
   <string name="contextmenu_edit_bookmark">&contextmenu_edit_bookmark;</string>
   <string name="contextmenu_subscribe">&contextmenu_subscribe;</string>
   <string name="contextmenu_site_settings">&contextmenu_site_settings;</string>
   <string name="contextmenu_top_sites_edit">&contextmenu_top_sites_edit;</string>
   <string name="contextmenu_top_sites_pin">&contextmenu_top_sites_pin;</string>
   <string name="contextmenu_top_sites_unpin">&contextmenu_top_sites_unpin;</string>
   <string name="contextmenu_add_search_engine">&contextmenu_add_search_engine;</string>
+  <string name="contextmenu_mark_read">&contextmenu_mark_read;</string>
+  <string name="contextmenu_mark_unread">&contextmenu_mark_unread;</string>
 
   <string name="doorhanger_login_no_username">&doorhanger_login_no_username;</string>
   <string name="doorhanger_login_edit_title">&doorhanger_login_edit_title;</string>
   <string name="doorhanger_login_edit_username_hint">&doorhanger_login_edit_username_hint;</string>
   <string name="doorhanger_login_edit_password_hint">&doorhanger_login_edit_password_hint;</string>
   <string name="doorhanger_login_edit_toggle">&doorhanger_login_edit_toggle;</string>
   <string name="doorhanger_login_edit_toast_error">&doorhanger_login_edit_toast_error;</string>
   <string name="doorhanger_login_select_message">&doorhanger_login_select_message;</string>