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.app.Activity
19 import android.app.PendingIntent
20 import android.app.SearchManager
21 import android.content.ActivityNotFoundException
22 import android.content.Intent
23 import android.net.Uri
24 import android.os.Bundle
25 import android.provider.Browser
26 import android.text.TextUtils
27 import android.util.Log
28 import com.android.common.Search
29 import com.android.quicksearchbox.QsbApplication
30 import java.io.UnsupportedEncodingException
31 import java.net.URLEncoder
32 import java.util.Locale
33 
34 /**
35  * This class is purely here to get search queries and route them to the global
36  * [Intent.ACTION_WEB_SEARCH].
37  */
38 class GoogleSearch : Activity() {
39   // Used to figure out which domain to base search requests
40   // on.
41   private var mSearchDomainHelper: SearchBaseUrlHelper? = null
42 
43   @Override
onCreatenull44   protected override fun onCreate(savedInstanceState: Bundle?) {
45     super.onCreate(savedInstanceState)
46     val intent: Intent? = getIntent()
47     val action: String? = if (intent != null) intent.getAction() else null
48 
49     // This should probably be moved so as to
50     // send out the request to /checksearchdomain as early as possible.
51     mSearchDomainHelper = QsbApplication.get(this).searchBaseUrlHelper
52     if (Intent.ACTION_WEB_SEARCH.equals(action) || Intent.ACTION_SEARCH.equals(action)) {
53       handleWebSearchIntent(intent)
54     }
55     finish()
56   }
57 
handleWebSearchIntentnull58   private fun handleWebSearchIntent(intent: Intent?) {
59     val launchUriIntent: Intent? = createLaunchUriIntentFromSearchIntent(intent)
60 
61     @Suppress("DEPRECATION")
62     val pending: PendingIntent? =
63       intent?.getParcelableExtra(SearchManager.EXTRA_WEB_SEARCH_PENDINGINTENT)
64     if (pending == null || !launchPendingIntent(pending, launchUriIntent)) {
65       launchIntent(launchUriIntent)
66     }
67   }
68 
createLaunchUriIntentFromSearchIntentnull69   private fun createLaunchUriIntentFromSearchIntent(intent: Intent?): Intent? {
70     val query: String? = intent?.getStringExtra(SearchManager.QUERY)
71     if (TextUtils.isEmpty(query)) {
72       Log.w(TAG, "Got search intent with no query.")
73       return null
74     }
75 
76     // If the caller specified a 'source' url parameter, use that and if not use default.
77     val appSearchData: Bundle? = intent?.getBundleExtra(SearchManager.APP_DATA)
78     var source: String? = GoogleSearch.Companion.GOOGLE_SEARCH_SOURCE_UNKNOWN
79     if (appSearchData != null) {
80       source = appSearchData.getString(Search.SOURCE)
81     }
82 
83     // The browser can pass along an application id which it uses to figure out which
84     // window to place a new search into. So if this exists, we'll pass it back to
85     // the browser. Otherwise, add our own package name as the application id, so that
86     // the browser can organize all searches launched from this provider together.
87     var applicationId: String? = intent?.getStringExtra(Browser.EXTRA_APPLICATION_ID)
88     if (applicationId == null) {
89       applicationId = getPackageName()
90     }
91     return try {
92       val searchUri =
93         (mSearchDomainHelper!!.searchBaseUrl.toString() +
94           "&source=android-" +
95           source +
96           "&q=" +
97           URLEncoder.encode(query, "UTF-8"))
98       val launchUriIntent = Intent(Intent.ACTION_VIEW, Uri.parse(searchUri))
99       launchUriIntent.putExtra(Browser.EXTRA_APPLICATION_ID, applicationId)
100       launchUriIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
101       launchUriIntent
102     } catch (e: UnsupportedEncodingException) {
103       Log.w(TAG, "Error", e)
104       null
105     }
106   }
107 
launchIntentnull108   private fun launchIntent(intent: Intent?) {
109     try {
110       Log.i(TAG, "Launching intent: " + intent?.toUri(0))
111       startActivity(intent)
112     } catch (ex: ActivityNotFoundException) {
113       Log.w(TAG, "No activity found to handle: $intent")
114     }
115   }
116 
launchPendingIntentnull117   private fun launchPendingIntent(pending: PendingIntent, fillIn: Intent?): Boolean {
118     return try {
119       pending.send(this, Activity.RESULT_OK, fillIn)
120       true
121     } catch (ex: PendingIntent.CanceledException) {
122       Log.i(TAG, "Pending intent cancelled: $pending")
123       false
124     }
125   }
126 
127   companion object {
128     private const val TAG = "GoogleSearch"
129     private const val DBG = false
130 
131     // "source" parameter for Google search requests from unknown sources (e.g. apps). This will get
132     // prefixed with the string 'android-' before being sent on the wire.
133     const val GOOGLE_SEARCH_SOURCE_UNKNOWN = "unknown"
134 
135     /** Construct the language code (hl= parameter) for the given locale. */
getLanguagenull136     fun getLanguage(locale: Locale): String {
137       val language: String = locale.getLanguage()
138       val hl: StringBuilder = StringBuilder(language)
139       val country: String = locale.getCountry()
140       if (!TextUtils.isEmpty(country) && useLangCountryHl(language, country)) {
141         hl.append('-')
142         hl.append(country)
143       }
144       if (DBG) Log.d(TAG, "language $language, country $country -> hl=$hl")
145       return hl.toString()
146     }
147 
148     // TODO: This is a workaround for bug 3232296. When that is fixed, this method can be removed.
useLangCountryHlnull149     private fun useLangCountryHl(language: String, country: String): Boolean {
150       // lang-country is currently only supported for a small number of locales
151       return if ("en".equals(language)) {
152         "GB".equals(country)
153       } else if ("zh".equals(language)) {
154         "CN".equals(country) || "TW".equals(country)
155       } else if ("pt".equals(language)) {
156         "BR".equals(country) || "PT".equals(country)
157       } else {
158         false
159       }
160     }
161   }
162 }
163