- When SliceManager try to pinSlice, process may not have the permssion yet, so in Androix lib it use try/catch to avoid Security exception. However, if SliceManger quickly unpinSlice after pinSlice, process may still not get the permission yet, then due to no security exception, it cause the settings crash. Bug: 283065718 Test: atest passed Test: Manual test passed Change-Id: I2293fca73e65dfaa34237abe57e0c6a3fe0f62bb
318 lines
12 KiB
Java
318 lines
12 KiB
Java
/*
|
|
* Copyright (C) 2023 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.bluetooth;
|
|
|
|
import static android.app.slice.Slice.HINT_PERMISSION_REQUEST;
|
|
import static android.app.slice.Slice.HINT_TITLE;
|
|
import static android.app.slice.SliceItem.FORMAT_ACTION;
|
|
import static android.app.slice.SliceItem.FORMAT_IMAGE;
|
|
import static android.app.slice.SliceItem.FORMAT_SLICE;
|
|
import static android.app.slice.SliceItem.FORMAT_TEXT;
|
|
|
|
import android.app.PendingIntent;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.net.Uri;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.VisibleForTesting;
|
|
import androidx.core.graphics.drawable.IconCompat;
|
|
import androidx.lifecycle.LiveData;
|
|
import androidx.lifecycle.Observer;
|
|
import androidx.preference.Preference;
|
|
import androidx.preference.PreferenceCategory;
|
|
import androidx.preference.PreferenceScreen;
|
|
import androidx.slice.Slice;
|
|
import androidx.slice.SliceItem;
|
|
import androidx.slice.builders.ListBuilder;
|
|
import androidx.slice.builders.SliceAction;
|
|
import androidx.slice.widget.SliceLiveData;
|
|
|
|
import com.android.settings.R;
|
|
import com.android.settings.core.BasePreferenceController;
|
|
import com.android.settingslib.core.lifecycle.LifecycleObserver;
|
|
import com.android.settingslib.core.lifecycle.events.OnStart;
|
|
import com.android.settingslib.core.lifecycle.events.OnStop;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Optional;
|
|
|
|
/**
|
|
* The blocking preference with slice controller will make whole page invisible for a certain time
|
|
* until {@link Slice} is fully loaded.
|
|
*/
|
|
public class BlockingPrefWithSliceController extends BasePreferenceController implements
|
|
LifecycleObserver, OnStart, OnStop, Observer<Slice>, BasePreferenceController.UiBlocker{
|
|
private static final String TAG = "BlockingPrefWithSliceController";
|
|
|
|
private static final String PREFIX_KEY = "slice_preference_item_";
|
|
|
|
@VisibleForTesting
|
|
LiveData<Slice> mLiveData;
|
|
private Uri mUri;
|
|
@VisibleForTesting
|
|
PreferenceCategory mPreferenceCategory;
|
|
private List<Preference> mCurrentPreferencesList = new ArrayList<>();
|
|
@VisibleForTesting
|
|
String mSliceIntentAction = "";
|
|
@VisibleForTesting
|
|
String mSlicePendingIntentAction = "";
|
|
@VisibleForTesting
|
|
String mExtraIntent = "";
|
|
@VisibleForTesting
|
|
String mExtraPendingIntent = "";
|
|
|
|
public BlockingPrefWithSliceController(Context context, String preferenceKey) {
|
|
super(context, preferenceKey);
|
|
}
|
|
|
|
@Override
|
|
public void displayPreference(PreferenceScreen screen) {
|
|
super.displayPreference(screen);
|
|
mPreferenceCategory = screen.findPreference(getPreferenceKey());
|
|
mSliceIntentAction = mContext.getResources().getString(
|
|
R.string.config_bt_slice_intent_action);
|
|
mSlicePendingIntentAction = mContext.getResources().getString(
|
|
R.string.config_bt_slice_pending_intent_action);
|
|
mExtraIntent = mContext.getResources().getString(R.string.config_bt_slice_extra_intent);
|
|
mExtraPendingIntent = mContext.getResources().getString(
|
|
R.string.config_bt_slice_extra_pending_intent);
|
|
}
|
|
|
|
@Override
|
|
public int getAvailabilityStatus() {
|
|
return mUri != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
|
|
}
|
|
|
|
public void setSliceUri(Uri uri) {
|
|
mUri = uri;
|
|
mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> {
|
|
Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type);
|
|
});
|
|
|
|
//TODO(b/120803703): figure out why we need to remove observer first
|
|
mLiveData.removeObserver(this);
|
|
}
|
|
|
|
@Override
|
|
public void onStart() {
|
|
if (mLiveData == null) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
mLiveData.observeForever(this);
|
|
} catch (SecurityException e) {
|
|
Log.w(TAG, "observeForever - no permission");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onStop() {
|
|
if (mLiveData == null) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
mLiveData.removeObserver(this);
|
|
} catch (SecurityException e) {
|
|
Log.w(TAG, "removeObserver - no permission");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onChanged(Slice slice) {
|
|
updatePreferenceFromSlice(slice);
|
|
if (mUiBlockListener != null) {
|
|
mUiBlockListener.onBlockerWorkFinished(this);
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void updatePreferenceFromSlice(Slice slice) {
|
|
if (TextUtils.isEmpty(mSliceIntentAction)
|
|
|| TextUtils.isEmpty(mExtraIntent)
|
|
|| TextUtils.isEmpty(mSlicePendingIntentAction)
|
|
|| TextUtils.isEmpty(mExtraPendingIntent)) {
|
|
Log.d(TAG, "No configs");
|
|
return;
|
|
}
|
|
if (slice == null || slice.hasHint(HINT_PERMISSION_REQUEST)) {
|
|
Log.d(TAG, "Current slice: " + slice);
|
|
removePreferenceListFromPreferenceCategory();
|
|
return;
|
|
}
|
|
updatePreferenceListAndPreferenceCategory(parseSliceToPreferenceList(slice));
|
|
}
|
|
|
|
private List<Preference> parseSliceToPreferenceList(Slice slice) {
|
|
List<Preference> preferenceItemsList = new ArrayList<>();
|
|
List<SliceItem> items = slice.getItems();
|
|
int orderLevel = 0;
|
|
for (SliceItem sliceItem : items) {
|
|
// Parse the slice
|
|
if (sliceItem.getFormat().equals(FORMAT_SLICE)) {
|
|
Optional<CharSequence> title = extractTitleFromSlice(sliceItem.getSlice());
|
|
Optional<CharSequence> subtitle = extractSubtitleFromSlice(sliceItem.getSlice());
|
|
Optional<SliceAction> action = extractActionFromSlice(sliceItem.getSlice());
|
|
// Create preference
|
|
Optional<Preference> preferenceItem = createPreferenceItem(title, subtitle, action,
|
|
orderLevel);
|
|
if (preferenceItem.isPresent()) {
|
|
orderLevel++;
|
|
preferenceItemsList.add(preferenceItem.get());
|
|
}
|
|
}
|
|
}
|
|
return preferenceItemsList;
|
|
}
|
|
|
|
private Optional<Preference> createPreferenceItem(Optional<CharSequence> title,
|
|
Optional<CharSequence> subtitle, Optional<SliceAction> sliceAction, int orderLevel) {
|
|
Log.d(TAG, "Title: " + title.orElse("no title")
|
|
+ ", Subtitle: " + subtitle.orElse("no Subtitle")
|
|
+ ", Action: " + sliceAction.orElse(null));
|
|
if (!title.isPresent()) {
|
|
return Optional.empty();
|
|
}
|
|
String key = PREFIX_KEY + title.get();
|
|
Preference preference = mPreferenceCategory.findPreference(key);
|
|
if (preference == null) {
|
|
preference = new Preference(mContext);
|
|
preference.setKey(key);
|
|
mPreferenceCategory.addPreference(preference);
|
|
}
|
|
preference.setTitle(title.get());
|
|
preference.setOrder(orderLevel);
|
|
if (subtitle.isPresent()) {
|
|
preference.setSummary(subtitle.get());
|
|
}
|
|
if (sliceAction.isPresent()) {
|
|
// To support the settings' 2 panel feature, here can't use the slice's
|
|
// PendingIntent.send(). Since the PendingIntent.send() always take NEW_TASK flag.
|
|
// Therefore, transfer the slice's PendingIntent to Intent and start it
|
|
// without NEW_TASK.
|
|
preference.setIcon(sliceAction.get().getIcon().loadDrawable(mContext));
|
|
Intent intentFromSliceAction = sliceAction.get().getAction().getIntent();
|
|
Intent expectedActivityIntent = null;
|
|
Log.d(TAG, "SliceAction: intent's Action:" + intentFromSliceAction.getAction());
|
|
if (intentFromSliceAction.getAction().equals(mSliceIntentAction)) {
|
|
expectedActivityIntent = intentFromSliceAction
|
|
.getParcelableExtra(mExtraIntent, Intent.class);
|
|
} else if (intentFromSliceAction.getAction().equals(
|
|
mSlicePendingIntentAction)) {
|
|
PendingIntent pendingIntent = intentFromSliceAction
|
|
.getParcelableExtra(mExtraPendingIntent, PendingIntent.class);
|
|
expectedActivityIntent =
|
|
pendingIntent != null ? pendingIntent.getIntent() : null;
|
|
} else {
|
|
expectedActivityIntent = intentFromSliceAction;
|
|
}
|
|
if (expectedActivityIntent != null) {
|
|
Log.d(TAG, "setIntent: ActivityIntent" + expectedActivityIntent);
|
|
// Since UI needs to support the Settings' 2 panel feature, the intent can't use the
|
|
// FLAG_ACTIVITY_NEW_TASK. The above intent may have the FLAG_ACTIVITY_NEW_TASK
|
|
// flag, so removes it before startActivity(preference.setIntent).
|
|
expectedActivityIntent.removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
preference.setIntent(expectedActivityIntent);
|
|
} else {
|
|
Log.d(TAG, "setIntent: Intent is null");
|
|
}
|
|
}
|
|
|
|
return Optional.of(preference);
|
|
}
|
|
|
|
private void removePreferenceListFromPreferenceCategory() {
|
|
mCurrentPreferencesList.stream()
|
|
.forEach(p -> mPreferenceCategory.removePreference(p));
|
|
mCurrentPreferencesList.clear();
|
|
}
|
|
|
|
private void updatePreferenceListAndPreferenceCategory(List<Preference> newPreferenceList) {
|
|
List<Preference> removedItemList = new ArrayList<>(mCurrentPreferencesList);
|
|
for (Preference item : mCurrentPreferencesList) {
|
|
if (newPreferenceList.stream().anyMatch(p -> item.compareTo(p) == 0)) {
|
|
removedItemList.remove(item);
|
|
}
|
|
}
|
|
removedItemList.stream()
|
|
.forEach(p -> mPreferenceCategory.removePreference(p));
|
|
mCurrentPreferencesList = newPreferenceList;
|
|
}
|
|
|
|
private Optional<CharSequence> extractTitleFromSlice(Slice slice) {
|
|
return extractTextFromSlice(slice, HINT_TITLE);
|
|
}
|
|
|
|
private Optional<CharSequence> extractSubtitleFromSlice(Slice slice) {
|
|
// For subtitle items, there isn't a hint available.
|
|
return extractTextFromSlice(slice, /* hint= */ null);
|
|
}
|
|
|
|
private Optional<CharSequence> extractTextFromSlice(Slice slice, @Nullable String hint) {
|
|
for (SliceItem item : slice.getItems()) {
|
|
if (item.getFormat().equals(FORMAT_TEXT)
|
|
&& ((TextUtils.isEmpty(hint) && item.getHints().isEmpty())
|
|
|| (!TextUtils.isEmpty(hint) && item.hasHint(hint)))) {
|
|
return Optional.ofNullable(item.getText());
|
|
}
|
|
}
|
|
return Optional.empty();
|
|
}
|
|
|
|
private Optional<SliceAction> extractActionFromSlice(Slice slice) {
|
|
for (SliceItem item : slice.getItems()) {
|
|
if (item.getFormat().equals(FORMAT_SLICE)) {
|
|
if (item.hasHint(HINT_TITLE)) {
|
|
Optional<SliceAction> result = extractActionFromSlice(item.getSlice());
|
|
if (result.isPresent()) {
|
|
return result;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (item.getFormat().equals(FORMAT_ACTION)) {
|
|
Optional<IconCompat> icon = extractIconFromSlice(item.getSlice());
|
|
Optional<CharSequence> title = extractTitleFromSlice(item.getSlice());
|
|
if (icon.isPresent()) {
|
|
return Optional.of(
|
|
SliceAction.create(
|
|
item.getAction(),
|
|
icon.get(),
|
|
ListBuilder.ICON_IMAGE,
|
|
title.orElse(/* other= */ "")));
|
|
}
|
|
}
|
|
}
|
|
return Optional.empty();
|
|
}
|
|
|
|
private Optional<IconCompat> extractIconFromSlice(Slice slice) {
|
|
for (SliceItem item : slice.getItems()) {
|
|
if (item.getFormat().equals(FORMAT_IMAGE)) {
|
|
return Optional.of(item.getIcon());
|
|
}
|
|
}
|
|
return Optional.empty();
|
|
}
|
|
}
|