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