/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.quicksearchbox.google import android.content.ComponentName import android.content.Context import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.os.Build import android.os.Handler import android.text.TextUtils import android.util.Log import com.android.quicksearchbox.Config import com.android.quicksearchbox.R import com.android.quicksearchbox.Source import com.android.quicksearchbox.SourceResult import com.android.quicksearchbox.SuggestionCursor import com.android.quicksearchbox.util.NamedTaskExecutor import java.io.BufferedReader import java.io.IOException import java.io.InputStream import java.io.InputStreamReader import java.io.UnsupportedEncodingException import java.net.HttpURLConnection import java.net.URI import java.net.URL import java.net.URLEncoder import java.util.Locale import org.json.JSONArray import org.json.JSONException /** Use network-based Google Suggests to provide search suggestions. */ class GoogleSuggestClient( context: Context?, uiThread: Handler?, iconLoader: NamedTaskExecutor, config: Config ) : AbstractGoogleSource(context, uiThread, iconLoader) { private var mSuggestUri: String? private val mConnectTimeout: Int @get:Override override val intentComponent: ComponentName get() = ComponentName(context!!, GoogleSearch::class.java) @Override override fun queryInternal(query: String?): SourceResult? { return query(query) } @Override override fun queryExternal(query: String?): SourceResult? { return query(query) } /** * Queries for a given search term and returns a cursor containing suggestions ordered by best * match. */ private fun query(query: String?): SourceResult? { if (TextUtils.isEmpty(query)) { return null } if (!isNetworkConnected) { Log.i(LOG_TAG, "Not connected to network.") return null } var connection: HttpURLConnection? = null try { val encodedQuery: String = URLEncoder.encode(query, "UTF-8") if (mSuggestUri == null) { val l: Locale = Locale.getDefault() val language: String = GoogleSearch.getLanguage(l) mSuggestUri = context?.getResources()!!.getString(R.string.google_suggest_base, language) } val suggestUri = mSuggestUri + encodedQuery if (DBG) Log.d(LOG_TAG, "Sending request: $suggestUri") val url: URL = URI.create(suggestUri).toURL() connection = url.openConnection() as HttpURLConnection connection.setConnectTimeout(mConnectTimeout) connection.setRequestProperty("User-Agent", USER_AGENT) connection.setRequestMethod("GET") connection.setDoInput(true) connection.connect() val inputStream: InputStream = connection.getInputStream() if (connection.getResponseCode() == 200) { /* Goto http://www.google.com/complete/search?json=true&q=foo * to see what the data format looks like. It's basically a json * array containing 4 other arrays. We only care about the middle * 2 which contain the suggestions and their popularity. */ val reader = BufferedReader(InputStreamReader(inputStream)) val sb: StringBuilder = StringBuilder() var line: String? while (reader.readLine().also { line = it } != null) { sb.append(line).append("\n") } reader.close() val results = JSONArray(sb.toString()) val suggestions: JSONArray = results.getJSONArray(1) val popularity: JSONArray = results.getJSONArray(2) if (DBG) Log.d(LOG_TAG, "Got " + suggestions.length().toString() + " results") return GoogleSuggestCursor(this, query, suggestions, popularity) } else { if (DBG) Log.d(LOG_TAG, "Request failed " + connection.getResponseMessage()) } } catch (e: UnsupportedEncodingException) { Log.w(LOG_TAG, "Error", e) } catch (e: IOException) { Log.w(LOG_TAG, "Error", e) } catch (e: JSONException) { Log.w(LOG_TAG, "Error", e) } finally { if (connection != null) connection.disconnect() } return null } @Override override fun refreshShortcut(shortcutId: String?, extraData: String?): SuggestionCursor? { return null } private val isNetworkConnected: Boolean get() { val actNC = activeNetworkCapabilities return actNC != null && actNC.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) } private val activeNetworkCapabilities: NetworkCapabilities? get() { val connectivityManager = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val activeNetwork = connectivityManager.getActiveNetwork() return connectivityManager.getNetworkCapabilities(activeNetwork) } private class GoogleSuggestCursor( source: Source, userQuery: String?, suggestions: JSONArray, popularity: JSONArray ) : AbstractGoogleSourceResult(source, userQuery!!) { /* Contains the actual suggestions */ private val mSuggestions: JSONArray /* This contains the popularity of each suggestion * i.e. 165,000 results. It's not related to sorting. */ private val mPopularity: JSONArray @get:Override override val count: Int get() = mSuggestions.length() @get:Override override val suggestionQuery: String? get() = try { mSuggestions.getString(position) } catch (e: JSONException) { Log.w(LOG_TAG, "Error parsing response: $e") null } @get:Override override val suggestionText2: String? get() = try { mPopularity.getString(position) } catch (e: JSONException) { Log.w(LOG_TAG, "Error parsing response: $e") null } init { mSuggestions = suggestions mPopularity = popularity } } companion object { private const val DBG = false private const val LOG_TAG = "GoogleSearch" private val USER_AGENT = "Android/" + Build.VERSION.RELEASE // TODO: this should be defined somewhere private const val HTTP_TIMEOUT = "http.conn-manager.timeout" } init { mConnectTimeout = config.httpConnectTimeout // NOTE: Do not look up the resource here; Localization changes may not have completed // yet (e.g. we may still be reading the SIM card). mSuggestUri = null } }