ContextualCardLoader was waiting for eligible card checkers with a 300 ms timeout one by one in a loop. So if more than four checkers expire, the total waiting time will exceed the 1 sec homepage content loading timeout, which causes no card shows up. The solution is to leverage all the checkers' timeout starting time, and increase the timeout for all checkers. So that ContextualCardLoader just needs to wait for all checkers for at most 400 ms and then can get the results. Bug: 159236069 Test: robotest Change-Id: I601ac4151bf8be68b30eaabdb45a4e1ace95653f
232 lines
8.5 KiB
Java
232 lines
8.5 KiB
Java
/*
|
|
* 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 static com.android.settings.slices.CustomSliceRegistry.CONTEXTUAL_WIFI_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<List<ContextualCard>> {
|
|
|
|
@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<ContextualCard> result) {
|
|
|
|
}
|
|
|
|
@NonNull
|
|
@Override
|
|
public List<ContextualCard> loadInBackground() {
|
|
final List<ContextualCard> 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<ContextualCard> getDisplayableCards(List<ContextualCard> candidates) {
|
|
final List<ContextualCard> eligibleCards = filterEligibleCards(candidates);
|
|
final List<ContextualCard> stickyCards = new ArrayList<>();
|
|
final List<ContextualCard> visibleCards = new ArrayList<>();
|
|
final List<ContextualCard> 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.getFactory(mContext).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.getFactory(mContext).getContextualCardFeatureProvider(mContext);
|
|
return cardFeatureProvider.getContextualCards();
|
|
}
|
|
|
|
@VisibleForTesting
|
|
List<ContextualCard> filterEligibleCards(List<ContextualCard> candidates) {
|
|
final ExecutorService executor = Executors.newFixedThreadPool(candidates.size());
|
|
final List<ContextualCard> cards = new ArrayList<>();
|
|
List<Future<ContextualCard>> eligibleCards = new ArrayList<>();
|
|
|
|
final List<EligibleCardChecker> 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<ContextualCard> 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(CONTEXTUAL_WIFI_SLICE_URI)
|
|
|| card.getSliceUri().equals(BLUETOOTH_DEVICES_SLICE_URI);
|
|
}
|
|
|
|
public interface CardContentLoaderListener {
|
|
void onFinishCardLoading(List<ContextualCard> contextualCards);
|
|
}
|
|
}
|