Bug 1220928 - Clients and history loading. r=sebastian
☠☠ backed out by 52247b54de2a ☠ ☠
authorChenxia Liu <liuche@mozilla.com>
Wed, 24 Feb 2016 14:27:55 -0800
changeset 290824 ceb669fb17f23763eb4f9cfc53ad74d011e842a5
parent 290823 d64938914b3ed77c22ba4559b01d92907056b438
child 290825 f464c44bf88255e4aafe3fa64900aa13c0905339
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssebastian
bugs1220928
milestone48.0a1
Bug 1220928 - Clients and history loading. r=sebastian MozReview-Commit-ID: 1lCKVQH54C3
mobile/android/base/java/org/mozilla/gecko/RemoteTabsExpandableListAdapter.java
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java
mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
mobile/android/base/resources/layout/home_combined_history_panel.xml
--- a/mobile/android/base/java/org/mozilla/gecko/RemoteTabsExpandableListAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/RemoteTabsExpandableListAdapter.java
@@ -164,17 +164,17 @@ public class RemoteTabsExpandableListAda
         holder.nameView.setText(client.name);
         holder.nameView.setTextColor(ContextCompat.getColor(context, textColorResId));
 
         final long now = System.currentTimeMillis();
 
         // It's OK to access the DB on the main thread here, as we're just
         // getting a string.
         final GeckoProfile profile = GeckoProfile.get(context);
-        holder.lastModifiedView.setText(this.getLastSyncedString(context, now, client.lastModified));
+        holder.lastModifiedView.setText(getLastSyncedString(context, now, client.lastModified));
 
         // These views exists only in some of our group views: they are present
         // for the home panel groups and not for the tabs panel groups.
         // Therefore, we must handle null.
         if (holder.deviceTypeView != null) {
             holder.deviceTypeView.setImageResource(deviceTypeResId);
         }
 
@@ -231,16 +231,16 @@ public class RemoteTabsExpandableListAda
 
     /**
      * Return a relative "Last synced" time span for the given tab record.
      *
      * @param now local time.
      * @param time to format string for.
      * @return string describing time span
      */
