1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settingslib.widget; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.text.SpannableString; 22 import android.text.Spanned; 23 import android.text.TextUtils; 24 import android.text.method.LinkMovementMethod; 25 import android.text.style.ClickableSpan; 26 import android.text.style.URLSpan; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.view.View; 30 import android.widget.TextView; 31 32 import androidx.annotation.NonNull; 33 import androidx.annotation.StringRes; 34 import androidx.annotation.VisibleForTesting; 35 import androidx.preference.Preference; 36 import androidx.preference.PreferenceViewHolder; 37 38 import com.android.settingslib.widget.preference.footer.R; 39 40 import java.net.URISyntaxException; 41 42 /** 43 * A custom preference acting as "footer" of a page. It has a field for icon and text. It is added 44 * to screen as the last preference. 45 */ 46 public class FooterPreference extends Preference { 47 private static final String TAG = "FooterPreference"; 48 49 public static final String KEY_FOOTER = "footer_preference"; 50 private static final String INTENT_URL_PREFIX = "intent:"; 51 static final int ORDER_FOOTER = Integer.MAX_VALUE - 1; 52 @VisibleForTesting View.OnClickListener mLearnMoreListener; 53 @VisibleForTesting int mIconVisibility = View.VISIBLE; 54 private CharSequence mContentDescription; 55 private CharSequence mLearnMoreText; 56 private FooterLearnMoreSpan mLearnMoreSpan; 57 FooterPreference(Context context, AttributeSet attrs)58 public FooterPreference(Context context, AttributeSet attrs) { 59 super(context, attrs, com.android.settingslib.widget.theme.R.attr.footerPreferenceStyle); 60 init(); 61 } 62 FooterPreference(Context context)63 public FooterPreference(Context context) { 64 this(context, null); 65 } 66 linkifyTitle(TextView title)67 private void linkifyTitle(TextView title) { 68 final CharSequence text = getTitle(); 69 if (!(text instanceof Spanned)) { 70 return; 71 } 72 final ClickableSpan[] spans = 73 ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class); 74 if (spans.length == 0) { 75 return; 76 } 77 SpannableString spannable = new SpannableString(text); 78 for (ClickableSpan clickable : spans) { 79 if (!(clickable instanceof URLSpan)) { 80 continue; 81 } 82 final URLSpan urlSpan = (URLSpan) clickable; 83 final String url = urlSpan.getURL(); 84 if (url == null || !url.startsWith(INTENT_URL_PREFIX)) { 85 continue; 86 } 87 final int start = spannable.getSpanStart(urlSpan); 88 final int end = spannable.getSpanEnd(urlSpan); 89 spannable.removeSpan(urlSpan); 90 try { 91 final Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); 92 final ClickableSpan clickableSpan = 93 new ClickableSpan() { 94 @Override 95 public void onClick(@NonNull View textView) { 96 // May throw ActivityNotFoundException. Just let it propagate. 97 getContext().startActivity(intent); 98 } 99 }; 100 spannable.setSpan(clickableSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 101 } catch (URISyntaxException e) { 102 Log.e(TAG, "Invalid URI " + url, e); 103 } 104 } 105 title.setText(spannable); 106 title.setMovementMethod(LinkMovementMethod.getInstance()); 107 } 108 109 @Override onBindViewHolder(PreferenceViewHolder holder)110 public void onBindViewHolder(PreferenceViewHolder holder) { 111 super.onBindViewHolder(holder); 112 TextView title = holder.itemView.findViewById(android.R.id.title); 113 if (title != null) { 114 if (!TextUtils.isEmpty(mContentDescription)) { 115 title.setContentDescription(mContentDescription); 116 } 117 linkifyTitle(title); 118 } 119 120 TextView learnMore = holder.itemView.findViewById(R.id.settingslib_learn_more); 121 if (learnMore != null) { 122 if (mLearnMoreListener != null) { 123 learnMore.setVisibility(View.VISIBLE); 124 if (TextUtils.isEmpty(mLearnMoreText)) { 125 mLearnMoreText = learnMore.getText(); 126 } else { 127 learnMore.setText(mLearnMoreText); 128 } 129 SpannableString learnMoreText = new SpannableString(mLearnMoreText); 130 if (mLearnMoreSpan != null) { 131 learnMoreText.removeSpan(mLearnMoreSpan); 132 } 133 mLearnMoreSpan = new FooterLearnMoreSpan(mLearnMoreListener); 134 learnMoreText.setSpan(mLearnMoreSpan, 0, learnMoreText.length(), 0); 135 learnMore.setText(learnMoreText); 136 } else { 137 learnMore.setVisibility(View.GONE); 138 } 139 } 140 141 View icon = holder.itemView.findViewById(R.id.icon_frame); 142 if (icon != null) { 143 icon.setVisibility(mIconVisibility); 144 } 145 } 146 147 @Override setSummary(CharSequence summary)148 public void setSummary(CharSequence summary) { 149 setTitle(summary); 150 } 151 152 @Override setSummary(int summaryResId)153 public void setSummary(int summaryResId) { 154 setTitle(summaryResId); 155 } 156 157 @Override getSummary()158 public CharSequence getSummary() { 159 return getTitle(); 160 } 161 162 /** 163 * To set content description of the {@link FooterPreference}. This can use for talkback 164 * environment if developer wants to have a customization content. 165 * 166 * @param contentDescription The resource id of the content description. 167 */ setContentDescription(CharSequence contentDescription)168 public void setContentDescription(CharSequence contentDescription) { 169 if (!TextUtils.equals(mContentDescription, contentDescription)) { 170 mContentDescription = contentDescription; 171 notifyChanged(); 172 } 173 } 174 175 /** Return the content description of footer preference. */ 176 @VisibleForTesting getContentDescription()177 CharSequence getContentDescription() { 178 return mContentDescription; 179 } 180 181 /** 182 * Sets the learn more text. 183 * 184 * @param learnMoreText The string of the learn more text. 185 */ setLearnMoreText(CharSequence learnMoreText)186 public void setLearnMoreText(CharSequence learnMoreText) { 187 if (!TextUtils.equals(mLearnMoreText, learnMoreText)) { 188 mLearnMoreText = learnMoreText; 189 notifyChanged(); 190 } 191 } 192 193 /** Assign an action for the learn more link. */ setLearnMoreAction(View.OnClickListener listener)194 public void setLearnMoreAction(View.OnClickListener listener) { 195 if (mLearnMoreListener != listener) { 196 mLearnMoreListener = listener; 197 notifyChanged(); 198 } 199 } 200 201 /** Set visibility of footer icon. */ setIconVisibility(int iconVisibility)202 public void setIconVisibility(int iconVisibility) { 203 if (mIconVisibility == iconVisibility) { 204 return; 205 } 206 mIconVisibility = iconVisibility; 207 notifyChanged(); 208 } 209 init()210 private void init() { 211 setLayoutResource(R.layout.preference_footer); 212 if (getIcon() == null) { 213 setIcon(R.drawable.settingslib_ic_info_outline_24); 214 } 215 setOrder(ORDER_FOOTER); 216 if (TextUtils.isEmpty(getKey())) { 217 setKey(KEY_FOOTER); 218 } 219 setSelectable(false); 220 } 221 222 /** The builder is convenient to creat a dynamic FooterPreference. */ 223 public static class Builder { 224 private Context mContext; 225 private String mKey; 226 private CharSequence mTitle; 227 private CharSequence mContentDescription; 228 private CharSequence mLearnMoreText; 229 Builder(@onNull Context context)230 public Builder(@NonNull Context context) { 231 mContext = context; 232 } 233 234 /** 235 * To set the key value of the {@link FooterPreference}. 236 * 237 * @param key The key value. 238 */ setKey(@onNull String key)239 public Builder setKey(@NonNull String key) { 240 mKey = key; 241 return this; 242 } 243 244 /** 245 * To set the title of the {@link FooterPreference}. 246 * 247 * @param title The title. 248 */ setTitle(CharSequence title)249 public Builder setTitle(CharSequence title) { 250 mTitle = title; 251 return this; 252 } 253 254 /** 255 * To set the title of the {@link FooterPreference}. 256 * 257 * @param titleResId The resource id of the title. 258 */ setTitle(@tringRes int titleResId)259 public Builder setTitle(@StringRes int titleResId) { 260 mTitle = mContext.getText(titleResId); 261 return this; 262 } 263 264 /** 265 * To set content description of the {@link FooterPreference}. This can use for talkback 266 * environment if developer wants to have a customization content. 267 * 268 * @param contentDescription The resource id of the content description. 269 */ setContentDescription(CharSequence contentDescription)270 public Builder setContentDescription(CharSequence contentDescription) { 271 mContentDescription = contentDescription; 272 return this; 273 } 274 275 /** 276 * To set content description of the {@link FooterPreference}. This can use for talkback 277 * environment if developer wants to have a customization content. 278 * 279 * @param contentDescriptionResId The resource id of the content description. 280 */ setContentDescription(@tringRes int contentDescriptionResId)281 public Builder setContentDescription(@StringRes int contentDescriptionResId) { 282 mContentDescription = mContext.getText(contentDescriptionResId); 283 return this; 284 } 285 286 /** 287 * To set learn more string of the learn more text. This can use for talkback environment if 288 * developer wants to have a customization content. 289 * 290 * @param learnMoreText The resource id of the learn more string. 291 */ setLearnMoreText(CharSequence learnMoreText)292 public Builder setLearnMoreText(CharSequence learnMoreText) { 293 mLearnMoreText = learnMoreText; 294 return this; 295 } 296 297 /** 298 * To set learn more string of the {@link FooterPreference}. This can use for talkback 299 * environment if developer wants to have a customization content. 300 * 301 * @param learnMoreTextResId The resource id of the learn more string. 302 */ setLearnMoreText(@tringRes int learnMoreTextResId)303 public Builder setLearnMoreText(@StringRes int learnMoreTextResId) { 304 mLearnMoreText = mContext.getText(learnMoreTextResId); 305 return this; 306 } 307 308 /** To generate the {@link FooterPreference}. */ build()309 public FooterPreference build() { 310 final FooterPreference footerPreference = new FooterPreference(mContext); 311 footerPreference.setSelectable(false); 312 if (TextUtils.isEmpty(mTitle)) { 313 throw new IllegalArgumentException("Footer title cannot be empty!"); 314 } 315 footerPreference.setTitle(mTitle); 316 if (!TextUtils.isEmpty(mKey)) { 317 footerPreference.setKey(mKey); 318 } 319 320 if (!TextUtils.isEmpty(mContentDescription)) { 321 footerPreference.setContentDescription(mContentDescription); 322 } 323 324 if (!TextUtils.isEmpty(mLearnMoreText)) { 325 footerPreference.setLearnMoreText(mLearnMoreText); 326 } 327 return footerPreference; 328 } 329 } 330 331 /** A {@link URLSpan} that opens a support page when clicked */ 332 static class FooterLearnMoreSpan extends URLSpan { 333 334 private final View.OnClickListener mClickListener; 335 FooterLearnMoreSpan(View.OnClickListener clickListener)336 FooterLearnMoreSpan(View.OnClickListener clickListener) { 337 // sets the url to empty string so we can prevent any other span processing from 338 // clearing things we need in this string. 339 super(""); 340 mClickListener = clickListener; 341 } 342 343 @Override onClick(View widget)344 public void onClick(View widget) { 345 if (mClickListener != null) { 346 mClickListener.onClick(widget); 347 } 348 } 349 } 350 } 351