1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.dashboard;
18 
19 import android.content.Context;
20 import android.os.Bundle;
21 import android.support.annotation.VisibleForTesting;
22 import android.support.v14.preference.PreferenceFragment;
23 import android.support.v7.preference.Preference;
24 import android.support.v7.preference.PreferenceGroup;
25 import android.support.v7.preference.PreferenceScreen;
26 import android.text.TextUtils;
27 import android.util.Log;
28 
29 import com.android.internal.logging.nano.MetricsProto;
30 import com.android.settings.R;
31 import com.android.settings.core.instrumentation.Instrumentable;
32 import com.android.settings.core.instrumentation.MetricsFeatureProvider;
33 import com.android.settings.core.lifecycle.LifecycleObserver;
34 import com.android.settings.core.lifecycle.events.OnCreate;
35 import com.android.settings.core.lifecycle.events.OnSaveInstanceState;
36 import com.android.settings.overlay.FeatureFactory;
37 
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.List;
41 
42 public class ProgressiveDisclosureMixin implements Preference.OnPreferenceClickListener,
43         LifecycleObserver, OnCreate, OnSaveInstanceState {
44 
45     private static final String TAG = "ProgressiveDisclosure";
46     private static final String STATE_USER_EXPANDED = "state_user_expanded";
47     private static final int DEFAULT_TILE_LIMIT = 300;
48 
49     private final Context mContext;
50     // Collapsed preference sorted by order.
51     private final List<Preference> mCollapsedPrefs = new ArrayList<>();
52     private final MetricsFeatureProvider mMetricsFeatureProvider;
53     private final PreferenceFragment mFragment;
54     private /* final */ ExpandPreference mExpandButton;
55 
56     private int mTileLimit = DEFAULT_TILE_LIMIT;
57     private boolean mUserExpanded;
58 
ProgressiveDisclosureMixin(Context context, PreferenceFragment fragment, boolean keepExpanded)59     public ProgressiveDisclosureMixin(Context context,
60             PreferenceFragment fragment, boolean keepExpanded) {
61         mContext = context;
62         mFragment = fragment;
63         mExpandButton = new ExpandPreference(context);
64         mExpandButton.setOnPreferenceClickListener(this);
65         mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
66         mUserExpanded = keepExpanded;
67     }
68 
69     @Override
onCreate(Bundle savedInstanceState)70     public void onCreate(Bundle savedInstanceState) {
71         if (savedInstanceState != null) {
72             mUserExpanded = savedInstanceState.getBoolean(STATE_USER_EXPANDED, false);
73         }
74     }
75 
76     @Override
onSaveInstanceState(Bundle outState)77     public void onSaveInstanceState(Bundle outState) {
78         outState.putBoolean(STATE_USER_EXPANDED, mUserExpanded);
79     }
80 
81     @Override
onPreferenceClick(Preference preference)82     public boolean onPreferenceClick(Preference preference) {
83         if (preference instanceof ExpandPreference) {
84             final PreferenceScreen screen = mFragment.getPreferenceScreen();
85             if (screen != null) {
86                 screen.removePreference(preference);
87                 for (Preference pref : mCollapsedPrefs) {
88                     screen.addPreference(pref);
89                 }
90                 mCollapsedPrefs.clear();
91                 mUserExpanded = true;
92                 final int metricsCategory;
93                 if (mFragment instanceof Instrumentable) {
94                     metricsCategory = ((Instrumentable) mFragment).getMetricsCategory();
95                 } else {
96                     metricsCategory = MetricsProto.MetricsEvent.VIEW_UNKNOWN;
97                 }
98                 mMetricsFeatureProvider.actionWithSource(mContext, metricsCategory,
99                         MetricsProto.MetricsEvent.ACTION_SETTINGS_ADVANCED_BUTTON_EXPAND);
100             }
101         }
102         return false;
103     }
104 
105     /**
106      * Sets the threshold to start collapsing preferences when there are too many.
107      */
setTileLimit(int limit)108     public void setTileLimit(int limit) {
109         mTileLimit = limit;
110     }
111 
112     /**
113      * Whether the controller is in collapsed state.
114      */
isCollapsed()115     public boolean isCollapsed() {
116         return !mCollapsedPrefs.isEmpty();
117     }
118 
119     /**
120      * Whether the screen should be collapsed.
121      */
shouldCollapse(PreferenceScreen screen)122     public boolean shouldCollapse(PreferenceScreen screen) {
123         return !mUserExpanded && screen.getPreferenceCount() > mTileLimit;
124     }
125 
126     /**
127      * Collapse extra preferences and show a "More" button
128      */
collapse(PreferenceScreen screen)129     public void collapse(PreferenceScreen screen) {
130         final int itemCount = screen.getPreferenceCount();
131         if (!shouldCollapse(screen)) {
132             return;
133         }
134         if (!mCollapsedPrefs.isEmpty()) {
135             Log.w(TAG, "collapsed list should ALWAYS BE EMPTY before collapsing!");
136         }
137 
138         for (int i = itemCount - 1; i >= mTileLimit; i--) {
139             final Preference preference = screen.getPreference(i);
140             addToCollapsedList(preference);
141             screen.removePreference(preference);
142         }
143         screen.addPreference(mExpandButton);
144     }
145 
146     /**
147      * Adds preference to screen. If there are too many preference on screen, adds it to
148      * collapsed list instead.
149      */
addPreference(PreferenceScreen screen, Preference pref)150     public void addPreference(PreferenceScreen screen, Preference pref) {
151         // Either add to screen, or to collapsed list.
152         if (isCollapsed()) {
153             // insert the preference to right position.
154             final int lastPreferenceIndex = screen.getPreferenceCount() - 2;
155             if (lastPreferenceIndex >= 0) {
156                 final Preference lastPreference = screen.getPreference(lastPreferenceIndex);
157                 if (lastPreference.getOrder() > pref.getOrder()) {
158                     // insert to screen and move the last pref to collapsed list.
159                     screen.removePreference(lastPreference);
160                     screen.addPreference(pref);
161                     addToCollapsedList(lastPreference);
162                 } else {
163                     // Insert to collapsed list.
164                     addToCollapsedList(pref);
165                 }
166             } else {
167                 // Couldn't find last preference on screen, just add to collapsed list.
168                 addToCollapsedList(pref);
169             }
170         } else if (shouldCollapse(screen)) {
171             // About to have too many tiles on scree, collapse and add pref to collapsed list.
172             screen.addPreference(pref);
173             collapse(screen);
174         } else {
175             // No need to collapse, add to screen directly.
176             screen.addPreference(pref);
177         }
178     }
179 
180     /**
181      * Removes preference. If the preference is on screen, remove it from screen. If the
182      * preference is in collapsed list, remove it from list.
183      */
removePreference(PreferenceScreen screen, String key)184     public void removePreference(PreferenceScreen screen, String key) {
185         // Try removing from screen.
186         final Preference preference = screen.findPreference(key);
187         if (preference != null) {
188             screen.removePreference(preference);
189             return;
190         }
191         // Didn't find on screen, try removing from collapsed list.
192         for (int i = 0; i < mCollapsedPrefs.size(); i++) {
193             final Preference pref = mCollapsedPrefs.get(i);
194             if (TextUtils.equals(key, pref.getKey())) {
195                 mCollapsedPrefs.remove(pref);
196                 if (mCollapsedPrefs.isEmpty()) {
197                     // Removed last element, remove expand button too.
198                     screen.removePreference(mExpandButton);
199                 } else {
200                     updateExpandButtonSummary();
201                 }
202                 return;
203             }
204         }
205     }
206 
207     /**
208      * Finds preference by key, either from screen or from collapsed list.
209      */
findPreference(PreferenceScreen screen, CharSequence key)210     public Preference findPreference(PreferenceScreen screen, CharSequence key) {
211         Preference preference = screen.findPreference(key);
212         if (preference != null) {
213             return preference;
214         }
215         for (int i = 0; i < mCollapsedPrefs.size(); i++) {
216             final Preference pref = mCollapsedPrefs.get(i);
217             if (TextUtils.equals(key, pref.getKey())) {
218                 return pref;
219             }
220             if (pref instanceof PreferenceGroup) {
221                 final Preference returnedPreference = ((PreferenceGroup) pref).findPreference(key);
222                 if (returnedPreference != null) {
223                     return returnedPreference;
224                 }
225             }
226         }
227         Log.d(TAG, "Cannot find preference with key " + key);
228         return null;
229     }
230 
231     /**
232      * Add preference to collapsed list.
233      */
234     @VisibleForTesting
addToCollapsedList(Preference preference)235     void addToCollapsedList(Preference preference) {
236         // Insert preference based on it's order.
237         int insertionIndex = Collections.binarySearch(mCollapsedPrefs, preference);
238         if (insertionIndex < 0) {
239             insertionIndex = insertionIndex * -1 - 1;
240         }
241         mCollapsedPrefs.add(insertionIndex, preference);
242         updateExpandButtonSummary();
243     }
244 
245     @VisibleForTesting
getCollapsedPrefs()246     List<Preference> getCollapsedPrefs() {
247         return mCollapsedPrefs;
248     }
249 
250     @VisibleForTesting
updateExpandButtonSummary()251     void updateExpandButtonSummary() {
252         final int size = mCollapsedPrefs.size();
253         if (size == 0) {
254             mExpandButton.setSummary(null);
255         } else if (size == 1) {
256             mExpandButton.setSummary(mCollapsedPrefs.get(0).getTitle());
257         } else {
258             CharSequence summary = mCollapsedPrefs.get(0).getTitle();
259             for (int i = 1; i < size; i++) {
260                 final CharSequence nextSummary = mCollapsedPrefs.get(i).getTitle();
261                 if (!TextUtils.isEmpty(nextSummary)) {
262                     summary = mContext.getString(R.string.join_many_items_middle, summary,
263                             nextSummary);
264                 }
265             }
266             mExpandButton.setSummary(summary);
267         }
268     }
269 }
270