-    public String getLastSyncedString(Context context, long now, long time) {
+    public static String getLastSyncedString(Context context, long now, long time) {
         if (new Date(time).before(EARLIEST_VALID_SYNCED_DATE)) {
             return context.getString(R.string.remote_tabs_never_synced);
         }
         final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
         return context.getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
@@ -1,16 +1,18 @@
 /* -*- 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.support.v7.widget.RecyclerView;
+
+import android.content.Context;
 import android.database.Cursor;
-import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.RemoteClient;
 
 import java.util.Collections;
 import java.util.List;
@@ -18,21 +20,46 @@ import java.util.List;
 public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistoryItem> {
     // These ordinal positions are used in CombinedHistoryAdapter as viewType.
     private enum ItemType {
         CLIENT, HISTORY
     }
 
     private List<RemoteClient> remoteClients = Collections.emptyList();
     private Cursor historyCursor;
+    private final Context context;
+
+    public CombinedHistoryAdapter(Context context) {
+        super();
+        this.context = context;
+    }
+
+    public void setClients(List<RemoteClient> clients) {
+        remoteClients = clients;
+        notifyDataSetChanged();
+    }
+
+    public void setHistory(Cursor history) {
+        historyCursor = history;
+        notifyDataSetChanged();
+    }
+
+    private int transformPosition(ItemType type, int position) {
+        if (type == ItemType.CLIENT) {
+            return position;
+        } else {
+            return position - (remoteClients == null ? 0 : remoteClients.size());
+        }
+    }
 
     @Override
     public CombinedHistoryItem onCreateViewHolder(ViewGroup viewGroup, int viewType) {
         final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
         final View view;
+
         if (viewType == ItemType.CLIENT.ordinal()) {
             view = inflater.inflate(R.layout.home_remote_tabs_group, viewGroup, false);
             return new CombinedHistoryItem.ClientItem(view);
         } else {
             view = inflater.inflate(R.layout.home_item_row, viewGroup, false);
             return new CombinedHistoryItem.HistoryItem(view);
         }
     }
@@ -46,11 +73,28 @@ public class CombinedHistoryAdapter exte
     @Override
     public int getItemCount() {
         final int remoteSize = remoteClients.size();
         final int historySize = historyCursor == null ? 0 : historyCursor.getCount();
         return remoteSize + historySize;
     }
 
     @Override
-    public void onBindViewHolder(CombinedHistoryItem viewHolder, int position) {}
+    public void onBindViewHolder(CombinedHistoryItem viewHolder, int position) {
+        final ItemType itemType = ItemType.values()[getItemViewType(position)];
+        final int localPosition = transformPosition(itemType, position);
 
+        switch (itemType) {
+            case CLIENT:
+                final CombinedHistoryItem.ClientItem clientItem = (CombinedHistoryItem.ClientItem) viewHolder;
+                final RemoteClient client = remoteClients.get(localPosition);
+                clientItem.bind(client, context);
+                break;
+
+            case HISTORY:
+                if (historyCursor == null || !historyCursor.moveToPosition(localPosition)) {
+                    throw new IllegalStateException("Couldn't move cursor to position " + localPosition);
+                }
+                ((CombinedHistoryItem.HistoryItem) viewHolder).bind(historyCursor);
+                break;
+        }
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
@@ -1,27 +1,56 @@
 /* -*- 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.support.v4.content.ContextCompat;
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.RemoteTabsExpandableListAdapter;
+import org.mozilla.gecko.db.RemoteClient;
 
 public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
     public CombinedHistoryItem(View view) {
         super(view);
     }
 
     public static class HistoryItem extends CombinedHistoryItem {
         public HistoryItem(View view) {
             super(view);
         }
+
+        public void bind(Cursor historyCursor) {
+            final TwoLinePageRow pageRow = (TwoLinePageRow) this.itemView;
+            pageRow.setShowIcons(true);
+            pageRow.updateFromCursor(historyCursor);
+        }
     }
 
     public static class ClientItem extends CombinedHistoryItem {
+        final TextView nameView;
+        final ImageView deviceTypeView;
+        final TextView lastModifiedView;
+
         public ClientItem(View view) {
             super(view);
+            nameView = (TextView) view.findViewById(R.id.client);
+            deviceTypeView = (ImageView) view.findViewById(R.id.device_type);
+            lastModifiedView = (TextView) view.findViewById(R.id.last_synced);
         }
 
+        public void bind(RemoteClient client, Context context) {
+                this.nameView.setText(client.name);
+                this.nameView.setTextColor(ContextCompat.getColor(context, R.color.placeholder_active_grey));
+                this.deviceTypeView.setImageResource("desktop".equals(client.deviceType) ? R.drawable.sync_desktop : R.drawable.sync_mobile);
+
+                final long now = System.currentTimeMillis();
+                this.lastModifiedView.setText(RemoteTabsExpandableListAdapter.getLastSyncedString(context, now, client.lastModified));
+        }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
@@ -1,12 +1,134 @@
 /* -*- 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.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.support.v4.content.Loader;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.RemoteClient;
+
+import java.util.List;
+
 public class CombinedHistoryPanel extends HomeFragment {
+    private static final String LOGTAG = "GeckoCombinedHistoryPnl";
+    private final int LOADER_ID_HISTORY = 0;
+    private final int LOADER_ID_REMOTE = 1;
+
+    private CombinedHistoryRecyclerView mRecyclerView;
+    private CombinedHistoryAdapter mAdapter;
+    private CursorLoaderCallbacks mCursorLoaderCallbacks;
+
+    // The button view for clearing browsing history.
+    private View mClearHistoryButton;
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.home_combined_history_panel, container, false);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        mRecyclerView = (CombinedHistoryRecyclerView) view.findViewById(R.id.combined_recycler_view);
+        mAdapter = new CombinedHistoryAdapter(getContext());
+        mRecyclerView.setAdapter(mAdapter);
+        mClearHistoryButton = view.findViewById(R.id.clear_history_button);
+        // TODO: link up click handler for clear history button
+        // TODO: Handle date headers.
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        mCursorLoaderCallbacks = new CursorLoaderCallbacks();
+    }
 
     @Override
-    protected void load() {}
+    protected void load() {
+        getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks);
+        getLoaderManager().initLoader(LOADER_ID_REMOTE, null, mCursorLoaderCallbacks);
+    }
+
+    private static class RemoteTabsCursorLoader extends SimpleCursorLoader {
+        private final GeckoProfile mProfile;
+
+        public RemoteTabsCursorLoader(Context context) {
+            super(context);
+            mProfile = GeckoProfile.get(context);
+        }
+
+        @Override
+        public Cursor loadCursor() {
+            return mProfile.getDB().getTabsAccessor().getRemoteTabsCursor(getContext());
+        }
+    }
+
+    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();
+            // TODO: Handle time bracketing by fetching date ranges from cursor
+            return mDB.getRecentHistory(cr, HISTORY_LIMIT);
+        }
+    }
+
+    private class CursorLoaderCallbacks extends TransitionAwareCursorLoaderCallbacks {
+        private BrowserDB mDB;    // Pseudo-final: set in onCreateLoader.
+
+        @Override
+        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+            if (mDB == null) {
+                mDB = GeckoProfile.get(getActivity()).getDB();
+            }
+
+            switch (id) {
+                case LOADER_ID_HISTORY:
+                    return new HistoryCursorLoader(getContext());
+                case LOADER_ID_REMOTE:
+                    return new RemoteTabsCursorLoader(getContext());
+                default:
+                    Log.e(LOGTAG, "Unknown loader id!");
+                    return null;
+            }
+        }
+
+        protected void onLoadFinishedAfterTransitions(Loader<Cursor> loader, Cursor c) {
+            final int loaderId = loader.getId();
+            switch (loaderId) {
+                case LOADER_ID_HISTORY:
+                    mAdapter.setHistory(c);
+                    break;
+
+                case LOADER_ID_REMOTE:
+                    final List<RemoteClient> clients = mDB.getTabsAccessor().getClientsFromCursor(c);
+                    // TODO: Handle hidden clients
+                    mAdapter.setClients(clients);
+
+                    break;
+            }
+
+        }
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java
@@ -36,11 +36,12 @@ public class CombinedHistoryRecyclerView
         setLayoutManager(layoutManager);
     }
 
     @Override
     public void onItemClicked(RecyclerView recyclerView, int position, View v) {}
 
     @Override
     public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {
+        // TODO: open context menu if not a date title
         return showContextMenuForChild(this);
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
@@ -39,16 +39,17 @@ public final class HomeConfig {
      * to a default set of built-in panels. The DYNAMIC panel type is used by
      * third-party services to create panels with varying types of content.
      */
     @RobocopTarget
     public static enum PanelType implements Parcelable {
         TOP_SITES("top_sites", TopSitesPanel.class),
         BOOKMARKS("bookmarks", BookmarksPanel.class),
         HISTORY("history", HistoryPanel.class),
+        COMBINED_HISTORY("combined_history", CombinedHistoryPanel.class),
         REMOTE_TABS("remote_tabs", RemoteTabsPanel.class),
         READING_LIST("reading_list", ReadingListPanel.class),
         RECENT_TABS("recent_tabs", RecentTabsPanel.class),
         DYNAMIC("dynamic", DynamicPanel.class);
 
         private final String mId;
         private final Class<?> mPanelClass;
 
@@ -1589,16 +1590,17 @@ public final class HomeConfig {
     // to open specific panels without querying the active Home Panel
     // configuration. Because they don't consider the active configuration, it
     // is only sensible to do this for built-in panels (and not for dynamic
     // panels).
     private static final String TOP_SITES_PANEL_ID = "4becc86b-41eb-429a-a042-88fe8b5a094e";
     private static final String BOOKMARKS_PANEL_ID = "7f6d419a-cd6c-4e34-b26f-f68b1b551907";
     private static final String READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b";
     private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8";
+    private static final String COMBINED_HISTORY_PANEL_ID = "4d716ce2-e063-486d-9e7c-b190d7b04dc6";
     private static final String RECENT_TABS_PANEL_ID = "5c2601a5-eedc-4477-b297-ce4cef52adf8";
     private static final String REMOTE_TABS_PANEL_ID = "72429afd-8d8b-43d8-9189-14b779c563d0";
 
     private final HomeConfigBackend mBackend;
 
     public HomeConfig(HomeConfigBackend backend) {
         mBackend = backend;
     }
@@ -1629,16 +1631,17 @@ public final class HomeConfig {
     public static int getTitleResourceIdForBuiltinPanelType(PanelType panelType) {
         switch(panelType) {
         case TOP_SITES:
             return R.string.home_top_sites_title;
 
         case BOOKMARKS:
             return R.string.bookmarks_title;
 
+        case COMBINED_HISTORY:
         case HISTORY:
             return R.string.home_history_title;
 
         case REMOTE_TABS:
             return R.string.home_remote_tabs_title;
 
         case READING_LIST:
             return R.string.reading_list_title;
@@ -1657,16 +1660,19 @@ public final class HomeConfig {
             return TOP_SITES_PANEL_ID;
 
         case BOOKMARKS:
             return BOOKMARKS_PANEL_ID;
 
         case HISTORY:
             return HISTORY_PANEL_ID;
 
+        case COMBINED_HISTORY:
+            return COMBINED_HISTORY_PANEL_ID;
+
         case REMOTE_TABS:
             return REMOTE_TABS_PANEL_ID;
 
         case READING_LIST:
             return READING_LIST_PANEL_ID;
 
         case RECENT_TABS:
             return RECENT_TABS_PANEL_ID;
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
@@ -70,16 +70,17 @@ public class HomeConfigPrefsBackend impl
     private State loadDefaultConfig() {
         final ArrayList<PanelConfig> panelConfigs = new ArrayList<PanelConfig>();
 
         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.TOP_SITES,
                                                   EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)));
 
         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.BOOKMARKS));
         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.HISTORY));
+        panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.COMBINED_HISTORY));
 
         // We disable Synced Tabs for guest mode / restricted profiles.
         if (Restrictions.isAllowed(mContext, Restrictable.MODIFY_ACCOUNTS)) {
             panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.REMOTE_TABS));
         }
 
         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.RECENT_TABS));
         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.READING_LIST));
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_combined_history_panel.xml
@@ -0,0 +1,19 @@
+<?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="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical">
+
+    <org.mozilla.gecko.home.CombinedHistoryRecyclerView
+            android:id="@+id/combined_recycler_view"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"/>
+
+    <include layout="@layout/home_history_clear_button"/>
+
+</LinearLayout>