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.ComponentName 19 import android.content.Context 20 import android.net.ConnectivityManager 21 import android.net.NetworkCapabilities 22 import android.os.Build 23 import android.os.Handler 24 import android.text.TextUtils 25 import android.util.Log 26 import com.android.quicksearchbox.Config 27 import com.android.quicksearchbox.R 28 import com.android.quicksearchbox.Source 29 import com.android.quicksearchbox.SourceResult 30 import com.android.quicksearchbox.SuggestionCursor 31 import com.android.quicksearchbox.util.NamedTaskExecutor 32 import java.io.BufferedReader 33 import java.io.IOException 34 import java.io.InputStream 35 import java.io.InputStreamReader 36 import java.io.UnsupportedEncodingException 37 import java.net.HttpURLConnection 38 import java.net.URI 39 import java.net.URL 40 import java.net.URLEncoder 41 import java.util.Locale 42 import org.json.JSONArray 43 import org.json.JSONException 44 45 /** Use network-based Google Suggests to provide search suggestions. */ 46 class GoogleSuggestClient( 47 context: Context?, 48 uiThread: Handler?, 49 iconLoader: NamedTaskExecutor, 50 config: Config 51 ) : AbstractGoogleSource(context, uiThread, iconLoader) { 52 private var mSuggestUri: String? 53 private val mConnectTimeout: Int 54 55 @get:Override 56 override val intentComponent: ComponentName 57 get() = ComponentName(context!!, GoogleSearch::class.java) 58 59 @Override queryInternalnull60 override fun queryInternal(query: String?): SourceResult? { 61 return query(query) 62 } 63 64 @Override queryExternalnull65 override fun queryExternal(query: String?): SourceResult? { 66 return query(query) 67 } 68 69 /** 70 * Queries for a given search term and returns a cursor containing suggestions ordered by best 71 * match. 72 */ querynull73 private fun query(query: String?): SourceResult? { 74 if (TextUtils.isEmpty(query)) { 75 return null 76 } 77 if (!isNetworkConnected) { 78 Log.i(LOG_TAG, "Not connected to network.") 79 return null 80 } 81 var connection: HttpURLConnection? = null 82 try { 83 val encodedQuery: String = URLEncoder.encode(query, "UTF-8") 84 if (mSuggestUri == null) { 85 val l: Locale = Locale.getDefault() 86 val language: String = GoogleSearch.getLanguage(l) 87 mSuggestUri = context?.getResources()!!.getString(R.string.google_suggest_base, language) 88 } 89 val suggestUri = mSuggestUri + encodedQuery 90 if (DBG) Log.d(LOG_TAG, "Sending request: $suggestUri") 91 val url: URL = URI.create(suggestUri).toURL() 92 connection = url.openConnection() as HttpURLConnection 93 connection.setConnectTimeout(mConnectTimeout) 94 connection.setRequestProperty("User-Agent", USER_AGENT) 95 connection.setRequestMethod("GET") 96 connection.setDoInput(true) 97 connection.connect() 98 val inputStream: InputStream = connection.getInputStream() 99 if (connection.getResponseCode() == 200) { 100 101 /* Goto http://www.google.com/complete/search?json=true&q=foo 102 * to see what the data format looks like. It's basically a json 103 * array containing 4 other arrays. We only care about the middle 104 * 2 which contain the suggestions and their popularity. 105 */ 106 val reader = BufferedReader(InputStreamReader(inputStream)) 107 val sb: StringBuilder = StringBuilder() 108 var line: String? 109 while (reader.readLine().also { line = it } != null) { 110 sb.append(line).append("\n") 111 } 112 reader.close() 113 val results = JSONArray(sb.toString()) 114 val suggestions: JSONArray = results.getJSONArray(1) 115 val popularity: JSONArray = results.getJSONArray(2) 116 if (DBG) Log.d(LOG_TAG, "Got " + suggestions.length().toString() + " results") 117 return GoogleSuggestCursor(this, query, suggestions, popularity) 118 } else { 119 if (DBG) Log.d(LOG_TAG, "Request failed " + connection.getResponseMessage()) 120 } 121 } catch (e: UnsupportedEncodingException) { 122 Log.w(LOG_TAG, "Error", e) 123 } catch (e: IOException) { 124 Log.w(LOG_TAG, "Error", e) 125 } catch (e: JSONException) { 126 Log.w(LOG_TAG, "Error", e) 127 } finally { 128 if (connection != null) connection.disconnect() 129 } 130 return null 131 } 132 133 @Override refreshShortcutnull134 override fun refreshShortcut(shortcutId: String?, extraData: String?): SuggestionCursor? { 135 return null 136 } 137 138 private val isNetworkConnected: Boolean 139 get() { 140 val actNC = activeNetworkCapabilities 141 return actNC != null && actNC.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) 142 } 143 private val activeNetworkCapabilities: NetworkCapabilities? 144 get() { 145 val connectivityManager = 146 context?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 147 val activeNetwork = connectivityManager.getActiveNetwork() 148 return connectivityManager.getNetworkCapabilities(activeNetwork) 149 } 150 151 private class GoogleSuggestCursor( 152 source: Source, 153 userQuery: String?, 154 suggestions: JSONArray, 155 popularity: JSONArray 156 ) : AbstractGoogleSourceResult(source, userQuery!!) { 157 /* Contains the actual suggestions */ 158 private val mSuggestions: JSONArray 159 160 /* This contains the popularity of each suggestion 161 * i.e. 165,000 results. It's not related to sorting. 162 */ 163 private val mPopularity: JSONArray 164 165 @get:Override 166 override val count: Int 167 get() = mSuggestions.length() 168 169 @get:Override 170 override val suggestionQuery: String? 171 get() = 172 try { 173 mSuggestions.getString(position) 174 } catch (e: JSONException) { 175 Log.w(LOG_TAG, "Error parsing response: $e") 176 null 177 } 178 179 @get:Override 180 override val suggestionText2: String? 181 get() = 182 try { 183 mPopularity.getString(position) 184 } catch (e: JSONException) { 185 Log.w(LOG_TAG, "Error parsing response: $e") 186 null 187 } 188 189 init { 190 mSuggestions = suggestions 191 mPopularity = popularity 192 } 193 } 194 195 companion object { 196 private const val DBG = false 197 private const val LOG_TAG = "GoogleSearch" 198 private val USER_AGENT = "Android/" + Build.VERSION.RELEASE 199 200 // TODO: this should be defined somewhere 201 private const val HTTP_TIMEOUT = "http.conn-manager.timeout" 202 } 203 204 init { 205 mConnectTimeout = config.httpConnectTimeout 206 // NOTE: Do not look up the resource here; Localization changes may not have completed 207 // yet (e.g. we may still be reading the SIM card). 208 mSuggestUri = null 209 } 210 } 211