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.os.AsyncTask;
26 import android.util.Log;
27 
28 import androidx.annotation.VisibleForTesting;
29 import androidx.slice.Slice;
30 import androidx.slice.SliceMetadata;
31 import androidx.slice.SliceViewManager;
32 import androidx.slice.core.SliceAction;
33 
34 import com.android.settings.overlay.FeatureFactory;
35 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
36 import com.android.settingslib.utils.ThreadUtils;
37 
38 import java.util.List;
39 import java.util.concurrent.Callable;
40 
41 public class EligibleCardChecker implements Callable<ContextualCard> {
42 
43     private static final String TAG = "EligibleCardChecker";
44 
45     private final Context mContext;
46 
47     @VisibleForTesting
48     ContextualCard mCard;
49 
EligibleCardChecker(Context context, ContextualCard card)50     EligibleCardChecker(Context context, ContextualCard card) {
51         mContext = context;
52         mCard = card;
53     }
54 
55     @Override
call()56     public ContextualCard call() {
57         final long startTime = System.currentTimeMillis();
58         final MetricsFeatureProvider metricsFeatureProvider =
59                 FeatureFactory.getFactory(mContext).getMetricsFeatureProvider();
60         ContextualCard result;
61 
62         if (isCardEligibleToDisplay(mCard)) {
63             metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN,
64                     SettingsEnums.ACTION_CONTEXTUAL_CARD_ELIGIBILITY,
65                     SettingsEnums.SETTINGS_HOMEPAGE,
66                     mCard.getTextSliceUri() /* key */, 1 /* true */);
67             result = mCard;
68         } else {
69             metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN,
70                     SettingsEnums.ACTION_CONTEXTUAL_CARD_ELIGIBILITY,
71                     SettingsEnums.SETTINGS_HOMEPAGE,
72                     mCard.getTextSliceUri() /* key */, 0 /* false */);
73             result = null;
74         }
75         // Log individual card loading time
76         metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN,
77                 SettingsEnums.ACTION_CONTEXTUAL_CARD_LOAD,
78                 SettingsEnums.SETTINGS_HOMEPAGE,
79                 mCard.getTextSliceUri() /* key */,
80                 (int) (System.currentTimeMillis() - startTime) /* value */);
81 
82         return result;
83     }
84 
85     @VisibleForTesting
isCardEligibleToDisplay(ContextualCard card)86     boolean isCardEligibleToDisplay(ContextualCard card) {
87         if (card.getRankingScore() < 0) {
88             return false;
89         }
90 
91         final Uri uri = card.getSliceUri();
92         if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
93             return false;
94         }
95 
96         final Slice slice = bindSlice(uri);
97 
98         if (slice == null || slice.hasHint(HINT_ERROR)) {
99             Log.w(TAG, "Failed to bind slice, not eligible for display " + uri);
100             return false;
101         }
102 
103         mCard = card.mutate().setSlice(slice).build();
104 
105         if (isSliceToggleable(slice)) {
106             mCard = card.mutate().setHasInlineAction(true).build();
107         }
108 
109         return true;
110     }
111 
112     @VisibleForTesting
bindSlice(Uri uri)113     Slice bindSlice(Uri uri) {
114         final SliceViewManager manager = SliceViewManager.getInstance(mContext);
115         final SliceViewManager.SliceCallback callback = slice -> { };
116 
117         // Register a trivial callback to pin the slice
118         manager.registerSliceCallback(uri, callback);
119         final Slice slice = manager.bindSlice(uri);
120 
121         // Workaround of unpinning slice in the same SerialExecutor of AsyncTask as SliceCallback's
122         // observer.
123         ThreadUtils.postOnMainThread(() -> AsyncTask.execute(() -> {
124             try {
125                 manager.unregisterSliceCallback(uri, callback);
126             } catch (SecurityException e) {
127                 Log.d(TAG, "No permission currently: " + e);
128             }
129         }));
130 
131         return slice;
132     }
133 
134     @VisibleForTesting
isSliceToggleable(Slice slice)135     boolean isSliceToggleable(Slice slice) {
136         final SliceMetadata metadata = SliceMetadata.from(mContext, slice);
137         final List<SliceAction> toggles = metadata.getToggles();
138 
139         return !toggles.isEmpty();
140     }
141 }
142