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.app.LoaderManager; 21 import android.content.Context; 22 import android.os.Bundle; 23 import android.os.Handler; 24 import android.service.settings.suggestions.Suggestion; 25 import android.support.annotation.VisibleForTesting; 26 import android.support.annotation.WorkerThread; 27 import android.support.v7.widget.LinearLayoutManager; 28 import android.util.Log; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 33 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 34 import com.android.settings.R; 35 import com.android.settings.core.InstrumentedFragment; 36 import com.android.settings.dashboard.conditional.Condition; 37 import com.android.settings.dashboard.conditional.ConditionManager; 38 import com.android.settings.dashboard.conditional.ConditionManager.ConditionListener; 39 import com.android.settings.dashboard.conditional.FocusRecyclerView; 40 import com.android.settings.dashboard.conditional.FocusRecyclerView.FocusListener; 41 import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; 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.suggestions.SuggestionControllerMixin; 49 import com.android.settingslib.utils.ThreadUtils; 50 51 import java.util.List; 52 53 public class DashboardSummary extends InstrumentedFragment 54 implements CategoryListener, ConditionListener, 55 FocusListener, SuggestionControllerMixin.SuggestionControllerHost { 56 public static final boolean DEBUG = false; 57 private static final boolean DEBUG_TIMING = false; 58 private static final int MAX_WAIT_MILLIS = 3000; 59 private static final String TAG = "DashboardSummary"; 60 61 private static final String STATE_SCROLL_POSITION = "scroll_position"; 62 private static final String STATE_CATEGORIES_CHANGE_CALLED = "categories_change_called"; 63 64 private final Handler mHandler = new Handler(); 65 66 private FocusRecyclerView mDashboard; 67 private DashboardAdapter mAdapter; 68 private SummaryLoader mSummaryLoader; 69 private ConditionManager mConditionManager; 70 private LinearLayoutManager mLayoutManager; 71 private SuggestionControllerMixin mSuggestionControllerMixin; 72 private DashboardFeatureProvider mDashboardFeatureProvider; 73 @VisibleForTesting 74 boolean mIsOnCategoriesChangedCalled; 75 private boolean mOnConditionsChangedCalled; 76 77 private DashboardCategory mStagingCategory; 78 private List<Suggestion> mStagingSuggestions; 79 80 @Override getMetricsCategory()81 public int getMetricsCategory() { 82 return MetricsEvent.DASHBOARD_SUMMARY; 83 } 84 85 @Override onAttach(Context context)86 public void onAttach(Context context) { 87 super.onAttach(context); 88 Log.d(TAG, "Creating SuggestionControllerMixin"); 89 final SuggestionFeatureProvider suggestionFeatureProvider = FeatureFactory 90 .getFactory(context) 91 .getSuggestionFeatureProvider(context); 92 if (suggestionFeatureProvider.isSuggestionEnabled(context)) { 93 mSuggestionControllerMixin = new SuggestionControllerMixin(context, this /* host */, 94 getLifecycle(), suggestionFeatureProvider 95 .getSuggestionServiceComponent()); 96 } 97 } 98 99 @Override getLoaderManager()100 public LoaderManager getLoaderManager() { 101 if (!isAdded()) { 102 return null; 103 } 104 return super.getLoaderManager(); 105 } 106 107 @Override onCreate(Bundle savedInstanceState)108 public void onCreate(Bundle savedInstanceState) { 109 long startTime = System.currentTimeMillis(); 110 super.onCreate(savedInstanceState); 111 Log.d(TAG, "Starting DashboardSummary"); 112 final Activity activity = getActivity(); 113 mDashboardFeatureProvider = FeatureFactory.getFactory(activity) 114 .getDashboardFeatureProvider(activity); 115 116 mSummaryLoader = new SummaryLoader(activity, CategoryKey.CATEGORY_HOMEPAGE); 117 118 mConditionManager = ConditionManager.get(activity, false); 119 getLifecycle().addObserver(mConditionManager); 120 if (savedInstanceState != null) { 121 mIsOnCategoriesChangedCalled = 122 savedInstanceState.getBoolean(STATE_CATEGORIES_CHANGE_CALLED); 123 } 124 if (DEBUG_TIMING) { 125 Log.d(TAG, "onCreate took " + (System.currentTimeMillis() - startTime) + " ms"); 126 } 127 } 128 129 @Override onDestroy()130 public void onDestroy() { 131 mSummaryLoader.release(); 132 super.onDestroy(); 133 } 134 135 @Override onResume()136 public void onResume() { 137 long startTime = System.currentTimeMillis(); 138 super.onResume(); 139 140 ((SettingsDrawerActivity) getActivity()).addCategoryListener(this); 141 mSummaryLoader.setListening(true); 142 final int metricsCategory = getMetricsCategory(); 143 for (Condition c : mConditionManager.getConditions()) { 144 if (c.shouldShow()) { 145 mMetricsFeatureProvider.visible(getContext(), metricsCategory, 146 c.getMetricsConstant()); 147 } 148 } 149 if (DEBUG_TIMING) { 150 Log.d(TAG, "onResume took " + (System.currentTimeMillis() - startTime) + " ms"); 151 } 152 } 153 154 @Override onPause()155 public void onPause() { 156 super.onPause(); 157 158 ((SettingsDrawerActivity) getActivity()).remCategoryListener(this); 159 mSummaryLoader.setListening(false); 160 for (Condition c : mConditionManager.getConditions()) { 161 if (c.shouldShow()) { 162 mMetricsFeatureProvider.hidden(getContext(), c.getMetricsConstant()); 163 } 164 } 165 } 166 167 @Override onWindowFocusChanged(boolean hasWindowFocus)168 public void onWindowFocusChanged(boolean hasWindowFocus) { 169 long startTime = System.currentTimeMillis(); 170 if (hasWindowFocus) { 171 Log.d(TAG, "Listening for condition changes"); 172 mConditionManager.addListener(this); 173 Log.d(TAG, "conditions refreshed"); 174 mConditionManager.refreshAll(); 175 } else { 176 Log.d(TAG, "Stopped listening for condition changes"); 177 mConditionManager.remListener(this); 178 } 179 if (DEBUG_TIMING) { 180 Log.d(TAG, "onWindowFocusChanged took " 181 + (System.currentTimeMillis() - startTime) + " ms"); 182 } 183 } 184 185 @Override onSaveInstanceState(Bundle outState)186 public void onSaveInstanceState(Bundle outState) { 187 super.onSaveInstanceState(outState); 188 if (mLayoutManager == null) { 189 return; 190 } 191 outState.putBoolean(STATE_CATEGORIES_CHANGE_CALLED, mIsOnCategoriesChangedCalled); 192 outState.putInt(STATE_SCROLL_POSITION, mLayoutManager.findFirstVisibleItemPosition()); 193 } 194 195 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle)196 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { 197 long startTime = System.currentTimeMillis(); 198 final View root = inflater.inflate(R.layout.dashboard, container, false); 199 mDashboard = root.findViewById(R.id.dashboard_container); 200 mLayoutManager = new LinearLayoutManager(getContext()); 201 mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); 202 if (bundle != null) { 203 int scrollPosition = bundle.getInt(STATE_SCROLL_POSITION); 204 mLayoutManager.scrollToPosition(scrollPosition); 205 } 206 mDashboard.setLayoutManager(mLayoutManager); 207 mDashboard.setHasFixedSize(true); 208 mDashboard.setListener(this); 209 mDashboard.setItemAnimator(new DashboardItemAnimator()); 210 mAdapter = new DashboardAdapter(getContext(), bundle, 211 mConditionManager.getConditions(), mSuggestionControllerMixin, getLifecycle()); 212 mDashboard.setAdapter(mAdapter); 213 mSummaryLoader.setSummaryConsumer(mAdapter); 214 ActionBarShadowController.attachToRecyclerView( 215 getActivity().findViewById(R.id.search_bar_container), getLifecycle(), mDashboard); 216 rebuildUI(); 217 if (DEBUG_TIMING) { 218 Log.d(TAG, "onCreateView took " 219 + (System.currentTimeMillis() - startTime) + " ms"); 220 } 221 return root; 222 } 223 224 @VisibleForTesting rebuildUI()225 void rebuildUI() { 226 ThreadUtils.postOnBackgroundThread(() -> updateCategory()); 227 } 228 229 @Override onCategoriesChanged()230 public void onCategoriesChanged() { 231 // Bypass rebuildUI() on the first call of onCategoriesChanged, since rebuildUI() happens 232 // in onViewCreated as well when app starts. But, on the subsequent calls we need to 233 // rebuildUI() because there might be some changes to suggestions and categories. 234 if (mIsOnCategoriesChangedCalled) { 235 rebuildUI(); 236 } 237 mIsOnCategoriesChangedCalled = true; 238 } 239 240 @Override onConditionsChanged()241 public void onConditionsChanged() { 242 Log.d(TAG, "onConditionsChanged"); 243 // Bypass refreshing the conditions on the first call of onConditionsChanged. 244 // onConditionsChanged is called immediately everytime we start listening to the conditions 245 // change when we gain window focus. Since the conditions are passed to the adapter's 246 // constructor when we create the view, the first handling is not necessary. 247 // But, on the subsequent calls we need to handle it because there might be real changes to 248 // conditions. 249 if (mOnConditionsChangedCalled) { 250 final boolean scrollToTop = 251 mLayoutManager.findFirstCompletelyVisibleItemPosition() <= 1; 252 mAdapter.setConditions(mConditionManager.getConditions()); 253 if (scrollToTop) { 254 mDashboard.scrollToPosition(0); 255 } 256 } else { 257 mOnConditionsChangedCalled = true; 258 } 259 } 260 261 @Override onSuggestionReady(List<Suggestion> suggestions)262 public void onSuggestionReady(List<Suggestion> suggestions) { 263 mStagingSuggestions = suggestions; 264 mAdapter.setSuggestions(suggestions); 265 if (mStagingCategory != null) { 266 Log.d(TAG, "Category has loaded, setting category from suggestionReady"); 267 mHandler.removeCallbacksAndMessages(null); 268 mAdapter.setCategory(mStagingCategory); 269 } 270 } 271 272 @WorkerThread updateCategory()273 void updateCategory() { 274 final DashboardCategory category = mDashboardFeatureProvider.getTilesForCategory( 275 CategoryKey.CATEGORY_HOMEPAGE); 276 mSummaryLoader.updateSummaryToCache(category); 277 mStagingCategory = category; 278 if (mSuggestionControllerMixin == null) { 279 ThreadUtils.postOnMainThread(() -> mAdapter.setCategory(mStagingCategory)); 280 return; 281 } 282 if (mSuggestionControllerMixin.isSuggestionLoaded()) { 283 Log.d(TAG, "Suggestion has loaded, setting suggestion/category"); 284 ThreadUtils.postOnMainThread(() -> { 285 if (mStagingSuggestions != null) { 286 mAdapter.setSuggestions(mStagingSuggestions); 287 } 288 mAdapter.setCategory(mStagingCategory); 289 }); 290 } else { 291 Log.d(TAG, "Suggestion NOT loaded, delaying setCategory by " + MAX_WAIT_MILLIS + "ms"); 292 mHandler.postDelayed(() -> mAdapter.setCategory(mStagingCategory), MAX_WAIT_MILLIS); 293 } 294 } 295 } 296