diff --git a/res/layout/preference_animated_image.xml b/res/layout/preference_animated_image.xml new file mode 100644 index 00000000000..305b03630e1 --- /dev/null +++ b/res/layout/preference_animated_image.xml @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/preference_static_text.xml b/res/layout/preference_static_text.xml new file mode 100644 index 00000000000..b36fd58b1f3 --- /dev/null +++ b/res/layout/preference_static_text.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java b/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java index 18700dc0c4c..73df875a2be 100644 --- a/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java +++ b/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java @@ -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; } diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java index 5eb33133fcf..ee73e6fe02e 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettings.java +++ b/src/com/android/settings/accessibility/AccessibilitySettings.java @@ -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)) { diff --git a/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizard.java b/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizard.java index 16f5fcdd245..64ed4869285 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizard.java +++ b/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizard.java @@ -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) { diff --git a/src/com/android/settings/accessibility/AnimatedImagePreference.java b/src/com/android/settings/accessibility/AnimatedImagePreference.java new file mode 100644 index 00000000000..ea9e1f825f7 --- /dev/null +++ b/src/com/android/settings/accessibility/AnimatedImagePreference.java @@ -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 animated webp 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(); + } + } +} diff --git a/src/com/android/settings/accessibility/HtmlTextPreference.java b/src/com/android/settings/accessibility/HtmlTextPreference.java new file mode 100644 index 00000000000..0c295e37a51 --- /dev/null +++ b/src/com/android/settings/accessibility/HtmlTextPreference.java @@ -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 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 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 = ""; + final String targetEnd = "(?i)"; + final String replacementEnd = ""; + 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; + } +} diff --git a/src/com/android/settings/accessibility/StaticTextPreference.java b/src/com/android/settings/accessibility/StaticTextPreference.java new file mode 100644 index 00000000000..32707480b98 --- /dev/null +++ b/src/com/android/settings/accessibility/StaticTextPreference.java @@ -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); + } +} diff --git a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java index f30af54fc36..ea293b7b864 100644 --- a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java @@ -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); } } diff --git a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java index a4b18787fdb..094b909f444 100644 --- a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java @@ -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 , 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 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; } } diff --git a/tests/robotests/src/com/android/settings/accessibility/HtmlTextPreferenceTest.java b/tests/robotests/src/com/android/settings/accessibility/HtmlTextPreferenceTest.java new file mode 100644 index 00000000000..5ac5bad728d --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/HtmlTextPreferenceTest.java @@ -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 static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.text.Editable; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.xml.sax.XMLReader; + +import java.util.ArrayList; +import java.util.List; + +/** Tests for {@link HtmlTextPreference} */ +@RunWith(RobolectricTestRunner.class) +public final class HtmlTextPreferenceTest { + + private HtmlTextPreference mHtmlTextPreference; + private PreferenceViewHolder mPreferenceViewHolder; + private String mHandledTag; + private final Html.TagHandler mTagHandler = new Html.TagHandler() { + @Override + public void handleTag(boolean opening, String tag, Editable editable, XMLReader xmlReader) { + mHandledTag = tag; + } + }; + + @Before + public void setUp() { + final Context context = RuntimeEnvironment.application; + mHtmlTextPreference = new HtmlTextPreference(context); + + final LayoutInflater inflater = LayoutInflater.from(context); + final View view = + inflater.inflate(R.layout.preference_static_text, null); + mPreferenceViewHolder = PreferenceViewHolder.createInstanceForTests(view); + } + + @Test + public void testUnsupportedTagList_keepRealContentWithoutTag() { + final List testUnsupportedTagList = new ArrayList<>(); + testUnsupportedTagList.add("testTag"); + final String testStr = "Real description"; + final String expectedStr = "Real description"; + final String expectedTag = "unsupportedtag1"; + + mHtmlTextPreference.setUnsupportedTagList(testUnsupportedTagList); + mHtmlTextPreference.setSummary(testStr); + mHtmlTextPreference.setTagHandler(mTagHandler); + mHtmlTextPreference.onBindViewHolder(mPreferenceViewHolder); + + final TextView summaryView = mPreferenceViewHolder.itemView.findViewById( + android.R.id.summary); + assertThat(summaryView.getText().toString()).isEqualTo(expectedStr); + assertThat(mHandledTag).isEqualTo(expectedTag); + } +}