diff options
author | Treehugger Robot <treehugger-gerrit@google.com> | 2022-08-29 20:35:11 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2022-08-29 20:35:11 +0000 |
commit | 19fee172cc1afd33aa393a6ea9829f47b7ad1afc (patch) | |
tree | 6339bf594a247ef3370db610cac4dc318e7a5bdb | |
parent | c6f67a487a0af47ff4c974cb30585e735c72d1c1 (diff) | |
parent | 6d1e8b50a2afda7e290f28401ff8766d3d5f6c8c (diff) | |
download | QuickSearchBox-19fee172cc1afd33aa393a6ea9829f47b7ad1afc.tar.gz |
Merge changes from topics "DelayingSuggestionsAdapter", "SearchActivityViewSinglePane", "SuggestionViewInflater", "SuggestionsListAdapter", "SuggestionsView", "WebSearchSuggestionView" am: af2d414b1c am: fa5bc6533d am: 55bb94f3be am: 6d1e8b50a2
Original change: https://android-review.googlesource.com/c/platform/packages/apps/QuickSearchBox/+/2197991
Change-Id: I81fb633d1f942dae34a1accb9aaa265d875cd7df
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
14 files changed, 820 insertions, 45 deletions
@@ -39,13 +39,20 @@ android_app { "src/**/quicksearchbox/ui/CorpusView.java", "src/**/quicksearchbox/ui/DefaultSuggestionView.java", "src/**/quicksearchbox/ui/DefaultSuggestionViewFactory.java", + "src/**/quicksearchbox/ui/DelayingSuggestionsAdapter.java", "src/**/quicksearchbox/ui/QueryTextView.java", "src/**/quicksearchbox/ui/SearchActivityView.java", + "src/**/quicksearchbox/ui/SearchActivityViewSinglePane.java", "src/**/quicksearchbox/ui/SuggestionClickListener.java", "src/**/quicksearchbox/ui/SuggestionsAdapter.java", + "src/**/quicksearchbox/ui/SuggestionsAdapterBase.java", + "src/**/quicksearchbox/ui/SuggestionsListAdapter.java", "src/**/quicksearchbox/ui/SuggestionsListView.java", + "src/**/quicksearchbox/ui/SuggestionsView.java", "src/**/quicksearchbox/ui/SuggestionView.java", "src/**/quicksearchbox/ui/SuggestionViewFactory.java", + "src/**/quicksearchbox/ui/SuggestionViewInflater.java", + "src/**/quicksearchbox/ui/WebSearchSuggestionView.java", "src/**/quicksearchbox/util/AsyncDataSetObservable.java", "src/**/quicksearchbox/util/BarrierConsumer.java", "src/**/quicksearchbox/util/BatchingNamedTaskExecutor.java", diff --git a/src/com/android/quicksearchbox/AbstractSuggestionWrapper.kt b/src/com/android/quicksearchbox/AbstractSuggestionWrapper.kt index ce43cc0..cac1db9 100644 --- a/src/com/android/quicksearchbox/AbstractSuggestionWrapper.kt +++ b/src/com/android/quicksearchbox/AbstractSuggestionWrapper.kt @@ -24,43 +24,43 @@ abstract class AbstractSuggestionWrapper : Suggestion { /** * Gets the current suggestion. */ - protected abstract fun current(): Suggestion + protected abstract fun current(): Suggestion? override val shortcutId: String? - get() = current().shortcutId + get() = current()?.shortcutId override val suggestionFormat: String? - get() = current().suggestionFormat + get() = current()?.suggestionFormat override val suggestionIcon1: String? - get() = current().suggestionIcon1 + get() = current()?.suggestionIcon1 override val suggestionIcon2: String? - get() = current().suggestionIcon2 + get() = current()?.suggestionIcon2 override val suggestionIntentAction: String? - get() = current().suggestionIntentAction + get() = current()?.suggestionIntentAction override val suggestionIntentComponent: ComponentName? - get() = current().suggestionIntentComponent + get() = current()?.suggestionIntentComponent override val suggestionIntentDataString: String? - get() = current().suggestionIntentDataString + get() = current()?.suggestionIntentDataString override val suggestionIntentExtraData: String? - get() = current().suggestionIntentExtraData + get() = current()?.suggestionIntentExtraData override val suggestionLogType: String? - get() = current().suggestionLogType + get() = current()?.suggestionLogType override val suggestionQuery: String? - get() = current().suggestionQuery + get() = current()?.suggestionQuery override val suggestionSource: Source? - get() = current().suggestionSource + get() = current()?.suggestionSource override val suggestionText1: String? - get() = current().suggestionText1 + get() = current()?.suggestionText1 override val suggestionText2: String? - get() = current().suggestionText2 + get() = current()?.suggestionText2 override val suggestionText2Url: String? - get() = current().suggestionText2Url + get() = current()?.suggestionText2Url override val isSpinnerWhileRefreshing: Boolean - get() = current().isSpinnerWhileRefreshing + get() = current()?.isSpinnerWhileRefreshing == true override val isSuggestionShortcut: Boolean - get() = current().isSuggestionShortcut + get() = current()?.isSuggestionShortcut == true override val isWebSearchSuggestion: Boolean - get() = current().isWebSearchSuggestion + get() = current()?.isWebSearchSuggestion == true override val isHistorySuggestion: Boolean - get() = current().isHistorySuggestion + get() = current()?.isHistorySuggestion == true override val extras: SuggestionExtras? - get() = current().extras + get() = current()?.extras }
\ No newline at end of file diff --git a/src/com/android/quicksearchbox/SearchActivity.kt b/src/com/android/quicksearchbox/SearchActivity.kt index cc5cfd0..0620b97 100644 --- a/src/com/android/quicksearchbox/SearchActivity.kt +++ b/src/com/android/quicksearchbox/SearchActivity.kt @@ -382,8 +382,8 @@ class SearchActivity : Activity() { return true } - protected fun launchSuggestion(suggestions: SuggestionCursor, position: Int) { - suggestions.moveTo(position) + protected fun launchSuggestion(suggestions: SuggestionCursor?, position: Int) { + suggestions?.moveTo(position) val intent: Intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData) launchIntent(intent) } diff --git a/src/com/android/quicksearchbox/SuggestionPosition.kt b/src/com/android/quicksearchbox/SuggestionPosition.kt index 783ab4b..43b6895 100644 --- a/src/com/android/quicksearchbox/SuggestionPosition.kt +++ b/src/com/android/quicksearchbox/SuggestionPosition.kt @@ -20,16 +20,16 @@ package com.android.quicksearchbox * */ class SuggestionPosition @JvmOverloads constructor( - val cursor: SuggestionCursor, - val position: Int = cursor.position + val cursor: SuggestionCursor?, + val position: Int = cursor!!.position ) : AbstractSuggestionWrapper() { /** * Gets the suggestion cursor, moved to point to the right suggestion. */ @Override - override fun current(): Suggestion { - cursor.moveTo(position) + override fun current(): Suggestion? { + cursor?.moveTo(position) return cursor } diff --git a/src/com/android/quicksearchbox/SuggestionUtils.kt b/src/com/android/quicksearchbox/SuggestionUtils.kt index f7e7c5b..0c3f806 100644 --- a/src/com/android/quicksearchbox/SuggestionUtils.kt +++ b/src/com/android/quicksearchbox/SuggestionUtils.kt @@ -29,12 +29,12 @@ import kotlin.text.StringBuilder */ object SuggestionUtils { @JvmStatic - fun getSuggestionIntent(suggestion: SuggestionCursor, appSearchData: Bundle?): Intent { - val action: String? = suggestion.suggestionIntentAction - val data: String? = suggestion.suggestionIntentDataString - val query: String? = suggestion.suggestionQuery - val userQuery: String? = suggestion.userQuery - val extraData: String? = suggestion.suggestionIntentExtraData + fun getSuggestionIntent(suggestion: SuggestionCursor?, appSearchData: Bundle?): Intent { + val action: String? = suggestion?.suggestionIntentAction + val data: String? = suggestion?.suggestionIntentDataString + val query: String? = suggestion?.suggestionQuery + val userQuery: String? = suggestion?.userQuery + val extraData: String? = suggestion?.suggestionIntentExtraData // Now build the Intent val intent = Intent(action) @@ -55,7 +55,7 @@ object SuggestionUtils { if (appSearchData != null) { intent.putExtra(SearchManager.APP_DATA, appSearchData) } - intent.setComponent(suggestion.suggestionIntentComponent) + intent.setComponent(suggestion?.suggestionIntentComponent) return intent } diff --git a/src/com/android/quicksearchbox/ui/DelayingSuggestionsAdapter.kt b/src/com/android/quicksearchbox/ui/DelayingSuggestionsAdapter.kt new file mode 100644 index 0000000..be4e98a --- /dev/null +++ b/src/com/android/quicksearchbox/ui/DelayingSuggestionsAdapter.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.database.DataSetObserver +import android.util.Log +import android.view.View.OnFocusChangeListener + +import com.android.quicksearchbox.SuggestionCursor +import com.android.quicksearchbox.SuggestionPosition +import com.android.quicksearchbox.Suggestions + +/** + * A [SuggestionsListAdapter] that doesn't expose the new suggestions until there are some results + * to show. + */ +class DelayingSuggestionsAdapter<A>(private val mDelayedAdapter: SuggestionsAdapterBase<A>) : + SuggestionsAdapter<A> { + private var mPendingDataSetObserver: DataSetObserver? = null + private var mPendingSuggestions: Suggestions? = null + fun close() { + setPendingSuggestions(null) + mDelayedAdapter.close() + } + + /** Gets whether the given suggestions are non-empty for the selected source. */ + private fun shouldPublish(suggestions: Suggestions?): Boolean { + if (suggestions!!.isDone) return true + val cursor: SuggestionCursor? = suggestions.getResult() + return cursor != null && cursor.count > 0 + } + + private fun setPendingSuggestions(suggestions: Suggestions?) { + if (mPendingSuggestions === suggestions) { + return + } + if (mDelayedAdapter.isClosed) { + suggestions?.release() + return + } + if (mPendingDataSetObserver == null) { + mPendingDataSetObserver = PendingSuggestionsObserver() + } + if (mPendingSuggestions != null) { + mPendingSuggestions!!.unregisterDataSetObserver(mPendingDataSetObserver) + // Close old suggestions, but only if they are not also the current + // suggestions. + if (mPendingSuggestions !== this.suggestions) { + mPendingSuggestions!!.release() + } + } + mPendingSuggestions = suggestions + if (mPendingSuggestions != null) { + mPendingSuggestions!!.registerDataSetObserver(mPendingDataSetObserver) + } + } + + protected fun onPendingSuggestionsChanged() { + if (DBG) Log.d(TAG, "onPendingSuggestionsChanged(), mPendingSuggestions=" + mPendingSuggestions) + if (shouldPublish(mPendingSuggestions)) { + if (DBG) Log.d(TAG, "Suggestions now available, publishing: $mPendingSuggestions") + mDelayedAdapter.suggestions = mPendingSuggestions + // The suggestions are no longer pending. + setPendingSuggestions(null) + } + } + + private inner class PendingSuggestionsObserver : DataSetObserver() { + @Override + override fun onChanged() { + onPendingSuggestionsChanged() + } + } + + @get:Override + override val listAdapter: A + get() = mDelayedAdapter.listAdapter + val currentPromotedSuggestions: SuggestionCursor? + get() = mDelayedAdapter.currentSuggestions + + // Clear any old pending suggestions. + @get:Override + @set:Override + override var suggestions: Suggestions? + get() = mDelayedAdapter.suggestions + set(suggestions) { + if (suggestions == null) { + mDelayedAdapter.suggestions = null + setPendingSuggestions(null) + return + } + if (shouldPublish(suggestions)) { + if (DBG) + Log.d( + TAG, + "Publishing suggestions immediately: $suggestions" + ) + mDelayedAdapter.suggestions = suggestions + // Clear any old pending suggestions. + setPendingSuggestions(null) + } else { + if (DBG) + Log.d( + TAG, + "Delaying suggestions publishing: $suggestions" + ) + setPendingSuggestions(suggestions) + } + } + + @Override + override fun getSuggestion(suggestionId: Long): SuggestionPosition? { + return mDelayedAdapter.getSuggestion(suggestionId) + } + + @Override + override fun onSuggestionClicked(suggestionId: Long) { + mDelayedAdapter.onSuggestionClicked(suggestionId) + } + + @Override + override fun onSuggestionQueryRefineClicked(suggestionId: Long) { + mDelayedAdapter.onSuggestionQueryRefineClicked(suggestionId) + } + + @Override + override fun setOnFocusChangeListener(l: OnFocusChangeListener?) { + mDelayedAdapter.setOnFocusChangeListener(l) + } + + @Override + override fun setSuggestionClickListener(listener: SuggestionClickListener?) { + mDelayedAdapter.setSuggestionClickListener(listener) + } + + @get:Override + override val isEmpty: Boolean + get() = mDelayedAdapter.isEmpty + + companion object { + private const val DBG = false + private const val TAG = "QSB.DelayingSuggestionsAdapter" + } +} diff --git a/src/com/android/quicksearchbox/ui/SearchActivityView.kt b/src/com/android/quicksearchbox/ui/SearchActivityView.kt index 64852dd..8e8dcac 100644 --- a/src/com/android/quicksearchbox/ui/SearchActivityView.kt +++ b/src/com/android/quicksearchbox/ui/SearchActivityView.kt @@ -34,13 +34,10 @@ import android.widget.ListAdapter import android.widget.RelativeLayout import android.widget.TextView import android.widget.TextView.OnEditorActionListener - import com.android.quicksearchbox.* import com.android.quicksearchbox.R - -import kotlin.collections.ArrayList - import java.util.Arrays +import kotlin.collections.ArrayList abstract class SearchActivityView : RelativeLayout { @JvmField protected var mQueryTextView: QueryTextView? = null @@ -49,8 +46,8 @@ abstract class SearchActivityView : RelativeLayout { @JvmField protected var mQueryWasEmpty = true @JvmField protected var mQueryTextEmptyBg: Drawable? = null protected var mQueryTextNotEmptyBg: Drawable? = null - @JvmField protected var mSuggestionsView: SuggestionsListView<ListAdapter>? = null - @JvmField protected var mSuggestionsAdapter: SuggestionsAdapter<ListAdapter>? = null + @JvmField protected var mSuggestionsView: SuggestionsListView<ListAdapter?>? = null + @JvmField protected var mSuggestionsAdapter: SuggestionsAdapter<ListAdapter?>? = null @JvmField protected var mSearchGoButton: ImageButton? = null @JvmField protected var mVoiceSearchButton: ImageButton? = null @JvmField protected var mButtonsKeyListener: ButtonsKeyListener? = null @@ -60,14 +57,14 @@ abstract class SearchActivityView : RelativeLayout { @JvmField protected var mExitClickListener: View.OnClickListener? = null constructor(context: Context?) : super(context) - constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) - constructor( + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor( context: Context?, attrs: AttributeSet?, defStyle: Int ) : super(context, attrs, defStyle) - @Override + @Override protected override fun onFinishInflate() { mQueryTextView = findViewById(R.id.search_src_text) as QueryTextView? mSuggestionsView = findViewById(R.id.suggestions) as SuggestionsView? @@ -116,7 +113,7 @@ abstract class SearchActivityView : RelativeLayout { protected val voiceSearch: VoiceSearch? get() = qsbApplication.voiceSearch - protected fun createSuggestionsAdapter(): SuggestionsAdapter<ListAdapter> { + protected fun createSuggestionsAdapter(): SuggestionsAdapter<ListAdapter?> { return DelayingSuggestionsAdapter(SuggestionsListAdapter(qsbApplication.suggestionViewFactory)) } diff --git a/src/com/android/quicksearchbox/ui/SearchActivityViewSinglePane.kt b/src/com/android/quicksearchbox/ui/SearchActivityViewSinglePane.kt new file mode 100644 index 0000000..a245234 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SearchActivityViewSinglePane.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.util.AttributeSet + +/** Finishes the containing activity on BACK, even if input method is showing. */ +class SearchActivityViewSinglePane : SearchActivityView { + constructor(context: Context?) : super(context) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor( + context: Context?, + attrs: AttributeSet?, + defStyle: Int + ) : super(context, attrs, defStyle) + + @Override + override fun onResume() { + focusQueryTextView() + } + + @Override + override fun considerHidingInputMethod() { + mQueryTextView!!.hideInputMethod() + } + + @Override override fun onStop() {} +} diff --git a/src/com/android/quicksearchbox/ui/SuggestionViewInflater.kt b/src/com/android/quicksearchbox/ui/SuggestionViewInflater.kt new file mode 100644 index 0000000..a71745a --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SuggestionViewInflater.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup + +import com.android.quicksearchbox.Suggestion +import com.android.quicksearchbox.SuggestionCursor + +/** Suggestion view factory that inflates views from XML. */ +open class SuggestionViewInflater( + private val mViewType: String, + viewClass: Class<out SuggestionView?>, + layoutId: Int, + context: Context? +) : SuggestionViewFactory { + private val mViewClass: Class<*> + private val mLayoutId: Int + private val mContext: Context? + + protected val inflater: LayoutInflater + get() = mContext?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + + override val suggestionViewTypes: Collection<String> + get() = listOf(mViewType) + + override fun getView( + suggestion: SuggestionCursor?, + userQuery: String?, + convertView: View?, + parent: ViewGroup? + ): View? { + var mConvertView: View? = convertView + if (mConvertView == null || !mConvertView::class.equals(mViewClass)) { + val layoutId = mLayoutId + mConvertView = inflater.inflate(layoutId, parent, false) + } + if (mConvertView !is SuggestionView) { + throw IllegalArgumentException("Not a SuggestionView: $mConvertView") + } + (mConvertView as SuggestionView).bindAsSuggestion(suggestion, userQuery) + return mConvertView + } + + override fun getViewType(suggestion: Suggestion?): String { + return mViewType + } + + override fun canCreateView(suggestion: Suggestion?): Boolean { + return true + } + + /** + * @param viewType The unique type of views inflated by this factory + * @param viewClass The expected type of view classes. + * @param layoutId resource ID of layout to use. + * @param context Context to use for inflating the views. + */ + init { + mViewClass = viewClass + mLayoutId = layoutId + mContext = context + } +} diff --git a/src/com/android/quicksearchbox/ui/SuggestionsAdapterBase.kt b/src/com/android/quicksearchbox/ui/SuggestionsAdapterBase.kt new file mode 100644 index 0000000..f334d5d --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SuggestionsAdapterBase.kt @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import com.android.quicksearchbox.Suggestion +import com.android.quicksearchbox.SuggestionCursor +import com.android.quicksearchbox.SuggestionPosition +import com.android.quicksearchbox.Suggestions + +import android.database.DataSetObserver +import android.util.Log +import android.view.View +import android.view.View.OnFocusChangeListener +import android.view.ViewGroup + +import kotlin.collections.HashMap + +/** Base class for suggestions adapters. The templated class A is the list adapter class. */ +abstract class SuggestionsAdapterBase<A> +protected constructor(private val mViewFactory: SuggestionViewFactory) : SuggestionsAdapter<A> { + private var mDataSetObserver: DataSetObserver? = null + var currentSuggestions: SuggestionCursor? = null + private set + private val mViewTypeMap: HashMap<String, Int> + private var mSuggestions: Suggestions? = null + private var mSuggestionClickListener: SuggestionClickListener? = null + private var mOnFocusChangeListener: OnFocusChangeListener? = null + var isClosed = false + private set + + @get:Override abstract override val isEmpty: Boolean + fun close() { + suggestions = null + isClosed = true + } + + @Override + override fun setSuggestionClickListener(listener: SuggestionClickListener?) { + mSuggestionClickListener = listener + } + + @Override + override fun setOnFocusChangeListener(l: OnFocusChangeListener?) { + mOnFocusChangeListener = l + } + + // TODO: delay the change if there are no suggestions for the currently visible tab. + @get:Override + @set:Override + override var suggestions: Suggestions? + get() = mSuggestions!! + set(suggestions) { + if (mSuggestions === suggestions) { + return + } + if (isClosed) { + suggestions?.release() + return + } + if (mDataSetObserver == null) { + mDataSetObserver = MySuggestionsObserver() + } + // TODO: delay the change if there are no suggestions for the currently visible tab. + if (mSuggestions != null) { + mSuggestions!!.unregisterDataSetObserver(mDataSetObserver) + mSuggestions!!.release() + } + mSuggestions = suggestions + if (mSuggestions != null) { + mSuggestions!!.registerDataSetObserver(mDataSetObserver) + } + onSuggestionsChanged() + } + + @Override abstract override fun getSuggestion(suggestionId: Long): SuggestionPosition + protected val count: Int + get() = if (currentSuggestions == null) 0 else currentSuggestions!!.count + + protected fun getSuggestion(position: Int): SuggestionPosition? { + return if (currentSuggestions == null) null + else SuggestionPosition(currentSuggestions!!, position) + } + + protected val viewTypeCount: Int + get() = mViewTypeMap.size + + private fun suggestionViewType(suggestion: Suggestion): String? { + val viewType = mViewFactory.getViewType(suggestion) + if (!mViewTypeMap.containsKey(viewType)) { + throw IllegalStateException("Unknown viewType $viewType") + } + return viewType + } + + protected fun getSuggestionViewType(cursor: SuggestionCursor?, position: Int): Int { + if (cursor == null) { + return 0 + } + cursor.moveTo(position) + return mViewTypeMap.get(suggestionViewType(cursor)!!) as Int + } + + protected val suggestionViewTypeCount: Int + get() = mViewTypeMap.size + + protected fun getView( + suggestions: SuggestionCursor?, + position: Int, + suggestionId: Long, + convertView: View?, + parent: ViewGroup? + ): View? { + suggestions?.moveTo(position) + val v: View? = mViewFactory.getView(suggestions, suggestions?.userQuery, convertView, parent) + if (v is SuggestionView) { + (v as SuggestionView?)!!.bindAdapter(this, suggestionId) + } else { + val l = SuggestionViewClickListener(suggestionId) + v?.setOnClickListener(l) + } + if (mOnFocusChangeListener != null) { + v?.setOnFocusChangeListener(mOnFocusChangeListener) + } + return v + } + + protected fun onSuggestionsChanged() { + if (DBG) Log.d(TAG, "onSuggestionsChanged($mSuggestions)") + var cursor: SuggestionCursor? = null + if (mSuggestions != null) { + cursor = mSuggestions!!.getResult() + } + changeSuggestions(cursor) + } + + /** + * Replace the cursor. + * + * This does not close the old cursor. Instead, all the cursors are closed in [.setSuggestions]. + */ + private fun changeSuggestions(newCursor: SuggestionCursor?) { + if (DBG) { + Log.d(TAG, "changeCursor(" + newCursor + ") count=" + (newCursor?.count ?: 0)) + } + if (newCursor === currentSuggestions) { + if (newCursor != null) { + // Shortcuts may have changed without the cursor changing. + notifyDataSetChanged() + } + return + } + currentSuggestions = newCursor + if (currentSuggestions != null) { + notifyDataSetChanged() + } else { + notifyDataSetInvalidated() + } + } + + @Override + override fun onSuggestionClicked(suggestionId: Long) { + if (isClosed) { + Log.w(TAG, "onSuggestionClicked after close") + } else if (mSuggestionClickListener != null) { + mSuggestionClickListener!!.onSuggestionClicked(this, suggestionId) + } + } + + @Override + override fun onSuggestionQueryRefineClicked(suggestionId: Long) { + if (isClosed) { + Log.w(TAG, "onSuggestionQueryRefineClicked after close") + } else if (mSuggestionClickListener != null) { + mSuggestionClickListener!!.onSuggestionQueryRefineClicked(this, suggestionId) + } + } + + @get:Override abstract override val listAdapter: A + protected abstract fun notifyDataSetInvalidated() + protected abstract fun notifyDataSetChanged() + private inner class MySuggestionsObserver : DataSetObserver() { + @Override + override fun onChanged() { + onSuggestionsChanged() + } + } + + private inner class SuggestionViewClickListener(private val mSuggestionId: Long) : + View.OnClickListener { + @Override + override fun onClick(v: View?) { + onSuggestionClicked(mSuggestionId) + } + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.SuggestionsAdapter" + } + + init { + mViewTypeMap = hashMapOf<String, Int>() + for (viewType in mViewFactory.suggestionViewTypes) { + if (!mViewTypeMap.containsKey(viewType)) { + mViewTypeMap.put(viewType, mViewTypeMap.size) + } + } + } +} diff --git a/src/com/android/quicksearchbox/ui/SuggestionsListAdapter.kt b/src/com/android/quicksearchbox/ui/SuggestionsListAdapter.kt new file mode 100644 index 0000000..e6ace36 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SuggestionsListAdapter.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.ListAdapter + +import com.android.quicksearchbox.SuggestionCursor +import com.android.quicksearchbox.SuggestionPosition + +/** Uses a [Suggestions] object to back a [SuggestionsView]. */ +class SuggestionsListAdapter(viewFactory: SuggestionViewFactory?) : + SuggestionsAdapterBase<ListAdapter?>(viewFactory!!) { + private val mAdapter: SuggestionsListAdapter.Adapter + + @get:Override + override val isEmpty: Boolean + get() = mAdapter.getCount() == 0 + + @Override + override fun getSuggestion(suggestionId: Long): SuggestionPosition { + return SuggestionPosition(currentSuggestions, suggestionId.toInt()) + } + + @get:Override + override val listAdapter: BaseAdapter + get() = mAdapter + + @Override + public override fun notifyDataSetChanged() { + mAdapter.notifyDataSetChanged() + } + + @Override + public override fun notifyDataSetInvalidated() { + mAdapter.notifyDataSetInvalidated() + } + + internal inner class Adapter : BaseAdapter() { + @Override + override fun getCount(): Int { + val s: SuggestionCursor? = currentSuggestions + return s?.count ?: 0 + } + + @Override + override fun getItem(position: Int): Any? { + return getSuggestion(position) + } + + @Override + override fun getItemId(position: Int): Long { + return position.toLong() + } + + @Override + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? { + return this@SuggestionsListAdapter.getView( + currentSuggestions, + position, + position.toLong(), + convertView, + parent + ) + } + + @Override + override fun getItemViewType(position: Int): Int { + return getSuggestionViewType(currentSuggestions, position) + } + + @Override + override fun getViewTypeCount(): Int { + return suggestionViewTypeCount + } + } + + init { + mAdapter = Adapter() + } +} diff --git a/src/com/android/quicksearchbox/ui/SuggestionsListView.kt b/src/com/android/quicksearchbox/ui/SuggestionsListView.kt index ab8c829..75a0d77 100644 --- a/src/com/android/quicksearchbox/ui/SuggestionsListView.kt +++ b/src/com/android/quicksearchbox/ui/SuggestionsListView.kt @@ -46,12 +46,12 @@ interface SuggestionsListView<A> { /** * Sets the adapter for the list. See [AbsListView.setAdapter] */ - fun setSuggestionsAdapter(adapter: SuggestionsAdapter<A>?) + fun setSuggestionsAdapter(adapter: SuggestionsAdapter<A?>?) /** * Gets the adapter for the list. */ - fun getSuggestionsAdapter(): SuggestionsAdapter<A>? + fun getSuggestionsAdapter(): SuggestionsAdapter<A?>? /** * Gets the ID of the currently selected item. diff --git a/src/com/android/quicksearchbox/ui/SuggestionsView.kt b/src/com/android/quicksearchbox/ui/SuggestionsView.kt new file mode 100644 index 0000000..29f1efa --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SuggestionsView.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.util.AttributeSet +import android.widget.ListAdapter +import android.widget.ListView + +import com.android.quicksearchbox.SuggestionPosition + +/** Holds a list of suggestions. */ +class SuggestionsView(context: Context?, attrs: AttributeSet?) : + ListView(context, attrs), SuggestionsListView<ListAdapter?> { + private var mSuggestionsAdapter: SuggestionsAdapter<ListAdapter?>? = null + + @Override + override fun setSuggestionsAdapter(adapter: SuggestionsAdapter<ListAdapter?>?) { + super.setAdapter(adapter?.listAdapter) + mSuggestionsAdapter = adapter + } + + @Override + override fun getSuggestionsAdapter(): SuggestionsAdapter<ListAdapter?>? { + return mSuggestionsAdapter + } + + @Override + override fun onFinishInflate() { + super.onFinishInflate() + setItemsCanFocus(true) + } + + /** + * Gets the position of the selected suggestion. + * + * @return A 0-based index, or `-1` if no suggestion is selected. + */ + val selectedPosition: Int + get() = getSelectedItemPosition() + + /** + * Gets the selected suggestion. + * + * @return `null` if no suggestion is selected. + */ + val selectedSuggestion: SuggestionPosition + get() = getSelectedItem() as SuggestionPosition + + companion object { + private const val DBG = false + private const val TAG = "QSB.SuggestionsView" + } +} diff --git a/src/com/android/quicksearchbox/ui/WebSearchSuggestionView.kt b/src/com/android/quicksearchbox/ui/WebSearchSuggestionView.kt new file mode 100644 index 0000000..421c427 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/WebSearchSuggestionView.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.util.AttributeSet +import android.view.KeyEvent +import android.view.View + +import com.android.quicksearchbox.QsbApplication +import com.android.quicksearchbox.R +import com.android.quicksearchbox.Suggestion +import com.android.quicksearchbox.SuggestionFormatter + +/** View for web search suggestions. */ +class WebSearchSuggestionView(context: Context?, attrs: AttributeSet?) : + BaseSuggestionView(context, attrs) { + private val mSuggestionFormatter: SuggestionFormatter? + + @Override + override fun onFinishInflate() { + super.onFinishInflate() + val keyListener: WebSearchSuggestionView.KeyListener = KeyListener() + setOnKeyListener(keyListener) + mIcon2?.setOnKeyListener(keyListener) + mIcon2?.setOnClickListener( + object : OnClickListener { + override fun onClick(v: View?) { + onSuggestionQueryRefineClicked() + } + } + ) + mIcon2?.setFocusable(true) + } + + @Override + override fun bindAsSuggestion(suggestion: Suggestion?, userQuery: String?) { + super.bindAsSuggestion(suggestion, userQuery) + val text1 = mSuggestionFormatter?.formatSuggestion(userQuery, suggestion?.suggestionText1) + setText1(text1) + setIsHistorySuggestion(suggestion?.isHistorySuggestion) + } + + private fun setIsHistorySuggestion(isHistory: Boolean?) { + if (isHistory == true) { + mIcon1?.setImageResource(R.drawable.ic_history_suggestion) + mIcon1?.setVisibility(VISIBLE) + } else { + mIcon1?.setVisibility(INVISIBLE) + } + } + + private inner class KeyListener : View.OnKeyListener { + override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean { + var consumed = false + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && v !== mIcon2) { + consumed = mIcon2!!.requestFocus() + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && v === mIcon2) { + consumed = requestFocus() + } + } + return consumed + } + } + + class Factory(context: Context?) : + SuggestionViewInflater( + VIEW_ID, + WebSearchSuggestionView::class.java, + R.layout.web_search_suggestion, + context + ) { + @Override + override fun canCreateView(suggestion: Suggestion?): Boolean { + return suggestion!!.isWebSearchSuggestion + } + } + + companion object { + private const val VIEW_ID = "web_search" + } + + init { + mSuggestionFormatter = QsbApplication[context].suggestionFormatter + } +} |