1 /*
2  * Copyright (C) 2023 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.bluetooth;
18 
19 import static android.app.slice.Slice.HINT_PERMISSION_REQUEST;
20 import static android.app.slice.Slice.HINT_TITLE;
21 import static android.app.slice.SliceItem.FORMAT_ACTION;
22 import static android.app.slice.SliceItem.FORMAT_IMAGE;
23 import static android.app.slice.SliceItem.FORMAT_SLICE;
24 import static android.app.slice.SliceItem.FORMAT_TEXT;
25 
26 import android.app.PendingIntent;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.net.Uri;
30 import android.text.TextUtils;
31 import android.util.Log;
32 
33 import androidx.annotation.Nullable;
34 import androidx.annotation.VisibleForTesting;
35 import androidx.core.graphics.drawable.IconCompat;
36 import androidx.lifecycle.LiveData;
37 import androidx.lifecycle.Observer;
38 import androidx.preference.Preference;
39 import androidx.preference.PreferenceCategory;
40 import androidx.preference.PreferenceScreen;
41 import androidx.slice.Slice;
42 import androidx.slice.SliceItem;
43 import androidx.slice.builders.ListBuilder;
44 import androidx.slice.builders.SliceAction;
45 import androidx.slice.widget.SliceLiveData;
46 
47 import com.android.settings.R;
48 import com.android.settings.core.BasePreferenceController;
49 import com.android.settingslib.core.lifecycle.LifecycleObserver;
50 import com.android.settingslib.core.lifecycle.events.OnStart;
51 import com.android.settingslib.core.lifecycle.events.OnStop;
52 
53 import java.util.ArrayList;
54 import java.util.List;
55 import java.util.Optional;
56 
57 /**
58  * The blocking preference with slice controller will make whole page invisible for a certain time
59  * until {@link Slice} is fully loaded.
60  */
61 public class BlockingPrefWithSliceController extends BasePreferenceController implements
62         LifecycleObserver, OnStart, OnStop, Observer<Slice>, BasePreferenceController.UiBlocker {
63     private static final String TAG = "BlockingPrefWithSliceController";
64 
65     private static final String PREFIX_KEY = "slice_preference_item_";
66 
67     @VisibleForTesting
68     LiveData<Slice> mLiveData;
69     private Uri mUri;
70     @VisibleForTesting
71     PreferenceCategory mPreferenceCategory;
72     private List<Preference> mCurrentPreferencesList = new ArrayList<>();
73     @VisibleForTesting
74     String mSliceIntentAction = "";
75     @VisibleForTesting
76     String mSlicePendingIntentAction = "";
77     @VisibleForTesting
78     String mExtraIntent = "";
79     @VisibleForTesting
80     String mExtraPendingIntent = "";
81 
BlockingPrefWithSliceController(Context context, String preferenceKey)82     public BlockingPrefWithSliceController(Context context, String preferenceKey) {
83         super(context, preferenceKey);
84     }
85 
86     @Override
displayPreference(PreferenceScreen screen)87     public void displayPreference(PreferenceScreen screen) {
88         super.displayPreference(screen);
89         mPreferenceCategory = screen.findPreference(getPreferenceKey());
90         mSliceIntentAction = mContext.getResources().getString(
91                 R.string.config_bt_slice_intent_action);
92         mSlicePendingIntentAction = mContext.getResources().getString(
93                 R.string.config_bt_slice_pending_intent_action);
94         mExtraIntent = mContext.getResources().getString(R.string.config_bt_slice_extra_intent);
95         mExtraPendingIntent = mContext.getResources().getString(
96                 R.string.config_bt_slice_extra_pending_intent);
97     }
98 
99     @Override
getAvailabilityStatus()100     public int getAvailabilityStatus() {
101         return mUri != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
102     }
103 
setSliceUri(Uri uri)104     public void setSliceUri(Uri uri) {
105         mUri = uri;
106         mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> {
107             Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type);
108         });
109 
110         //TODO(b/120803703): figure out why we need to remove observer first
111         mLiveData.removeObserver(this);
112     }
113 
114     @Override
onStart()115     public void onStart() {
116         if (mLiveData == null) {
117             return;
118         }
119 
120         try {
121             mLiveData.observeForever(this);
122         } catch (SecurityException e) {
123             Log.w(TAG, "observeForever - no permission");
124         }
125     }
126 
127     @Override
onStop()128     public void onStop() {
129         if (mLiveData == null) {
130             return;
131         }
132 
133         try {
134             mLiveData.removeObserver(this);
135         } catch (SecurityException e) {
136             Log.w(TAG, "removeObserver - no permission");
137         }
138     }
139 
140     @Override
onChanged(Slice slice)141     public void onChanged(Slice slice) {
142         updatePreferenceFromSlice(slice);
143         if (mUiBlockListener != null) {
144             mUiBlockListener.onBlockerWorkFinished(this);
145         }
146     }
147 
148     @VisibleForTesting
updatePreferenceFromSlice(Slice slice)149     void updatePreferenceFromSlice(Slice slice) {
150         if (TextUtils.isEmpty(mSliceIntentAction)
151                 || TextUtils.isEmpty(mExtraIntent)
152                 || TextUtils.isEmpty(mSlicePendingIntentAction)
153                 || TextUtils.isEmpty(mExtraPendingIntent)) {
154             Log.d(TAG, "No configs");
155             return;
156         }
157         if (slice == null || slice.hasHint(HINT_PERMISSION_REQUEST)) {
158             Log.d(TAG, "Current slice: " + slice);
159             removePreferenceListFromPreferenceCategory();
160             return;
161         }
162         updatePreferenceListAndPreferenceCategory(parseSliceToPreferenceList(slice));
163     }
164 
parseSliceToPreferenceList(Slice slice)165     private List<Preference> parseSliceToPreferenceList(Slice slice) {
166         List<Preference> preferenceItemsList = new ArrayList<>();
167         List<SliceItem> items = slice.getItems();
168         int orderLevel = 0;
169         for (SliceItem sliceItem : items) {
170             // Parse the slice
171             if (sliceItem.getFormat().equals(FORMAT_SLICE)) {
172                 Optional<CharSequence> title = extractTitleFromSlice(sliceItem.getSlice());
173                 Optional<CharSequence> subtitle = extractSubtitleFromSlice(sliceItem.getSlice());
174                 Optional<SliceAction> action = extractActionFromSlice(sliceItem.getSlice());
175                 // Create preference
176                 Optional<Preference> preferenceItem = createPreferenceItem(title, subtitle, action,
177                         orderLevel);
178                 if (preferenceItem.isPresent()) {
179                     orderLevel++;
180                     preferenceItemsList.add(preferenceItem.get());
181                 }
182             }
183         }
184         return preferenceItemsList;
185     }
186 
createPreferenceItem(Optional<CharSequence> title, Optional<CharSequence> subtitle, Optional<SliceAction> sliceAction, int orderLevel)187     private Optional<Preference> createPreferenceItem(Optional<CharSequence> title,
188             Optional<CharSequence> subtitle, Optional<SliceAction> sliceAction, int orderLevel) {
189         Log.d(TAG, "Title: " + title.orElse("no title")
190                 + ", Subtitle: " + subtitle.orElse("no Subtitle")
191                 + ", Action: " + sliceAction.orElse(null));
192         if (!title.isPresent()) {
193             return Optional.empty();
194         }
195         String key = PREFIX_KEY + title.get();
196         Preference preference = mPreferenceCategory.findPreference(key);
197         if (preference == null) {
198             preference = new Preference(mContext);
199             preference.setKey(key);
200             mPreferenceCategory.addPreference(preference);
201         }
202         preference.setTitle(title.get());
203         preference.setOrder(orderLevel);
204         if (subtitle.isPresent()) {
205             preference.setSummary(subtitle.get());
206         }
207         if (sliceAction.isPresent()) {
208             // To support the settings' 2 panel feature, here can't use the slice's
209             // PendingIntent.send(). Since the PendingIntent.send() always take NEW_TASK flag.
210             // Therefore, transfer the slice's PendingIntent to Intent and start it
211             // without NEW_TASK.
212             preference.setIcon(sliceAction.get().getIcon().loadDrawable(mContext));
213             Intent intentFromSliceAction = sliceAction.get().getAction().getIntent();
214             Intent expectedActivityIntent = null;
215             Log.d(TAG, "SliceAction: intent's Action:" + intentFromSliceAction.getAction());
216             if (intentFromSliceAction.getAction().equals(mSliceIntentAction)) {
217                 expectedActivityIntent = intentFromSliceAction
218                         .getParcelableExtra(mExtraIntent, Intent.class);
219             } else if (intentFromSliceAction.getAction().equals(
220                     mSlicePendingIntentAction)) {
221                 PendingIntent pendingIntent = intentFromSliceAction
222                         .getParcelableExtra(mExtraPendingIntent, PendingIntent.class);
223                 expectedActivityIntent =
224                         pendingIntent != null ? pendingIntent.getIntent() : null;
225             } else {
226                 expectedActivityIntent = intentFromSliceAction;
227             }
228             if (expectedActivityIntent != null && expectedActivityIntent.resolveActivity(
229                     mContext.getPackageManager()) != null) {
230                 Log.d(TAG, "setIntent: ActivityIntent" + expectedActivityIntent);
231                 // Since UI needs to support the Settings' 2 panel feature, the intent can't use the
232                 // FLAG_ACTIVITY_NEW_TASK. The above intent may have the FLAG_ACTIVITY_NEW_TASK
233                 // flag, so removes it before startActivity(preference.setIntent).
234                 expectedActivityIntent.removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
235                 preference.setIntent(expectedActivityIntent);
236             } else {
237                 Log.d(TAG, "setIntent: Intent is null");
238                 preference.setSelectable(false);
239             }
240         }
241 
242         return Optional.of(preference);
243     }
244 
removePreferenceListFromPreferenceCategory()245     private void removePreferenceListFromPreferenceCategory() {
246         mCurrentPreferencesList.stream()
247                 .forEach(p -> mPreferenceCategory.removePreference(p));
248         mCurrentPreferencesList.clear();
249     }
250 
updatePreferenceListAndPreferenceCategory(List<Preference> newPreferenceList)251     private void updatePreferenceListAndPreferenceCategory(List<Preference> newPreferenceList) {
252         List<Preference> removedItemList = new ArrayList<>(mCurrentPreferencesList);
253         for (Preference item : mCurrentPreferencesList) {
254             if (newPreferenceList.stream().anyMatch(p -> item.compareTo(p) == 0)) {
255                 removedItemList.remove(item);
256             }
257         }
258         removedItemList.stream()
259                 .forEach(p -> mPreferenceCategory.removePreference(p));
260         mCurrentPreferencesList = newPreferenceList;
261     }
262 
extractTitleFromSlice(Slice slice)263     private Optional<CharSequence> extractTitleFromSlice(Slice slice) {
264         return extractTextFromSlice(slice, HINT_TITLE);
265     }
266 
extractSubtitleFromSlice(Slice slice)267     private Optional<CharSequence> extractSubtitleFromSlice(Slice slice) {
268         // For subtitle items, there isn't a hint available.
269         return extractTextFromSlice(slice, /* hint= */ null);
270     }
271 
extractTextFromSlice(Slice slice, @Nullable String hint)272     private Optional<CharSequence> extractTextFromSlice(Slice slice, @Nullable String hint) {
273         for (SliceItem item : slice.getItems()) {
274             if (item.getFormat().equals(FORMAT_TEXT)
275                     && ((TextUtils.isEmpty(hint) && item.getHints().isEmpty())
276                     || (!TextUtils.isEmpty(hint) && item.hasHint(hint)))) {
277                 return Optional.ofNullable(item.getText());
278             }
279         }
280         return Optional.empty();
281     }
282 
extractActionFromSlice(Slice slice)283     private Optional<SliceAction> extractActionFromSlice(Slice slice) {
284         for (SliceItem item : slice.getItems()) {
285             if (item.getFormat().equals(FORMAT_SLICE)) {
286                 if (item.hasHint(HINT_TITLE)) {
287                     Optional<SliceAction> result = extractActionFromSlice(item.getSlice());
288                     if (result.isPresent()) {
289                         return result;
290                     }
291                 }
292                 continue;
293             }
294 
295             if (item.getFormat().equals(FORMAT_ACTION)) {
296                 Optional<IconCompat> icon = extractIconFromSlice(item.getSlice());
297                 Optional<CharSequence> title = extractTitleFromSlice(item.getSlice());
298                 if (icon.isPresent()) {
299                     return Optional.of(
300                             SliceAction.create(
301                                     item.getAction(),
302                                     icon.get(),
303                                     ListBuilder.ICON_IMAGE,
304                                     title.orElse(/* other= */ "")));
305                 }
306             }
307         }
308         return Optional.empty();
309     }
310 
extractIconFromSlice(Slice slice)311     private Optional<IconCompat> extractIconFromSlice(Slice slice) {
312         for (SliceItem item : slice.getItems()) {
313             if (item.getFormat().equals(FORMAT_IMAGE)) {
314                 return Optional.of(item.getIcon());
315             }
316         }
317         return Optional.empty();
318     }
319 }
320