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.Log;
21 
22 import androidx.annotation.NonNull;
23 import androidx.annotation.VisibleForTesting;
24 
25 import com.android.settings.homepage.contextualcards.ContextualCard;
26 import com.android.settingslib.utils.ThreadUtils;
27 
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.concurrent.Callable;
31 import java.util.concurrent.ExecutionException;
32 import java.util.concurrent.Future;
33 import java.util.concurrent.TimeUnit;
34 import java.util.concurrent.TimeoutException;
35 
36 public class ConditionManager {
37     private static final String TAG = "ConditionManager";
38 
39     @VisibleForTesting
40     final List<ConditionalCardController> mCardControllers;
41 
42     private static final long DISPLAYABLE_CHECKER_TIMEOUT_MS = 20;
43 
44     private final Context mAppContext;
45     private final ConditionListener mListener;
46 
47     private boolean mIsListeningToStateChange;
48 
ConditionManager(Context context, ConditionListener listener)49     public ConditionManager(Context context, ConditionListener listener) {
50         mAppContext = context.getApplicationContext();
51         mCardControllers = new ArrayList<>();
52         mListener = listener;
53         initCandidates();
54     }
55 
56     /**
57      * Returns a list of {@link ContextualCard}s eligible for display.
58      */
getDisplayableCards()59     public List<ContextualCard> getDisplayableCards() {
60         final List<ContextualCard> cards = new ArrayList<>();
61         final List<Future<ContextualCard>> displayableCards = new ArrayList<>();
62         // Check displayable future
63         for (ConditionalCardController card : mCardControllers) {
64             final DisplayableChecker checker = new DisplayableChecker(getController(card.getId()));
65             displayableCards.add(ThreadUtils.postOnBackgroundThread(checker));
66         }
67         // Collect future and add displayable cards
68         for (Future<ContextualCard> cardFuture : displayableCards) {
69             try {
70                 final ContextualCard card = cardFuture.get(
71                         DISPLAYABLE_CHECKER_TIMEOUT_MS, TimeUnit.MILLISECONDS);
72                 if (card != null) {
73                     cards.add(card);
74                 }
75             } catch (InterruptedException | ExecutionException | TimeoutException e) {
76                 Log.w(TAG, "Failed to get displayable state for card, likely timeout. Skipping", e);
77             }
78         }
79         return cards;
80     }
81 
82     /**
83      * Handler when the card is clicked.
84      *
85      * @see {@link ConditionalCardController#onPrimaryClick(Context)}
86      */
onPrimaryClick(Context context, long id)87     public void onPrimaryClick(Context context, long id) {
88         getController(id).onPrimaryClick(context);
89     }
90 
91     /**
92      * Handler when the card action is clicked.
93      *
94      * @see {@link ConditionalCardController#onActionClick()}
95      */
onActionClick(long id)96     public void onActionClick(long id) {
97         getController(id).onActionClick();
98     }
99 
100     /**
101      * Start monitoring state change for all conditions
102      */
startMonitoringStateChange()103     public void startMonitoringStateChange() {
104         if (mIsListeningToStateChange) {
105             Log.d(TAG, "Already listening to condition state changes, skipping monitor setup");
106         } else {
107             mIsListeningToStateChange = true;
108             for (ConditionalCardController controller : mCardControllers) {
109                 controller.startMonitoringStateChange();
110             }
111         }
112         // Force a refresh on listener
113         onConditionChanged();
114     }
115 
116     /**
117      * Stop monitoring state change for all conditions
118      */
stopMonitoringStateChange()119     public void stopMonitoringStateChange() {
120         if (!mIsListeningToStateChange) {
121             Log.d(TAG, "Not listening to condition state changes, skipping");
122             return;
123         }
124         for (ConditionalCardController controller : mCardControllers) {
125             controller.stopMonitoringStateChange();
126         }
127         mIsListeningToStateChange = false;
128     }
129 
130     /**
131      * Called when some conditional card's state has changed
132      */
onConditionChanged()133     void onConditionChanged() {
134         if (mListener != null) {
135             mListener.onConditionsChanged();
136         }
137     }
138 
139     @NonNull
getController(long id)140     private <T extends ConditionalCardController> T getController(long id) {
141         for (ConditionalCardController controller : mCardControllers) {
142             if (controller.getId() == id) {
143                 return (T) controller;
144             }
145         }
146         throw new IllegalStateException("Cannot find controller for " + id);
147     }
148 
initCandidates()149     private void initCandidates() {
150         // Initialize controllers first.
151         mCardControllers.add(new AirplaneModeConditionController(mAppContext, this /* manager */));
152         mCardControllers.add(
153                 new BackgroundDataConditionController(mAppContext, this /* manager */));
154         mCardControllers.add(new BatterySaverConditionController(mAppContext, this /* manager */));
155         mCardControllers.add(new CellularDataConditionController(mAppContext, this /* manager */));
156         mCardControllers.add(new DndConditionCardController(mAppContext, this /* manager */));
157         mCardControllers.add(new HotspotConditionController(mAppContext, this /* manager */));
158         mCardControllers.add(new NightDisplayConditionController(mAppContext, this /* manager */));
159         mCardControllers.add(new RingerVibrateConditionController(mAppContext, this /* manager */));
160         mCardControllers.add(new RingerMutedConditionController(mAppContext, this /* manager */));
161         mCardControllers.add(new WorkModeConditionController(mAppContext, this /* manager */));
162         mCardControllers.add(new GrayscaleConditionController(mAppContext, this /* manager */));
163     }
164 
165     /**
166      * Returns card if controller says it's displayable. Otherwise returns null.
167      */
168     public static class DisplayableChecker implements Callable<ContextualCard> {
169 
170         private final ConditionalCardController mController;
171 
DisplayableChecker(ConditionalCardController controller)172         private DisplayableChecker(ConditionalCardController controller) {
173             mController = controller;
174         }
175 
176         @Override
call()177         public ContextualCard call() throws Exception {
178             return mController.isDisplayable() ? mController.buildContextualCard() : null;
179         }
180     }
181 }
182