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.conditional;
18 
19 import android.content.Context;
20 import android.util.ArrayMap;
21 
22 import androidx.annotation.VisibleForTesting;
23 
24 import com.android.settings.homepage.contextualcards.ContextualCard;
25 import com.android.settings.homepage.contextualcards.ContextualCardController;
26 import com.android.settings.homepage.contextualcards.ContextualCardUpdateListener;
27 import com.android.settingslib.core.lifecycle.LifecycleObserver;
28 import com.android.settingslib.core.lifecycle.events.OnStart;
29 import com.android.settingslib.core.lifecycle.events.OnStop;
30 
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.stream.Collectors;
36 
37 /**
38  * This controller triggers the loading of conditional cards and monitors state changes to
39  * update the homepage.
40  */
41 public class ConditionContextualCardController implements ContextualCardController,
42         ConditionListener, LifecycleObserver, OnStart, OnStop {
43     public static final int EXPANDING_THRESHOLD = 0;
44 
45     private static final double UNSUPPORTED_RANKING = -99999.0;
46     private static final String TAG = "ConditionCtxCardCtrl";
47     private static final String CONDITION_FOOTER = "condition_footer";
48     private static final String CONDITION_HEADER = "condition_header";
49 
50     private final Context mContext;
51     private final ConditionManager mConditionManager;
52 
53     private ContextualCardUpdateListener mListener;
54     private boolean mIsExpanded;
55 
ConditionContextualCardController(Context context)56     public ConditionContextualCardController(Context context) {
57         mContext = context;
58         mConditionManager = new ConditionManager(context.getApplicationContext(), this);
59         mConditionManager.startMonitoringStateChange();
60     }
61 
setIsExpanded(boolean isExpanded)62     public void setIsExpanded(boolean isExpanded) {
63         mIsExpanded = isExpanded;
64     }
65 
66     @Override
setCardUpdateListener(ContextualCardUpdateListener listener)67     public void setCardUpdateListener(ContextualCardUpdateListener listener) {
68         mListener = listener;
69     }
70 
71     @Override
getCardType()72     public int getCardType() {
73         return ContextualCard.CardType.CONDITIONAL;
74     }
75 
76     @Override
onStart()77     public void onStart() {
78         mConditionManager.startMonitoringStateChange();
79     }
80 
81     @Override
onStop()82     public void onStop() {
83         mConditionManager.stopMonitoringStateChange();
84     }
85 
86     @Override
onPrimaryClick(ContextualCard contextualCard)87     public void onPrimaryClick(ContextualCard contextualCard) {
88         final ConditionalContextualCard card = (ConditionalContextualCard) contextualCard;
89         mConditionManager.onPrimaryClick(mContext, card.getConditionId());
90     }
91 
92     @Override
onActionClick(ContextualCard contextualCard)93     public void onActionClick(ContextualCard contextualCard) {
94         final ConditionalContextualCard card = (ConditionalContextualCard) contextualCard;
95         mConditionManager.onActionClick(card.getConditionId());
96     }
97 
98     @Override
onDismissed(ContextualCard contextualCard)99     public void onDismissed(ContextualCard contextualCard) {
100 
101     }
102 
103     @Override
onConditionsChanged()104     public void onConditionsChanged() {
105         if (mListener == null) {
106             return;
107         }
108         final List<ContextualCard> conditionCards = mConditionManager.getDisplayableCards();
109         final Map<Integer, List<ContextualCard>> conditionalCards =
110                 buildConditionalCardsWithFooterOrHeader(conditionCards);
111         mListener.onContextualCardUpdated(conditionalCards);
112     }
113 
114     /**
115      * According to conditional cards, build a map that includes conditional cards, header card and
116      * footer card.
117      *
118      * Rules:
119      * - The last one of conditional cards will be displayed as a full-width card if the size of
120      * conditional cards is odd number. The rest will be displayed as a half-width card.
121      * - By default conditional cards will be collapsed if there are more than TWO cards.
122      *
123      * For examples:
124      * - Only one conditional card: Returns a map that contains a full-width conditional card,
125      * no header card and no footer card.
126      * <p>Map{(CONDITIONAL, conditionCards), (CONDITIONAL_FOOTER, EMPTY_LIST), (CONDITIONAL_HEADER,
127      * EMPTY_LIST)}</p>
128      * - Two conditional cards: Returns a map that contains two half-width conditional cards,
129      * no header card and no footer card.
130      * <p>Map{(CONDITIONAL, conditionCards), (CONDITIONAL_FOOTER, EMPTY_LIST), (CONDITIONAL_HEADER,
131      * EMPTY_LIST)}</p>
132      * - Three conditional cards or above: By default, returns a map that contains no conditional
133      * card, one header card and no footer card. If conditional cards are expanded, will returns a
134      * map that contains three conditional cards, no header card and one footer card.
135      * If expanding conditional cards:
136      * <p>Map{(CONDITIONAL, conditionCards), (CONDITIONAL_FOOTER, footerCards), (CONDITIONAL_HEADER,
137      * EMPTY_LIST)}</p>
138      * If collapsing conditional cards:
139      * <p>Map{(CONDITIONAL, EMPTY_LIST), (CONDITIONAL_FOOTER, EMPTY_LIST), (CONDITIONAL_HEADER,
140      * headerCards)}</p>
141      *
142      * @param conditionCards A list of conditional cards that are from {@link
143      * ConditionManager#getDisplayableCards}
144      * @return A map contained three types of lists
145      */
146     @VisibleForTesting
buildConditionalCardsWithFooterOrHeader( List<ContextualCard> conditionCards)147     Map<Integer, List<ContextualCard>> buildConditionalCardsWithFooterOrHeader(
148             List<ContextualCard> conditionCards) {
149         final Map<Integer, List<ContextualCard>> conditionalCards = new ArrayMap<>();
150         conditionalCards.put(ContextualCard.CardType.CONDITIONAL,
151                 getExpandedConditionalCards(conditionCards));
152         conditionalCards.put(ContextualCard.CardType.CONDITIONAL_FOOTER,
153                 getConditionalFooterCard(conditionCards));
154         conditionalCards.put(ContextualCard.CardType.CONDITIONAL_HEADER,
155                 getConditionalHeaderCard(conditionCards));
156         return conditionalCards;
157     }
158 
getExpandedConditionalCards(List<ContextualCard> conditionCards)159     private List<ContextualCard> getExpandedConditionalCards(List<ContextualCard> conditionCards) {
160         if (conditionCards.isEmpty() || (conditionCards.size() > EXPANDING_THRESHOLD
161                 && !mIsExpanded)) {
162             return Collections.EMPTY_LIST;
163         }
164         final List<ContextualCard> expandedCards = conditionCards.stream().collect(
165                 Collectors.toList());
166         final boolean isOddNumber = expandedCards.size() % 2 == 1;
167         if (isOddNumber) {
168             final int lastIndex = expandedCards.size() - 1;
169             final ConditionalContextualCard card =
170                     (ConditionalContextualCard) expandedCards.get(lastIndex);
171             expandedCards.set(lastIndex, card.mutate().setViewType(
172                     ConditionContextualCardRenderer.VIEW_TYPE_FULL_WIDTH).build());
173         }
174         return expandedCards;
175     }
176 
getConditionalFooterCard(List<ContextualCard> conditionCards)177     private List<ContextualCard> getConditionalFooterCard(List<ContextualCard> conditionCards) {
178         if (!conditionCards.isEmpty() && mIsExpanded
179                 && conditionCards.size() > EXPANDING_THRESHOLD) {
180             final List<ContextualCard> footerCards = new ArrayList<>();
181             footerCards.add(new ConditionFooterContextualCard.Builder()
182                     .setName(CONDITION_FOOTER)
183                     .setRankingScore(UNSUPPORTED_RANKING)
184                     .setViewType(ConditionFooterContextualCardRenderer.VIEW_TYPE)
185                     .build());
186             return footerCards;
187         }
188         return Collections.EMPTY_LIST;
189     }
190 
getConditionalHeaderCard(List<ContextualCard> conditionCards)191     private List<ContextualCard> getConditionalHeaderCard(List<ContextualCard> conditionCards) {
192         if (!conditionCards.isEmpty() && !mIsExpanded
193                 && conditionCards.size() > EXPANDING_THRESHOLD) {
194             final List<ContextualCard> headerCards = new ArrayList<>();
195             headerCards.add(new ConditionHeaderContextualCard.Builder()
196                     .setConditionalCards(conditionCards)
197                     .setName(CONDITION_HEADER)
198                     .setRankingScore(UNSUPPORTED_RANKING)
199                     .setViewType(ConditionHeaderContextualCardRenderer.VIEW_TYPE)
200                     .build());
201             return headerCards;
202         }
203         return Collections.EMPTY_LIST;
204     }
205 }
206