/* * 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 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 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 *

* 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 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()); 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)) .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); return new ListBuilder(context, sliceData.getUri(), SLICE_TTL_MILLIS) .addRow(rowBuilder -> rowBuilder .setTitle(sliceData.getTitle()) .setSubtitle(subtitleText) .setPrimaryAction( new SliceAction(contentIntent, icon, sliceData.getTitle()))) .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()); 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)) .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 Slice buildUnavailableSlice(Context context, SliceData data, BasePreferenceController controller) { final String title = data.getTitle(); 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)) .build(); } }