1 /* 2 * Copyright (C) 2016 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.example.android.networkconnect; 18 19 import android.content.Context; 20 import android.net.ConnectivityManager; 21 import android.net.NetworkInfo; 22 import android.os.AsyncTask; 23 import android.os.Bundle; 24 import android.support.annotation.Nullable; 25 import android.support.v4.app.Fragment; 26 import android.support.v4.app.FragmentManager; 27 import android.util.Log; 28 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.InputStreamReader; 32 import java.io.Reader; 33 import java.net.URL; 34 35 import javax.net.ssl.HttpsURLConnection; 36 37 /** 38 * Implementation of headless Fragment that runs an AsyncTask to fetch data from the network. 39 */ 40 public class NetworkFragment extends Fragment { 41 public static final String TAG = "NetworkFragment"; 42 43 private static final String URL_KEY = "UrlKey"; 44 45 private DownloadCallback mCallback; 46 private DownloadTask mDownloadTask; 47 private String mUrlString; 48 49 /** 50 * Static initializer for NetworkFragment that sets the URL of the host it will be downloading 51 * from. 52 */ getInstance(FragmentManager fragmentManager, String url)53 public static NetworkFragment getInstance(FragmentManager fragmentManager, String url) { 54 // Recover NetworkFragment in case we are re-creating the Activity due to a config change. 55 // This is necessary because NetworkFragment might have a task that began running before 56 // the config change and has not finished yet. 57 // The NetworkFragment is recoverable via this method because it calls 58 // setRetainInstance(true) upon creation. 59 NetworkFragment networkFragment = (NetworkFragment) fragmentManager 60 .findFragmentByTag(NetworkFragment.TAG); 61 if (networkFragment == null) { 62 networkFragment = new NetworkFragment(); 63 Bundle args = new Bundle(); 64 args.putString(URL_KEY, url); 65 networkFragment.setArguments(args); 66 fragmentManager.beginTransaction().add(networkFragment, TAG).commit(); 67 } 68 return networkFragment; 69 } 70 71 @Override onCreate(@ullable Bundle savedInstanceState)72 public void onCreate(@Nullable Bundle savedInstanceState) { 73 super.onCreate(savedInstanceState); 74 // Retain this Fragment across configuration changes in the host Activity. 75 setRetainInstance(true); 76 mUrlString = getArguments().getString(URL_KEY); 77 } 78 79 @Override onAttach(Context context)80 public void onAttach(Context context) { 81 super.onAttach(context); 82 // Host Activity will handle callbacks from task. 83 mCallback = (DownloadCallback)context; 84 } 85 86 @Override onDetach()87 public void onDetach() { 88 super.onDetach(); 89 // Clear reference to host Activity. 90 mCallback = null; 91 } 92 93 @Override onDestroy()94 public void onDestroy() { 95 // Cancel task when Fragment is destroyed. 96 cancelDownload(); 97 super.onDestroy(); 98 } 99 100 /** 101 * Start non-blocking execution of DownloadTask. 102 */ startDownload()103 public void startDownload() { 104 cancelDownload(); 105 mDownloadTask = new DownloadTask(); 106 mDownloadTask.execute(mUrlString); 107 } 108 109 /** 110 * Cancel (and interrupt if necessary) any ongoing DownloadTask execution. 111 */ cancelDownload()112 public void cancelDownload() { 113 if (mDownloadTask != null) { 114 mDownloadTask.cancel(true); 115 mDownloadTask = null; 116 } 117 } 118 119 /** 120 * Implementation of AsyncTask that runs a network operation on a background thread. 121 */ 122 private class DownloadTask extends AsyncTask<String, Integer, DownloadTask.Result> { 123 124 /** 125 * Wrapper class that serves as a union of a result value and an exception. When the 126 * download task has completed, either the result value or exception can be a non-null 127 * value. This allows you to pass exceptions to the UI thread that were thrown during 128 * doInBackground(). 129 */ 130 class Result { 131 public String mResultValue; 132 public Exception mException; Result(String resultValue)133 public Result(String resultValue) { 134 mResultValue = resultValue; 135 } Result(Exception exception)136 public Result(Exception exception) { 137 mException = exception; 138 } 139 } 140 141 /** 142 * Cancel background network operation if we do not have network connectivity. 143 */ 144 @Override onPreExecute()145 protected void onPreExecute() { 146 if (mCallback != null) { 147 NetworkInfo networkInfo = mCallback.getActiveNetworkInfo(); 148 if (networkInfo == null || !networkInfo.isConnected() || 149 (networkInfo.getType() != ConnectivityManager.TYPE_WIFI 150 && networkInfo.getType() != ConnectivityManager.TYPE_MOBILE)) { 151 // If no connectivity, cancel task and update Callback with null data. 152 mCallback.updateFromDownload(null); 153 cancel(true); 154 } 155 } 156 } 157 158 /** 159 * Defines work to perform on the background thread. 160 */ 161 @Override doInBackground(String... urls)162 protected Result doInBackground(String... urls) { 163 Result result = null; 164 if (!isCancelled() && urls != null && urls.length > 0) { 165 String urlString = urls[0]; 166 try { 167 URL url = new URL(urlString); 168 String resultString = downloadUrl(url); 169 if (resultString != null) { 170 result = new Result(resultString); 171 } else { 172 throw new IOException("No response received."); 173 } 174 } catch(Exception e) { 175 result = new Result(e); 176 } 177 } 178 return result; 179 } 180 181 /** 182 * Send DownloadCallback a progress update. 183 */ 184 @Override onProgressUpdate(Integer... values)185 protected void onProgressUpdate(Integer... values) { 186 super.onProgressUpdate(values); 187 if (values.length >= 2) { 188 mCallback.onProgressUpdate(values[0], values[1]); 189 } 190 } 191 192 /** 193 * Updates the DownloadCallback with the result. 194 */ 195 @Override onPostExecute(Result result)196 protected void onPostExecute(Result result) { 197 if (result != null && mCallback != null) { 198 if (result.mException != null) { 199 mCallback.updateFromDownload(result.mException.getMessage()); 200 } else if (result.mResultValue != null) { 201 mCallback.updateFromDownload(result.mResultValue); 202 } 203 mCallback.finishDownloading(); 204 } 205 } 206 207 /** 208 * Override to add special behavior for cancelled AsyncTask. 209 */ 210 @Override onCancelled(Result result)211 protected void onCancelled(Result result) { 212 } 213 214 /** 215 * Given a URL, sets up a connection and gets the HTTP response body from the server. 216 * If the network request is successful, it returns the response body in String form. Otherwise, 217 * it will throw an IOException. 218 */ downloadUrl(URL url)219 private String downloadUrl(URL url) throws IOException { 220 InputStream stream = null; 221 HttpsURLConnection connection = null; 222 String result = null; 223 try { 224 connection = (HttpsURLConnection) url.openConnection(); 225 // Timeout for reading InputStream arbitrarily set to 3000ms. 226 connection.setReadTimeout(3000); 227 // Timeout for connection.connect() arbitrarily set to 3000ms. 228 connection.setConnectTimeout(3000); 229 // For this use case, set HTTP method to GET. 230 connection.setRequestMethod("GET"); 231 // Already true by default but setting just in case; needs to be true since this request 232 // is carrying an input (response) body. 233 connection.setDoInput(true); 234 // Open communications link (network traffic occurs here). 235 connection.connect(); 236 publishProgress(DownloadCallback.Progress.CONNECT_SUCCESS); 237 int responseCode = connection.getResponseCode(); 238 if (responseCode != HttpsURLConnection.HTTP_OK) { 239 throw new IOException("HTTP error code: " + responseCode); 240 } 241 // Retrieve the response body as an InputStream. 242 stream = connection.getInputStream(); 243 publishProgress(DownloadCallback.Progress.GET_INPUT_STREAM_SUCCESS, 0); 244 if (stream != null) { 245 // Converts Stream to String with max length of 500. 246 result = readStream(stream, 500); 247 publishProgress(DownloadCallback.Progress.PROCESS_INPUT_STREAM_SUCCESS, 0); 248 } 249 } finally { 250 // Close Stream and disconnect HTTPS connection. 251 if (stream != null) { 252 stream.close(); 253 } 254 if (connection != null) { 255 connection.disconnect(); 256 } 257 } 258 return result; 259 } 260 261 /** 262 * Converts the contents of an InputStream to a String. 263 */ readStream(InputStream stream, int maxLength)264 private String readStream(InputStream stream, int maxLength) throws IOException { 265 String result = null; 266 // Read InputStream using the UTF-8 charset. 267 InputStreamReader reader = new InputStreamReader(stream, "UTF-8"); 268 // Create temporary buffer to hold Stream data with specified max length. 269 char[] buffer = new char[maxLength]; 270 // Populate temporary buffer with Stream data. 271 int numChars = 0; 272 int readSize = 0; 273 while (numChars < maxLength && readSize != -1) { 274 numChars += readSize; 275 int pct = (100 * numChars) / maxLength; 276 publishProgress(DownloadCallback.Progress.PROCESS_INPUT_STREAM_IN_PROGRESS, pct); 277 readSize = reader.read(buffer, numChars, buffer.length - numChars); 278 } 279 if (numChars != -1) { 280 // The stream was not empty. 281 // Create String that is actual length of response body if actual length was less than 282 // max length. 283 numChars = Math.min(numChars, maxLength); 284 result = new String(buffer, 0, numChars); 285 } 286 return result; 287 } 288 } 289 } 290