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