blob: 610cfdd7a7cd9447e21fd69fe3e74d5ebb8d955a [file] [log] [blame]
/*
* 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
}
}