- Use FLAG_ACTIVITY_NEW_TASK to prevent activity stack contains many activites Fixes: 142231489 Test: 1. Press volume button and click in volume settings button 2. Press Media Volume to open SoundSettings 3. Repeat 1, Press Call Volume to open SoundSettings 4. Repeat 1, Press Alarm Volume to open SoundSettings 5. Press back, should see volume panel Change-Id: I87cd75fbd43f77446191ad50ed7a002a2202337f
461 lines
20 KiB
Java
461 lines
20 KiB
Java
/*
|
|
* Copyright (C) 2017 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.slices;
|
|
|
|
import static com.android.settings.core.BasePreferenceController.DISABLED_DEPENDENT_SETTING;
|
|
import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY;
|
|
import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_PLATFORM_DEFINED;
|
|
|
|
import android.annotation.ColorInt;
|
|
import android.app.PendingIntent;
|
|
import android.app.settings.SettingsEnums;
|
|
import android.content.ContentResolver;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.net.Uri;
|
|
import android.os.Bundle;
|
|
import android.provider.SettingsSlicesContract;
|
|
import android.text.TextUtils;
|
|
import android.util.ArraySet;
|
|
import android.util.Log;
|
|
import android.util.Pair;
|
|
|
|
import androidx.annotation.VisibleForTesting;
|
|
import androidx.core.graphics.drawable.IconCompat;
|
|
import androidx.slice.Slice;
|
|
import androidx.slice.builders.ListBuilder;
|
|
import androidx.slice.builders.ListBuilder.InputRangeBuilder;
|
|
import androidx.slice.builders.ListBuilder.RowBuilder;
|
|
import androidx.slice.builders.SliceAction;
|
|
|
|
import com.android.settings.R;
|
|
import com.android.settings.SettingsActivity;
|
|
import com.android.settings.SubSettings;
|
|
import com.android.settings.Utils;
|
|
import com.android.settings.core.BasePreferenceController;
|
|
import com.android.settings.core.SliderPreferenceController;
|
|
import com.android.settings.core.SubSettingLauncher;
|
|
import com.android.settings.core.TogglePreferenceController;
|
|
import com.android.settings.overlay.FeatureFactory;
|
|
import com.android.settingslib.core.AbstractPreferenceController;
|
|
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
* Utility class to build Slices objects and Preference Controllers based on the Database managed
|
|
* by {@link SlicesDatabaseHelper}
|
|
*/
|
|
public class SliceBuilderUtils {
|
|
|
|
private static final String TAG = "SliceBuilder";
|
|
|
|
/**
|
|
* Build a Slice from {@link SliceData}.
|
|
*
|
|
* @return a {@link Slice} based on the data provided by {@param sliceData}.
|
|
* Will build an {@link Intent} based Slice unless the Preference Controller name in
|
|
* {@param sliceData} is an inline controller.
|
|
*/
|
|
public static Slice buildSlice(Context context, SliceData sliceData) {
|
|
// Reload theme for switching dark mode on/off
|
|
context.getTheme().applyStyle(R.style.Theme_Settings_Home, true /* force */);
|
|
Log.d(TAG, "Creating slice for: " + sliceData.getPreferenceController());
|
|
final BasePreferenceController controller = getPreferenceController(context, sliceData);
|
|
FeatureFactory.getFactory(context).getMetricsFeatureProvider()
|
|
.action(SettingsEnums.PAGE_UNKNOWN,
|
|
SettingsEnums.ACTION_SETTINGS_SLICE_REQUESTED,
|
|
SettingsEnums.PAGE_UNKNOWN,
|
|
sliceData.getKey(),
|
|
0);
|
|
|
|
if (!controller.isAvailable()) {
|
|
// Cannot guarantee setting page is accessible, let the presenter handle error case.
|
|
return null;
|
|
}
|
|
|
|
if (controller.getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) {
|
|
return buildUnavailableSlice(context, sliceData);
|
|
}
|
|
|
|
if (controller.isCopyableSlice()) {
|
|
return buildCopyableSlice(context, sliceData, controller);
|
|
}
|
|
|
|
switch (sliceData.getSliceType()) {
|
|
case SliceData.SliceType.INTENT:
|
|
return buildIntentSlice(context, sliceData, controller);
|
|
case SliceData.SliceType.SWITCH:
|
|
return buildToggleSlice(context, sliceData, controller);
|
|
case SliceData.SliceType.SLIDER:
|
|
return buildSliderSlice(context, sliceData, controller);
|
|
default:
|
|
throw new IllegalArgumentException(
|
|
"Slice type passed was invalid: " + sliceData.getSliceType());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return the {@link SliceData.SliceType} for the {@param controllerClassName} and key.
|
|
*/
|
|
@SliceData.SliceType
|
|
public static int getSliceType(Context context, String controllerClassName,
|
|
String controllerKey) {
|
|
BasePreferenceController controller = getPreferenceController(context, controllerClassName,
|
|
controllerKey);
|
|
return controller.getSliceType();
|
|
}
|
|
|
|
/**
|
|
* Splits the Settings Slice Uri path into its two expected components:
|
|
* - intent/action
|
|
* - key
|
|
* <p>
|
|
* Examples of valid paths are:
|
|
* - /intent/wifi
|
|
* - /intent/bluetooth
|
|
* - /action/wifi
|
|
* - /action/accessibility/servicename
|
|
*
|
|
* @param uri of the Slice. Follows pattern outlined in {@link SettingsSliceProvider}.
|
|
* @return Pair whose first element {@code true} if the path is prepended with "intent", and
|
|
* second is a key.
|
|
*/
|
|
public static Pair<Boolean, String> getPathData(Uri uri) {
|
|
final String path = uri.getPath();
|
|
final String[] split = path.split("/", 3);
|
|
|
|
// Split should be: [{}, SLICE_TYPE, KEY].
|
|
// Example: "/action/wifi" -> [{}, "action", "wifi"]
|
|
// "/action/longer/path" -> [{}, "action", "longer/path"]
|
|
if (split.length != 3) {
|
|
return null;
|
|
}
|
|
|
|
final boolean isIntent = TextUtils.equals(SettingsSlicesContract.PATH_SETTING_INTENT,
|
|
split[1]);
|
|
|
|
return new Pair<>(isIntent, split[2]);
|
|
}
|
|
|
|
/**
|
|
* Looks at the controller classname in in {@link SliceData} from {@param sliceData}
|
|
* and attempts to build an {@link AbstractPreferenceController}.
|
|
*/
|
|
public static BasePreferenceController getPreferenceController(Context context,
|
|
SliceData sliceData) {
|
|
return getPreferenceController(context, sliceData.getPreferenceController(),
|
|
sliceData.getKey());
|
|
}
|
|
|
|
/**
|
|
* @return {@link PendingIntent} for a non-primary {@link SliceAction}.
|
|
*/
|
|
public static PendingIntent getActionIntent(Context context, String action, SliceData data) {
|
|
final Intent intent = new Intent(action)
|
|
.setData(data.getUri())
|
|
.setClass(context, SliceBroadcastReceiver.class)
|
|
.putExtra(EXTRA_SLICE_KEY, data.getKey())
|
|
.putExtra(EXTRA_SLICE_PLATFORM_DEFINED, data.isPlatformDefined());
|
|
return PendingIntent.getBroadcast(context, 0 /* requestCode */, intent,
|
|
PendingIntent.FLAG_CANCEL_CURRENT);
|
|
}
|
|
|
|
/**
|
|
* @return {@link PendingIntent} for the primary {@link SliceAction}.
|
|
*/
|
|
public static PendingIntent getContentPendingIntent(Context context, SliceData sliceData) {
|
|
final Intent intent = getContentIntent(context, sliceData);
|
|
return PendingIntent.getActivity(context, 0 /* requestCode */, intent, 0 /* flags */);
|
|
}
|
|
|
|
/**
|
|
* @return the summary text for a {@link Slice} built for {@param sliceData}.
|
|
*/
|
|
public static CharSequence getSubtitleText(Context context,
|
|
BasePreferenceController controller, SliceData sliceData) {
|
|
|
|
// Priority 1 : User prefers showing the dynamic summary in slice view rather than static
|
|
// summary. Note it doesn't require a valid summary - so we can force some slices to have
|
|
// empty summaries (ex: volume).
|
|
if (controller.useDynamicSliceSummary()) {
|
|
return controller.getSummary();
|
|
}
|
|
|
|
// Priority 2: Show summary from slice data.
|
|
CharSequence summaryText = sliceData.getSummary();
|
|
if (isValidSummary(context, summaryText)) {
|
|
return summaryText;
|
|
}
|
|
|
|
// Priority 3: Show screen title.
|
|
summaryText = sliceData.getScreenTitle();
|
|
if (isValidSummary(context, summaryText) && !TextUtils.equals(summaryText,
|
|
sliceData.getTitle())) {
|
|
return summaryText;
|
|
}
|
|
|
|
// Priority 4: Show empty text.
|
|
return "";
|
|
}
|
|
|
|
public static Uri getUri(String path, boolean isPlatformSlice) {
|
|
final String authority = isPlatformSlice
|
|
? SettingsSlicesContract.AUTHORITY
|
|
: SettingsSliceProvider.SLICE_AUTHORITY;
|
|
return new Uri.Builder()
|
|
.scheme(ContentResolver.SCHEME_CONTENT)
|
|
.authority(authority)
|
|
.appendPath(path)
|
|
.build();
|
|
}
|
|
|
|
public static Intent buildSearchResultPageIntent(Context context, String className, String key,
|
|
String screenTitle, int sourceMetricsCategory) {
|
|
final Bundle args = new Bundle();
|
|
args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
|
|
final Intent searchDestination = new SubSettingLauncher(context)
|
|
.setDestination(className)
|
|
.setArguments(args)
|
|
.setTitleText(screenTitle)
|
|
.setSourceMetricsCategory(sourceMetricsCategory)
|
|
.toIntent();
|
|
searchDestination.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key)
|
|
.setAction("com.android.settings.SEARCH_RESULT_TRAMPOLINE")
|
|
.setComponent(null);
|
|
searchDestination.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
|
|
return searchDestination;
|
|
}
|
|
|
|
public static Intent getContentIntent(Context context, SliceData sliceData) {
|
|
final Uri contentUri = new Uri.Builder().appendPath(sliceData.getKey()).build();
|
|
final Intent intent = buildSearchResultPageIntent(context,
|
|
sliceData.getFragmentClassName(), sliceData.getKey(),
|
|
sliceData.getScreenTitle().toString(), 0 /* TODO */);
|
|
intent.setClassName(context.getPackageName(), SubSettings.class.getName());
|
|
intent.setData(contentUri);
|
|
return intent;
|
|
}
|
|
|
|
private static Slice buildToggleSlice(Context context, SliceData sliceData,
|
|
BasePreferenceController controller) {
|
|
final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
|
|
final IconCompat icon = getSafeIcon(context, sliceData);
|
|
final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
|
|
@ColorInt final int color = Utils.getColorAccentDefaultColor(context);
|
|
final TogglePreferenceController toggleController =
|
|
(TogglePreferenceController) controller;
|
|
final SliceAction sliceAction = getToggleAction(context, sliceData,
|
|
toggleController.isChecked());
|
|
final Set<String> keywords = buildSliceKeywords(sliceData);
|
|
|
|
return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY)
|
|
.setAccentColor(color)
|
|
.addRow(new RowBuilder()
|
|
.setTitle(sliceData.getTitle())
|
|
.setSubtitle(subtitleText)
|
|
.setPrimaryAction(
|
|
SliceAction.createDeeplink(contentIntent, icon,
|
|
ListBuilder.ICON_IMAGE, sliceData.getTitle()))
|
|
.addEndItem(sliceAction))
|
|
.setKeywords(keywords)
|
|
.build();
|
|
}
|
|
|
|
private static Slice buildIntentSlice(Context context, SliceData sliceData,
|
|
BasePreferenceController controller) {
|
|
final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
|
|
final IconCompat icon = getSafeIcon(context, sliceData);
|
|
final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
|
|
@ColorInt final int color = Utils.getColorAccentDefaultColor(context);
|
|
final Set<String> keywords = buildSliceKeywords(sliceData);
|
|
|
|
return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY)
|
|
.setAccentColor(color)
|
|
.addRow(new RowBuilder()
|
|
.setTitle(sliceData.getTitle())
|
|
.setSubtitle(subtitleText)
|
|
.setPrimaryAction(
|
|
SliceAction.createDeeplink(contentIntent, icon,
|
|
ListBuilder.ICON_IMAGE,
|
|
sliceData.getTitle())))
|
|
.setKeywords(keywords)
|
|
.build();
|
|
}
|
|
|
|
private static Slice buildSliderSlice(Context context, SliceData sliceData,
|
|
BasePreferenceController controller) {
|
|
final SliderPreferenceController sliderController = (SliderPreferenceController) controller;
|
|
if (sliderController.getMax() <= sliderController.getMin()) {
|
|
Log.e(TAG, "Invalid sliderController: " + sliderController.getPreferenceKey());
|
|
return null;
|
|
}
|
|
final PendingIntent actionIntent = getSliderAction(context, sliceData);
|
|
final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
|
|
final IconCompat icon = getSafeIcon(context, sliceData);
|
|
@ColorInt final int color = Utils.getColorAccentDefaultColor(context);
|
|
final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
|
|
final SliceAction primaryAction = SliceAction.createDeeplink(contentIntent, icon,
|
|
ListBuilder.ICON_IMAGE, sliceData.getTitle());
|
|
final Set<String> keywords = buildSliceKeywords(sliceData);
|
|
|
|
return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY)
|
|
.setAccentColor(color)
|
|
.addInputRange(new InputRangeBuilder()
|
|
.setTitle(sliceData.getTitle())
|
|
.setSubtitle(subtitleText)
|
|
.setPrimaryAction(primaryAction)
|
|
.setMax(sliderController.getMax())
|
|
.setMin(sliderController.getMin())
|
|
.setValue(sliderController.getSliderPosition())
|
|
.setInputAction(actionIntent))
|
|
.setKeywords(keywords)
|
|
.build();
|
|
}
|
|
|
|
private static Slice buildCopyableSlice(Context context, SliceData sliceData,
|
|
BasePreferenceController controller) {
|
|
final SliceAction copyableAction = getCopyableAction(context, sliceData);
|
|
final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
|
|
final IconCompat icon = getSafeIcon(context, sliceData);
|
|
final SliceAction primaryAction = SliceAction.createDeeplink(contentIntent, icon,
|
|
ListBuilder.ICON_IMAGE,
|
|
sliceData.getTitle());
|
|
final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
|
|
@ColorInt final int color = Utils.getColorAccentDefaultColor(context);
|
|
final Set<String> keywords = buildSliceKeywords(sliceData);
|
|
|
|
return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY)
|
|
.setAccentColor(color)
|
|
.addRow(new RowBuilder()
|
|
.setTitle(sliceData.getTitle())
|
|
.setSubtitle(subtitleText)
|
|
.setPrimaryAction(primaryAction)
|
|
.addEndItem(copyableAction))
|
|
.setKeywords(keywords)
|
|
.build();
|
|
}
|
|
|
|
private static BasePreferenceController getPreferenceController(Context context,
|
|
String controllerClassName, String controllerKey) {
|
|
try {
|
|
return BasePreferenceController.createInstance(context, controllerClassName);
|
|
} catch (IllegalStateException e) {
|
|
// Do nothing
|
|
}
|
|
|
|
return BasePreferenceController.createInstance(context, controllerClassName, controllerKey);
|
|
}
|
|
|
|
private static SliceAction getToggleAction(Context context, SliceData sliceData,
|
|
boolean isChecked) {
|
|
PendingIntent actionIntent = getActionIntent(context,
|
|
SettingsSliceProvider.ACTION_TOGGLE_CHANGED, sliceData);
|
|
return SliceAction.createToggle(actionIntent, null, isChecked);
|
|
}
|
|
|
|
private static PendingIntent getSliderAction(Context context, SliceData sliceData) {
|
|
return getActionIntent(context, SettingsSliceProvider.ACTION_SLIDER_CHANGED, sliceData);
|
|
}
|
|
|
|
private static SliceAction getCopyableAction(Context context, SliceData sliceData) {
|
|
final PendingIntent intent = getActionIntent(context,
|
|
SettingsSliceProvider.ACTION_COPY, sliceData);
|
|
final IconCompat icon = IconCompat.createWithResource(context,
|
|
R.drawable.ic_content_copy_grey600_24dp);
|
|
return SliceAction.create(intent, icon, ListBuilder.ICON_IMAGE, sliceData.getTitle());
|
|
}
|
|
|
|
private static boolean isValidSummary(Context context, CharSequence summary) {
|
|
if (summary == null || TextUtils.isEmpty(summary.toString().trim())) {
|
|
return false;
|
|
}
|
|
|
|
final CharSequence placeHolder = context.getText(R.string.summary_placeholder);
|
|
final CharSequence doublePlaceHolder =
|
|
context.getText(R.string.summary_two_lines_placeholder);
|
|
|
|
return !(TextUtils.equals(summary, placeHolder)
|
|
|| TextUtils.equals(summary, doublePlaceHolder));
|
|
}
|
|
|
|
private static Set<String> buildSliceKeywords(SliceData data) {
|
|
final Set<String> keywords = new ArraySet<>();
|
|
|
|
keywords.add(data.getTitle());
|
|
|
|
if (!TextUtils.equals(data.getTitle(), data.getScreenTitle())) {
|
|
keywords.add(data.getScreenTitle().toString());
|
|
}
|
|
|
|
final String keywordString = data.getKeywords();
|
|
if (keywordString != null) {
|
|
final String[] keywordArray = keywordString.split(",");
|
|
final List<String> strippedKeywords = Arrays.stream(keywordArray)
|
|
.map(s -> s = s.trim())
|
|
.collect(Collectors.toList());
|
|
keywords.addAll(strippedKeywords);
|
|
}
|
|
|
|
return keywords;
|
|
}
|
|
|
|
private static Slice buildUnavailableSlice(Context context, SliceData data) {
|
|
final String title = data.getTitle();
|
|
final Set<String> keywords = buildSliceKeywords(data);
|
|
@ColorInt final int color = Utils.getColorAccentDefaultColor(context);
|
|
|
|
final String customSubtitle = data.getUnavailableSliceSubtitle();
|
|
final CharSequence subtitle = !TextUtils.isEmpty(customSubtitle) ? customSubtitle
|
|
: context.getText(R.string.disabled_dependent_setting_summary);
|
|
final IconCompat icon = getSafeIcon(context, data);
|
|
final SliceAction primaryAction = SliceAction.createDeeplink(
|
|
getContentPendingIntent(context, data),
|
|
icon, ListBuilder.ICON_IMAGE, title);
|
|
|
|
return new ListBuilder(context, data.getUri(), ListBuilder.INFINITY)
|
|
.setAccentColor(color)
|
|
.addRow(new RowBuilder()
|
|
.setTitle(title)
|
|
.setTitleItem(icon, ListBuilder.ICON_IMAGE)
|
|
.setSubtitle(subtitle)
|
|
.setPrimaryAction(primaryAction))
|
|
.setKeywords(keywords)
|
|
.build();
|
|
}
|
|
|
|
@VisibleForTesting
|
|
static IconCompat getSafeIcon(Context context, SliceData data) {
|
|
int iconResource = data.getIconResource();
|
|
|
|
if (iconResource == 0) {
|
|
iconResource = R.drawable.ic_settings_accent;
|
|
}
|
|
try {
|
|
return IconCompat.createWithResource(context, iconResource);
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "Falling back to settings icon because there is an error getting slice icon "
|
|
+ data.getUri(), e);
|
|
return IconCompat.createWithResource(context, R.drawable.ic_settings_accent);
|
|
}
|
|
}
|
|
}
|