1 /* 2 * Copyright (C) 2014 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.app.Activity; 20 import android.content.Context; 21 import android.os.AsyncTask; 22 import android.os.Bundle; 23 import android.os.Handler; 24 import android.support.annotation.VisibleForTesting; 25 import android.support.v7.widget.LinearLayoutManager; 26 import android.util.Log; 27 import android.view.LayoutInflater; 28 import android.view.View; 29 import android.view.ViewGroup; 30 31 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 32 import com.android.settings.R; 33 import com.android.settings.core.InstrumentedFragment; 34 import com.android.settings.dashboard.conditional.Condition; 35 import com.android.settings.dashboard.conditional.ConditionManager; 36 import com.android.settings.dashboard.conditional.ConditionManager.ConditionListener; 37 import com.android.settings.dashboard.conditional.FocusRecyclerView; 38 import com.android.settings.dashboard.conditional.FocusRecyclerView.FocusListener; 39 import com.android.settings.dashboard.suggestions.SuggestionDismissController; 40 import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; 41 import com.android.settings.dashboard.suggestions.SuggestionsChecks; 42 import com.android.settings.overlay.FeatureFactory; 43 import com.android.settings.widget.ActionBarShadowController; 44 import com.android.settingslib.drawer.CategoryKey; 45 import com.android.settingslib.drawer.DashboardCategory; 46 import com.android.settingslib.drawer.SettingsDrawerActivity; 47 import com.android.settingslib.drawer.SettingsDrawerActivity.CategoryListener; 48 import com.android.settingslib.drawer.Tile; 49 import com.android.settingslib.suggestions.SuggestionList; 50 import com.android.settingslib.suggestions.SuggestionParser; 51 52 import java.util.ArrayList; 53 import java.util.List; 54 55 public class DashboardSummary extends InstrumentedFragment 56 implements CategoryListener, ConditionListener, 57 FocusListener, SuggestionDismissController.Callback { 58 public static final boolean DEBUG = false; 59 private static final boolean DEBUG_TIMING = false; 60 private static final int MAX_WAIT_MILLIS = 700; 61 private static final String TAG = "DashboardSummary"; 62 63 64 private static final String EXTRA_SCROLL_POSITION = "scroll_position"; 65 66 private final Handler mHandler = new Handler(); 67 68 private FocusRecyclerView mDashboard; 69 private DashboardAdapter mAdapter; 70 private SummaryLoader mSummaryLoader; 71 private ConditionManager mConditionManager; 72 private SuggestionParser mSuggestionParser; 73 private LinearLayoutManager mLayoutManager; 74 private SuggestionsChecks mSuggestionsChecks; 75 private DashboardFeatureProvider mDashboardFeatureProvider; 76 private SuggestionFeatureProvider mSuggestionFeatureProvider; 77 private boolean isOnCategoriesChangedCalled; 78 private boolean mOnConditionsChangedCalled; 79 80 @Override getMetricsCategory()81 public int getMetricsCategory() { 82 return MetricsEvent.DASHBOARD_SUMMARY; 83 } 84 85 @Override onCreate(Bundle savedInstanceState)86 public void onCreate(Bundle savedInstanceState) { 87 long startTime = System.currentTimeMillis(); 88 super.onCreate(savedInstanceState); 89 final Activity activity = getActivity(); 90 mDashboardFeatureProvider = FeatureFactory.getFactory(activity) 91 .getDashboardFeatureProvider(activity); 92 mSuggestionFeatureProvider = FeatureFactory.getFactory(activity) 93 .getSuggestionFeatureProvider(activity); 94 95 mSummaryLoader = new SummaryLoader(activity, CategoryKey.CATEGORY_HOMEPAGE); 96 97 mConditionManager = ConditionManager.get(activity, false); 98 getLifecycle().addObserver(mConditionManager); 99 if (mSuggestionFeatureProvider.isSuggestionEnabled(activity)) { 100 mSuggestionParser = new SuggestionParser(activity, 101 mSuggestionFeatureProvider.getSharedPrefs(activity), R.xml.suggestion_ordering); 102 mSuggestionsChecks = new SuggestionsChecks(getContext()); 103 } 104 if (DEBUG_TIMING) { 105 Log.d(TAG, "onCreate took " + (System.currentTimeMillis() - startTime) 106 + " ms"); 107 } 108 } 109 110 @Override onDestroy()111 public void onDestroy() { 112 mSummaryLoader.release(); 113 super.onDestroy(); 114 } 115 116 @Override onResume()117 public void onResume() { 118 long startTime = System.currentTimeMillis(); 119 super.onResume(); 120 121 ((SettingsDrawerActivity) getActivity()).addCategoryListener(this); 122 mSummaryLoader.setListening(true); 123 final int metricsCategory = getMetricsCategory(); 124 for (Condition c : mConditionManager.getConditions()) { 125 if (c.shouldShow()) { 126 mMetricsFeatureProvider.visible(getContext(), metricsCategory, 127 c.getMetricsConstant()); 128 } 129 } 130 if (DEBUG_TIMING) { 131 Log.d(TAG, "onResume took " + (System.currentTimeMillis() - startTime) + " ms"); 132 } 133 } 134 135 @Override onPause()136 public void onPause() { 137 super.onPause(); 138 139 ((SettingsDrawerActivity) getActivity()).remCategoryListener(this); 140 mSummaryLoader.setListening(false); 141 for (Condition c : mConditionManager.getConditions()) { 142 if (c.shouldShow()) { 143 mMetricsFeatureProvider.hidden(getContext(), c.getMetricsConstant()); 144 } 145 } 146 if (!getActivity().isChangingConfigurations()) { 147 mAdapter.onPause(); 148 } 149 } 150 151 @Override onWindowFocusChanged(boolean hasWindowFocus)152 public void onWindowFocusChanged(boolean hasWindowFocus) { 153 long startTime = System.currentTimeMillis(); 154 if (hasWindowFocus) { 155 Log.d(TAG, "Listening for condition changes"); 156 mConditionManager.addListener(this); 157 Log.d(TAG, "conditions refreshed"); 158 mConditionManager.refreshAll(); 159 } else { 160 Log.d(TAG, "Stopped listening for condition changes"); 161 mConditionManager.remListener(this); 162 } 163 if (DEBUG_TIMING) { 164 Log.d(TAG, "onWindowFocusChanged took " 165 + (System.currentTimeMillis() - startTime) + " ms"); 166 } 167 } 168 169 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)170 public View onCreateView(LayoutInflater inflater, ViewGroup container, 171 Bundle savedInstanceState) { 172 return inflater.inflate(R.layout.dashboard, container, false); 173 } 174 175 @Override onSaveInstanceState(Bundle outState)176 public void onSaveInstanceState(Bundle outState) { 177 super.onSaveInstanceState(outState); 178 if (mLayoutManager == null) return; 179 outState.putInt(EXTRA_SCROLL_POSITION, mLayoutManager.findFirstVisibleItemPosition()); 180 if (mAdapter != null) { 181 mAdapter.onSaveInstanceState(outState); 182 } 183 } 184 185 @Override onViewCreated(View view, Bundle bundle)186 public void onViewCreated(View view, Bundle bundle) { 187 long startTime = System.currentTimeMillis(); 188 mDashboard = view.findViewById(R.id.dashboard_container); 189 mLayoutManager = new LinearLayoutManager(getContext()); 190 mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); 191 if (bundle != null) { 192 int scrollPosition = bundle.getInt(EXTRA_SCROLL_POSITION); 193 mLayoutManager.scrollToPosition(scrollPosition); 194 } 195 mDashboard.setLayoutManager(mLayoutManager); 196 mDashboard.setHasFixedSize(true); 197 mDashboard.setListener(this); 198 mAdapter = new DashboardAdapter(getContext(), bundle, mConditionManager.getConditions(), 199 mSuggestionParser, this /* SuggestionDismissController.Callback */); 200 mDashboard.setAdapter(mAdapter); 201 mDashboard.setItemAnimator(new DashboardItemAnimator()); 202 mSummaryLoader.setSummaryConsumer(mAdapter); 203 ActionBarShadowController.attachToRecyclerView( 204 getActivity().findViewById(R.id.search_bar_container), getLifecycle(), mDashboard); 205 206 if (DEBUG_TIMING) { 207 Log.d(TAG, "onViewCreated took " 208 + (System.currentTimeMillis() - startTime) + " ms"); 209 } 210 rebuildUI(); 211 } 212 213 @VisibleForTesting rebuildUI()214 void rebuildUI() { 215 if (!mSuggestionFeatureProvider.isSuggestionEnabled(getContext())) { 216 Log.d(TAG, "Suggestion feature is disabled, skipping suggestion entirely"); 217 updateCategoryAndSuggestion(null /* tiles */); 218 } else { 219 new SuggestionLoader().execute(); 220 // Set categories on their own if loading suggestions takes too long. 221 mHandler.postDelayed(() -> { 222 updateCategoryAndSuggestion(null /* tiles */); 223 }, MAX_WAIT_MILLIS); 224 } 225 } 226 227 @Override onCategoriesChanged()228 public void onCategoriesChanged() { 229 // Bypass rebuildUI() on the first call of onCategoriesChanged, since rebuildUI() happens 230 // in onViewCreated as well when app starts. But, on the subsequent calls we need to 231 // rebuildUI() because there might be some changes to suggestions and categories. 232 if (isOnCategoriesChangedCalled) { 233 rebuildUI(); 234 } 235 isOnCategoriesChangedCalled = true; 236 } 237 238 @Override onConditionsChanged()239 public void onConditionsChanged() { 240 Log.d(TAG, "onConditionsChanged"); 241 // Bypass refreshing the conditions on the first call of onConditionsChanged. 242 // onConditionsChanged is called immediately everytime we start listening to the conditions 243 // change when we gain window focus. Since the conditions are passed to the adapter's 244 // constructor when we create the view, the first handling is not necessary. 245 // But, on the subsequent calls we need to handle it because there might be real changes to 246 // conditions. 247 if (mOnConditionsChangedCalled) { 248 final boolean scrollToTop = 249 mLayoutManager.findFirstCompletelyVisibleItemPosition() <= 1; 250 mAdapter.setConditions(mConditionManager.getConditions()); 251 if (scrollToTop) { 252 mDashboard.scrollToPosition(0); 253 } 254 } else { 255 mOnConditionsChangedCalled = true; 256 } 257 } 258 259 @Override getSuggestionForPosition(int position)260 public Tile getSuggestionForPosition(int position) { 261 return mAdapter.getSuggestion(position); 262 } 263 264 @Override onSuggestionDismissed(Tile suggestion)265 public void onSuggestionDismissed(Tile suggestion) { 266 mAdapter.onSuggestionDismissed(suggestion); 267 } 268 269 private class SuggestionLoader extends AsyncTask<Void, Void, List<Tile>> { 270 @Override doInBackground(Void... params)271 protected List<Tile> doInBackground(Void... params) { 272 final Context context = getContext(); 273 boolean isSmartSuggestionEnabled = 274 mSuggestionFeatureProvider.isSmartSuggestionEnabled(context); 275 final SuggestionList sl = mSuggestionParser.getSuggestions(isSmartSuggestionEnabled); 276 final List<Tile> suggestions = sl.getSuggestions(); 277 278 if (isSmartSuggestionEnabled) { 279 List<String> suggestionIds = new ArrayList<>(suggestions.size()); 280 for (Tile suggestion : suggestions) { 281 suggestionIds.add(mSuggestionFeatureProvider.getSuggestionIdentifier( 282 context, suggestion)); 283 } 284 // TODO: create a Suggestion class to maintain the id and other info 285 mSuggestionFeatureProvider.rankSuggestions(suggestions, suggestionIds); 286 } 287 for (int i = 0; i < suggestions.size(); i++) { 288 Tile suggestion = suggestions.get(i); 289 if (mSuggestionsChecks.isSuggestionComplete(suggestion)) { 290 suggestions.remove(i--); 291 } 292 } 293 if (sl.isExclusiveSuggestionCategory()) { 294 mSuggestionFeatureProvider.filterExclusiveSuggestions(suggestions); 295 } 296 return suggestions; 297 } 298 299 @Override onPostExecute(List<Tile> tiles)300 protected void onPostExecute(List<Tile> tiles) { 301 // tell handler that suggestions were loaded quickly enough 302 mHandler.removeCallbacksAndMessages(null); 303 updateCategoryAndSuggestion(tiles); 304 } 305 } 306 307 @VisibleForTesting updateCategoryAndSuggestion(List<Tile> suggestions)308 void updateCategoryAndSuggestion(List<Tile> suggestions) { 309 final Activity activity = getActivity(); 310 if (activity == null) { 311 return; 312 } 313 314 final DashboardCategory category = mDashboardFeatureProvider.getTilesForCategory( 315 CategoryKey.CATEGORY_HOMEPAGE); 316 mSummaryLoader.updateSummaryToCache(category); 317 if (suggestions != null) { 318 mAdapter.setCategoriesAndSuggestions(category, suggestions); 319 } else { 320 mAdapter.setCategory(category); 321 } 322 } 323 324 } 325