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.intelligence.ContextualCardProto.ContextualCard.Category.STICKY_VALUE; 20 import static com.android.settings.slices.CustomSliceRegistry.BLUETOOTH_DEVICES_SLICE_URI; 21 import static com.android.settings.slices.CustomSliceRegistry.CONTEXTUAL_WIFI_SLICE_URI; 22 23 import android.app.settings.SettingsEnums; 24 import android.content.Context; 25 import android.database.ContentObserver; 26 import android.database.Cursor; 27 import android.net.Uri; 28 import android.os.Handler; 29 import android.os.Looper; 30 import android.provider.Settings; 31 import android.util.Log; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.VisibleForTesting; 35 36 import com.android.settings.R; 37 import com.android.settings.homepage.contextualcards.logging.ContextualCardLogUtils; 38 import com.android.settings.overlay.FeatureFactory; 39 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 40 import com.android.settingslib.utils.AsyncLoaderCompat; 41 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.concurrent.ExecutorService; 45 import java.util.concurrent.Executors; 46 import java.util.concurrent.Future; 47 import java.util.concurrent.TimeUnit; 48 import java.util.stream.Collectors; 49 50 public class ContextualCardLoader extends AsyncLoaderCompat<List<ContextualCard>> { 51 52 @VisibleForTesting 53 static final int DEFAULT_CARD_COUNT = 3; 54 @VisibleForTesting 55 static final String CONTEXTUAL_CARD_COUNT = "contextual_card_count"; 56 static final int CARD_CONTENT_LOADER_ID = 1; 57 58 private static final String TAG = "ContextualCardLoader"; 59 private static final long ELIGIBILITY_CHECKER_TIMEOUT_MS = 400; 60 61 private final ContentObserver mObserver = new ContentObserver( 62 new Handler(Looper.getMainLooper())) { 63 @Override 64 public void onChange(boolean selfChange, Uri uri) { 65 if (isStarted()) { 66 mNotifyUri = uri; 67 forceLoad(); 68 } 69 } 70 }; 71 72 @VisibleForTesting 73 Uri mNotifyUri; 74 75 private final Context mContext; 76 ContextualCardLoader(Context context)77 ContextualCardLoader(Context context) { 78 super(context); 79 mContext = context.getApplicationContext(); 80 } 81 82 @Override onStartLoading()83 protected void onStartLoading() { 84 super.onStartLoading(); 85 mNotifyUri = null; 86 mContext.getContentResolver().registerContentObserver(CardContentProvider.REFRESH_CARD_URI, 87 false /*notifyForDescendants*/, mObserver); 88 mContext.getContentResolver().registerContentObserver(CardContentProvider.DELETE_CARD_URI, 89 false /*notifyForDescendants*/, mObserver); 90 } 91 92 @Override onStopLoading()93 protected void onStopLoading() { 94 super.onStopLoading(); 95 mContext.getContentResolver().unregisterContentObserver(mObserver); 96 } 97 98 @Override onDiscardResult(List<ContextualCard> result)99 protected void onDiscardResult(List<ContextualCard> result) { 100 101 } 102 103 @NonNull 104 @Override loadInBackground()105 public List<ContextualCard> loadInBackground() { 106 final List<ContextualCard> result = new ArrayList<>(); 107 if (mContext.getResources().getBoolean(R.bool.config_use_legacy_suggestion)) { 108 Log.d(TAG, "Skipping - in legacy suggestion mode"); 109 return result; 110 } 111 try (Cursor cursor = getContextualCardsFromProvider()) { 112 if (cursor.getCount() > 0) { 113 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 114 final ContextualCard card = new ContextualCard(cursor); 115 if (isLargeCard(card)) { 116 result.add(card.mutate().setIsLargeCard(true).build()); 117 } else { 118 result.add(card); 119 } 120 } 121 } 122 } 123 return getDisplayableCards(result); 124 } 125 126 // Get final displayed cards and log what cards will be displayed/hidden 127 @VisibleForTesting getDisplayableCards(List<ContextualCard> candidates)128 List<ContextualCard> getDisplayableCards(List<ContextualCard> candidates) { 129 final List<ContextualCard> eligibleCards = filterEligibleCards(candidates); 130 final List<ContextualCard> stickyCards = new ArrayList<>(); 131 final List<ContextualCard> visibleCards = new ArrayList<>(); 132 final List<ContextualCard> hiddenCards = new ArrayList<>(); 133 134 final int maxCardCount = getCardCount(); 135 eligibleCards.forEach(card -> { 136 if (card.getCategory() != STICKY_VALUE) { 137 return; 138 } 139 if (stickyCards.size() < maxCardCount) { 140 stickyCards.add(card); 141 } else { 142 hiddenCards.add(card); 143 } 144 }); 145 146 final int nonStickyCardCount = maxCardCount - stickyCards.size(); 147 eligibleCards.forEach(card -> { 148 if (card.getCategory() == STICKY_VALUE) { 149 return; 150 } 151 if (visibleCards.size() < nonStickyCardCount) { 152 visibleCards.add(card); 153 } else { 154 hiddenCards.add(card); 155 } 156 }); 157 visibleCards.addAll(stickyCards); 158 159 if (!CardContentProvider.DELETE_CARD_URI.equals(mNotifyUri)) { 160 final MetricsFeatureProvider metricsFeatureProvider = 161 FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); 162 163 metricsFeatureProvider.action(mContext, 164 SettingsEnums.ACTION_CONTEXTUAL_CARD_NOT_SHOW, 165 ContextualCardLogUtils.buildCardListLog(hiddenCards)); 166 } 167 return visibleCards; 168 } 169 170 @VisibleForTesting getCardCount()171 int getCardCount() { 172 // Return the card count if Settings.Global has KEY_CONTEXTUAL_CARD_COUNT key, 173 // otherwise return the default one. 174 return Settings.Global.getInt(mContext.getContentResolver(), 175 CONTEXTUAL_CARD_COUNT, DEFAULT_CARD_COUNT); 176 } 177 178 @VisibleForTesting getContextualCardsFromProvider()179 Cursor getContextualCardsFromProvider() { 180 final ContextualCardFeatureProvider cardFeatureProvider = 181 FeatureFactory.getFactory(mContext).getContextualCardFeatureProvider(mContext); 182 return cardFeatureProvider.getContextualCards(); 183 } 184 185 @VisibleForTesting filterEligibleCards(List<ContextualCard> candidates)186 List<ContextualCard> filterEligibleCards(List<ContextualCard> candidates) { 187 if (candidates.isEmpty()) { 188 return candidates; 189 } 190 191 final ExecutorService executor = Executors.newFixedThreadPool(candidates.size()); 192 final List<ContextualCard> cards = new ArrayList<>(); 193 List<Future<ContextualCard>> eligibleCards = new ArrayList<>(); 194 195 final List<EligibleCardChecker> checkers = candidates.stream() 196 .map(card -> new EligibleCardChecker(mContext, card)) 197 .collect(Collectors.toList()); 198 try { 199 eligibleCards = executor.invokeAll(checkers, ELIGIBILITY_CHECKER_TIMEOUT_MS, 200 TimeUnit.MILLISECONDS); 201 } catch (InterruptedException e) { 202 Log.w(TAG, "Failed to get eligible states for all cards", e); 203 } 204 executor.shutdown(); 205 206 // Collect future and eligible cards 207 for (int i = 0; i < eligibleCards.size(); i++) { 208 final Future<ContextualCard> cardFuture = eligibleCards.get(i); 209 if (cardFuture.isCancelled()) { 210 Log.w(TAG, "Timeout getting eligible state for card: " 211 + candidates.get(i).getSliceUri()); 212 continue; 213 } 214 215 try { 216 final ContextualCard card = cardFuture.get(); 217 if (card != null) { 218 cards.add(card); 219 } 220 } catch (Exception e) { 221 Log.w(TAG, "Failed to get eligible state for card", e); 222 } 223 } 224 return cards; 225 } 226 isLargeCard(ContextualCard card)227 private boolean isLargeCard(ContextualCard card) { 228 return card.getSliceUri().equals(CONTEXTUAL_WIFI_SLICE_URI) 229 || card.getSliceUri().equals(BLUETOOTH_DEVICES_SLICE_URI); 230 } 231 232 public interface CardContentLoaderListener { onFinishCardLoading(List<ContextualCard> contextualCards)233 void onFinishCardLoading(List<ContextualCard> contextualCards); 234 } 235 } 236