Bug 1220928 - Add empty state. r=sebastian
authorChenxia Liu <liuche@mozilla.com>
Tue, 15 Mar 2016 11:45:21 -0700
changeset 291298 5539cc5e598073da1b0d53a35596f00bca92cff5
parent 291297 1ee51352897e5ed2e1d0f1277c35937758c5d42e
child 291299 0242801375876b7f7a711546f543f361282c4ece
push id74545
push userkwierso@gmail.com
push dateFri, 01 Apr 2016 23:05:42 +0000
treeherdermozilla-inbound@c410d4e20586 [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 - Add empty state. r=sebastian MozReview-Commit-ID: Ger04bA0aaC
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
mobile/android/base/resources/layout/home_combined_history_panel.xml
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
@@ -145,16 +145,23 @@ public class CombinedHistoryAdapter exte
             return (clientChildren == null) ? 0 : clientChildren.size();
         } else {
             final int remoteSize = remoteClients.size();
             final int historySize = historyCursor == null ? 0 : historyCursor.getCount();
             return remoteSize + historySize;
         }
     }
 
+    public boolean containsHistory() {
+        if (historyCursor == null) {
+            return false;
+        }
+        return (historyCursor.getCount() > 0);
+    }
+
     @Override
     public void onBindViewHolder(CombinedHistoryItem viewHolder, int position) {
         final ItemType itemType = ItemType.viewTypeToItemType(getItemViewType(position));
         final int localPosition = transformPosition(itemType, position);
 
         switch (itemType) {
             case CLIENT:
                 final CombinedHistoryItem.ClientItem clientItem = (CombinedHistoryItem.ClientItem) viewHolder;
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
@@ -9,49 +9,65 @@ import android.app.AlertDialog;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.support.v4.app.LoaderManager;
 import android.support.v4.content.Loader;
 import android.support.v7.widget.DefaultItemAnimator;
-import android.support.v7.widget.RecyclerView;
+import android.text.SpannableStringBuilder;
+import android.text.TextPaint;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
+import android.text.style.UnderlineSpan;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewStub;
 import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.json.JSONArray;
+import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.Restrictions;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.RemoteClient;
+import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.widget.DividerItemDecoration;
 
 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;
 
+    // String placeholders to mark formatting.
+    private final static String FORMAT_S1 = "%1$s";
+    private final static String FORMAT_S2 = "%2$s";
+
     private CombinedHistoryRecyclerView mRecyclerView;
     private CombinedHistoryAdapter mAdapter;
     private CursorLoaderCallbacks mCursorLoaderCallbacks;
 
     private OnPanelLevelChangeListener.PanelLevel mPanelLevel = OnPanelLevelChangeListener.PanelLevel.PARENT;
     private Button mPanelFooterButton;
+    // Reference to the View to display when there are no results.
+    private View mEmptyView;
 
     public interface OnPanelLevelChangeListener {
         enum PanelLevel {
         PARENT, CHILD
         }
 
         void onPanelLevelChange(PanelLevel level);
     }
@@ -69,19 +85,17 @@ public class CombinedHistoryPanel extend
         mAdapter = new CombinedHistoryAdapter(getContext());
         mRecyclerView.setAdapter(mAdapter);
         mRecyclerView.setItemAnimator(new DefaultItemAnimator());
         mRecyclerView.addItemDecoration(new DividerItemDecoration(getContext()));
         mRecyclerView.setOnHistoryClickedListener(mUrlOpenListener);
         mRecyclerView.setOnPanelLevelChangeListener(new OnLevelChangeListener());
         mPanelFooterButton = (Button) view.findViewById(R.id.clear_history_button);
         mPanelFooterButton.setOnClickListener(new OnFooterButtonClickListener());
-        mPanelFooterButton.setVisibility(View.VISIBLE);
 
-        // TODO: Check if empty state
         // TODO: Handle date headers.
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
         mCursorLoaderCallbacks = new CursorLoaderCallbacks();
     }
@@ -150,40 +164,53 @@ public class CombinedHistoryPanel extend
                 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;
             }
 
