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 package com.android.quicksearchbox
17 
18 import android.app.SearchManager
19 import android.content.ComponentName
20 import android.content.Intent
21 import android.database.Cursor
22 import android.database.DataSetObserver
23 import android.net.Uri
24 import android.util.Log
25 
26 abstract class CursorBackedSuggestionCursor(override val userQuery: String?, cursor: Cursor?) :
27   SuggestionCursor {
28 
29   /** The suggestions, or `null` if the suggestions query failed. */
30   @JvmField protected val mCursor: Cursor?
31 
32   /** Column index of [SearchManager.SUGGEST_COLUMN_FORMAT] in @{link mCursor}. */
33   private val mFormatCol: Int
34 
35   /** Column index of [SearchManager.SUGGEST_COLUMN_TEXT_1] in @{link mCursor}. */
36   private val mText1Col: Int
37 
38   /** Column index of [SearchManager.SUGGEST_COLUMN_TEXT_2] in @{link mCursor}. */
39   private val mText2Col: Int
40 
41   /** Column index of [SearchManager.SUGGEST_COLUMN_TEXT_2_URL] in @{link mCursor}. */
42   private val mText2UrlCol: Int
43 
44   /** Column index of [SearchManager.SUGGEST_COLUMN_ICON_1] in @{link mCursor}. */
45   private val mIcon1Col: Int
46 
47   /** Column index of [SearchManager.SUGGEST_COLUMN_ICON_1] in @{link mCursor}. */
48   private val mIcon2Col: Int
49 
50   /** Column index of [SearchManager.SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING] in @{link mCursor}. */
51   private val mRefreshSpinnerCol: Int
52 
53   /** True if this result has been closed. */
54   private var mClosed = false
55   abstract override val suggestionSource: Source?
56   override val suggestionLogType: String?
57     get() = getStringOrNull(SUGGEST_COLUMN_LOG_TYPE)
58 
closenull59   override fun close() {
60     if (DBG) Log.d(TAG, "close()")
61     if (mClosed) {
62       throw IllegalStateException("Double close()")
63     }
64     mClosed = true
65     if (mCursor != null) {
66       try {
67         mCursor.close()
68       } catch (ex: RuntimeException) {
69         // all operations on cross-process cursors can throw random exceptions
70         Log.e(TAG, "close() failed, ", ex)
71       }
72     }
73   }
74 
75   @Override
finalizenull76   protected fun finalize() {
77     if (!mClosed) {
78       Log.e(TAG, "LEAK! Finalized without being closed: " + toString())
79     }
80   }
81 
82   override val count: Int
83     get() {
84       if (mClosed) {
85         throw IllegalStateException("getCount() after close()")
86       }
87       return if (mCursor == null) 0
88       else
89         try {
90           mCursor.getCount()
91         } catch (ex: RuntimeException) {
92           // all operations on cross-process cursors can throw random exceptions
93           Log.e(TAG, "getCount() failed, ", ex)
94           0
95         }
96     }
97 
moveTonull98   override fun moveTo(pos: Int) {
99     if (mClosed) {
100       throw IllegalStateException("moveTo($pos) after close()")
101     }
102     try {
103       if (!mCursor!!.moveToPosition(pos)) {
104         Log.e(TAG, "moveToPosition($pos) failed, count=$count")
105       }
106     } catch (ex: RuntimeException) {
107       // all operations on cross-process cursors can throw random exceptions
108       Log.e(TAG, "moveToPosition() failed, ", ex)
109     }
110   }
111 
moveToNextnull112   override fun moveToNext(): Boolean {
113     if (mClosed) {
114       throw IllegalStateException("moveToNext() after close()")
115     }
116     return try {
117       mCursor!!.moveToNext()
118     } catch (ex: RuntimeException) {
119       // all operations on cross-process cursors can throw random exceptions
120       Log.e(TAG, "moveToNext() failed, ", ex)
121       false
122     }
123   }
124 
125   override val position: Int
126     get() {
127       if (mClosed) {
128         throw IllegalStateException("get() on position after close()")
129       }
130       return try {
131         mCursor!!.position
132       } catch (ex: RuntimeException) {
133         // all operations on cross-process cursors can throw random exceptions
134         Log.e(TAG, "get() on position failed, ", ex)
135         -1
136       }
137     }
138   override val shortcutId: String?
139     get() = getStringOrNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID)
140   override val suggestionFormat: String?
141     get() = getStringOrNull(mFormatCol)
142   override val suggestionText1: String?
143     get() = getStringOrNull(mText1Col)
144   override val suggestionText2: String?
145     get() = getStringOrNull(mText2Col)
146   override val suggestionText2Url: String?
147     get() = getStringOrNull(mText2UrlCol)
148   override val suggestionIcon1: String?
149     get() = getStringOrNull(mIcon1Col)
150   override val suggestionIcon2: String?
151     get() = getStringOrNull(mIcon2Col)
152   override val isSpinnerWhileRefreshing: Boolean
153     get() = "true".equals(getStringOrNull(mRefreshSpinnerCol))
154 
155   /** Gets the intent action for the current suggestion. */
156   override val suggestionIntentAction: String?
157     get() {
158       val action: String? = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_ACTION)
159       return action
160     }
161   abstract override val suggestionIntentComponent: ComponentName?
162 
163   /** Gets the query for the current suggestion. */
164   override val suggestionQuery: String?
165     get() = getStringOrNull(SearchManager.SUGGEST_COLUMN_QUERY)
166 
167   override val suggestionIntentDataString: String?
168     get() {
169       // use specific data if supplied, or default data if supplied
170       var data: String? = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_DATA)
171       if (data == null) {
172         data = suggestionSource?.defaultIntentData
173       }
174       // then, if an ID was provided, append it.
175       if (data != null) {
176         val id: String? = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID)
177         if (id != null) {
178           data = data.toString() + "/" + Uri.encode(id)
179         }
180       }
181       return data
182     }
183 
184   /** Gets the intent extra data for the current suggestion. */
185   override val suggestionIntentExtraData: String?
186     get() = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA)
187   override val isWebSearchSuggestion: Boolean
188     get() = Intent.ACTION_WEB_SEARCH.equals(suggestionIntentAction)
189 
190   /**
191    * Gets the index of a column in [.mCursor] by name.
192    *
193    * @return The index, or `-1` if the column was not found.
194    */
getColumnIndexnull195   protected fun getColumnIndex(colName: String?): Int {
196     return if (mCursor == null) -1
197     else
198       try {
199         mCursor.getColumnIndex(colName)
200       } catch (ex: RuntimeException) {
201         // all operations on cross-process cursors can throw random exceptions
202         Log.e(TAG, "getColumnIndex() failed, ", ex)
203         -1
204       }
205   }
206 
207   /**
208    * Gets the string value of a column in [.mCursor] by column index.
209    *
210    * @param col Column index.
211    * @return The string value, or `null`.
212    */
getStringOrNullnull213   protected fun getStringOrNull(col: Int): String? {
214     if (mCursor == null) return null
215     return if (col == -1) {
216       null
217     } else
218       try {
219         mCursor.getString(col)
220       } catch (ex: RuntimeException) {
221         // all operations on cross-process cursors can throw random exceptions
222         Log.e(TAG, "getString() failed, ", ex)
223         null
224       }
225   }
226 
227   /**
228    * Gets the string value of a column in [.mCursor] by column name.
229    *
230    * @param colName Column name.
231    * @return The string value, or `null`.
232    */
getStringOrNullnull233   protected fun getStringOrNull(colName: String?): String? {
234     val col = getColumnIndex(colName)
235     return getStringOrNull(col)
236   }
237 
registerDataSetObservernull238   override fun registerDataSetObserver(observer: DataSetObserver?) {
239     // We don't watch Cursor-backed SuggestionCursors for changes
240   }
241 
unregisterDataSetObservernull242   override fun unregisterDataSetObserver(observer: DataSetObserver?) {
243     // We don't watch Cursor-backed SuggestionCursors for changes
244   }
245 
246   @Override
toStringnull247   override fun toString(): String {
248     return this::class.simpleName.toString() + "[" + userQuery + "]"
249   }
250 
251   companion object {
252     private const val DBG = false
253     protected const val TAG = "QSB.CursorBackedSuggestionCursor"
254     const val SUGGEST_COLUMN_LOG_TYPE = "suggest_log_type"
255   }
256 
257   init {
258     mCursor = cursor
259     mFormatCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT)
260     mText1Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)
261     mText2Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2)
262     mText2UrlCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL)
263     mIcon1Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1)
264     mIcon2Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2)
265     mRefreshSpinnerCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING)
266   }
267 }
268