/*
This file is part of Reactive Cascade which is released under The MIT License.
See license.md , https://github.com/futurice/cascade and http://reactivecascade.com for details.
This is open source for the common good. Please contribute improvements by pull request or contact paulirotta@gmail.com
*/
package com.reactivecascade.util;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.net.NetworkInfo;
import android.net.wifi.SupplicantState;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresPermission;
import android.support.annotation.WorkerThread;
import android.telephony.TelephonyManager;
import com.reactivecascade.functional.RunnableAltFuture;
import com.reactivecascade.functional.SettableAltFuture;
import com.reactivecascade.i.IAltFuture;
import com.reactivecascade.i.IGettable;
import com.reactivecascade.i.IThreadType;
import java.io.IOException;
import java.util.Collection;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.internal.framed.Header;
import static android.telephony.TelephonyManager.NETWORK_TYPE_1xRTT;
import static android.telephony.TelephonyManager.NETWORK_TYPE_CDMA;
import static android.telephony.TelephonyManager.NETWORK_TYPE_EDGE;
import static android.telephony.TelephonyManager.NETWORK_TYPE_EHRPD;
import static android.telephony.TelephonyManager.NETWORK_TYPE_EVDO_0;
import static android.telephony.TelephonyManager.NETWORK_TYPE_EVDO_A;
import static android.telephony.TelephonyManager.NETWORK_TYPE_EVDO_B;
import static android.telephony.TelephonyManager.NETWORK_TYPE_GPRS;
import static android.telephony.TelephonyManager.NETWORK_TYPE_HSDPA;
import static android.telephony.TelephonyManager.NETWORK_TYPE_HSPA;
import static android.telephony.TelephonyManager.NETWORK_TYPE_HSPAP;
import static android.telephony.TelephonyManager.NETWORK_TYPE_HSUPA;
import static android.telephony.TelephonyManager.NETWORK_TYPE_IDEN;
import static android.telephony.TelephonyManager.NETWORK_TYPE_LTE;
import static android.telephony.TelephonyManager.NETWORK_TYPE_UMTS;
import static android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN;
import static com.reactivecascade.Async.NET_READ;
import static com.reactivecascade.Async.NET_WRITE;
/**
* OkHttp convenience wrapper methods
*/
public final class NetUtil extends Origin {
public enum NetType {NET_2G, NET_2_5G, NET_3G, NET_3_5G, NET_4G, NET_5G}
private static final int MAX_NUMBER_OF_WIFI_NET_CONNECTIONS = 6;
private static final int MAX_NUMBER_OF_3G_NET_CONNECTIONS = 4;
private static final int MAX_NUMBER_OF_2G_NET_CONNECTIONS = 2;
@NonNull
private final OkHttpClient mOkHttpClient;
@NonNull
private final TelephonyManager mTelephonyManager;
@NonNull
private final WifiManager mWifiManager;
@NonNull
private final IThreadType mNetReadThreadType;
@NonNull
private final IThreadType mNetWriteThreadType;
@RequiresPermission(allOf = {
Manifest.permission.INTERNET,
Manifest.permission.ACCESS_NETWORK_STATE,
Manifest.permission.ACCESS_WIFI_STATE})
public NetUtil(@NonNull final Context context) {
this(context, NET_READ, NET_WRITE);
}
public NetUtil(@NonNull Context context,
@NonNull IThreadType netReadThreadType,
@NonNull IThreadType netWriteThreadType) {
this.mNetReadThreadType = netReadThreadType;
this.mNetWriteThreadType = netWriteThreadType;
mOkHttpClient = new OkHttpClient();
mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
mWifiManager = (WifiManager) context.getSystemService(Activity.WIFI_SERVICE);
}
@NonNull
@WorkerThread
public <T> Response get(@NonNull T url) throws IOException {
return get(url, null);
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<?, Response> getAsync(@NonNull final T url) {
return new RunnableAltFuture<>(mNetReadThreadType,
() -> {
return get(url, null);
});
}
/**
* Take the output from the previous step in the chain as the URL.
* <p>
* Note that the input object has {@link T#toString()} is called to generate the URL at the last
* possible moment before the network connection is opened. This may be useful to delay the decision
* of which URL will be used, for example to decide what is the highest priority use of the network
* at that moment.
*
* @param <T> the output of the upchain step
* @return alt future of the network response
*/
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<T, Response> getAsync() {
return new RunnableAltFuture<>(mNetReadThreadType,
(T url) ->
get(url.toString(), null));
}
/**
* @param urlGettable
* @param <T>
* @return
* @throws IOException if problem with network
* @throws IllegalStateException if <code>urlGettable</code> can not yet be determined.
* Consider using <code>urlGettable.then()</code> instead.
*/
@NonNull
@WorkerThread
public <T> Response get(@NonNull IGettable<T> urlGettable) throws IOException {
return get(urlGettable.get().toString());
}
@NonNull
@WorkerThread
public <T> Response get(@NonNull T url,
@Nullable Collection<Header> headers) throws IOException {
if (headers == null) {
RCLog.d(getOrigin(), "get " + url);
} else {
RCLog.d(getOrigin(), "get " + url + " with " + headers.size() + " custom headers");
}
return execute(setupCall(url,
builder -> {
addHeaders(builder, headers);
}));
}
/**
* @param urlGettable
* @param headers
* @param <T>
* @return
* @throws IOException
* @throws IllegalStateException if <code>urlGettable</code> can not be determined. Consider using
* <code>urlGettable.then(headers)</code> instead.
*/
@NonNull
@WorkerThread
public <T> Response get(@NonNull IGettable<T> urlGettable,
@Nullable Collection<Header> headers) throws IOException {
return get(urlGettable.get(), headers);
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<T, Response> getAsync(
@Nullable final Collection<Header> headers) {
return new RunnableAltFuture<>(mNetReadThreadType,
(T url) -> get(url.toString(), headers));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
@SuppressWarnings("unchecked")
public <T> IAltFuture<T, Response> getAsync(
@NonNull final T url,
final IGettable<Collection<Header>> headersGettable) {
if (headersGettable instanceof IAltFuture) {
return ((IAltFuture<?, T>) headersGettable)
.on(mNetReadThreadType)
.then(() -> get(url, headersGettable.get()));
}
return new RunnableAltFuture<>(mNetReadThreadType, () ->
get(url, headersGettable.get()));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<T, Response> getAsync(
@NonNull final T url,
@Nullable final Collection<Header> headers) {
return new RunnableAltFuture<>(mNetReadThreadType, () ->
get(url, headers));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
@SuppressWarnings("unchecked")
public <T> IAltFuture<T, Response> getAsync(@NonNull IGettable<T> urlGettable,
@NonNull IGettable<Collection<Header>> headersGettable) {
IAltFuture<Response, Response> chainedAltFuture = new SettableAltFuture<>(mNetReadThreadType);
boolean chained = false;
if (urlGettable instanceof IAltFuture) {
chainedAltFuture = chainedAltFuture.await((IAltFuture<?, Response>) urlGettable);
chained = true;
}
if (headersGettable instanceof IAltFuture) {
chainedAltFuture = chainedAltFuture
.await((IAltFuture<?, Collection<Header>>) headersGettable);
chained = true;
}
//TODO Compound chaining
// if (chained) {
// final IAltFuture<Response, Response> tailOfChain = chainedAltFuture.then(() ->
// get(urlGettable.get(), headersGettable.get()));
//
//// FIXME We need to return the chain as a single entity, or does forking fire up this alternate chain and back down?
// return new CompoundAltFuture<>(headOfChain, tailOfChain);
// }
return new RunnableAltFuture<>(mNetReadThreadType,
() -> get(urlGettable.get(),
headersGettable.get()));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<T, Response> getAsync(
@NonNull IGettable<Collection<Header>> headersGettable) {
return new RunnableAltFuture<>(mNetReadThreadType,
(T url) -> get(url, headersGettable.get()));
}
@NonNull
@WorkerThread
public <T> Response put(@NonNull T url,
@NonNull RequestBody body) throws IOException {
return put(url, null, body);
}
@NonNull
@WorkerThread
public <T> Response put(@NonNull T url,
@Nullable Collection<Header> headers,
@NonNull RequestBody body) throws IOException {
RCLog.d(getOrigin(), "put " + url + " headers=" + headers + " body=" + body);
return execute(setupCall(url,
builder -> {
addHeaders(builder, headers);
builder.put(body);
}));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<?, Response> putAsync(@NonNull T url,
@NonNull RequestBody body) {
return new RunnableAltFuture<>(mNetWriteThreadType,
() -> put(url, body));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<RequestBody, Response> putAsync(@NonNull T url) {
return new RunnableAltFuture<>(mNetWriteThreadType,
(RequestBody body) -> put(url, body));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<T, Response> putAsync(@NonNull RequestBody body) {
return new RunnableAltFuture<>(mNetWriteThreadType,
(T url) -> put(url, body));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<?, Response> putAsync(@NonNull T url,
@Nullable Collection<Header> headers,
@NonNull RequestBody body) {
return new RunnableAltFuture<>(mNetWriteThreadType,
() -> put(url, headers, body));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<T, Response> putAsync(@Nullable Collection<Header> headers,
@NonNull RequestBody body) {
return new RunnableAltFuture<>(mNetWriteThreadType, (T url) ->
put(url, headers, body));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<RequestBody, Response> putAsync(@NonNull T url,
@Nullable Collection<Header> headers) {
return new RunnableAltFuture<>(mNetWriteThreadType, (RequestBody body) ->
put(url, headers, body));
}
@NonNull
@WorkerThread
public <T> Response post(@NonNull T url,
@NonNull RequestBody body) throws IOException {
return post(url, null, body);
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<?, Response> postAsync(@NonNull T url,
@NonNull RequestBody body) {
return new RunnableAltFuture<>(mNetWriteThreadType,
() -> post(url, null, body));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<T, Response> postAsync(@NonNull RequestBody body) {
return new RunnableAltFuture<>(mNetWriteThreadType,
(T url) -> post(url, null, body));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<RequestBody, Response> postAsync(@NonNull T url) {
return new RunnableAltFuture<>(mNetWriteThreadType,
(RequestBody body) -> post(url, null, body));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<?, Response> postAsync(@NonNull T url,
@Nullable Collection<Header> headers,
@NonNull RequestBody body) {
return new RunnableAltFuture<>(mNetWriteThreadType,
() -> post(url, headers, body));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<T, Response> postAsync(@Nullable Collection<Header> headers,
@NonNull RequestBody body) {
return new RunnableAltFuture<>(mNetWriteThreadType,
(T url) -> post(url, headers, body));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<RequestBody, Response> postAsync(@NonNull T url,
@Nullable Collection<Header> headers) {
return new RunnableAltFuture<>(mNetWriteThreadType, (RequestBody body) ->
post(url, headers, body));
}
@NonNull
@WorkerThread
public <T> Response post(@NonNull T url,
@Nullable Collection<Header> headers,
@NonNull RequestBody body) throws IOException {
RCLog.d(getOrigin(), "post " + url);
final Call call = setupCall(url,
builder -> {
addHeaders(builder, headers);
builder.post(body);
});
return execute(call);
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<?, Response> deleteAsync(@NonNull T url) {
return new RunnableAltFuture<>(mNetWriteThreadType, () ->
delete(url, null));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<T, Response> deleteAsync() {
return new RunnableAltFuture<>(mNetWriteThreadType,
(T url) -> delete(url, null));
}
@NonNull
@WorkerThread
public <T> Response delete(@NonNull T url) throws IOException {
return delete(url, null);
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<?, Response> deleteAsync(@NonNull final T url,
@Nullable final Collection<Header> headers) {
return new RunnableAltFuture<>(mNetWriteThreadType,
() -> delete(url, headers));
}
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <T> IAltFuture<T, Response> deleteAsync(@Nullable Collection<Header> headers) {
return new RunnableAltFuture<>(mNetWriteThreadType, (T url) ->
delete(url, headers));
}
@NonNull
@WorkerThread
public <T> Response delete(@NonNull T url,
@Nullable Collection<Header> headers) throws IOException {
RCLog.d(getOrigin(), "delete " + url);
final Call call = setupCall(url, builder -> {
addHeaders(builder, headers);
builder.delete();
});
return execute(call);
}
private void addHeaders(@NonNull Request.Builder builder,
@Nullable Collection<Header> headers) {
if (headers == null) {
return;
}
for (final Header header : headers) {
builder.addHeader(header.name.utf8(), header.value.utf8());
}
}
@NonNull
private <T> Call setupCall(@NonNull T url,
@Nullable BuilderModifier builderModifier) throws IOException {
final Request.Builder builder = new Request.Builder().url(url.toString());
if (builderModifier != null) {
builderModifier.modify(builder);
}
return mOkHttpClient.newCall(builder.build());
}
/**
* Complete the okhttp Call action synchronously on the current thread.
* <p>
* We are explicitly using our own threading model for debuggability and concurrency management
* reasons rather than delegating that to the library
*
* @param call
* @return
* @throws IOException
*/
@NonNull
@WorkerThread
private Response execute(@NonNull Call call) throws IOException {
final Response response = call.execute();
if (response.isRedirect()) {
final String location = response.headers().get("Location");
RCLog.d(getOrigin(), "Following HTTP redirect to " + location);
return get(location);
}
if (!response.isSuccessful()) {
String s = "Unexpected response code " + response;
IOException e = new IOException(s);
RCLog.e(getOrigin(), s, e);
throw e;
}
return response;
}
//TODO Use max number of NET connections on startup split adapt as these change
@RequiresPermission(android.Manifest.permission.ACCESS_WIFI_STATE)
public int getMaxNumberOfNetConnections() {
if (isWifi()) {
return MAX_NUMBER_OF_WIFI_NET_CONNECTIONS;
}
switch (getNetworkType()) {
case NET_2G:
case NET_2_5G:
return MAX_NUMBER_OF_2G_NET_CONNECTIONS;
case NET_3G:
case NET_3_5G:
case NET_4G:
default:
return MAX_NUMBER_OF_3G_NET_CONNECTIONS;
}
}
/**
* Check if a current network WIFI connection is CONNECTED
*
* @return
*/
@RequiresPermission(android.Manifest.permission.ACCESS_WIFI_STATE)
public boolean isWifi() {
SupplicantState s = mWifiManager.getConnectionInfo().getSupplicantState();
NetworkInfo.DetailedState state = WifiInfo.getDetailedStateOf(s);
return state == NetworkInfo.DetailedState.CONNECTED || state == NetworkInfo.DetailedState.OBTAINING_IPADDR;
}
@NonNull
@RequiresPermission(android.Manifest.permission.ACCESS_WIFI_STATE)
public NetType getNetworkType() {
switch (mTelephonyManager.getNetworkType()) {
case NETWORK_TYPE_UNKNOWN:
case NETWORK_TYPE_CDMA:
case NETWORK_TYPE_GPRS:
case NETWORK_TYPE_IDEN:
return NetType.NET_2G;
case NETWORK_TYPE_EDGE:
return NetType.NET_2_5G;
case NETWORK_TYPE_UMTS:
case NETWORK_TYPE_1xRTT:
return NetType.NET_3G;
case NETWORK_TYPE_EHRPD:
case NETWORK_TYPE_EVDO_0:
case NETWORK_TYPE_EVDO_A:
case NETWORK_TYPE_EVDO_B:
case NETWORK_TYPE_HSPA:
case NETWORK_TYPE_HSPAP:
case NETWORK_TYPE_HSUPA:
case NETWORK_TYPE_HSDPA:
return NetType.NET_3_5G;
case NETWORK_TYPE_LTE:
default:
return NetType.NET_4G;
}
}
/**
* Functional interface to do something to an OkHttp request builder before dispatch
*/
private interface BuilderModifier {
void modify(Request.Builder builder);
}
}