blob: cc41075a1924a3811eb9dfabe8329880aa8e5826 [file] [log] [blame]
/*
* 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<String, Integer, DownloadTask.Result> {
/**
* 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;
}
}
}