/* * Copyright (C) 2016 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.example.android.networkconnect; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.net.URL; import javax.net.ssl.HttpsURLConnection; /** * Implementation of headless Fragment that runs an AsyncTask to fetch data from the network. */ public class NetworkFragment extends Fragment { public static final String TAG = "NetworkFragment"; private static final String URL_KEY = "UrlKey"; private DownloadCallback mCallback; private DownloadTask mDownloadTask; private String mUrlString; /** * Static initializer for NetworkFragment that sets the URL of the host it will be downloading * from. */ public static NetworkFragment getInstance(FragmentManager fragmentManager, String url) { // Recover NetworkFragment in case we are re-creating the Activity due to a config change. // This is necessary because NetworkFragment might have a task that began running before // the config change and has not finished yet. // The NetworkFragment is recoverable via this method because it calls // setRetainInstance(true) upon creation. NetworkFragment networkFragment = (NetworkFragment) fragmentManager .findFragmentByTag(NetworkFragment.TAG); if (networkFragment == null) { networkFragment = new NetworkFragment(); Bundle args = new Bundle(); args.putString(URL_KEY, url); networkFragment.setArguments(args); fragmentManager.beginTransaction().add(networkFragment, TAG).commit(); } return networkFragment; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Retain this Fragment across configuration changes in the host Activity. setRetainInstance(true); mUrlString = getArguments().getString(URL_KEY); } @Override public void onAttach(Context context) { super.onAttach(context); // Host Activity will handle callbacks from task. mCallback = (DownloadCallback)context; } @Override public void onDetach() { super.onDetach(); // Clear reference to host Activity. mCallback = null; } @Override public void onDestroy() { // Cancel task when Fragment is destroyed. cancelDownload(); super.onDestroy(); } /** * Start non-blocking execution of DownloadTask. */ public void startDownload() { cancelDownload(); mDownloadTask = new DownloadTask(); mDownloadTask.execute(mUrlString); } /** * Cancel (and interrupt if necessary) any ongoing DownloadTask execution. */ public void cancelDownload() { if (mDownloadTask != null) { mDownloadTask.cancel(true); mDownloadTask = null; } } /** * Implementation of AsyncTask that runs a network operation on a background thread. */ private class DownloadTask extends AsyncTask { /** * Wrapper class that serves as a union of a result value and an exception. When the * download task has completed, either the result value or exception can be a non-null * value. This allows you to pass exceptions to the UI thread that were thrown during * doInBackground(). */ class Result { public String mResultValue; public Exception mException; public Result(String resultValue) { mResultValue = resultValue; } public Result(Exception exception) { mException = exception; } } /** * Cancel background network operation if we do not have network connectivity. */ @Override protected void onPreExecute() { if (mCallback != null) { NetworkInfo networkInfo = mCallback.getActiveNetworkInfo(); if (networkInfo == null || !networkInfo.isConnected() || (networkInfo.getType() != ConnectivityManager.TYPE_WIFI && networkInfo.getType() != ConnectivityManager.TYPE_MOBILE)) { // If no connectivity, cancel task and update Callback with null data. mCallback.updateFromDownload(null); cancel(true); } } } /** * Defines work to perform on the background thread. */ @Override protected Result doInBackground(String... urls) { Result result = null; if (!isCancelled() && urls != null && urls.length > 0) { String urlString = urls[0]; try { URL url = new URL(urlString); String resultString = downloadUrl(url); if (resultString != null) { result = new Result(resultString); } else { throw new IOException("No response received."); } } catch(Exception e) { result = new Result(e); } } return result; } /** * Send DownloadCallback a progress update. */ @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); if (values.length >= 2) { mCallback.onProgressUpdate(values[0], values[1]); } } /** * Updates the DownloadCallback with the result. */ @Override protected void onPostExecute(Result result) { if (result != null && mCallback != null) { if (result.mException != null) { mCallback.updateFromDownload(result.mException.getMessage()); } else if (result.mResultValue != null) { mCallback.updateFromDownload(result.mResultValue); } mCallback.finishDownloading(); } } /** * Override to add special behavior for cancelled AsyncTask. */ @Override protected void onCancelled(Result result) { } /** * Given a URL, sets up a connection and gets the HTTP response body from the server. * If the network request is successful, it returns the response body in String form. Otherwise, * it will throw an IOException. */ private String downloadUrl(URL url) throws IOException { InputStream stream = null; HttpsURLConnection connection = null; String result = null; try { connection = (HttpsURLConnection) url.openConnection(); // Timeout for reading InputStream arbitrarily set to 3000ms. connection.setReadTimeout(3000); // Timeout for connection.connect() arbitrarily set to 3000ms. connection.setConnectTimeout(3000); // For this use case, set HTTP method to GET. connection.setRequestMethod("GET"); // Already true by default but setting just in case; needs to be true since this request // is carrying an input (response) body. connection.setDoInput(true); // Open communications link (network traffic occurs here). connection.connect(); publishProgress(DownloadCallback.Progress.CONNECT_SUCCESS); int responseCode = connection.getResponseCode(); if (responseCode != HttpsURLConnection.HTTP_OK) { throw new IOException("HTTP error code: " + responseCode); } // Retrieve the response body as an InputStream. stream = connection.getInputStream(); publishProgress(DownloadCallback.Progress.GET_INPUT_STREAM_SUCCESS, 0); if (stream != null) { // Converts Stream to String with max length of 500. result = readStream(stream, 500); publishProgress(DownloadCallback.Progress.PROCESS_INPUT_STREAM_SUCCESS, 0); } } finally { // Close Stream and disconnect HTTPS connection. if (stream != null) { stream.close(); } if (connection != null) { connection.disconnect(); } } return result; } /** * Converts the contents of an InputStream to a String. */ private String readStream(InputStream stream, int maxLength) throws IOException { String result = null; // Read InputStream using the UTF-8 charset. InputStreamReader reader = new InputStreamReader(stream, "UTF-8"); // Create temporary buffer to hold Stream data with specified max length. char[] buffer = new char[maxLength]; // Populate temporary buffer with Stream data. int numChars = 0; int readSize = 0; while (numChars < maxLength && readSize != -1) { numChars += readSize; int pct = (100 * numChars) / maxLength; publishProgress(DownloadCallback.Progress.PROCESS_INPUT_STREAM_IN_PROGRESS, pct); readSize = reader.read(buffer, numChars, buffer.length - numChars); } if (numChars != -1) { // The stream was not empty. // Create String that is actual length of response body if actual length was less than // max length. numChars = Math.min(numChars, maxLength); result = new String(buffer, 0, numChars); } return result; } } }