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.google
17 
18 import android.content.Context
19 import android.content.SharedPreferences
20 import android.util.Log
21 import com.android.quicksearchbox.R
22 import com.android.quicksearchbox.SearchSettings
23 import com.android.quicksearchbox.SearchSettingsImpl
24 import com.android.quicksearchbox.util.HttpHelper
25 import java.util.Locale
26 import kotlinx.coroutines.CoroutineScope
27 import kotlinx.coroutines.Dispatchers
28 import kotlinx.coroutines.async
29 
30 /** Helper to build the base URL for all search requests. */
31 class SearchBaseUrlHelper(
32   context: Context?,
33   helper: HttpHelper,
34   searchSettings: SearchSettings,
35   prefs: SharedPreferences
36 ) : SharedPreferences.OnSharedPreferenceChangeListener {
37   private val mHttpHelper: HttpHelper
38   private val mContext: Context?
39   private val mSearchSettings: SearchSettings
40   private val scope = CoroutineScope(Dispatchers.IO)
41 
42   /**
43    * Update the base search url, either: (a) it has never been set (first run) (b) it has expired
44    * (c) if the caller forces an update by setting the "force" parameter.
45    *
46    * @param force if true, then the URL is reset whether or not it has expired.
47    */
maybeUpdateBaseUrlSettingnull48   fun maybeUpdateBaseUrlSetting(force: Boolean) {
49     val lastUpdateTime: Long = mSearchSettings.searchBaseDomainApplyTime
50     val currentTime: Long = System.currentTimeMillis()
51     if (
52       force || lastUpdateTime == -1L || currentTime - lastUpdateTime >= SEARCH_BASE_URL_EXPIRY_MS
53     ) {
54       if (mSearchSettings.shouldUseGoogleCom()) {
55         setSearchBaseDomain(defaultBaseDomain)
56       } else {
57         checkSearchDomain()
58       }
59     }
60   }
61 
62   /** @return the base url for searches. */
63   val searchBaseUrl: String?
64     get() =
65       mContext
66         ?.getResources()
67         ?.getString(
68           R.string.google_search_base_pattern,
69           searchDomain,
70           GoogleSearch.getLanguage(Locale.getDefault())
71         ) // This is required to deal with the case wherein getSearchDomain
72   // is called before checkSearchDomain returns a valid URL. This will
73   // happen *only* on the first run of the app when the "use google.com"
74   // option is unchecked. In other cases, the previously set domain (or
75   // the default) will be returned.
76   //
77   // We have no choice in this case but to use the default search domain.
78   /**
79    * @return the search domain. This is of the form "google.co.xx" or "google.com", used by UI code.
80    */
81   val searchDomain: String?
82     get() {
83       var domain: String? = mSearchSettings.searchBaseDomain
84       if (domain == null) {
85         if (DBG) {
86           Log.w(
87             TAG,
88             "Search base domain was null, last apply time=" +
89               mSearchSettings.searchBaseDomainApplyTime
90           )
91         }
92 
93         // This is required to deal with the case wherein getSearchDomain
94         // is called before checkSearchDomain returns a valid URL. This will
95         // happen *only* on the first run of the app when the "use google.com"
96         // option is unchecked. In other cases, the previously set domain (or
97         // the default) will be returned.
98         //
99         // We have no choice in this case but to use the default search domain.
100         domain = defaultBaseDomain
101       }
102       if (domain?.startsWith(".") == true) {
103         if (DBG) Log.d(TAG, "Prepending www to $domain")
104         domain = "www$domain"
105       }
106       return domain
107     }
108 
109   /**
110    * Issue a request to google.com/searchdomaincheck to retrieve the base URL for search requests.
111    */
checkSearchDomainnull112   private fun checkSearchDomain() {
113     val request = HttpHelper.GetRequest(DOMAIN_CHECK_URL)
114     scope.async {
115       if (DBG) Log.d(TAG, "Starting request to /searchdomaincheck")
116       var domain: String?
117       try {
118         domain = mHttpHelper[request]
119       } catch (e: Exception) {
120         if (DBG) Log.d(TAG, "Request to /searchdomaincheck failed : $e")
121         // Swallow any exceptions thrown by the HTTP helper, in
122         // this rare case, we just use the default URL.
123         domain = defaultBaseDomain
124       }
125       if (DBG) Log.d(TAG, "Request to /searchdomaincheck succeeded")
126       setSearchBaseDomain(domain)
127     }
128   }
129 
130   private val defaultBaseDomain: String?
131     get() = mContext?.getResources()?.getString(R.string.default_search_domain)
132 
setSearchBaseDomainnull133   private fun setSearchBaseDomain(domain: String?) {
134     if (DBG) Log.d(TAG, "Setting search domain to : $domain")
135     mSearchSettings.searchBaseDomain = domain
136   }
137 
138   @Override
onSharedPreferenceChangednull139   override fun onSharedPreferenceChanged(pref: SharedPreferences?, key: String?) {
140     // Listen for changes only to the SEARCH_BASE_URL preference.
141     if (DBG) Log.d(TAG, "Handling changed preference : $key")
142     if (SearchSettingsImpl.USE_GOOGLE_COM_PREF.equals(key)) {
143       maybeUpdateBaseUrlSetting(true)
144     }
145   }
146 
147   companion object {
148     private const val DBG = false
149     private const val TAG = "QSB.SearchBaseUrlHelper"
150     private const val DOMAIN_CHECK_URL = "https://www.google.com/searchdomaincheck?format=domain"
151     private const val SEARCH_BASE_URL_EXPIRY_MS = 24 * 3600 * 1000L
152   }
153 
154   /**
155    * Note that this constructor will spawn a thread to issue a HTTP request if shouldUseGoogleCom is
156    * false.
157    */
158   init {
159     mHttpHelper = helper
160     mContext = context
161     mSearchSettings = searchSettings
162 
163     // Note: This earlier used an inner class, but that causes issues
164     // because SharedPreferencesImpl uses a WeakHashMap< > and the listener
165     // will be GC'ed unless we keep a reference to it here.
166     prefs.registerOnSharedPreferenceChangeListener(this)
167     maybeUpdateBaseUrlSetting(false)
168   }
169 }
170