Merge "Support the rich content for accessibility service (1/n)"

This commit is contained in:
PETER LIANG
2019-12-05 06:41:32 +00:00
committed by Android (Google) Code Review
11 changed files with 555 additions and 10 deletions

View File

@@ -164,7 +164,10 @@ public class AccessibilityDetailsSettingsFragment extends InstrumentedFragment {
new ComponentName(packageName, settingsClassName).flattenToString());
}
extras.putParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME, componentName);
extras.putInt(AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES, info.getAnimatedImageRes());
final String htmlDescription = info.loadHtmlDescription(getActivity().getPackageManager());
extras.putString(AccessibilitySettings.EXTRA_HTML_DESCRIPTION, htmlDescription);
return extras;
}

View File

@@ -111,6 +111,8 @@ public class AccessibilitySettings extends DashboardFragment {
static final String EXTRA_SETTINGS_COMPONENT_NAME = "settings_component_name";
static final String EXTRA_VIDEO_RAW_RESOURCE_ID = "video_resource";
static final String EXTRA_LAUNCHED_FROM_SUW = "from_suw";
static final String EXTRA_ANIMATED_IMAGE_RES = "animated_image_res";
static final String EXTRA_HTML_DESCRIPTION = "html_description";
// Timeout before we update the services if packages are added/removed
// since the AccessibilityManagerService has to do that processing first
@@ -409,6 +411,10 @@ public class AccessibilitySettings extends DashboardFragment {
extras.putString(EXTRA_TITLE, title);
extras.putParcelable(EXTRA_RESOLVE_INFO, resolveInfo);
extras.putString(EXTRA_SUMMARY, description);
extras.putInt(EXTRA_ANIMATED_IMAGE_RES, info.getAnimatedImageRes());
final String htmlDescription = info.loadHtmlDescription(getPackageManager());
extras.putString(AccessibilitySettings.EXTRA_HTML_DESCRIPTION, htmlDescription);
final String settingsClassName = info.getSettingsActivityName();
if (!TextUtils.isEmpty(settingsClassName)) {

View File

@@ -169,6 +169,9 @@ public class AccessibilitySettingsForSetupWizard extends SettingsPreferenceFragm
description = getString(R.string.accessibility_service_default_description);
}
extras.putString(AccessibilitySettings.EXTRA_SUMMARY, description);
final String htmlDescription = info.loadHtmlDescription(getPackageManager());
extras.putString(AccessibilitySettings.EXTRA_HTML_DESCRIPTION, htmlDescription);
}
private static void configureMagnificationPreferenceIfNeeded(Preference preference) {

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2019 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.content.Context;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.widget.ImageView;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
/**
* A custom {@link ImageView} preference for showing animated or static image, such as <a
* href="https://developers.google.com/speed/webp/">animated webp</a> and static png.
*/
public class AnimatedImagePreference extends Preference {
private boolean mDividerAllowedAbove = false;
private Uri mImageUri;
AnimatedImagePreference(Context context) {
super(context);
setLayoutResource(R.layout.preference_animated_image);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
holder.setDividerAllowedAbove(mDividerAllowedAbove);
final ImageView imageView = holder.itemView.findViewById(R.id.animated_img);
if (imageView != null && mImageUri != null) {
imageView.setImageURI(mImageUri);
final Drawable drawable = imageView.getDrawable();
if (drawable != null) {
if (drawable instanceof AnimatedImageDrawable) {
((AnimatedImageDrawable) drawable).start();
}
}
}
}
/**
* Sets divider whether to show in preference above.
*
* @param allowed true will be drawn on above this item
*/
public void setDividerAllowedAbove(boolean allowed) {
if (allowed != mDividerAllowedAbove) {
mDividerAllowedAbove = allowed;
notifyChanged();
}
}
/**
* Set image uri to display image in {@link ImageView}
*
* @param imageUri the Uri of an image
*/
public void setImageUri(Uri imageUri) {
if (imageUri != null && !imageUri.equals(mImageUri)) {
mImageUri = imageUri;
notifyChanged();
}
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright (C) 2019 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.content.Context;
import android.text.Html;
import android.text.TextUtils;
import android.widget.TextView;
import androidx.preference.PreferenceViewHolder;
import java.util.List;
import java.util.regex.Pattern;
/**
* A custom {@link android.widget.TextView} preference that shows html text with a custom tag
* filter.
*/
public final class HtmlTextPreference extends StaticTextPreference {
private boolean mDividerAllowedAbove = false;
private int mFlag = Html.FROM_HTML_MODE_COMPACT;
private Html.ImageGetter mImageGetter;
private Html.TagHandler mTagHandler;
private List<String> mUnsupportedTagList;
HtmlTextPreference(Context context) {
super(context);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
holder.setDividerAllowedAbove(mDividerAllowedAbove);
final TextView summaryView = holder.itemView.findViewById(android.R.id.summary);
if (summaryView != null && !TextUtils.isEmpty(getSummary())) {
final String filteredText = getFilteredText(getSummary().toString());
summaryView.setText(Html.fromHtml(filteredText, mFlag, mImageGetter, mTagHandler));
}
}
/**
* Sets divider whether to show in preference above.
*
* @param allowed true will be drawn on above this item
*/
public void setDividerAllowedAbove(boolean allowed) {
if (allowed != mDividerAllowedAbove) {
mDividerAllowedAbove = allowed;
notifyChanged();
}
}
/**
* Sets the flag to which text format to be applied.
*
* @param flag to indicate that html text format
*/
public void setFlag(int flag) {
if (flag != mFlag) {
mFlag = flag;
notifyChanged();
}
}
/**
* Sets image getter and help to load corresponding images when parsing.
*
* @param imageGetter to load image by image tag content
*/
public void setImageGetter(Html.ImageGetter imageGetter) {
if (imageGetter != null && !imageGetter.equals(mImageGetter)) {
mImageGetter = imageGetter;
notifyChanged();
}
}
/**
* Sets tag handler to handle the unsupported tags.
*
* @param tagHandler the handler for unhandled tags
*/
public void setTagHandler(Html.TagHandler tagHandler) {
if (tagHandler != null && !tagHandler.equals(mTagHandler)) {
mTagHandler = tagHandler;
notifyChanged();
}
}
/**
* Sets unsupported tag list, the text will be filtered though this list in advanced.
*
* @param unsupportedTagList the list of unsupported tags
*/
public void setUnsupportedTagList(List<String> unsupportedTagList) {
if (unsupportedTagList != null && !unsupportedTagList.equals(mUnsupportedTagList)) {
mUnsupportedTagList = unsupportedTagList;
notifyChanged();
}
}
private String getFilteredText(String text) {
if (mUnsupportedTagList == null) {
return text;
}
int i = 1;
for (String tag : mUnsupportedTagList) {
if (!TextUtils.isEmpty(text)) {
final String index = String.valueOf(i++);
final String targetStart1 = "(?i)<" + tag + " ";
final String targetStart2 = "(?i)<" + tag + ">";
final String replacementStart1 = "<unsupportedtag" + index + " ";
final String replacementStart2 = "<unsupportedtag" + index + ">";
final String targetEnd = "(?i)</" + tag + ">";
final String replacementEnd = "</unsupportedtag" + index + ">";
text = Pattern.compile(targetStart1).matcher(text).replaceAll(replacementStart1);
text = Pattern.compile(targetStart2).matcher(text).replaceAll(replacementStart2);
text = Pattern.compile(targetEnd).matcher(text).replaceAll(replacementEnd);
}
}
return text;
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2019 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.content.Context;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
/**
* A custom {@link android.widget.TextView} preference that removes the title and summary
* restriction from platform {@link Preference} implementation and the icon location is kept as
* gravity top instead of center.
*/
public class StaticTextPreference extends Preference {
StaticTextPreference(Context context) {
super(context);
setLayoutResource(R.layout.preference_static_text);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
}
}

View File

@@ -24,6 +24,7 @@ import android.app.Dialog;
import android.app.admin.DevicePolicyManager;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -69,8 +70,6 @@ public class ToggleAccessibilityServicePreferenceFragment extends ToggleFeatureP
}
};
private ComponentName mComponentName;
private Dialog mDialog;
private final View.OnClickListener mViewOnClickListener =
@@ -341,5 +340,15 @@ public class ToggleAccessibilityServicePreferenceFragment extends ToggleFeatureP
}
mComponentName = arguments.getParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME);
// Settings animated image.
int animatedImageRes = arguments.getInt(AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES);
mImageUri = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.authority(mComponentName.getPackageName())
.appendPath(String.valueOf(animatedImageRes))
.build();
// Settings html description.
mHtmlDescription = arguments.getCharSequence(AccessibilitySettings.EXTRA_HTML_DESCRIPTION);
}
}

