Add A11y Slices
Add AccessibilityPreferenceController, which wraps all a11y settings since they are share common infrastructure for enabling, current value, and availability. We add an overlay for OEMs to declare their bundled a11y services. This is the only list of services that will be possible to enabled via Settings slices. Accessibility Slices are built by getting a list of valid services, and indexing the service names as a key in the Slices DB. When they are built at runtime, they use the generic A11yPrefController to get the status and enable/disable the service. Bug: 67997836 Bug: 67997672 Test: robotests Change-Id: I66f905bf1c55eecb937945c4675c12bcbc96d698
This commit is contained in:
@@ -128,4 +128,7 @@
|
|||||||
doesn't interact well with scroll view -->
|
doesn't interact well with scroll view -->
|
||||||
<bool name="config_lock_pattern_minimal_ui">true</bool>
|
<bool name="config_lock_pattern_minimal_ui">true</bool>
|
||||||
|
|
||||||
|
<!-- List of a11y components on the device allowed to be enabled by Settings Slices -->
|
||||||
|
<string-array name="config_settings_slices_accessibility_components" translatable="false"/>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@@ -342,6 +342,21 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements
|
|||||||
return super.onPreferenceTreeClick(preference);
|
return super.onPreferenceTreeClick(preference);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static CharSequence getServiceSummary(Context context, AccessibilityServiceInfo info,
|
||||||
|
boolean serviceEnabled) {
|
||||||
|
final String serviceState = serviceEnabled
|
||||||
|
? context.getString(R.string.accessibility_summary_state_enabled)
|
||||||
|
: context.getString(R.string.accessibility_summary_state_disabled);
|
||||||
|
final CharSequence serviceSummary = info.loadSummary(context.getPackageManager());
|
||||||
|
final String stateSummaryCombo = context.getString(
|
||||||
|
R.string.preference_summary_default_combination,
|
||||||
|
serviceState, serviceSummary);
|
||||||
|
|
||||||
|
return (TextUtils.isEmpty(serviceSummary))
|
||||||
|
? serviceState
|
||||||
|
: stateSummaryCombo;
|
||||||
|
}
|
||||||
|
|
||||||
private void handleToggleTextContrastPreferenceClick() {
|
private void handleToggleTextContrastPreferenceClick() {
|
||||||
Settings.Secure.putInt(getContentResolver(),
|
Settings.Secure.putInt(getContentResolver(),
|
||||||
Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED,
|
Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED,
|
||||||
@@ -543,15 +558,9 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements
|
|||||||
preference.setSummary(R.string.accessibility_summary_state_stopped);
|
preference.setSummary(R.string.accessibility_summary_state_stopped);
|
||||||
description = getString(R.string.accessibility_description_state_stopped);
|
description = getString(R.string.accessibility_description_state_stopped);
|
||||||
} else {
|
} else {
|
||||||
final String serviceState = serviceEnabled ?
|
final CharSequence serviceSummary = getServiceSummary(getContext(), info,
|
||||||
getString(R.string.accessibility_summary_state_enabled) :
|
serviceEnabled);
|
||||||
getString(R.string.accessibility_summary_state_disabled);
|
preference.setSummary(serviceSummary);
|
||||||
final CharSequence serviceSummary = info.loadSummary(getPackageManager());
|
|
||||||
final String stateSummaryCombo = getString(
|
|
||||||
R.string.preference_summary_default_combination,
|
|
||||||
serviceState, serviceSummary);
|
|
||||||
preference.setSummary((TextUtils.isEmpty(serviceSummary)) ? serviceState
|
|
||||||
: stateSummaryCombo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable all accessibility services that are not permitted.
|
// Disable all accessibility services that are not permitted.
|
||||||
|
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.accessibility;
|
||||||
|
|
||||||
|
import android.accessibilityservice.AccessibilityServiceInfo;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import android.view.accessibility.AccessibilityManager;
|
||||||
|
|
||||||
|
import com.android.settings.accessibility.AccessibilitySettings;
|
||||||
|
import com.android.settings.core.TogglePreferenceController;
|
||||||
|
import com.android.settingslib.accessibility.AccessibilityUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PreferenceController for accessibility services to be used by Slices.
|
||||||
|
* Wraps the common logic which enables accessibility services and checks their availability.
|
||||||
|
* <p>
|
||||||
|
* Should not be used in a {@link com.android.settings.dashboard.DashboardFragment}.
|
||||||
|
*/
|
||||||
|
public class AccessibilitySlicePreferenceController extends TogglePreferenceController {
|
||||||
|
|
||||||
|
private final ComponentName mComponentName;
|
||||||
|
|
||||||
|
private final int ON = 1;
|
||||||
|
private final int OFF = 0;
|
||||||
|
|
||||||
|
public AccessibilitySlicePreferenceController(Context context, String preferenceKey) {
|
||||||
|
super(context, preferenceKey);
|
||||||
|
mComponentName = ComponentName.unflattenFromString(getPreferenceKey());
|
||||||
|
|
||||||
|
if (mComponentName == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Illegal Component Name from: " + preferenceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getSummary() {
|
||||||
|
final AccessibilityServiceInfo serviceInfo = getAccessibilityServiceInfo();
|
||||||
|
return serviceInfo == null
|
||||||
|
? "" : AccessibilitySettings.getServiceSummary(mContext, serviceInfo, isChecked());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isChecked() {
|
||||||
|
final ContentResolver contentResolver = mContext.getContentResolver();
|
||||||
|
final boolean accessibilityEnabled = Settings.Secure.getInt(contentResolver,
|
||||||
|
Settings.Secure.ACCESSIBILITY_ENABLED, OFF) == ON;
|
||||||
|
|
||||||
|
if (!accessibilityEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Set<ComponentName> componentNames =
|
||||||
|
AccessibilityUtils.getEnabledServicesFromSettings(mContext);
|
||||||
|
|
||||||
|
return componentNames.contains(mComponentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setChecked(boolean isChecked) {
|
||||||
|
if (getAccessibilityServiceInfo() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AccessibilityUtils.setAccessibilityServiceState(mContext, mComponentName, isChecked);
|
||||||
|
return isChecked == isChecked(); // Verify that it was probably changed.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAvailabilityStatus() {
|
||||||
|
// Return unsupported when the service is disabled or not installed.
|
||||||
|
return getAccessibilityServiceInfo() == null ? DISABLED_UNSUPPORTED : AVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AccessibilityServiceInfo getAccessibilityServiceInfo() {
|
||||||
|
final AccessibilityManager accessibilityManager = mContext.getSystemService(
|
||||||
|
AccessibilityManager.class);
|
||||||
|
final List<AccessibilityServiceInfo> serviceList =
|
||||||
|
accessibilityManager.getInstalledAccessibilityServiceList();
|
||||||
|
|
||||||
|
for (AccessibilityServiceInfo serviceInfo : serviceList) {
|
||||||
|
if (mComponentName.equals(serviceInfo.getComponentName())) {
|
||||||
|
return serviceInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@@ -148,7 +148,7 @@ public class SettingsSliceProvider extends SliceProvider {
|
|||||||
void loadSlice(Uri uri) {
|
void loadSlice(Uri uri) {
|
||||||
long startBuildTime = System.currentTimeMillis();
|
long startBuildTime = System.currentTimeMillis();
|
||||||
|
|
||||||
SliceData sliceData = mSlicesDatabaseAccessor.getSliceDataFromUri(uri);
|
final SliceData sliceData = mSlicesDatabaseAccessor.getSliceDataFromUri(uri);
|
||||||
mSliceDataCache.put(uri, sliceData);
|
mSliceDataCache.put(uri, sliceData);
|
||||||
getContext().getContentResolver().notifyChange(uri, null /* content observer */);
|
getContext().getContentResolver().notifyChange(uri, null /* content observer */);
|
||||||
|
|
||||||
|
@@ -113,13 +113,13 @@ public class SliceBuilderUtils {
|
|||||||
* - key
|
* - key
|
||||||
* <p>
|
* <p>
|
||||||
* Examples of valid paths are:
|
* Examples of valid paths are:
|
||||||
* - intent/wifi
|
* - /intent/wifi
|
||||||
* - intent/bluetooth
|
* - /intent/bluetooth
|
||||||
* - action/wifi
|
* - /action/wifi
|
||||||
* - action/accessibility/servicename
|
* - /action/accessibility/servicename
|
||||||
*
|
*
|
||||||
* @param uri of the Slice. Follows pattern outlined in {@link SettingsSliceProvider}.
|
* @param uri of the Slice. Follows pattern outlined in {@link SettingsSliceProvider}.
|
||||||
* @return Pair whose first element {@code true} if the path is prepended with "action", and
|
* @return Pair whose first element {@code true} if the path is prepended with "intent", and
|
||||||
* second is a key.
|
* second is a key.
|
||||||
*/
|
*/
|
||||||
public static Pair<Boolean, String> getPathData(Uri uri) {
|
public static Pair<Boolean, String> getPathData(Uri uri) {
|
||||||
@@ -133,10 +133,10 @@ public class SliceBuilderUtils {
|
|||||||
throw new IllegalArgumentException("Uri (" + uri + ") has incomplete path: " + path);
|
throw new IllegalArgumentException("Uri (" + uri + ") has incomplete path: " + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean isInline = TextUtils.equals(SettingsSlicesContract.PATH_SETTING_ACTION,
|
final boolean isIntent = TextUtils.equals(SettingsSlicesContract.PATH_SETTING_INTENT,
|
||||||
split[1]);
|
split[1]);
|
||||||
|
|
||||||
return new Pair<>(isInline, split[2]);
|
return new Pair<>(isIntent, split[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -215,8 +215,8 @@ public class SliceBuilderUtils {
|
|||||||
static Intent getContentIntent(Context context, SliceData sliceData) {
|
static Intent getContentIntent(Context context, SliceData sliceData) {
|
||||||
final Uri contentUri = new Uri.Builder().appendPath(sliceData.getKey()).build();
|
final Uri contentUri = new Uri.Builder().appendPath(sliceData.getKey()).build();
|
||||||
final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context,
|
final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context,
|
||||||
sliceData.getFragmentClassName(), sliceData.getKey(), sliceData.getScreenTitle(),
|
sliceData.getFragmentClassName(), sliceData.getKey(),
|
||||||
0 /* TODO */);
|
sliceData.getScreenTitle().toString(), 0 /* TODO */);
|
||||||
intent.setClassName(context.getPackageName(), SubSettings.class.getName());
|
intent.setClassName(context.getPackageName(), SubSettings.class.getName());
|
||||||
intent.setData(contentUri);
|
intent.setData(contentUri);
|
||||||
return intent;
|
return intent;
|
||||||
|
@@ -57,7 +57,7 @@ public class SliceData {
|
|||||||
|
|
||||||
private final String mSummary;
|
private final String mSummary;
|
||||||
|
|
||||||
private final String mScreenTitle;
|
private final CharSequence mScreenTitle;
|
||||||
|
|
||||||
private final int mIconResource;
|
private final int mIconResource;
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ public class SliceData {
|
|||||||
return mSummary;
|
return mSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getScreenTitle() {
|
public CharSequence getScreenTitle() {
|
||||||
return mScreenTitle;
|
return mScreenTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ public class SliceData {
|
|||||||
|
|
||||||
private String mSummary;
|
private String mSummary;
|
||||||
|
|
||||||
private String mScreenTitle;
|
private CharSequence mScreenTitle;
|
||||||
|
|
||||||
private int mIconResource;
|
private int mIconResource;
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ public class SliceData {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder setScreenTitle(String screenTitle) {
|
public Builder setScreenTitle(CharSequence screenTitle) {
|
||||||
mScreenTitle = screenTitle;
|
mScreenTitle = screenTitle;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,12 @@ import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_PLATFO
|
|||||||
import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_SUMMARY;
|
import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_SUMMARY;
|
||||||
import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_TITLE;
|
import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_TITLE;
|
||||||
|
|
||||||
|
import android.accessibilityservice.AccessibilityServiceInfo;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.ResolveInfo;
|
||||||
|
import android.content.pm.ServiceInfo;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.content.res.XmlResourceParser;
|
import android.content.res.XmlResourceParser;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -32,9 +37,14 @@ import android.text.TextUtils;
|
|||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Xml;
|
import android.util.Xml;
|
||||||
|
import android.view.accessibility.AccessibilityManager;
|
||||||
|
|
||||||
|
import com.android.settings.accessibility.AccessibilitySlicePreferenceController;
|
||||||
import com.android.settings.core.PreferenceXmlParserUtils;
|
import com.android.settings.core.PreferenceXmlParserUtils;
|
||||||
import com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag;
|
import com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag;
|
||||||
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
|
import com.android.settings.R;
|
||||||
|
import com.android.settings.accessibility.AccessibilitySettings;
|
||||||
import com.android.settings.dashboard.DashboardFragment;
|
import com.android.settings.dashboard.DashboardFragment;
|
||||||
import com.android.settings.overlay.FeatureFactory;
|
import com.android.settings.overlay.FeatureFactory;
|
||||||
import com.android.settings.search.DatabaseIndexingUtils;
|
import com.android.settings.search.DatabaseIndexingUtils;
|
||||||
@@ -46,10 +56,16 @@ import org.xmlpull.v1.XmlPullParserException;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts {@link DashboardFragment} to {@link SliceData}.
|
* Converts all Slice sources into {@link SliceData}.
|
||||||
|
* This includes:
|
||||||
|
* - All {@link DashboardFragment DashboardFragments} indexed by settings search
|
||||||
|
* - Accessibility services
|
||||||
*/
|
*/
|
||||||
class SliceDataConverter {
|
class SliceDataConverter {
|
||||||
|
|
||||||
@@ -101,6 +117,8 @@ class SliceDataConverter {
|
|||||||
mSliceData.addAll(providerSliceData);
|
mSliceData.addAll(providerSliceData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final List<SliceData> a11ySliceData = getAccessibilitySliceData();
|
||||||
|
mSliceData.addAll(a11ySliceData);
|
||||||
return mSliceData;
|
return mSliceData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,4 +226,58 @@ class SliceDataConverter {
|
|||||||
}
|
}
|
||||||
return xmlSliceData;
|
return xmlSliceData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<SliceData> getAccessibilitySliceData() {
|
||||||
|
final List<SliceData> sliceData = new ArrayList<>();
|
||||||
|
|
||||||
|
final String accessibilityControllerClassName =
|
||||||
|
AccessibilitySlicePreferenceController.class.getName();
|
||||||
|
final String fragmentClassName = AccessibilitySettings.class.getName();
|
||||||
|
final CharSequence screenTitle = mContext.getText(R.string.accessibility_settings);
|
||||||
|
|
||||||
|
final SliceData.Builder sliceDataBuilder = new SliceData.Builder()
|
||||||
|
.setFragmentName(fragmentClassName)
|
||||||
|
.setScreenTitle(screenTitle)
|
||||||
|
.setPreferenceControllerClassName(accessibilityControllerClassName);
|
||||||
|
|
||||||
|
final Set<String> a11yServiceNames = new HashSet<>();
|
||||||
|
Collections.addAll(a11yServiceNames, mContext.getResources()
|
||||||
|
.getStringArray(R.array.config_settings_slices_accessibility_components));
|
||||||
|
final List<AccessibilityServiceInfo> installedServices = getAccessibilityServiceInfoList();
|
||||||
|
final PackageManager packageManager = mContext.getPackageManager();
|
||||||
|
|
||||||
|
for (AccessibilityServiceInfo a11yServiceInfo : installedServices) {
|
||||||
|
final ResolveInfo resolveInfo = a11yServiceInfo.getResolveInfo();
|
||||||
|
final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
|
||||||
|
final String packageName = serviceInfo.packageName;
|
||||||
|
final ComponentName componentName = new ComponentName(packageName, serviceInfo.name);
|
||||||
|
final String flattenedName = componentName.flattenToString();
|
||||||
|
|
||||||
|
if (!a11yServiceNames.contains(flattenedName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String title = resolveInfo.loadLabel(packageManager).toString();
|
||||||
|
int iconResource = resolveInfo.getIconResource();
|
||||||
|
if (iconResource == 0) {
|
||||||
|
iconResource = R.mipmap.ic_accessibility_generic;
|
||||||
|
}
|
||||||
|
|
||||||
|
sliceDataBuilder.setKey(flattenedName)
|
||||||
|
.setTitle(title)
|
||||||
|
.setIcon(iconResource)
|
||||||
|
.setSliceType(SliceData.SliceType.SWITCH);
|
||||||
|
|
||||||
|
sliceData.add(sliceDataBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return sliceData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
List<AccessibilityServiceInfo> getAccessibilityServiceInfoList() {
|
||||||
|
final AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(
|
||||||
|
mContext);
|
||||||
|
return accessibilityManager.getInstalledAccessibilityServiceList();
|
||||||
|
}
|
||||||
}
|
}
|
@@ -76,7 +76,7 @@ public class SlicesDatabaseAccessor {
|
|||||||
*/
|
*/
|
||||||
public SliceData getSliceDataFromKey(String key) {
|
public SliceData getSliceDataFromKey(String key) {
|
||||||
Cursor cursor = getIndexedSliceData(key);
|
Cursor cursor = getIndexedSliceData(key);
|
||||||
return buildSliceData(cursor, null /* uri */, false /* isInlineOnly */);
|
return buildSliceData(cursor, null /* uri */, false /* isIntentOnly */);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Cursor getIndexedSliceData(String path) {
|
private Cursor getIndexedSliceData(String path) {
|
||||||
@@ -111,7 +111,7 @@ public class SlicesDatabaseAccessor {
|
|||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private SliceData buildSliceData(Cursor cursor, Uri uri, boolean isInlineOnly) {
|
private SliceData buildSliceData(Cursor cursor, Uri uri, boolean isIntentOnly) {
|
||||||
final String key = cursor.getString(cursor.getColumnIndex(IndexColumns.KEY));
|
final String key = cursor.getString(cursor.getColumnIndex(IndexColumns.KEY));
|
||||||
final String title = cursor.getString(cursor.getColumnIndex(IndexColumns.TITLE));
|
final String title = cursor.getString(cursor.getColumnIndex(IndexColumns.TITLE));
|
||||||
final String summary = cursor.getString(cursor.getColumnIndex(IndexColumns.SUMMARY));
|
final String summary = cursor.getString(cursor.getColumnIndex(IndexColumns.SUMMARY));
|
||||||
@@ -127,7 +127,7 @@ public class SlicesDatabaseAccessor {
|
|||||||
int sliceType = cursor.getInt(
|
int sliceType = cursor.getInt(
|
||||||
cursor.getColumnIndex(IndexColumns.SLICE_TYPE));
|
cursor.getColumnIndex(IndexColumns.SLICE_TYPE));
|
||||||
|
|
||||||
if (!isInlineOnly) {
|
if (isIntentOnly) {
|
||||||
sliceType = SliceData.SliceType.INTENT;
|
sliceType = SliceData.SliceType.INTENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -104,7 +104,7 @@ class SlicesIndexer implements Runnable {
|
|||||||
values.put(IndexColumns.KEY, dataRow.getKey());
|
values.put(IndexColumns.KEY, dataRow.getKey());
|
||||||
values.put(IndexColumns.TITLE, dataRow.getTitle());
|
values.put(IndexColumns.TITLE, dataRow.getTitle());
|
||||||
values.put(IndexColumns.SUMMARY, dataRow.getSummary());
|
values.put(IndexColumns.SUMMARY, dataRow.getSummary());
|
||||||
values.put(IndexColumns.SCREENTITLE, dataRow.getScreenTitle());
|
values.put(IndexColumns.SCREENTITLE, dataRow.getScreenTitle().toString());
|
||||||
values.put(IndexColumns.ICON_RESOURCE, dataRow.getIconResource());
|
values.put(IndexColumns.ICON_RESOURCE, dataRow.getIconResource());
|
||||||
values.put(IndexColumns.FRAGMENT, dataRow.getFragmentClassName());
|
values.put(IndexColumns.FRAGMENT, dataRow.getFragmentClassName());
|
||||||
values.put(IndexColumns.CONTROLLER, dataRow.getPreferenceController());
|
values.put(IndexColumns.CONTROLLER, dataRow.getPreferenceController());
|
||||||
|
@@ -1,2 +1,4 @@
|
|||||||
com.android.settings.testutils.FakeToggleController
|
com.android.settings.testutils.FakeToggleController
|
||||||
com.android.settings.testutils.FakeSliderController
|
com.android.settings.testutils.FakeSliderController
|
||||||
|
com.android.settings.core.TogglePreferenceControllerTest$FakeToggle
|
||||||
|
com.android.settings.accessibility.AccessibilitySlicePreferenceController
|
||||||
|
@@ -62,4 +62,9 @@
|
|||||||
<bool name="config_show_wifi_ip_address">false</bool>
|
<bool name="config_show_wifi_ip_address">false</bool>
|
||||||
<bool name="config_show_wifi_mac_address">false</bool>
|
<bool name="config_show_wifi_mac_address">false</bool>
|
||||||
<bool name="config_disable_uninstall_update">true</bool>
|
<bool name="config_disable_uninstall_update">true</bool>
|
||||||
|
|
||||||
|
<!-- List of a11y components on the device allowed to be enabled by Settings Slices -->
|
||||||
|
<string-array name="config_settings_slices_accessibility_components" translatable="false">
|
||||||
|
<item>fake_package/fake_service</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.accessibility;
|
||||||
|
|
||||||
|
import static com.android.settings.core.BasePreferenceController.AVAILABLE;
|
||||||
|
import static com.android.settings.core.BasePreferenceController.DISABLED_UNSUPPORTED;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import android.accessibilityservice.AccessibilityServiceInfo;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.ResolveInfo;
|
||||||
|
import android.content.pm.ServiceInfo;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import android.view.accessibility.AccessibilityManager;
|
||||||
|
|
||||||
|
import com.android.settings.accessibility.AccessibilitySlicePreferenceController;
|
||||||
|
import com.android.settings.testutils.SettingsRobolectricTestRunner;
|
||||||
|
import com.android.settingslib.accessibility.AccessibilityUtils;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.RuntimeEnvironment;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.robolectric.shadow.api.Shadow;
|
||||||
|
import org.robolectric.shadows.ShadowAccessibilityManager;
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
@RunWith(SettingsRobolectricTestRunner.class)
|
||||||
|
public class AccessibilitySlicePreferenceControllerTest {
|
||||||
|
|
||||||
|
private final String PACKAGE_NAME = "com.android.settings.fake";
|
||||||
|
private final String CLASS_NAME = "com.android.settings.fake.classname";
|
||||||
|
private final String SERVICE_NAME = PACKAGE_NAME + "/" + CLASS_NAME;
|
||||||
|
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
|
private AccessibilitySlicePreferenceController mController;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
mContext = RuntimeEnvironment.application;
|
||||||
|
|
||||||
|
final ContentResolver contentResolver = mContext.getContentResolver();
|
||||||
|
Settings.Secure.putInt(contentResolver, Settings.Secure.ACCESSIBILITY_ENABLED, 1 /* on */);
|
||||||
|
Settings.Secure.putString(contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
|
||||||
|
SERVICE_NAME);
|
||||||
|
|
||||||
|
// Register the fake a11y Service
|
||||||
|
ShadowAccessibilityManager shadowAccessibilityManager = Shadow.extract(
|
||||||
|
RuntimeEnvironment.application.getSystemService(AccessibilityManager.class));
|
||||||
|
shadowAccessibilityManager.setInstalledAccessibilityServiceList(getFakeServiceList());
|
||||||
|
|
||||||
|
mController = new AccessibilitySlicePreferenceController(mContext, SERVICE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getAvailability_availableService_returnsAvailable() {
|
||||||
|
assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getAvailability_unknownService_returnsUnsupported() {
|
||||||
|
AccessibilitySlicePreferenceController controller =
|
||||||
|
new AccessibilitySlicePreferenceController(mContext, "fake_service/name");
|
||||||
|
|
||||||
|
assertThat(controller.getAvailabilityStatus()).isEqualTo(DISABLED_UNSUPPORTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setChecked_availableService_serviceIsEnabled() {
|
||||||
|
mController.setChecked(true);
|
||||||
|
|
||||||
|
assertThat(mController.isChecked()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setNotChecked_availableService_serviceIsDisabled() {
|
||||||
|
mController.setChecked(false);
|
||||||
|
|
||||||
|
assertThat(mController.isChecked()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isChecked_serviceEnabled_returnsTrue() {
|
||||||
|
AccessibilityUtils.setAccessibilityServiceState(mContext,
|
||||||
|
ComponentName.unflattenFromString(mController.getPreferenceKey()), true);
|
||||||
|
|
||||||
|
assertThat(mController.isChecked()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isChecked_serviceNotEnabled_returnsFalse() {
|
||||||
|
AccessibilitySlicePreferenceController controller =
|
||||||
|
new AccessibilitySlicePreferenceController(mContext, "fake_service/name");
|
||||||
|
|
||||||
|
assertThat(controller.isChecked()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void illegalServiceName_exceptionThrown() {
|
||||||
|
new AccessibilitySlicePreferenceController(mContext, "not_split_by_slash");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<AccessibilityServiceInfo> getFakeServiceList() {
|
||||||
|
final List<AccessibilityServiceInfo> infoList = new ArrayList<>();
|
||||||
|
|
||||||
|
final ServiceInfo serviceInfo = new ServiceInfo();
|
||||||
|
serviceInfo.packageName = PACKAGE_NAME;
|
||||||
|
serviceInfo.name = CLASS_NAME;
|
||||||
|
|
||||||
|
final ResolveInfo resolveInfo = new ResolveInfo();
|
||||||
|
resolveInfo.serviceInfo = serviceInfo;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final AccessibilityServiceInfo info = new AccessibilityServiceInfo(resolveInfo,
|
||||||
|
mContext);
|
||||||
|
ComponentName componentName = new ComponentName(PACKAGE_NAME, CLASS_NAME);
|
||||||
|
info.setComponentName(componentName);
|
||||||
|
infoList.add(info);
|
||||||
|
} catch (XmlPullParserException | IOException e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return infoList;
|
||||||
|
}
|
||||||
|
}
|
@@ -277,7 +277,7 @@ public class SliceBuilderUtilsTest {
|
|||||||
|
|
||||||
final Pair<Boolean, String> pathPair = SliceBuilderUtils.getPathData(uri);
|
final Pair<Boolean, String> pathPair = SliceBuilderUtils.getPathData(uri);
|
||||||
|
|
||||||
assertThat(pathPair.first).isFalse();
|
assertThat(pathPair.first).isTrue();
|
||||||
assertThat(pathPair.second).isEqualTo(KEY);
|
assertThat(pathPair.second).isEqualTo(KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +291,7 @@ public class SliceBuilderUtilsTest {
|
|||||||
|
|
||||||
final Pair<Boolean, String> pathPair = SliceBuilderUtils.getPathData(uri);
|
final Pair<Boolean, String> pathPair = SliceBuilderUtils.getPathData(uri);
|
||||||
|
|
||||||
assertThat(pathPair.first).isTrue();
|
assertThat(pathPair.first).isFalse();
|
||||||
assertThat(pathPair.second).isEqualTo(KEY);
|
assertThat(pathPair.second).isEqualTo(KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +316,7 @@ public class SliceBuilderUtilsTest {
|
|||||||
|
|
||||||
final Pair<Boolean, String> pathPair = SliceBuilderUtils.getPathData(uri);
|
final Pair<Boolean, String> pathPair = SliceBuilderUtils.getPathData(uri);
|
||||||
|
|
||||||
assertThat(pathPair.first).isTrue();
|
assertThat(pathPair.first).isFalse();
|
||||||
assertThat(pathPair.second).isEqualTo(KEY + "/" + KEY);
|
assertThat(pathPair.second).isEqualTo(KEY + "/" + KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -17,8 +17,22 @@
|
|||||||
package com.android.settings.slices;
|
package com.android.settings.slices;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
|
||||||
|
import android.accessibilityservice.AccessibilityServiceInfo;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.ResolveInfo;
|
||||||
|
import android.content.pm.ServiceInfo;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import com.android.settings.R;
|
||||||
|
import com.android.settings.accessibility.AccessibilitySettings;
|
||||||
|
import com.android.settings.accessibility.AccessibilitySlicePreferenceController;
|
||||||
import com.android.settings.search.FakeIndexProvider;
|
import com.android.settings.search.FakeIndexProvider;
|
||||||
import com.android.settings.search.SearchFeatureProvider;
|
import com.android.settings.search.SearchFeatureProvider;
|
||||||
import com.android.settings.search.SearchFeatureProviderImpl;
|
import com.android.settings.search.SearchFeatureProviderImpl;
|
||||||
@@ -32,17 +46,29 @@ import org.junit.runner.RunWith;
|
|||||||
import org.robolectric.RuntimeEnvironment;
|
import org.robolectric.RuntimeEnvironment;
|
||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RunWith(SettingsRobolectricTestRunner.class)
|
@RunWith(SettingsRobolectricTestRunner.class)
|
||||||
public class SliceDataConverterTest {
|
public class SliceDataConverterTest {
|
||||||
|
|
||||||
private final String fakeKey = "key";
|
private final String FAKE_KEY = "key";
|
||||||
private final String fakeTitle = "title";
|
private final String FAKE_TITLE = "title";
|
||||||
private final String fakeSummary = "summary";
|
private final String FAKE_SUMMARY = "summary";
|
||||||
private final String fakeScreenTitle = "screen_title";
|
private final String FAKE_SCREEN_TITLE = "screen_title";
|
||||||
private final String fakeFragmentClassName = FakeIndexProvider.class.getName();
|
private final String FAKE_FRAGMENT_CLASSNAME = FakeIndexProvider.class.getName();
|
||||||
private final String fakeControllerName = FakePreferenceController.class.getName();
|
private final String FAKE_CONTROLLER_NAME = FakePreferenceController.class.getName();
|
||||||
|
|
||||||
|
private final String ACCESSIBILITY_FRAGMENT = AccessibilitySettings.class.getName();
|
||||||
|
private final String A11Y_CONTROLLER_NAME =
|
||||||
|
AccessibilitySlicePreferenceController.class.getName();
|
||||||
|
private final String FAKE_SERVICE_NAME = "fake_service";
|
||||||
|
private final String FAKE_ACCESSIBILITY_PACKAGE = "fake_package";
|
||||||
|
private final String FAKE_A11Y_SERVICE_NAME =
|
||||||
|
FAKE_ACCESSIBILITY_PACKAGE + "/" + FAKE_SERVICE_NAME;
|
||||||
|
private final int FAKE_ICON = 1234;
|
||||||
|
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
private SliceDataConverter mSliceDataConverter;
|
private SliceDataConverter mSliceDataConverter;
|
||||||
private SearchFeatureProvider mSearchFeatureProvider;
|
private SearchFeatureProvider mSearchFeatureProvider;
|
||||||
@@ -50,7 +76,8 @@ public class SliceDataConverterTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
mSliceDataConverter = new SliceDataConverter(RuntimeEnvironment.application);
|
mContext = RuntimeEnvironment.application;
|
||||||
|
mSliceDataConverter = spy(new SliceDataConverter(RuntimeEnvironment.application));
|
||||||
mSearchFeatureProvider = new SearchFeatureProviderImpl();
|
mSearchFeatureProvider = new SearchFeatureProviderImpl();
|
||||||
mFakeFeatureFactory = FakeFeatureFactory.setupForTest();
|
mFakeFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||||
mFakeFeatureFactory.searchFeatureProvider = mSearchFeatureProvider;
|
mFakeFeatureFactory.searchFeatureProvider = mSearchFeatureProvider;
|
||||||
@@ -68,20 +95,64 @@ public class SliceDataConverterTest {
|
|||||||
mSearchFeatureProvider.getSearchIndexableResources().getProviderValues()
|
mSearchFeatureProvider.getSearchIndexableResources().getProviderValues()
|
||||||
.add(FakeIndexProvider.class);
|
.add(FakeIndexProvider.class);
|
||||||
|
|
||||||
|
doReturn(getFakeService()).when(mSliceDataConverter).getAccessibilityServiceInfoList();
|
||||||
|
|
||||||
List<SliceData> sliceDataList = mSliceDataConverter.getSliceData();
|
List<SliceData> sliceDataList = mSliceDataConverter.getSliceData();
|
||||||
|
|
||||||
assertThat(sliceDataList).hasSize(1);
|
assertThat(sliceDataList).hasSize(2);
|
||||||
SliceData fakeSlice = sliceDataList.get(0);
|
SliceData fakeSlice0 = sliceDataList.get(0);
|
||||||
|
SliceData fakeSlice1 = sliceDataList.get(1);
|
||||||
|
|
||||||
assertThat(fakeSlice.getKey()).isEqualTo(fakeKey);
|
// Should not assume the order of the data list.
|
||||||
assertThat(fakeSlice.getTitle()).isEqualTo(fakeTitle);
|
if (TextUtils.equals(fakeSlice0.getKey(), FAKE_KEY)) {
|
||||||
assertThat(fakeSlice.getSummary()).isEqualTo(fakeSummary);
|
assertFakeSlice(fakeSlice0);
|
||||||
assertThat(fakeSlice.getScreenTitle()).isEqualTo(fakeScreenTitle);
|
assertFakeA11ySlice(fakeSlice1);
|
||||||
|
} else {
|
||||||
|
assertFakeSlice(fakeSlice1);
|
||||||
|
assertFakeA11ySlice(fakeSlice0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertFakeSlice(SliceData fakeSlice) {
|
||||||
|
assertThat(fakeSlice.getKey()).isEqualTo(FAKE_KEY);
|
||||||
|
assertThat(fakeSlice.getTitle()).isEqualTo(FAKE_TITLE);
|
||||||
|
assertThat(fakeSlice.getSummary()).isEqualTo(FAKE_SUMMARY);
|
||||||
|
assertThat(fakeSlice.getScreenTitle()).isEqualTo(FAKE_SCREEN_TITLE);
|
||||||
assertThat(fakeSlice.getIconResource()).isNotNull();
|
assertThat(fakeSlice.getIconResource()).isNotNull();
|
||||||
assertThat(fakeSlice.getUri()).isNull();
|
assertThat(fakeSlice.getUri()).isNull();
|
||||||
assertThat(fakeSlice.getFragmentClassName()).isEqualTo(fakeFragmentClassName);
|
assertThat(fakeSlice.getFragmentClassName()).isEqualTo(FAKE_FRAGMENT_CLASSNAME);
|
||||||
assertThat(fakeSlice.getPreferenceController()).isEqualTo(fakeControllerName);
|
assertThat(fakeSlice.getPreferenceController()).isEqualTo(FAKE_CONTROLLER_NAME);
|
||||||
assertThat(fakeSlice.getSliceType()).isEqualTo(SliceData.SliceType.SLIDER); // from XML
|
assertThat(fakeSlice.getSliceType()).isEqualTo(SliceData.SliceType.SLIDER);
|
||||||
assertThat(fakeSlice.isPlatformDefined()).isTrue(); // from XML
|
assertThat(fakeSlice.isPlatformDefined()).isTrue(); // from XML
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertFakeA11ySlice(SliceData fakeSlice) {
|
||||||
|
assertThat(fakeSlice.getKey()).isEqualTo(FAKE_A11Y_SERVICE_NAME);
|
||||||
|
assertThat(fakeSlice.getTitle()).isEqualTo(FAKE_TITLE);
|
||||||
|
assertThat(fakeSlice.getSummary()).isNull();
|
||||||
|
assertThat(fakeSlice.getScreenTitle()).isEqualTo(
|
||||||
|
mContext.getString(R.string.accessibility_settings));
|
||||||
|
assertThat(fakeSlice.getIconResource()).isEqualTo(FAKE_ICON);
|
||||||
|
assertThat(fakeSlice.getUri()).isNull();
|
||||||
|
assertThat(fakeSlice.getFragmentClassName()).isEqualTo(ACCESSIBILITY_FRAGMENT);
|
||||||
|
assertThat(fakeSlice.getPreferenceController()).isEqualTo(A11Y_CONTROLLER_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is fragile. Should be replaced by a proper fake Service if possible.
|
||||||
|
private List<AccessibilityServiceInfo> getFakeService() {
|
||||||
|
List<AccessibilityServiceInfo> serviceInfoList = new ArrayList<>();
|
||||||
|
AccessibilityServiceInfo serviceInfo = spy(new AccessibilityServiceInfo());
|
||||||
|
|
||||||
|
ResolveInfo resolveInfo = spy(new ResolveInfo());
|
||||||
|
resolveInfo.serviceInfo = new ServiceInfo();
|
||||||
|
resolveInfo.serviceInfo.name = FAKE_SERVICE_NAME;
|
||||||
|
resolveInfo.serviceInfo.packageName = FAKE_ACCESSIBILITY_PACKAGE;
|
||||||
|
doReturn(FAKE_TITLE).when(resolveInfo).loadLabel(any(PackageManager.class));
|
||||||
|
doReturn(FAKE_ICON).when(resolveInfo).getIconResource();
|
||||||
|
|
||||||
|
doReturn(resolveInfo).when(serviceInfo).getResolveInfo();
|
||||||
|
serviceInfoList.add(serviceInfo);
|
||||||
|
|
||||||
|
return serviceInfoList;
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user