1 /* 2 * Copyright (C) 2019 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 android.app.slice.Slice.HINT_ERROR; 20 21 import android.app.settings.SettingsEnums; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.net.Uri; 25 import android.util.Log; 26 27 import androidx.annotation.VisibleForTesting; 28 import androidx.slice.Slice; 29 import androidx.slice.SliceMetadata; 30 import androidx.slice.SliceViewManager; 31 import androidx.slice.core.SliceAction; 32 33 import com.android.settings.overlay.FeatureFactory; 34 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 35 36 import java.util.List; 37 import java.util.concurrent.Callable; 38 import java.util.concurrent.CountDownLatch; 39 import java.util.concurrent.TimeUnit; 40 41 public class EligibleCardChecker implements Callable<ContextualCard> { 42 43 private static final String TAG = "EligibleCardChecker"; 44 private static final long LATCH_TIMEOUT_MS = 200; 45 46 private final Context mContext; 47 48 @VisibleForTesting 49 ContextualCard mCard; 50 EligibleCardChecker(Context context, ContextualCard card)51 EligibleCardChecker(Context context, ContextualCard card) { 52 mContext = context; 53 mCard = card; 54 } 55 56 @Override call()57 public ContextualCard call() throws Exception { 58 final long startTime = System.currentTimeMillis(); 59 final MetricsFeatureProvider metricsFeatureProvider = 60 FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); 61 ContextualCard result; 62 63 if (isCardEligibleToDisplay(mCard)) { 64 metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 65 SettingsEnums.ACTION_CONTEXTUAL_CARD_ELIGIBILITY, 66 SettingsEnums.SETTINGS_HOMEPAGE, 67 mCard.getTextSliceUri() /* key */, 1 /* true */); 68 result = mCard; 69 } else { 70 metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 71 SettingsEnums.ACTION_CONTEXTUAL_CARD_ELIGIBILITY, 72 SettingsEnums.SETTINGS_HOMEPAGE, 73 mCard.getTextSliceUri() /* key */, 0 /* false */); 74 result = null; 75 } 76 // Log individual card loading time 77 metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 78 SettingsEnums.ACTION_CONTEXTUAL_CARD_LOAD, 79 SettingsEnums.SETTINGS_HOMEPAGE, 80 mCard.getTextSliceUri() /* key */, 81 (int) (System.currentTimeMillis() - startTime) /* value */); 82 83 return result; 84 } 85 86 @VisibleForTesting isCardEligibleToDisplay(ContextualCard card)87 boolean isCardEligibleToDisplay(ContextualCard card) { 88 if (card.getRankingScore() < 0) { 89 return false; 90 } 91 if (card.isCustomCard()) { 92 return true; 93 } 94 95 final Uri uri = card.getSliceUri(); 96 if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { 97 return false; 98 } 99 100 final Slice slice = bindSlice(uri); 101 102 if (isSliceToggleable(slice)) { 103 mCard = card.mutate().setHasInlineAction(true).build(); 104 } 105 106 if (slice == null || slice.hasHint(HINT_ERROR)) { 107 Log.w(TAG, "Failed to bind slice, not eligible for display " + uri); 108 return false; 109 } 110 return true; 111 } 112 113 @VisibleForTesting bindSlice(Uri uri)114 Slice bindSlice(Uri uri) { 115 final SliceViewManager manager = SliceViewManager.getInstance(mContext); 116 final Slice[] returnSlice = new Slice[1]; 117 final CountDownLatch latch = new CountDownLatch(1); 118 final SliceViewManager.SliceCallback callback = 119 new SliceViewManager.SliceCallback() { 120 @Override 121 public void onSliceUpdated(Slice slice) { 122 try { 123 // We are just making sure the existence of the slice, so ignore 124 // slice loading state here. 125 returnSlice[0] = slice; 126 latch.countDown(); 127 } catch (Exception e) { 128 Log.w(TAG, uri + " cannot be indexed", e); 129 } finally { 130 manager.unregisterSliceCallback(uri, this); 131 } 132 } 133 }; 134 // Register a callback until we get a loaded slice. 135 manager.registerSliceCallback(uri, callback); 136 // Trigger the binding. 137 callback.onSliceUpdated(manager.bindSlice(uri)); 138 try { 139 latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS); 140 } catch (InterruptedException e) { 141 Log.w(TAG, "Error waiting for slice binding for uri" + uri, e); 142 manager.unregisterSliceCallback(uri, callback); 143 } 144 return returnSlice[0]; 145 } 146 147 @VisibleForTesting isSliceToggleable(Slice slice)148 boolean isSliceToggleable(Slice slice) { 149 final SliceMetadata metadata = SliceMetadata.from(mContext, slice); 150 final List<SliceAction> toggles = metadata.getToggles(); 151 152 return !toggles.isEmpty(); 153 } 154 } 155