2 * Copyright (C) 2018 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings.homepage.contextualcards;
19 import static android.app.slice.Slice.HINT_ERROR;
21 import static androidx.slice.widget.SliceLiveData.SUPPORTED_SPECS;
23 import static com.android.settings.slices.CustomSliceRegistry.BLUETOOTH_DEVICES_SLICE_URI;
24 import static com.android.settings.slices.CustomSliceRegistry.WIFI_SLICE_URI;
26 import android.content.ContentProviderClient;
27 import android.content.ContentResolver;
28 import android.content.Context;
29 import android.database.ContentObserver;
30 import android.database.Cursor;
31 import android.net.Uri;
32 import android.os.Handler;
33 import android.os.Looper;
34 import android.util.Log;
36 import androidx.annotation.NonNull;
37 import androidx.annotation.VisibleForTesting;
38 import androidx.slice.Slice;
40 import com.android.settings.overlay.FeatureFactory;
41 import com.android.settingslib.utils.AsyncLoaderCompat;
43 import java.util.ArrayList;
44 import java.util.List;
45 import java.util.stream.Collectors;
47 public class ContextualCardLoader extends AsyncLoaderCompat<List<ContextualCard>> {
50 static final int DEFAULT_CARD_COUNT = 4;
51 static final int CARD_CONTENT_LOADER_ID = 1;
53 private static final String TAG = "ContextualCardLoader";
55 private final ContentObserver mObserver = new ContentObserver(
56 new Handler(Looper.getMainLooper())) {
58 public void onChange(boolean selfChange) {
65 private Context mContext;
67 ContextualCardLoader(Context context) {
69 mContext = context.getApplicationContext();
73 protected void onStartLoading() {
74 super.onStartLoading();
75 mContext.getContentResolver().registerContentObserver(CardContentProvider.URI,
76 false /*notifyForDescendants*/, mObserver);
80 protected void onStopLoading() {
81 super.onStopLoading();
82 mContext.getContentResolver().unregisterContentObserver(mObserver);
86 protected void onDiscardResult(List<ContextualCard> result) {
92 public List<ContextualCard> loadInBackground() {
93 final List<ContextualCard> result = new ArrayList<>();
94 try (Cursor cursor = getContextualCardsFromProvider()) {
95 if (cursor.getCount() > 0) {
96 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
97 final ContextualCard card = new ContextualCard(cursor);
98 if (card.isCustomCard()) {
99 //TODO(b/114688391): Load and generate custom card,then add into list
100 } else if (isLargeCard(card)) {
101 result.add(card.mutate().setIsLargeCard(true).build());
108 return getFinalDisplayableCards(result);
111 // Get final displayed cards and log what cards will be displayed/hidden
113 List<ContextualCard> getFinalDisplayableCards(List<ContextualCard> candidates) {
114 final List<ContextualCard> eligibleCards = filterEligibleCards(candidates);
115 final List<ContextualCard> visibleCards = new ArrayList<>();
116 final List<ContextualCard> hiddenCards = new ArrayList<>();
118 final int size = eligibleCards.size();
119 for (int i = 0; i < size; i++) {
120 if (i < DEFAULT_CARD_COUNT) {
121 visibleCards.add(eligibleCards.get(i));
123 hiddenCards.add(eligibleCards.get(i));
128 // The maximum cards are four small cards OR
129 // one large card with two small cards OR
131 if (visibleCards.size() <= 2 || getNumberOfLargeCard(visibleCards) == 0) {
136 if (visibleCards.size() == DEFAULT_CARD_COUNT) {
137 hiddenCards.add(visibleCards.remove(visibleCards.size() - 1));
140 if (getNumberOfLargeCard(visibleCards) == 1) {
141 // One large card with two small cards
145 hiddenCards.add(visibleCards.remove(visibleCards.size() - 1));
150 final ContextualCardFeatureProvider contextualCardFeatureProvider =
151 FeatureFactory.getFactory(mContext).getContextualCardFeatureProvider();
152 contextualCardFeatureProvider.logContextualCardDisplay(mContext, visibleCards,
158 Cursor getContextualCardsFromProvider() {
159 return CardDatabaseHelper.getInstance(mContext).getContextualCards();
163 List<ContextualCard> filterEligibleCards(List<ContextualCard> candidates) {
164 return candidates.stream().filter(card -> isCardEligibleToDisplay(card))
165 .collect(Collectors.toList());
169 boolean isCardEligibleToDisplay(ContextualCard card) {
170 if (card.isCustomCard()) {
174 final Uri uri = card.getSliceUri();
176 if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
180 //check if the uri has a provider associated with.
181 final ContentProviderClient provider =
182 mContext.getContentResolver().acquireContentProviderClient(uri);
183 if (provider == null) {
186 //release contentProviderClient to prevent from memory leak.
189 final Slice slice = Slice.bindSlice(mContext, uri, SUPPORTED_SPECS);
190 if (slice == null || slice.hasHint(HINT_ERROR)) {
191 Log.w(TAG, "Failed to bind slice, not eligible for display " + uri);
198 private int getNumberOfLargeCard(List<ContextualCard> cards) {
199 return (int) cards.stream()
200 .filter(card -> isLargeCard(card))
204 private boolean isLargeCard(ContextualCard card) {
205 return card.getSliceUri().equals(WIFI_SLICE_URI)
206 || card.getSliceUri().equals(BLUETOOTH_DEVICES_SLICE_URI);
209 public interface CardContentLoaderListener {
210 void onFinishCardLoading(List<ContextualCard> contextualCards);