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