/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.homepage.contextualcards; import static com.android.settings.intelligence.ContextualCardProto.ContextualCard.Category.STICKY_VALUE; import static com.android.settings.slices.CustomSliceRegistry.BLUETOOTH_DEVICES_SLICE_URI; import android.app.settings.SettingsEnums; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.provider.Settings; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.settings.R; import com.android.settings.homepage.contextualcards.logging.ContextualCardLogUtils; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.utils.AsyncLoaderCompat; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; public class ContextualCardLoader extends AsyncLoaderCompat> { @VisibleForTesting static final int DEFAULT_CARD_COUNT = 3; @VisibleForTesting static final String CONTEXTUAL_CARD_COUNT = "contextual_card_count"; static final int CARD_CONTENT_LOADER_ID = 1; private static final String TAG = "ContextualCardLoader"; private static final long ELIGIBILITY_CHECKER_TIMEOUT_MS = 400; private final ContentObserver mObserver = new ContentObserver( new Handler(Looper.getMainLooper())) { @Override public void onChange(boolean selfChange, Uri uri) { if (isStarted()) { mNotifyUri = uri; forceLoad(); } } }; @VisibleForTesting Uri mNotifyUri; private final Context mContext; ContextualCardLoader(Context context) { super(context); mContext = context.getApplicationContext(); } @Override protected void onStartLoading() { super.onStartLoading(); mNotifyUri = null; mContext.getContentResolver().registerContentObserver(CardContentProvider.REFRESH_CARD_URI, false /*notifyForDescendants*/, mObserver); mContext.getContentResolver().registerContentObserver(CardContentProvider.DELETE_CARD_URI, false /*notifyForDescendants*/, mObserver); } @Override protected void onStopLoading() { super.onStopLoading(); mContext.getContentResolver().unregisterContentObserver(mObserver); } @Override protected void onDiscardResult(List result) { } @NonNull @Override public List loadInBackground() { final List result = new ArrayList<>(); if (mContext.getResources().getBoolean(R.bool.config_use_legacy_suggestion)) { Log.d(TAG, "Skipping - in legacy suggestion mode"); return result; } try (Cursor cursor = getContextualCardsFromProvider()) { if (cursor.getCount() > 0) { for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { final ContextualCard card = new ContextualCard(cursor); if (isLargeCard(card)) { result.add(card.mutate().setIsLargeCard(true).build()); } else { result.add(card); } } } } return getDisplayableCards(result); } // Get final displayed cards and log what cards will be displayed/hidden @VisibleForTesting List getDisplayableCards(List candidates) { final List eligibleCards = filterEligibleCards(candidates); final List stickyCards = new ArrayList<>(); final List visibleCards = new ArrayList<>(); final List hiddenCards = new ArrayList<>(); final int maxCardCount = getCardCount(); eligibleCards.forEach(card -> { if (card.getCategory() != STICKY_VALUE) { return; } if (stickyCards.size() < maxCardCount) { stickyCards.add(card); } else { hiddenCards.add(card); } }); final int nonStickyCardCount = maxCardCount - stickyCards.size(); eligibleCards.forEach(card -> { if (card.getCategory() == STICKY_VALUE) { return; } if (visibleCards.size() < nonStickyCardCount) { visibleCards.add(card); } else { hiddenCards.add(card); } }); visibleCards.addAll(stickyCards); if (!CardContentProvider.DELETE_CARD_URI.equals(mNotifyUri)) { final MetricsFeatureProvider metricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); metricsFeatureProvider.action(mContext, SettingsEnums.ACTION_CONTEXTUAL_CARD_NOT_SHOW, ContextualCardLogUtils.buildCardListLog(hiddenCards)); } return visibleCards; } @VisibleForTesting int getCardCount() { // Return the card count if Settings.Global has KEY_CONTEXTUAL_CARD_COUNT key, // otherwise return the default one. return Settings.Global.getInt(mContext.getContentResolver(), CONTEXTUAL_CARD_COUNT, DEFAULT_CARD_COUNT); } @VisibleForTesting Cursor getContextualCardsFromProvider() { final ContextualCardFeatureProvider cardFeatureProvider = FeatureFactory.getFeatureFactory().getContextualCardFeatureProvider(mContext); return cardFeatureProvider.getContextualCards(); } @VisibleForTesting List filterEligibleCards(List candidates) { if (candidates.isEmpty()) { return candidates; } final ExecutorService executor = Executors.newFixedThreadPool(candidates.size()); final List cards = new ArrayList<>(); List> eligibleCards = new ArrayList<>(); final List checkers = candidates.stream() .map(card -> new EligibleCardChecker(mContext, card)) .collect(Collectors.toList()); try { eligibleCards = executor.invokeAll(checkers, ELIGIBILITY_CHECKER_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Log.w(TAG, "Failed to get eligible states for all cards", e); } executor.shutdown(); // Collect future and eligible cards for (int i = 0; i < eligibleCards.size(); i++) { final Future cardFuture = eligibleCards.get(i); if (cardFuture.isCancelled()) { Log.w(TAG, "Timeout getting eligible state for card: " + candidates.get(i).getSliceUri()); continue; } try { final ContextualCard card = cardFuture.get(); if (card != null) { cards.add(card); } } catch (Exception e) { Log.w(TAG, "Failed to get eligible state for card", e); } } return cards; } private boolean isLargeCard(ContextualCard card) { return card.getSliceUri().equals(BLUETOOTH_DEVICES_SLICE_URI); } public interface CardContentLoaderListener { void onFinishCardLoading(List contextualCards); } }