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 
17 package com.android.quicksearchbox.util
18 
19 import android.os.Build
20 import android.util.Log
21 import java.io.BufferedReader
22 import java.io.IOException
23 import java.io.InputStreamReader
24 import java.io.OutputStreamWriter
25 import java.net.HttpURLConnection
26 import java.net.URL
27 
28 /** Simple HTTP client API. */
29 class JavaNetHttpHelper(rewriter: HttpHelper.UrlRewriter, userAgent: String) : HttpHelper {
30   private var mConnectTimeout = 0
31   private var mReadTimeout = 0
32   private val mUserAgent: String
33   private val mRewriter: HttpHelper.UrlRewriter
34 
35   /**
36    * Executes a GET request and returns the response content.
37    *
38    * @param request Request.
39    * @return The response content. This is the empty string if the response contained no content.
40    * @throws IOException If an IO error occurs.
41    * @throws HttpException If the response has a status code other than 200.
42    */
43   @Throws(IOException::class, HttpHelper.HttpException::class)
getnull44   override operator fun get(request: HttpHelper.GetRequest?): String? {
45     return get(request?.url, request?.headers)
46   }
47 
48   /**
49    * Executes a GET request and returns the response content.
50    *
51    * @param url Request URI.
52    * @param requestHeaders Request headers.
53    * @return The response content. This is the empty string if the response contained no content.
54    * @throws IOException If an IO error occurs.
55    * @throws HttpException If the response has a status code other than 200.
56    */
57   @Throws(IOException::class, HttpHelper.HttpException::class)
getnull58   override operator fun get(url: String?, requestHeaders: MutableMap<String, String>?): String? {
59     var c: HttpURLConnection? = null
60     return try {
61       c = createConnection(url!!, requestHeaders)
62       c.setRequestMethod("GET")
63       c.connect()
64       getResponseFrom(c)
65     } finally {
66       if (c != null) {
67         c.disconnect()
68       }
69     }
70   }
71 
72   @Override
73   @Throws(IOException::class, HttpHelper.HttpException::class)
postnull74   override fun post(request: HttpHelper.PostRequest?): String? {
75     return post(request?.url, request?.headers, request?.content)
76   }
77 
78   @Throws(IOException::class, HttpHelper.HttpException::class)
postnull79   override fun post(
80     url: String?,
81     requestHeaders: MutableMap<String, String>?,
82     content: String?
83   ): String? {
84     var mRequestHeaders: MutableMap<String, String>? = requestHeaders
85     var c: HttpURLConnection? = null
86     return try {
87       if (mRequestHeaders == null) {
88         mRequestHeaders = mutableMapOf()
89       }
90       mRequestHeaders.put("Content-Length", Integer.toString(content?.length ?: 0))
91       c = createConnection(url!!, mRequestHeaders)
92       c.setDoOutput(content != null)
93       c.setRequestMethod("POST")
94       c.connect()
95       if (content != null) {
96         val writer = OutputStreamWriter(c.getOutputStream())
97         writer.write(content)
98         writer.close()
99       }
100       getResponseFrom(c)
101     } finally {
102       if (c != null) {
103         c.disconnect()
104       }
105     }
106   }
107 
108   @Throws(IOException::class, HttpHelper.HttpException::class)
createConnectionnull109   private fun createConnection(url: String, headers: Map<String, String>?): HttpURLConnection {
110     val u = URL(mRewriter.rewrite(url))
111     if (DBG) Log.d(TAG, "URL=$url rewritten='$u'")
112     val c: HttpURLConnection = u.openConnection() as HttpURLConnection
113     if (headers != null) {
114       for (e in headers.entries) {
115         val name: String = e.key
116         val value: String = e.value
117         if (DBG) Log.d(TAG, "  $name: $value")
118         c.addRequestProperty(name, value)
119       }
120     }
121     c.addRequestProperty(USER_AGENT_HEADER, mUserAgent)
122     if (mConnectTimeout != 0) {
123       c.setConnectTimeout(mConnectTimeout)
124     }
125     if (mReadTimeout != 0) {
126       c.setReadTimeout(mReadTimeout)
127     }
128     return c
129   }
130 
131   @Throws(IOException::class, HttpHelper.HttpException::class)
getResponseFromnull132   private fun getResponseFrom(c: HttpURLConnection): String {
133     if (c.getResponseCode() != HttpURLConnection.HTTP_OK) {
134       throw HttpHelper.HttpException(c.getResponseCode(), c.getResponseMessage())
135     }
136     if (DBG) {
137       Log.d(
138         TAG,
139         "Content-Type: " + c.getContentType().toString() + " (assuming " + DEFAULT_CHARSET + ")"
140       )
141     }
142     val reader = BufferedReader(InputStreamReader(c.getInputStream(), DEFAULT_CHARSET))
143     val string: StringBuilder = StringBuilder()
144     val chars = CharArray(BUFFER_SIZE)
145     var bytes: Int
146     while (reader.read(chars).also { bytes = it } != -1) {
147       string.append(chars, 0, bytes)
148     }
149     return string.toString()
150   }
151 
setConnectTimeoutnull152   override fun setConnectTimeout(timeoutMillis: Int) {
153     mConnectTimeout = timeoutMillis
154   }
155 
setReadTimeoutnull156   override fun setReadTimeout(timeoutMillis: Int) {
157     mReadTimeout = timeoutMillis
158   }
159 
160   /** A Url rewriter that does nothing, i.e., returns the url that is passed to it. */
161   class PassThroughRewriter : HttpHelper.UrlRewriter {
162     @Override
rewritenull163     override fun rewrite(url: String): String {
164       return url
165     }
166   }
167 
168   companion object {
169     private const val TAG = "QSB.JavaNetHttpHelper"
170     private const val DBG = false
171     private const val BUFFER_SIZE = 1024 * 4
172     private const val USER_AGENT_HEADER = "User-Agent"
173     private const val DEFAULT_CHARSET = "UTF-8"
174   }
175 
176   /**
177    * Creates a new HTTP helper.
178    *
179    * @param rewriter URI rewriter
180    * @param userAgent User agent string, e.g. "MyApp/1.0".
181    */
182   init {
183     mUserAgent = userAgent + " (" + Build.DEVICE + " " + Build.ID + ")"
184     mRewriter = rewriter
185   }
186 }
187