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.database.DataSetObserver 20 import android.util.Log 21 import android.view.View 22 import android.view.View.OnFocusChangeListener 23 import android.view.ViewGroup 24 import com.android.quicksearchbox.Suggestion 25 import com.android.quicksearchbox.SuggestionCursor 26 import com.android.quicksearchbox.SuggestionPosition 27 import com.android.quicksearchbox.Suggestions 28 import kotlin.collections.HashMap 29 30 /** Base class for suggestions adapters. The templated class A is the list adapter class. */ 31 abstract class SuggestionsAdapterBase<A> 32 protected constructor(private val mViewFactory: SuggestionViewFactory) : SuggestionsAdapter<A> { 33 private var mDataSetObserver: DataSetObserver? = null 34 var currentSuggestions: SuggestionCursor? = null 35 private set 36 private val mViewTypeMap: HashMap<String, Int> 37 private var mSuggestions: Suggestions? = null 38 private var mSuggestionClickListener: SuggestionClickListener? = null 39 private var mOnFocusChangeListener: OnFocusChangeListener? = null 40 var isClosed = false 41 private set 42 43 @get:Override abstract override val isEmpty: Boolean closenull44 fun close() { 45 suggestions = null 46 isClosed = true 47 } 48 49 @Override setSuggestionClickListenernull50 override fun setSuggestionClickListener(listener: SuggestionClickListener?) { 51 mSuggestionClickListener = listener 52 } 53 54 @Override setOnFocusChangeListenernull55 override fun setOnFocusChangeListener(l: OnFocusChangeListener?) { 56 mOnFocusChangeListener = l 57 } 58 59 // TODO: delay the change if there are no suggestions for the currently visible tab. 60 @get:Override 61 @set:Override 62 override var suggestions: Suggestions? 63 get() = mSuggestions 64 set(suggestions) { 65 if (mSuggestions === suggestions) { 66 return 67 } 68 if (isClosed) { 69 suggestions?.release() 70 return 71 } 72 if (mDataSetObserver == null) { 73 mDataSetObserver = MySuggestionsObserver() 74 } 75 // TODO: delay the change if there are no suggestions for the currently visible tab. 76 if (mSuggestions != null) { 77 mSuggestions!!.unregisterDataSetObserver(mDataSetObserver) 78 mSuggestions!!.release() 79 } 80 mSuggestions = suggestions 81 if (mSuggestions != null) { 82 mSuggestions!!.registerDataSetObserver(mDataSetObserver) 83 } 84 onSuggestionsChanged() 85 } 86 getSuggestionnull87 @Override abstract override fun getSuggestion(suggestionId: Long): SuggestionPosition 88 protected val count: Int 89 get() = if (currentSuggestions == null) 0 else currentSuggestions!!.count 90 91 protected fun getSuggestion(position: Int): SuggestionPosition? { 92 return if (currentSuggestions == null) null 93 else SuggestionPosition(currentSuggestions!!, position) 94 } 95 96 protected val viewTypeCount: Int 97 get() = mViewTypeMap.size 98 suggestionViewTypenull99 private fun suggestionViewType(suggestion: Suggestion): String? { 100 val viewType = mViewFactory.getViewType(suggestion) 101 if (!mViewTypeMap.containsKey(viewType)) { 102 throw IllegalStateException("Unknown viewType $viewType") 103 } 104 return viewType 105 } 106 getSuggestionViewTypenull107 protected fun getSuggestionViewType(cursor: SuggestionCursor?, position: Int): Int { 108 if (cursor == null) { 109 return 0 110 } 111 cursor.moveTo(position) 112 return mViewTypeMap.get(suggestionViewType(cursor)!!) as Int 113 } 114 115 protected val suggestionViewTypeCount: Int 116 get() = mViewTypeMap.size 117 getViewnull118 protected fun getView( 119 suggestions: SuggestionCursor?, 120 position: Int, 121 suggestionId: Long, 122 convertView: View?, 123 parent: ViewGroup? 124 ): View? { 125 suggestions?.moveTo(position) 126 val v: View? = mViewFactory.getView(suggestions, suggestions?.userQuery, convertView, parent) 127 if (v is SuggestionView) { 128 (v as SuggestionView?)!!.bindAdapter(this, suggestionId) 129 } else { 130 val l = SuggestionViewClickListener(suggestionId) 131 v?.setOnClickListener(l) 132 } 133 if (mOnFocusChangeListener != null) { 134 v?.setOnFocusChangeListener(mOnFocusChangeListener) 135 } 136 return v 137 } 138 onSuggestionsChangednull139 protected fun onSuggestionsChanged() { 140 if (DBG) Log.d(TAG, "onSuggestionsChanged($mSuggestions)") 141 var cursor: SuggestionCursor? = null 142 if (mSuggestions != null) { 143 cursor = mSuggestions!!.getResult() 144 } 145 changeSuggestions(cursor) 146 } 147 148 /** 149 * Replace the cursor. 150 * 151 * This does not close the old cursor. Instead, all the cursors are closed in [.setSuggestions]. 152 */ changeSuggestionsnull153 private fun changeSuggestions(newCursor: SuggestionCursor?) { 154 if (DBG) { 155 Log.d(TAG, "changeCursor(" + newCursor + ") count=" + (newCursor?.count ?: 0)) 156 } 157 if (newCursor === currentSuggestions) { 158 if (newCursor != null) { 159 // Shortcuts may have changed without the cursor changing. 160 notifyDataSetChanged() 161 } 162 return 163 } 164 currentSuggestions = newCursor 165 if (currentSuggestions != null) { 166 notifyDataSetChanged() 167 } else { 168 notifyDataSetInvalidated() 169 } 170 } 171 172 @Override onSuggestionClickednull173 override fun onSuggestionClicked(suggestionId: Long) { 174 if (isClosed) { 175 Log.w(TAG, "onSuggestionClicked after close") 176 } else if (mSuggestionClickListener != null) { 177 mSuggestionClickListener!!.onSuggestionClicked(this, suggestionId) 178 } 179 } 180 181 @Override onSuggestionQueryRefineClickednull182 override fun onSuggestionQueryRefineClicked(suggestionId: Long) { 183 if (isClosed) { 184 Log.w(TAG, "onSuggestionQueryRefineClicked after close") 185 } else if (mSuggestionClickListener != null) { 186 mSuggestionClickListener!!.onSuggestionQueryRefineClicked(this, suggestionId) 187 } 188 } 189 190 @get:Override abstract override val listAdapter: A notifyDataSetInvalidatednull191 protected abstract fun notifyDataSetInvalidated() 192 protected abstract fun notifyDataSetChanged() 193 private inner class MySuggestionsObserver : DataSetObserver() { 194 @Override 195 override fun onChanged() { 196 onSuggestionsChanged() 197 } 198 } 199 200 private inner class SuggestionViewClickListener(private val mSuggestionId: Long) : 201 View.OnClickListener { 202 @Override onClicknull203 override fun onClick(v: View?) { 204 onSuggestionClicked(mSuggestionId) 205 } 206 } 207 208 companion object { 209 private const val DBG = false 210 private const val TAG = "QSB.SuggestionsAdapter" 211 } 212 213 init { 214 mViewTypeMap = hashMapOf<String, Int>() 215 for (viewType in mViewFactory.suggestionViewTypes) { 216 if (!mViewTypeMap.containsKey(viewType)) { 217 mViewTypeMap.put(viewType, mViewTypeMap.size) 218 } 219 } 220 } 221 } 222