+            // Check and set empty state.
+            updateButtonFromLevel(mPanelLevel);
+            updateEmptyView(mAdapter.getItemCount() == 0);
         }
 
         public void onLoaderReset(Loader<Cursor> c) {
             mAdapter.setClients(Collections.<RemoteClient>emptyList());
             mAdapter.setHistory(null);
         }
     }
 
     protected class OnLevelChangeListener implements OnPanelLevelChangeListener {
         @Override
         public void onPanelLevelChange(PanelLevel level) {
-            mPanelLevel = level;
-            switch (mPanelLevel) {
-                case PARENT:
+            updateButtonFromLevel(level);
+        }
+    }
+
+    private void updateButtonFromLevel(OnPanelLevelChangeListener.PanelLevel level) {
+        mPanelLevel = level;
+        switch (level) {
+            case CHILD:
+                mPanelFooterButton.setVisibility(View.VISIBLE);
+                mPanelFooterButton.setText(R.string.home_open_all);
+                break;
+            case PARENT:
+                final boolean historyRestricted = !Restrictions.isAllowed(getActivity(), Restrictable.CLEAR_HISTORY);
+                if (historyRestricted || !mAdapter.containsHistory()) {
+                    mPanelFooterButton.setVisibility(View.GONE);
+                } else {
+                    mPanelFooterButton.setVisibility(View.VISIBLE);
                     mPanelFooterButton.setText(R.string.home_clear_history_button);
-                    break;
-                case CHILD:
-                    mPanelFooterButton.setText(R.string.home_open_all);
-                    break;
-            }
+                }
+                break;
         }
     }
 
     private class OnFooterButtonClickListener implements View.OnClickListener {
         @Override
         public void onClick(View view) {
             switch (mPanelLevel) {
                 case PARENT:
@@ -228,9 +255,100 @@ public class CombinedHistoryPanel extend
                         } catch (JSONException e) {
                             Log.e(LOGTAG, "Error making JSON message to open tabs");
                         }
                     }
                     break;
             }
         }
     }
+
+    private void updateEmptyView(boolean isEmpty) {
+        if (isEmpty) {
+            if (mEmptyView == null) {
+                // Set empty panel view if it needs to be shown and hasn't been inflated.
+                final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub);
+                mEmptyView = emptyViewStub.inflate();
+
+                final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image);
+                emptyIcon.setImageResource(R.drawable.icon_most_recent_empty);
+
+                final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text);
+                emptyText.setText(R.string.home_most_recent_empty);
+
+                final TextView emptyHint = (TextView) mEmptyView.findViewById(R.id.home_empty_hint);
+                final String hintText = getResources().getString(R.string.home_most_recent_emptyhint);
+
+                final SpannableStringBuilder hintBuilder = formatHintText(hintText);
+                if (hintBuilder != null) {
+                    emptyHint.setText(hintBuilder);
+                    emptyHint.setMovementMethod(LinkMovementMethod.getInstance());
+                    emptyHint.setVisibility(View.VISIBLE);
+                }
+
+                if (!Restrictions.isAllowed(getActivity(), Restrictable.PRIVATE_BROWSING)) {
+                    emptyHint.setVisibility(View.GONE);
+                }
+                mEmptyView.setVisibility(View.VISIBLE);
+            } else {
+                if (mEmptyView != null) {
+                    mEmptyView.setVisibility(View.GONE);
+                }
+            }
+        }
+    }
+    /**
+     * Make Span that is clickable, and underlined
+     * between the string markers <code>FORMAT_S1</code> and
+     * <code>FORMAT_S2</code>.
+     *
+     * @param text String to format
+     * @return formatted SpannableStringBuilder, or null if there
+     * is not any text to format.
+     */
+    private SpannableStringBuilder formatHintText(String text) {
+        // Set formatting as marked by string placeholders.
+        final int underlineStart = text.indexOf(FORMAT_S1);
+        final int underlineEnd = text.indexOf(FORMAT_S2);
+
+        // Check that there is text to be formatted.
+        if (underlineStart >= underlineEnd) {
+            return null;
+        }
+
+        final SpannableStringBuilder ssb = new SpannableStringBuilder(text);
+
+        // Set clickable text.
+        final ClickableSpan clickableSpan = new ClickableSpan() {
+            @Override
+            public void onClick(View widget) {
+                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.PANEL, "hint-private-browsing");
+                try {
+                    final JSONObject json = new JSONObject();
+                    json.put("type", "Menu:Open");
+                    EventDispatcher.getInstance().dispatchEvent(json, null);
+                } catch (JSONException e) {
+                    Log.e(LOGTAG, "Error forming JSON for Private Browsing contextual hint", e);
+                }
+            }
+        };
+
+        ssb.setSpan(clickableSpan, 0, text.length(), 0);
+
+        // Remove underlining set by ClickableSpan.
+        final UnderlineSpan noUnderlineSpan = new UnderlineSpan() {
+            @Override
+            public void updateDrawState(TextPaint textPaint) {
+                textPaint.setUnderlineText(false);
+            }
+        };
+
+        ssb.setSpan(noUnderlineSpan, 0, text.length(), 0);
+
+        // Add underlining for "Private Browsing".
+        ssb.setSpan(new UnderlineSpan(), underlineStart, underlineEnd, 0);
+
+        ssb.delete(underlineEnd, underlineEnd + FORMAT_S2.length());
+        ssb.delete(underlineStart, underlineStart + FORMAT_S1.length());
+
+        return ssb;
+    }
 }
--- a/mobile/android/base/resources/layout/home_combined_history_panel.xml
+++ b/mobile/android/base/resources/layout/home_combined_history_panel.xml
@@ -3,16 +3,21 @@
    - 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">
 
+    <ViewStub android:id="@+id/home_empty_view_stub"
+              android:layout="@layout/home_empty_panel"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"/>
+
     <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"/>