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
18 
19 import android.app.SearchManager
20 import android.content.Context
21 import android.content.Intent
22 import android.content.SharedPreferences
23 import android.content.SharedPreferences.Editor
24 import android.util.Log
25 import com.android.common.SharedPreferencesCompat
26 
27 /** Manages user settings. */
28 class SearchSettingsImpl(context: Context?, config: Config?) : SearchSettings {
29   private val mContext: Context?
30   protected val config: Config?
31   protected val context: Context?
32     get() = mContext
33 
upgradeSettingsIfNeedednull34   @Override override fun upgradeSettingsIfNeeded() {}
35 
36   val searchPreferences: SharedPreferences
37     get() = context!!.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
38 
storeBooleannull39   protected fun storeBoolean(name: String?, value: Boolean) {
40     SharedPreferencesCompat.apply(searchPreferences.edit().putBoolean(name, value))
41   }
42 
storeIntnull43   protected fun storeInt(name: String?, value: Int) {
44     SharedPreferencesCompat.apply(searchPreferences.edit().putInt(name, value))
45   }
46 
storeLongnull47   protected fun storeLong(name: String?, value: Long) {
48     SharedPreferencesCompat.apply(searchPreferences.edit().putLong(name, value))
49   }
50 
storeStringnull51   protected fun storeString(name: String?, value: String?) {
52     SharedPreferencesCompat.apply(searchPreferences.edit().putString(name, value))
53   }
54 
removePrefnull55   protected fun removePref(name: String?) {
56     SharedPreferencesCompat.apply(searchPreferences.edit().remove(name))
57   }
58 
59   /** Informs our listeners about the updated settings data. */
60   @Override
broadcastSettingsChangednull61   override fun broadcastSettingsChanged() {
62     // We use a message broadcast since the listeners could be in multiple processes.
63     val intent = Intent(SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED)
64     Log.i(TAG, "Broadcasting: $intent")
65     context?.sendBroadcast(intent)
66   }
67 
68   @Override
getNextVoiceSearchHintIndexnull69   override fun getNextVoiceSearchHintIndex(size: Int): Int {
70     val i = getAndIncrementIntPreference(searchPreferences, NEXT_VOICE_SEARCH_HINT_INDEX_PREF)
71     return i % size
72   }
73 
74   // TODO: Could this be made atomic to avoid races?
getAndIncrementIntPreferencenull75   private fun getAndIncrementIntPreference(prefs: SharedPreferences, name: String): Int {
76     val i: Int = prefs.getInt(name, 0)
77     storeInt(name, i + 1)
78     return i
79   }
80 
81   @Override
resetVoiceSearchHintFirstSeenTimenull82   override fun resetVoiceSearchHintFirstSeenTime() {
83     storeLong(FIRST_VOICE_HINT_DISPLAY_TIME, System.currentTimeMillis())
84   }
85 
86   @Override
haveVoiceSearchHintsExpirednull87   override fun haveVoiceSearchHintsExpired(currentVoiceSearchVersion: Int): Boolean {
88     val prefs: SharedPreferences = searchPreferences
89     return if (currentVoiceSearchVersion != 0) {
90       val currentTime: Long = System.currentTimeMillis()
91       val lastVoiceSearchVersion: Int = prefs.getInt(LAST_SEEN_VOICE_SEARCH_VERSION, 0)
92       var firstHintTime: Long = prefs.getLong(FIRST_VOICE_HINT_DISPLAY_TIME, 0)
93       if (firstHintTime == 0L || currentVoiceSearchVersion != lastVoiceSearchVersion) {
94         SharedPreferencesCompat.apply(
95           prefs
96             .edit()
97             .putInt(LAST_SEEN_VOICE_SEARCH_VERSION, currentVoiceSearchVersion)
98             .putLong(FIRST_VOICE_HINT_DISPLAY_TIME, currentTime)
99         )
100         firstHintTime = currentTime
101       }
102       if (currentTime - firstHintTime > config!!.voiceSearchHintActivePeriod) {
103         if (DBG) Log.d(TAG, "Voice search hint period expired; not showing hints.")
104         return true
105       } else {
106         false
107       }
108     } else {
109       if (DBG) Log.d(TAG, "Could not determine voice search version; not showing hints.")
110       true
111     }
112   }
113 
114   /** @return true if user searches should always be based at google.com, false otherwise. */
115   @Override
shouldUseGoogleComnull116   override fun shouldUseGoogleCom(): Boolean {
117     // Note that this preserves the old behaviour of using google.com
118     // for searches, with the gl= parameter set.
119     return searchPreferences.getBoolean(USE_GOOGLE_COM_PREF, true)
120   }
121 
122   @Override
setUseGoogleComnull123   override fun setUseGoogleCom(useGoogleCom: Boolean) {
124     storeBoolean(USE_GOOGLE_COM_PREF, useGoogleCom)
125   }
126 
127   @get:Override
128   override val searchBaseDomainApplyTime: Long
129     get() = searchPreferences.getLong(SEARCH_BASE_DOMAIN_APPLY_TIME, -1)
130 
131   // Note that the only time this will return null is on the first run
132   // of the app, or when settings have been cleared. Callers should
133   // ideally check that getSearchBaseDomainApplyTime() is not -1 before
134   // calling this function.
135   @get:Override
136   @set:Override
137   override var searchBaseDomain: String?
138     get() = searchPreferences.getString(SEARCH_BASE_DOMAIN_PREF, null)
139     set(searchBaseUrl) {
140       val sharedPrefEditor: Editor = searchPreferences.edit()
141       sharedPrefEditor.putString(SEARCH_BASE_DOMAIN_PREF, searchBaseUrl)
142       sharedPrefEditor.putLong(SEARCH_BASE_DOMAIN_APPLY_TIME, System.currentTimeMillis())
143       SharedPreferencesCompat.apply(sharedPrefEditor)
144     }
145 
146   companion object {
147     private const val DBG = false
148     private const val TAG = "QSB.SearchSettingsImpl"
149 
150     // Name of the preferences file used to store search preference
151     const val PREFERENCES_NAME = "SearchSettings"
152 
153     /** Preference key used for storing the index of the next voice search hint to show. */
154     private const val NEXT_VOICE_SEARCH_HINT_INDEX_PREF = "next_voice_search_hint"
155 
156     /** Preference key used to store the time at which the first voice search hint was displayed. */
157     private const val FIRST_VOICE_HINT_DISPLAY_TIME = "first_voice_search_hint_time"
158 
159     /** Preference key for the version of voice search we last got hints from. */
160     private const val LAST_SEEN_VOICE_SEARCH_VERSION = "voice_search_version"
161 
162     /**
163      * Preference key for storing whether searches always go to google.com. Public so that it can be
164      * used by PreferenceControllers.
165      */
166     const val USE_GOOGLE_COM_PREF = "use_google_com"
167 
168     /**
169      * Preference key for the base search URL. This value is normally set by a SearchBaseUrlHelper
170      * instance. Public so classes can listen to changes on this key.
171      */
172     const val SEARCH_BASE_DOMAIN_PREF = "search_base_domain"
173 
174     /**
175      * This is the time at which the base URL was stored, and is set using
176      * @link{System.currentTimeMillis()}.
177      */
178     private const val SEARCH_BASE_DOMAIN_APPLY_TIME = "search_base_domain_apply_time"
179   }
180 
181   init {
182     mContext = context
183     this.config = config
184   }
185 }
186