Files
app_Settings/src/com/android/settings/slices/SliceBuilderUtils.java
Matthew Fritze 47a44e8fa4 Add keywords to Slices
Attach the keywords used for Settings search to Slices.
Their primary use is helping match synonyms for presenters
which display slices without explicit Uri requests, like a launcher.

This changes:
- Updates database scheme
- Adds to SliceData object
- Grab keywords in the SliceDataConverter
- Set keywords on all slices

Test: robotests
Change-Id: I16c40d2380ffddaf0a87fb1b9cd58e95573b308f
Fixes: 78306195
2018-04-20 14:50:55 -07:00

371 lines
16 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.AVAILABLE;
import static com.android.settings.core.BasePreferenceController.DISABLED_DEPENDENT_SETTING;
import static com.android.settings.core.BasePreferenceController.DISABLED_FOR_USER;
import static com.android.settings.core.BasePreferenceController.DISABLED_UNSUPPORTED;
import static com.android.settings.core.BasePreferenceController.UNAVAILABLE_UNKNOWN;
import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY;
import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_PLATFORM_DEFINED;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.provider.SettingsSlicesContract;
import android.text.TextUtils;
import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.SubSettings;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.SliderPreferenceController;
import com.android.settings.core.TogglePreferenceController;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.search.DatabaseIndexingUtils;
import com.android.settingslib.core.AbstractPreferenceController;
import android.support.v4.graphics.drawable.IconCompat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import androidx.slice.Slice;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.SliceAction;
/**
* 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";
// A Slice should not be store for longer than 60,000 milliseconds / 1 minute.
public static final long SLICE_TTL_MILLIS = 60000;
/**
* 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) {
final BasePreferenceController controller = getPreferenceController(context, sliceData);
final Pair<Integer, Object> sliceNamePair =
Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME, sliceData.getKey());
// Log Slice requests using the same schema as SharedPreferenceLogger (but with a different
// action name).
FeatureFactory.getFactory(context).getMetricsFeatureProvider()
.action(context, MetricsEvent.ACTION_SETTINGS_SLICE_REQUESTED, sliceNamePair);
if (controller.getAvailabilityStatus() != AVAILABLE) {
return buildUnavailableSlice(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);
intent.setClass(context, SliceBroadcastReceiver.class);
intent.putExtra(EXTRA_SLICE_KEY, data.getKey());
intent.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 {@link PendingIntent} to the Settings home page.
*/
public static PendingIntent getSettingsIntent(Context context) {
final PackageManager manager = context.getPackageManager();
final Intent intent = manager.getLaunchIntentForPackage(context.getPackageName());
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,
AbstractPreferenceController controller, SliceData sliceData) {
CharSequence summaryText;
if (controller != null) {
summaryText = controller.getSummary();
if (isValidSummary(context, summaryText)) {
return summaryText;
}
}
summaryText = sliceData.getSummary();
if (isValidSummary(context, summaryText)) {
return summaryText;
}
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();
}
@VisibleForTesting
static Intent getContentIntent(Context context, SliceData sliceData) {
final Uri contentUri = new Uri.Builder().appendPath(sliceData.getKey()).build();
final Intent intent = DatabaseIndexingUtils.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 = IconCompat.createWithResource(context, sliceData.getIconResource());
final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
final TogglePreferenceController toggleController =
(TogglePreferenceController) controller;
final SliceAction sliceAction = getToggleAction(context, sliceData,
toggleController.isChecked());
final List<String> keywords = buildSliceKeywords(sliceData.getKeywords());
return new ListBuilder(context, sliceData.getUri(), SLICE_TTL_MILLIS)
.addRow(rowBuilder -> rowBuilder
.setTitle(sliceData.getTitle())
.setSubtitle(subtitleText)
.setPrimaryAction(
new SliceAction(contentIntent, icon, 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 = IconCompat.createWithResource(context, sliceData.getIconResource());
final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
final List<String> keywords = buildSliceKeywords(sliceData.getKeywords());
return new ListBuilder(context, sliceData.getUri(), SLICE_TTL_MILLIS)
.addRow(rowBuilder -> rowBuilder
.setTitle(sliceData.getTitle())
.setSubtitle(subtitleText)
.setPrimaryAction(
new SliceAction(contentIntent, icon, sliceData.getTitle())))
.setKeywords(keywords)
.build();
}
private static Slice buildSliderSlice(Context context, SliceData sliceData,
BasePreferenceController controller) {
final SliderPreferenceController sliderController = (SliderPreferenceController) controller;
final PendingIntent actionIntent = getSliderAction(context, sliceData);
final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
final IconCompat icon = IconCompat.createWithResource(context, sliceData.getIconResource());
final SliceAction primaryAction = new SliceAction(contentIntent, icon,
sliceData.getTitle());
final List<String> keywords = buildSliceKeywords(sliceData.getKeywords());
return new ListBuilder(context, sliceData.getUri(), SLICE_TTL_MILLIS)
.addInputRange(builder -> builder
.setTitle(sliceData.getTitle())
.setMax(sliderController.getMaxSteps())
.setValue(sliderController.getSliderPosition())
.setInputAction(actionIntent)
.setPrimaryAction(primaryAction))
.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 new SliceAction(actionIntent, null, isChecked);
}
private static PendingIntent getSliderAction(Context context, SliceData sliceData) {
return getActionIntent(context, SettingsSliceProvider.ACTION_SLIDER_CHANGED, sliceData);
}
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 List<String> buildSliceKeywords(String keywordString) {
if (keywordString == null) {
return new ArrayList<>();
}
final String[] keywords = keywordString.split(",");
return Arrays.asList(keywords);
}
private static Slice buildUnavailableSlice(Context context, SliceData data,
BasePreferenceController controller) {
final String title = data.getTitle();
final List<String> keywords = buildSliceKeywords(data.getKeywords());
final String summary;
final SliceAction primaryAction;
final IconCompat icon = IconCompat.createWithResource(context, data.getIconResource());
switch (controller.getAvailabilityStatus()) {
case DISABLED_UNSUPPORTED:
summary = context.getString(R.string.unsupported_setting_summary);
primaryAction = new SliceAction(getSettingsIntent(context), icon, title);
break;
case DISABLED_FOR_USER:
summary = context.getString(R.string.disabled_for_user_setting_summary);
primaryAction = new SliceAction(getContentPendingIntent(context, data), icon,
title);
break;
case DISABLED_DEPENDENT_SETTING:
summary = context.getString(R.string.disabled_dependent_setting_summary);
primaryAction = new SliceAction(getContentPendingIntent(context, data), icon,
title);
break;
case UNAVAILABLE_UNKNOWN:
default:
summary = context.getString(R.string.unknown_unavailability_setting_summary);
primaryAction = new SliceAction(getSettingsIntent(context), icon, title);
}
return new ListBuilder(context, data.getUri(), SLICE_TTL_MILLIS)
.addRow(builder -> builder
.setTitle(title)
.setSubtitle(summary)
.setPrimaryAction(primaryAction))
.setKeywords(keywords)
.build();
}
}