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