• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.homepage.contextualcards;
18 
19 import static com.android.settings.homepage.contextualcards.ContextualCardLoader.CARD_CONTENT_LOADER_ID;
20 import static com.android.settings.intelligence.ContextualCardProto.ContextualCard.Category.DEFERRED_SETUP_VALUE;
21 import static com.android.settings.intelligence.ContextualCardProto.ContextualCard.Category.SUGGESTION_VALUE;
22 
23 import static java.util.stream.Collectors.groupingBy;
24 
25 import android.app.settings.SettingsEnums;
26 import android.content.Context;
27 import android.os.Bundle;
28 import android.provider.Settings;
29 import android.text.format.DateUtils;
30 import android.util.ArrayMap;
31 import android.util.Log;
32 import android.widget.BaseAdapter;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 import androidx.annotation.VisibleForTesting;
37 import androidx.loader.app.LoaderManager;
38 import androidx.loader.content.Loader;
39 
40 import com.android.settings.homepage.contextualcards.conditional.ConditionalCardController;
41 import com.android.settings.homepage.contextualcards.logging.ContextualCardLogUtils;
42 import com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer;
43 import com.android.settings.overlay.FeatureFactory;
44 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
45 import com.android.settingslib.core.lifecycle.Lifecycle;
46 import com.android.settingslib.core.lifecycle.LifecycleObserver;
47 import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
48 import com.android.settingslib.core.lifecycle.events.OnStart;
49 import com.android.settingslib.core.lifecycle.events.OnStop;
50 
51 import java.util.ArrayList;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.Set;
55 import java.util.TreeSet;
56 import java.util.stream.Collectors;
57 
58 /**
59  * This is a centralized manager of multiple {@link ContextualCardController}.
60  *
61  * {@link ContextualCardManager} first loads data from {@link ContextualCardLoader} and gets back a
62  * list of {@link ContextualCard}. All subclasses of {@link ContextualCardController} are loaded
63  * here, which will then trigger the {@link ContextualCardController} to load its data and listen to
64  * corresponding changes. When every single {@link ContextualCardController} updates its data, the
65  * data will be passed here, then going through some sorting mechanisms. The
66  * {@link ContextualCardController} will end up building a list of {@link ContextualCard} for
67  * {@link ContextualCardsAdapter} and {@link BaseAdapter#notifyDataSetChanged()} will be called to
68  * get the page refreshed.
69  */
70 public class ContextualCardManager implements ContextualCardLoader.CardContentLoaderListener,
71         ContextualCardUpdateListener, LifecycleObserver, OnSaveInstanceState {
72 
73     @VisibleForTesting
74     static final long CARD_CONTENT_LOADER_TIMEOUT_MS = DateUtils.SECOND_IN_MILLIS;
75     @VisibleForTesting
76     static final String KEY_GLOBAL_CARD_LOADER_TIMEOUT = "global_card_loader_timeout_key";
77     @VisibleForTesting
78     static final String KEY_CONTEXTUAL_CARDS = "key_contextual_cards";
79 
80     private static final String TAG = "ContextualCardManager";
81 
82     //The list for Settings Custom Card
83     private static final int[] SETTINGS_CARDS =
84             {ContextualCard.CardType.CONDITIONAL, ContextualCard.CardType.LEGACY_SUGGESTION};
85 
86     private final Context mContext;
87     private final Lifecycle mLifecycle;
88     private final List<LifecycleObserver> mLifecycleObservers;
89     private ContextualCardUpdateListener mListener;
90 
91     @VisibleForTesting
92     final ControllerRendererPool mControllerRendererPool;
93     @VisibleForTesting
94     final List<ContextualCard> mContextualCards;
95     @VisibleForTesting
96     long mStartTime;
97     @VisibleForTesting
98     boolean mIsFirstLaunch;
99     @VisibleForTesting
100     List<String> mSavedCards;
101 
ContextualCardManager(Context context, Lifecycle lifecycle, Bundle savedInstanceState)102     public ContextualCardManager(Context context, Lifecycle lifecycle, Bundle savedInstanceState) {
103         mContext = context;
104         mLifecycle = lifecycle;
105         mContextualCards = new ArrayList<>();
106         mLifecycleObservers = new ArrayList<>();
107         mControllerRendererPool = new ControllerRendererPool();
108         mLifecycle.addObserver(this);
109         if (savedInstanceState == null) {
110             mIsFirstLaunch = true;
111             mSavedCards = null;
112         } else {
113             mSavedCards = savedInstanceState.getStringArrayList(KEY_CONTEXTUAL_CARDS);
114         }
115         //for data provided by Settings
116         for (@ContextualCard.CardType int cardType : SETTINGS_CARDS) {
117             setupController(cardType);
118         }
119     }
120 
loadContextualCards(LoaderManager loaderManager)121     void loadContextualCards(LoaderManager loaderManager) {
122         mStartTime = System.currentTimeMillis();
123         final CardContentLoaderCallbacks cardContentLoaderCallbacks =
124                 new CardContentLoaderCallbacks(mContext);
125         cardContentLoaderCallbacks.setListener(this);
126         // Use the cached data when navigating back to the first page and upon screen rotation.
127         loaderManager.initLoader(CARD_CONTENT_LOADER_ID, null /* bundle */,
128                 cardContentLoaderCallbacks);
129     }
130 
loadCardControllers()131     private void loadCardControllers() {
132         for (ContextualCard card : mContextualCards) {
133             setupController(card.getCardType());
134         }
135     }
136 
137     @VisibleForTesting
setupController(@ontextualCard.CardType int cardType)138     void setupController(@ContextualCard.CardType int cardType) {
139         final ContextualCardController controller = mControllerRendererPool.getController(mContext,
140                 cardType);
141         if (controller == null) {
142             Log.w(TAG, "Cannot find ContextualCardController for type " + cardType);
143             return;
144         }
145         controller.setCardUpdateListener(this);
146         if (controller instanceof LifecycleObserver && !mLifecycleObservers.contains(controller)) {
147             mLifecycleObservers.add((LifecycleObserver) controller);
148             mLifecycle.addObserver((LifecycleObserver) controller);
149         }
150     }
151 
152     @VisibleForTesting
sortCards(List<ContextualCard> cards)153     List<ContextualCard> sortCards(List<ContextualCard> cards) {
154         //take mContextualCards as the source and do the ranking based on the rule.
155         return cards.stream()
156                 .sorted((c1, c2) -> Double.compare(c2.getRankingScore(), c1.getRankingScore()))
157                 .collect(Collectors.toList());
158     }
159 
160     @Override
onContextualCardUpdated(Map<Integer, List<ContextualCard>> updateList)161     public void onContextualCardUpdated(Map<Integer, List<ContextualCard>> updateList) {
162         final Set<Integer> cardTypes = updateList.keySet();
163         //Remove the existing data that matches the certain cardType before inserting new data.
164         List<ContextualCard> cardsToKeep;
165 
166         // We are not sure how many card types will be in the database, so when the list coming
167         // from the database is empty (e.g. no eligible cards/cards are dismissed), we cannot
168         // assign a specific card type for its map which is sending here. Thus, we assume that
169         // except Conditional cards, all other cards are from the database. So when the map sent
170         // here is empty, we only keep Conditional cards.
171         if (cardTypes.isEmpty()) {
172             final Set<Integer> conditionalCardTypes = new TreeSet() {{
173                 add(ContextualCard.CardType.CONDITIONAL);
174                 add(ContextualCard.CardType.CONDITIONAL_HEADER);
175                 add(ContextualCard.CardType.CONDITIONAL_FOOTER);
176             }};
177             cardsToKeep = mContextualCards.stream()
178                     .filter(card -> conditionalCardTypes.contains(card.getCardType()))
179                     .collect(Collectors.toList());
180         } else {
181             cardsToKeep = mContextualCards.stream()
182                     .filter(card -> !cardTypes.contains(card.getCardType()))
183                     .collect(Collectors.toList());
184         }
185 
186         final List<ContextualCard> allCards = new ArrayList<>();
187         allCards.addAll(cardsToKeep);
188         allCards.addAll(
189                 updateList.values().stream().flatMap(List::stream).collect(Collectors.toList()));
190 
191         //replace with the new data
192         mContextualCards.clear();
193         final List<ContextualCard> sortedCards = sortCards(allCards);
194         mContextualCards.addAll(getCardsWithViewType(sortedCards));
195 
196         loadCardControllers();
197 
198         if (mListener != null) {
199             final Map<Integer, List<ContextualCard>> cardsToUpdate = new ArrayMap<>();
200             cardsToUpdate.put(ContextualCard.CardType.DEFAULT, mContextualCards);
201             mListener.onContextualCardUpdated(cardsToUpdate);
202         }
203     }
204 
205     @Override
onFinishCardLoading(List<ContextualCard> cards)206     public void onFinishCardLoading(List<ContextualCard> cards) {
207         final long loadTime = System.currentTimeMillis() - mStartTime;
208         Log.d(TAG, "Total loading time = " + loadTime);
209 
210         final List<ContextualCard> cardsToKeep = getCardsToKeep(cards);
211 
212         final MetricsFeatureProvider metricsFeatureProvider =
213                 FeatureFactory.getFactory(mContext).getMetricsFeatureProvider();
214 
215         //navigate back to the homepage, screen rotate or after card dismissal
216         if (!mIsFirstLaunch) {
217             onContextualCardUpdated(cardsToKeep.stream()
218                     .collect(groupingBy(ContextualCard::getCardType)));
219             metricsFeatureProvider.action(mContext,
220                     SettingsEnums.ACTION_CONTEXTUAL_CARD_SHOW,
221                     ContextualCardLogUtils.buildCardListLog(cardsToKeep));
222             return;
223         }
224 
225         final long timeoutLimit = getCardLoaderTimeout();
226         if (loadTime <= timeoutLimit) {
227             onContextualCardUpdated(cards.stream()
228                     .collect(groupingBy(ContextualCard::getCardType)));
229             metricsFeatureProvider.action(mContext,
230                     SettingsEnums.ACTION_CONTEXTUAL_CARD_SHOW,
231                     ContextualCardLogUtils.buildCardListLog(cards));
232         } else {
233             // log timeout occurrence
234             metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN,
235                     SettingsEnums.ACTION_CONTEXTUAL_CARD_LOAD_TIMEOUT,
236                     SettingsEnums.SETTINGS_HOMEPAGE,
237                     null /* key */, (int) loadTime /* value */);
238         }
239         //only log homepage display upon a fresh launch
240         final long totalTime = System.currentTimeMillis() - mStartTime;
241         metricsFeatureProvider.action(mContext,
242                 SettingsEnums.ACTION_CONTEXTUAL_HOME_SHOW, (int) totalTime);
243 
244         mIsFirstLaunch = false;
245     }
246 
247     @Override
onSaveInstanceState(Bundle outState)248     public void onSaveInstanceState(Bundle outState) {
249         final ArrayList<String> cards = mContextualCards.stream()
250                 .map(ContextualCard::getName)
251                 .collect(Collectors.toCollection(ArrayList::new));
252 
253         outState.putStringArrayList(KEY_CONTEXTUAL_CARDS, cards);
254     }
255 
onWindowFocusChanged(boolean hasWindowFocus)256     public void onWindowFocusChanged(boolean hasWindowFocus) {
257         // Duplicate a list to avoid java.util.ConcurrentModificationException.
258         final List<ContextualCard> cards = new ArrayList<>(mContextualCards);
259         boolean hasConditionController = false;
260         for (ContextualCard card : cards) {
261             final ContextualCardController controller = getControllerRendererPool()
262                     .getController(mContext, card.getCardType());
263             if (controller instanceof ConditionalCardController) {
264                 hasConditionController = true;
265             }
266             if (hasWindowFocus && controller instanceof OnStart) {
267                 ((OnStart) controller).onStart();
268             }
269             if (!hasWindowFocus && controller instanceof OnStop) {
270                 ((OnStop) controller).onStop();
271             }
272         }
273         // Conditional cards will always be refreshed whether or not there are conditional cards
274         // in the homepage.
275         if (!hasConditionController) {
276             final ContextualCardController controller = getControllerRendererPool()
277                     .getController(mContext, ContextualCard.CardType.CONDITIONAL);
278             if (hasWindowFocus && controller instanceof OnStart) {
279                 ((OnStart) controller).onStart();
280             }
281             if (!hasWindowFocus && controller instanceof OnStop) {
282                 ((OnStop) controller).onStop();
283             }
284         }
285     }
286 
getControllerRendererPool()287     public ControllerRendererPool getControllerRendererPool() {
288         return mControllerRendererPool;
289     }
290 
setListener(ContextualCardUpdateListener listener)291     void setListener(ContextualCardUpdateListener listener) {
292         mListener = listener;
293     }
294 
295     @VisibleForTesting
getCardsWithViewType(List<ContextualCard> cards)296     List<ContextualCard> getCardsWithViewType(List<ContextualCard> cards) {
297         if (cards.isEmpty()) {
298             return cards;
299         }
300 
301         final List<ContextualCard> result = getCardsWithDeferredSetupViewType(cards);
302         return getCardsWithSuggestionViewType(result);
303     }
304 
305     @VisibleForTesting
getCardLoaderTimeout()306     long getCardLoaderTimeout() {
307         // Return the timeout limit if Settings.Global has the KEY_GLOBAL_CARD_LOADER_TIMEOUT key,
308         // else return default timeout.
309         return Settings.Global.getLong(mContext.getContentResolver(),
310                 KEY_GLOBAL_CARD_LOADER_TIMEOUT, CARD_CONTENT_LOADER_TIMEOUT_MS);
311     }
312 
getCardsWithSuggestionViewType(List<ContextualCard> cards)313     private List<ContextualCard> getCardsWithSuggestionViewType(List<ContextualCard> cards) {
314         // Shows as half cards if 2 suggestion type of cards are next to each other.
315         // Shows as full card if 1 suggestion type of card lives alone.
316         final List<ContextualCard> result = new ArrayList<>(cards);
317         for (int index = 1; index < result.size(); index++) {
318             final ContextualCard previous = result.get(index - 1);
319             final ContextualCard current = result.get(index);
320             if (current.getCategory() == SUGGESTION_VALUE
321                     && previous.getCategory() == SUGGESTION_VALUE) {
322                 result.set(index - 1, previous.mutate().setViewType(
323                         SliceContextualCardRenderer.VIEW_TYPE_HALF_WIDTH).build());
324                 result.set(index, current.mutate().setViewType(
325                         SliceContextualCardRenderer.VIEW_TYPE_HALF_WIDTH).build());
326                 index++;
327             }
328         }
329         return result;
330     }
331 
getCardsWithDeferredSetupViewType(List<ContextualCard> cards)332     private List<ContextualCard> getCardsWithDeferredSetupViewType(List<ContextualCard> cards) {
333         // Find the deferred setup card and assign it with proper view type.
334         // Reason: The returned card list will mix deferred setup card and other suggestion cards
335         // after device running 1 days.
336         final List<ContextualCard> result = new ArrayList<>(cards);
337         for (int index = 0; index < result.size(); index++) {
338             final ContextualCard card = cards.get(index);
339             if (card.getCategory() == DEFERRED_SETUP_VALUE) {
340                 result.set(index, card.mutate().setViewType(
341                         SliceContextualCardRenderer.VIEW_TYPE_DEFERRED_SETUP).build());
342                 return result;
343             }
344         }
345         return result;
346     }
347 
348     @VisibleForTesting
getCardsToKeep(List<ContextualCard> cards)349     List<ContextualCard> getCardsToKeep(List<ContextualCard> cards) {
350         if (mSavedCards != null) {
351             //screen rotate
352             final List<ContextualCard> cardsToKeep = cards.stream()
353                     .filter(card -> mSavedCards.contains(card.getName()))
354                     .collect(Collectors.toList());
355             mSavedCards = null;
356             return cardsToKeep;
357         } else {
358             //navigate back to the homepage or after dismissing a card
359             return cards.stream()
360                     .filter(card -> mContextualCards.contains(card))
361                     .collect(Collectors.toList());
362         }
363     }
364 
365     static class CardContentLoaderCallbacks implements
366             LoaderManager.LoaderCallbacks<List<ContextualCard>> {
367 
368         private Context mContext;
369         private ContextualCardLoader.CardContentLoaderListener mListener;
370 
CardContentLoaderCallbacks(Context context)371         CardContentLoaderCallbacks(Context context) {
372             mContext = context.getApplicationContext();
373         }
374 
setListener(ContextualCardLoader.CardContentLoaderListener listener)375         protected void setListener(ContextualCardLoader.CardContentLoaderListener listener) {
376             mListener = listener;
377         }
378 
379         @NonNull
380         @Override
onCreateLoader(int id, @Nullable Bundle bundle)381         public Loader<List<ContextualCard>> onCreateLoader(int id, @Nullable Bundle bundle) {
382             if (id == CARD_CONTENT_LOADER_ID) {
383                 return new ContextualCardLoader(mContext);
384             } else {
385                 throw new IllegalArgumentException("Unknown loader id: " + id);
386             }
387         }
388 
389         @Override
onLoadFinished(@onNull Loader<List<ContextualCard>> loader, List<ContextualCard> contextualCards)390         public void onLoadFinished(@NonNull Loader<List<ContextualCard>> loader,
391                 List<ContextualCard> contextualCards) {
392             if (mListener != null) {
393                 mListener.onFinishCardLoading(contextualCards);
394             }
395         }
396 
397         @Override
onLoaderReset(@onNull Loader<List<ContextualCard>> loader)398         public void onLoaderReset(@NonNull Loader<List<ContextualCard>> loader) {
399 
400         }
401     }
402 }
403