/* * 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 import android.app.SearchManager import android.content.ComponentName import android.content.Intent import android.database.Cursor import android.database.DataSetObserver import android.net.Uri import android.util.Log abstract class CursorBackedSuggestionCursor(override val userQuery: String?, cursor: Cursor?) : SuggestionCursor { /** The suggestions, or `null` if the suggestions query failed. */ @JvmField protected val mCursor: Cursor? /** Column index of [SearchManager.SUGGEST_COLUMN_FORMAT] in @{link mCursor}. */ private val mFormatCol: Int /** Column index of [SearchManager.SUGGEST_COLUMN_TEXT_1] in @{link mCursor}. */ private val mText1Col: Int /** Column index of [SearchManager.SUGGEST_COLUMN_TEXT_2] in @{link mCursor}. */ private val mText2Col: Int /** Column index of [SearchManager.SUGGEST_COLUMN_TEXT_2_URL] in @{link mCursor}. */ private val mText2UrlCol: Int /** Column index of [SearchManager.SUGGEST_COLUMN_ICON_1] in @{link mCursor}. */ private val mIcon1Col: Int /** Column index of [SearchManager.SUGGEST_COLUMN_ICON_1] in @{link mCursor}. */ private val mIcon2Col: Int /** Column index of [SearchManager.SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING] in @{link mCursor}. */ private val mRefreshSpinnerCol: Int /** True if this result has been closed. */ private var mClosed = false abstract override val suggestionSource: Source? override val suggestionLogType: String? get() = getStringOrNull(SUGGEST_COLUMN_LOG_TYPE) override fun close() { if (DBG) Log.d(TAG, "close()") if (mClosed) { throw IllegalStateException("Double close()") } mClosed = true if (mCursor != null) { try { mCursor.close() } catch (ex: RuntimeException) { // all operations on cross-process cursors can throw random exceptions Log.e(TAG, "close() failed, ", ex) } } } @Override protected fun finalize() { if (!mClosed) { Log.e(TAG, "LEAK! Finalized without being closed: " + toString()) } } override val count: Int get() { if (mClosed) { throw IllegalStateException("getCount() after close()") } return if (mCursor == null) 0 else try { mCursor.getCount() } catch (ex: RuntimeException) { // all operations on cross-process cursors can throw random exceptions Log.e(TAG, "getCount() failed, ", ex) 0 } } override fun moveTo(pos: Int) { if (mClosed) { throw IllegalStateException("moveTo($pos) after close()") } try { if (!mCursor!!.moveToPosition(pos)) { Log.e(TAG, "moveToPosition($pos) failed, count=$count") } } catch (ex: RuntimeException) { // all operations on cross-process cursors can throw random exceptions Log.e(TAG, "moveToPosition() failed, ", ex) } } override fun moveToNext(): Boolean { if (mClosed) { throw IllegalStateException("moveToNext() after close()") } return try { mCursor!!.moveToNext() } catch (ex: RuntimeException) { // all operations on cross-process cursors can throw random exceptions Log.e(TAG, "moveToNext() failed, ", ex) false } } override val position: Int get() { if (mClosed) { throw IllegalStateException("get() on position after close()") } return try { mCursor!!.position } catch (ex: RuntimeException) { // all operations on cross-process cursors can throw random exceptions Log.e(TAG, "get() on position failed, ", ex) -1 } } override val shortcutId: String? get() = getStringOrNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID) override val suggestionFormat: String? get() = getStringOrNull(mFormatCol) override val suggestionText1: String? get() = getStringOrNull(mText1Col) override val suggestionText2: String? get() = getStringOrNull(mText2Col) override val suggestionText2Url: String? get() = getStringOrNull(mText2UrlCol) override val suggestionIcon1: String? get() = getStringOrNull(mIcon1Col) override val suggestionIcon2: String? get() = getStringOrNull(mIcon2Col) override val isSpinnerWhileRefreshing: Boolean get() = "true".equals(getStringOrNull(mRefreshSpinnerCol)) /** Gets the intent action for the current suggestion. */ override val suggestionIntentAction: String? get() { val action: String? = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_ACTION) return action } abstract override val suggestionIntentComponent: ComponentName? /** Gets the query for the current suggestion. */ override val suggestionQuery: String? get() = getStringOrNull(SearchManager.SUGGEST_COLUMN_QUERY) override val suggestionIntentDataString: String? get() { // use specific data if supplied, or default data if supplied var data: String? = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_DATA) if (data == null) { data = suggestionSource?.defaultIntentData } // then, if an ID was provided, append it. if (data != null) { val id: String? = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID) if (id != null) { data = data.toString() + "/" + Uri.encode(id) } } return data } /** Gets the intent extra data for the current suggestion. */ override val suggestionIntentExtraData: String? get() = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA) override val isWebSearchSuggestion: Boolean get() = Intent.ACTION_WEB_SEARCH.equals(suggestionIntentAction) /** * Gets the index of a column in [.mCursor] by name. * * @return The index, or `-1` if the column was not found. */ protected fun getColumnIndex(colName: String?): Int { return if (mCursor == null) -1 else try { mCursor.getColumnIndex(colName) } catch (ex: RuntimeException) { // all operations on cross-process cursors can throw random exceptions Log.e(TAG, "getColumnIndex() failed, ", ex) -1 } } /** * Gets the string value of a column in [.mCursor] by column index. * * @param col Column index. * @return The string value, or `null`. */ protected fun getStringOrNull(col: Int): String? { if (mCursor == null) return null return if (col == -1) { null } else try { mCursor.getString(col) } catch (ex: RuntimeException) { // all operations on cross-process cursors can throw random exceptions Log.e(TAG, "getString() failed, ", ex) null } } /** * Gets the string value of a column in [.mCursor] by column name. * * @param colName Column name. * @return The string value, or `null`. */ protected fun getStringOrNull(colName: String?): String? { val col = getColumnIndex(colName) return getStringOrNull(col) } override fun registerDataSetObserver(observer: DataSetObserver?) { // We don't watch Cursor-backed SuggestionCursors for changes } override fun unregisterDataSetObserver(observer: DataSetObserver?) { // We don't watch Cursor-backed SuggestionCursors for changes } @Override override fun toString(): String { return this::class.simpleName.toString() + "[" + userQuery + "]" } companion object { private const val DBG = false protected const val TAG = "QSB.CursorBackedSuggestionCursor" const val SUGGEST_COLUMN_LOG_TYPE = "suggest_log_type" } init { mCursor = cursor mFormatCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT) mText1Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1) mText2Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2) mText2UrlCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL) mIcon1Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1) mIcon2Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2) mRefreshSpinnerCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING) } }