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.database.DataSetObservable
19 import android.database.DataSetObserver
20 import android.util.Log
21 
22 /** Collects all corpus results for a single query. */
23 class Suggestions(val query: String, val source: Source) {
24 
25   /**
26    * The observers that want notifications of changes to the published suggestions. This object may
27    * be accessed on any thread.
28    */
29   private val mDataSetObservable: DataSetObservable = DataSetObservable()
30 
31   private var mResult: SourceResult? = null
32 
33   private var mRefCount = 0
34 
35   private var mDone = false
36 
37   /** True if [Suggestions.close] has been called. */
38   var isClosed = false
39     private set
40 
41   /**
42    * Gets the list of corpus results reported so far. Do not modify or hang on to the returned
43    * iterator.
44    */
getResultnull45   fun getResult(): SourceResult? {
46     return mResult
47   }
48 
getWebResultnull49   fun getWebResult(): SourceResult? {
50     return mResult
51   }
52 
acquirenull53   fun acquire() {
54     mRefCount++
55   }
56 
releasenull57   fun release() {
58     mRefCount--
59     if (mRefCount <= 0) {
60       close()
61     }
62   }
63 
64   /** Marks the suggestions set as complete, regardless of whether all corpora have returned. */
donenull65   fun done() {
66     mDone = true
67   }
68 
69   /**
70    * Checks whether all sources have reported. Must be called on the UI thread, or before this
71    * object is seen by the UI thread.
72    */
73   val isDone: Boolean
74     get() = mDone || mResult != null
75 
76   /**
77    * Adds a list of corpus results. Must be called on the UI thread, or before this object is seen
78    * by the UI thread.
79    */
addResultsnull80   fun addResults(result: SourceResult?) {
81     if (isClosed) {
82       result?.close()
83       return
84     }
85     if (DBG) {
86       Log.d(
87         TAG,
88         "addResults[" +
89           hashCode().toString() +
90           "] source:" +
91           result?.source?.name.toString() +
92           " results:" +
93           result?.count
94       )
95     }
96     if (query != result?.userQuery) {
97       throw IllegalArgumentException(
98         "Got result for wrong query: " + query + " != " + result?.userQuery
99       )
100     }
101     mResult = result
102     notifyDataSetChanged()
103   }
104 
105   /**
106    * Registers an observer that will be notified when the reported results or the done status
107    * changes.
108    */
registerDataSetObservernull109   fun registerDataSetObserver(observer: DataSetObserver?) {
110     if (isClosed) {
111       throw IllegalStateException("registerDataSetObserver() when closed")
112     }
113     mDataSetObservable.registerObserver(observer)
114   }
115 
116   /** Unregisters an observer. */
unregisterDataSetObservernull117   fun unregisterDataSetObserver(observer: DataSetObserver?) {
118     mDataSetObservable.unregisterObserver(observer)
119   }
120 
121   /** Calls [DataSetObserver.onChanged] on all observers. */
notifyDataSetChangednull122   protected fun notifyDataSetChanged() {
123     if (DBG) Log.d(TAG, "notifyDataSetChanged()")
124     mDataSetObservable.notifyChanged()
125   }
126 
127   /** Closes all the source results and unregisters all observers. */
closenull128   private fun close() {
129     if (DBG) Log.d(TAG, "close() [" + hashCode().toString() + "]")
130     if (isClosed) {
131       throw IllegalStateException("Double close()")
132     }
133     isClosed = true
134     mDataSetObservable.unregisterAll()
135     mResult?.close()
136     mResult = null
137   }
138 
139   @Override
finalizenull140   protected fun finalize() {
141     if (!isClosed) {
142       Log.e(TAG, "LEAK! Finalized without being closed: Suggestions[$query]")
143     }
144   }
145 
146   /**
147    * Gets the number of source results. Must be called on the UI thread, or before this object is
148    * seen by the UI thread.
149    */
150   val resultCount: Int
151     get() {
152       if (isClosed) {
153         throw IllegalStateException("Called resultCount when closed.")
154       }
155       return mResult?.count ?: 0
156     }
157 
158   @Override
toStringnull159   override fun toString(): String {
160     return "Suggestions@" +
161       hashCode().toString() +
162       "{source=" +
163       source.toString() +
164       ",resultCount=" +
165       resultCount.toString() +
166       "}"
167   }
168 
169   companion object {
170     private const val DBG = false
171     private const val TAG = "QSB.Suggestions"
172   }
173 }
174