View File

@@ -16,10 +16,16 @@
package com.android.settings.accessibility;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.view.View;
import android.widget.ImageView;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
@@ -29,7 +35,9 @@ import com.android.settings.SettingsActivity;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.widget.SwitchBar;
import com.android.settings.widget.ToggleSwitch;
import com.android.settingslib.widget.FooterPreference;
import java.util.ArrayList;
import java.util.List;
public abstract class ToggleFeaturePreferenceFragment extends SettingsPreferenceFragment {
@@ -40,6 +48,30 @@ public abstract class ToggleFeaturePreferenceFragment extends SettingsPreference
protected CharSequence mSettingsTitle;
protected Intent mSettingsIntent;
protected ComponentName mComponentName;
protected Uri mImageUri;
protected CharSequence mStaticDescription;
protected CharSequence mHtmlDescription;
private static final String ANCHOR_TAG = "a";
private static final String DRAWABLE_FOLDER = "drawable";
// For html description of accessibility service, third party developer must follow the rule,
// such as <img src="R.drawable.fileName"/>, a11y settings will get third party resources
// by this.
private static final String IMG_PREFIX = "R.drawable.";
private ImageView mImageGetterCacheView;
private final Html.ImageGetter mImageGetter = (String str) -> {
if (str != null && str.startsWith(IMG_PREFIX)) {
final String fileName = str.substring(IMG_PREFIX.length());
return getDrawableFromUri(Uri.parse(
ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
+ mComponentName.getPackageName() + "/" + DRAWABLE_FOLDER + "/"
+ fileName));
}
return null;
};
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -63,15 +95,45 @@ public abstract class ToggleFeaturePreferenceFragment extends SettingsPreference
onProcessArguments(getArguments());
updateSwitchBarText(mSwitchBar);
PreferenceScreen preferenceScreen = getPreferenceScreen();
// Show the "Settings" menu as if it were a preference screen
if (mSettingsTitle != null && mSettingsIntent != null) {
PreferenceScreen preferenceScreen = getPreferenceScreen();
Preference settingsPref = new Preference(preferenceScreen.getContext());
settingsPref.setTitle(mSettingsTitle);
settingsPref.setIconSpaceReserved(true);
settingsPref.setIntent(mSettingsIntent);
preferenceScreen.addPreference(settingsPref);
}
if (mImageUri != null) {
final AnimatedImagePreference animatedImagePreference = new AnimatedImagePreference(
preferenceScreen.getContext());
animatedImagePreference.setImageUri(mImageUri);
animatedImagePreference.setDividerAllowedAbove(true);
preferenceScreen.addPreference(animatedImagePreference);
}
if (mStaticDescription != null) {
final StaticTextPreference staticTextPreference = new StaticTextPreference(
preferenceScreen.getContext());
staticTextPreference.setSummary(mStaticDescription);
preferenceScreen.addPreference(staticTextPreference);
}
if (mHtmlDescription != null) {
// For accessibility service, avoid malicious links made by third party developer
final List<String> unsupportedTagList = new ArrayList<>();
unsupportedTagList.add(ANCHOR_TAG);
final HtmlTextPreference htmlTextPreference = new HtmlTextPreference(
preferenceScreen.getContext());
htmlTextPreference.setSummary(mHtmlDescription);
htmlTextPreference.setImageGetter(mImageGetter);
htmlTextPreference.setUnsupportedTagList(unsupportedTagList);
htmlTextPreference.setDividerAllowedAbove(true);
preferenceScreen.addPreference(htmlTextPreference);
}
}
@Override
@@ -139,17 +201,30 @@ public abstract class ToggleFeaturePreferenceFragment extends SettingsPreference
// Summary.
if (arguments.containsKey(AccessibilitySettings.EXTRA_SUMMARY_RES)) {
final int summary = arguments.getInt(AccessibilitySettings.EXTRA_SUMMARY_RES);
createFooterPreference(getText(summary));
mStaticDescription = getText(summary);
} else if (arguments.containsKey(AccessibilitySettings.EXTRA_SUMMARY)) {
final CharSequence summary = arguments.getCharSequence(
AccessibilitySettings.EXTRA_SUMMARY);
createFooterPreference(summary);
mStaticDescription = summary;
}
}
private void createFooterPreference(CharSequence title) {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
preferenceScreen.addPreference(new FooterPreference.Builder(getActivity()).setTitle(
title).build());
private Drawable getDrawableFromUri(Uri imageUri) {
if (mImageGetterCacheView == null) {
mImageGetterCacheView = new ImageView(getContext());
}
mImageGetterCacheView.setAdjustViewBounds(true);
mImageGetterCacheView.setImageURI(imageUri);
final Drawable drawable = mImageGetterCacheView.getDrawable().mutate();
if (drawable != null) {
drawable.setBounds(/* left= */0, /* top= */0, drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight());
}
mImageGetterCacheView.setImageURI(null);
mImageGetterCacheView.setImageDrawable(null);
return drawable;
}
}