Bug 1220928 - Clients and history loading. r=sebastian
authorChenxia Liu <liuche@mozilla.com>
Wed, 24 Feb 2016 14:27:55 -0800
changeset 329130 da52ef729a4ba7457bd456f0352f9117ea4cf2fa
parent 329129 c3b0d96c232954b2213768992448ec1391f750ee
child 329131 82af1bb19859da55d3ea73012d02a70d5b428c80
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssebastian
bugs1220928
milestone48.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 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,141 @@
 /* -*- 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.app.LoaderManager;
+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.Collections;
+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 implements LoaderManager.LoaderCallbacks<Cursor> {
+        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;
+            }
+        }
+
+        public void onLoadFinished(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;
+            }
+
+        }
+
+        public void onLoaderReset(Loader<Cursor> c) {
+            mAdapter.setClients(Collections.<RemoteClient>emptyList());
+            mAdapter.setHistory(null);
+        }
+    }
 }
--- 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>