1 /* 2 * Copyright (C) 2022 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.quicksearchbox.ui 18 19 import android.content.Context 20 import android.content.res.ColorStateList 21 import android.graphics.drawable.Drawable 22 import android.net.Uri 23 import android.text.Html 24 import android.text.Spannable 25 import android.text.SpannableString 26 import android.text.TextUtils 27 import android.text.style.TextAppearanceSpan 28 import android.util.AttributeSet 29 import android.util.Log 30 import android.view.View 31 import android.widget.ImageView 32 import android.widget.TextView 33 import com.android.quicksearchbox.R 34 import com.android.quicksearchbox.Source 35 import com.android.quicksearchbox.Suggestion 36 import com.android.quicksearchbox.util.Consumer 37 import com.android.quicksearchbox.util.NowOrLater 38 39 /** 40 * View for the items in the suggestions list. This includes promoted suggestions, sources, and 41 * suggestions under each source. 42 */ 43 class DefaultSuggestionView : BaseSuggestionView { 44 private val TAG = "QSB.DefaultSuggestionView" 45 private var mAsyncIcon1: DefaultSuggestionView.AsyncIcon? = null 46 private var mAsyncIcon2: DefaultSuggestionView.AsyncIcon? = null 47 48 constructor( 49 context: Context?, 50 attrs: AttributeSet?, 51 defStyle: Int 52 ) : super(context, attrs, defStyle) 53 54 constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) 55 constructor(context: Context?) : super(context) 56 57 @Override onFinishInflatenull58 override fun onFinishInflate() { 59 super.onFinishInflate() 60 mText1 = findViewById(R.id.text1) as TextView 61 mText2 = findViewById(R.id.text2) as TextView 62 mAsyncIcon1 = 63 object : AsyncIcon(mIcon1) { 64 // override default icon (when no other available) with default source icon 65 @Override 66 override fun getFallbackIconId(source: Source?): String { 67 return source?.sourceIconUri.toString() 68 } 69 70 @Override 71 override fun getFallbackIcon(source: Source?): Drawable? { 72 return source?.sourceIcon 73 } 74 } 75 mAsyncIcon2 = AsyncIcon(mIcon2) 76 } 77 78 @Override bindAsSuggestionnull79 override fun bindAsSuggestion(suggestion: Suggestion?, userQuery: String?) { 80 super.bindAsSuggestion(suggestion, userQuery) 81 val text1 = formatText(suggestion?.suggestionText1, suggestion) 82 var text2: CharSequence = suggestion?.suggestionText2Url as CharSequence 83 text2 = formatUrl(text2) 84 // If there is no text for the second line, allow the first line to be up to two lines 85 if (TextUtils.isEmpty(text2)) { 86 mText1?.setSingleLine(false) 87 mText1?.setMaxLines(2) 88 mText1?.setEllipsize(TextUtils.TruncateAt.START) 89 } else { 90 mText1?.setSingleLine(true) 91 mText1?.setMaxLines(1) 92 mText1?.setEllipsize(TextUtils.TruncateAt.MIDDLE) 93 } 94 setText1(text1) 95 setText2(text2) 96 mAsyncIcon1?.set(suggestion.suggestionSource, suggestion.suggestionIcon1) 97 mAsyncIcon2?.set(suggestion.suggestionSource, suggestion.suggestionIcon2) 98 if (DBG) { 99 Log.d( 100 TAG, 101 "bindAsSuggestion(), text1=" + 102 text1 + 103 ",text2=" + 104 text2 + 105 ",q='" + 106 userQuery + 107 ",fromHistory=" + 108 isFromHistory(suggestion) 109 ) 110 } 111 } 112 formatUrlnull113 private fun formatUrl(url: CharSequence): CharSequence { 114 val text = SpannableString(url) 115 val colors: ColorStateList = getResources().getColorStateList(R.color.url_text, null) 116 text.setSpan( 117 TextAppearanceSpan(null, 0, 0, colors, null), 118 0, 119 url.length, 120 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 121 ) 122 return text 123 } 124 formatTextnull125 private fun formatText(str: String?, suggestion: Suggestion?): CharSequence { 126 val isHtml = "html" == suggestion?.suggestionFormat 127 return if (isHtml && looksLikeHtml(str)) { 128 Html.fromHtml(str, Html.FROM_HTML_MODE_LEGACY) 129 } else { 130 str as CharSequence 131 } 132 } 133 looksLikeHtmlnull134 private fun looksLikeHtml(str: String?): Boolean { 135 if (TextUtils.isEmpty(str)) return false 136 for (i in str!!.length - 1 downTo 0) { 137 val c: Char = str[i] 138 if (c == '>' || c == '&') return true 139 } 140 return false 141 } 142 143 private open inner class AsyncIcon(view: ImageView?) { 144 private val mView: ImageView? 145 private var mCurrentId: String? = null 146 private var mWantedId: String? = null 147 setnull148 operator fun set(source: Source?, sourceIconId: String?) { 149 if (sourceIconId != null) { 150 // The iconId can just be a package-relative resource ID, which may overlap with 151 // other packages. Make sure it's globally unique. 152 val iconUri: Uri? = source?.getIconUri(sourceIconId) 153 val uniqueIconId: String? = if (iconUri == null) null else iconUri.toString() 154 mWantedId = uniqueIconId 155 if (!TextUtils.equals(mWantedId, mCurrentId)) { 156 if (DBG) Log.d(TAG, "getting icon Id=$uniqueIconId") 157 val icon: NowOrLater<Drawable?>? = source?.getIcon(sourceIconId) 158 if (icon!!.haveNow()) { 159 if (DBG) Log.d(TAG, "getIcon ready now") 160 handleNewDrawable(icon.now, uniqueIconId, source) 161 } else { 162 // make sure old icon is not visible while new one is loaded 163 if (DBG) Log.d(TAG, "getIcon getting later") 164 clearDrawable() 165 icon.getLater( 166 object : Consumer<Drawable?> { 167 @Override 168 override fun consume(value: Drawable?): Boolean { 169 if (DBG) { 170 Log.d(TAG, "IconConsumer.consume got id $uniqueIconId want id $mWantedId") 171 } 172 // ensure we have not been re-bound since the request was made. 173 if (TextUtils.equals(uniqueIconId, mWantedId)) { 174 handleNewDrawable(value, uniqueIconId, source) 175 return true 176 } 177 return false 178 } 179 } 180 ) 181 } 182 } 183 } else { 184 mWantedId = null 185 handleNewDrawable(null, null, source) 186 } 187 } 188 handleNewDrawablenull189 private fun handleNewDrawable(icon: Drawable?, id: String?, source: Source?) { 190 var mIcon: Drawable? = icon 191 if (mIcon == null) { 192 mWantedId = getFallbackIconId(source) 193 if (TextUtils.equals(mWantedId, mCurrentId)) { 194 return 195 } 196 mIcon = getFallbackIcon(source) 197 } 198 setDrawable(mIcon, id) 199 } 200 setDrawablenull201 private fun setDrawable(icon: Drawable?, id: String?) { 202 mCurrentId = id 203 setViewDrawable(mView, icon) 204 } 205 clearDrawablenull206 private fun clearDrawable() { 207 mCurrentId = null 208 mView?.setImageDrawable(null) 209 } 210 getFallbackIconIdnull211 protected open fun getFallbackIconId(source: Source?): String? { 212 return null 213 } 214 getFallbackIconnull215 protected open fun getFallbackIcon(source: Source?): Drawable? { 216 return null 217 } 218 219 init { 220 mView = view 221 } 222 } 223 224 class Factory(context: Context?) : 225 SuggestionViewInflater( 226 VIEW_ID, 227 DefaultSuggestionView::class.java, 228 R.layout.suggestion, 229 context 230 ) 231 232 companion object { 233 private const val DBG = false 234 private const val VIEW_ID = "default" 235 236 /** 237 * Sets the drawable in an image view, makes sure the view is only visible if there is a 238 * drawable. 239 */ setViewDrawablenull240 private fun setViewDrawable(v: ImageView?, drawable: Drawable?) { 241 // Set the icon even if the drawable is null, since we need to clear any 242 // previous icon. 243 v?.setImageDrawable(drawable) 244 if (drawable == null) { 245 v?.setVisibility(View.GONE) 246 } else { 247 v?.setVisibility(View.VISIBLE) 248 249 // This is a hack to get any animated drawables (like a 'working' spinner) 250 // to animate. You have to setVisible true on an AnimationDrawable to get 251 // it to start animating, but it must first have been false or else the 252 // call to setVisible will be ineffective. We need to clear up the story 253 // about animated drawables in the future, see http://b/1878430. 254 drawable.setVisible(false, false) 255 drawable.setVisible(true, false) 256 } 257 } 258 } 259 } 260