1 // Copyright 2021 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 package com.google.android.downloader;
16 
17 import static android.Manifest.permission.ACCESS_NETWORK_STATE;
18 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
19 import static androidx.core.content.ContextCompat.checkSelfPermission;
20 import static androidx.core.content.ContextCompat.getSystemService;
21 import static com.google.common.base.Preconditions.checkNotNull;
22 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
23 import static java.util.concurrent.TimeUnit.MILLISECONDS;
24 
25 import android.annotation.SuppressLint;
26 import android.annotation.TargetApi;
27 import android.content.BroadcastReceiver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.pm.PackageManager;
32 import android.net.ConnectivityManager;
33 import android.net.Network;
34 import android.net.NetworkCapabilities;
35 import android.net.NetworkInfo;
36 import android.os.Build.VERSION;
37 import android.os.Build.VERSION_CODES;
38 import androidx.annotation.RequiresPermission;
39 import androidx.core.net.ConnectivityManagerCompat;
40 import com.google.android.downloader.DownloadConstraints.NetworkType;
41 import com.google.common.annotations.VisibleForTesting;
42 import com.google.common.flogger.GoogleLogger;
43 import com.google.common.util.concurrent.Futures;
44 import com.google.common.util.concurrent.ListenableFuture;
45 import com.google.common.util.concurrent.ListenableFutureTask;
46 import java.util.concurrent.ScheduledExecutorService;
47 import java.util.concurrent.TimeoutException;
48 import javax.annotation.Nullable;
49 
50 /**
51  * Default implementation of {@link ConnectivityHandler}, relying on Android's {@link
52  * ConnectivityManager}.
53  */
54 public class AndroidConnectivityHandler implements ConnectivityHandler {
55   private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
56 
57   private final Context context;
58   private final ScheduledExecutorService scheduledExecutorService;
59   private final ConnectivityManager connectivityManager;
60   private final long timeoutMillis;
61 
62   /**
63    * Creates a new AndroidConnectivityHandler to handle connectivity checks for the Downloader.
64    *
65    * @param context the context to use for perform Android API checks. Will be retained, so should
66    *     not be a UI context.
67    * @param scheduledExecutorService a scheduled executor used to timeout operations waiting for
68    *     connectivity. Beware that there are problems with this, see go/executors-timing for
69    *     details.
70    * @param timeoutMillis how long to wait before timing out a connectivity check. If more than this
71    *     amount of time elapses, the connectivity check will timeout, and the {@link
72    *     ListenableFuture} returned by {@link #checkConnectivity} will resolve with a {@link
73    *     TimeoutException}.
74    */
AndroidConnectivityHandler( Context context, ScheduledExecutorService scheduledExecutorService, long timeoutMillis)75   public AndroidConnectivityHandler(
76       Context context, ScheduledExecutorService scheduledExecutorService, long timeoutMillis) {
77     if (PackageManager.PERMISSION_GRANTED != checkSelfPermission(context, ACCESS_NETWORK_STATE)) {
78       throw new IllegalStateException(
79           "AndroidConnectivityHandler requires the ACCESS_NETWORK_STATE permission.");
80     }
81 
82     this.context = context;
83     this.scheduledExecutorService = scheduledExecutorService;
84     this.connectivityManager = checkNotNull(getSystemService(context, ConnectivityManager.class));
85     this.timeoutMillis = timeoutMillis;
86   }
87 
88   @Override
89   @RequiresPermission(ACCESS_NETWORK_STATE)
checkConnectivity(DownloadConstraints constraints)90   public ListenableFuture<Void> checkConnectivity(DownloadConstraints constraints) {
91     if (connectivitySatisfied(constraints)) {
92       return Futures.immediateVoidFuture();
93     }
94 
95     ListenableFutureTask<Void> futureTask = ListenableFutureTask.create(() -> null);
96     // TODO: Using a receiver here isn't great. Ideally we'd use
97     // ConnectivityManager.requestNetwork(request, callback, timeout), but that's only available
98     // on SDK 26+, so we'd still need a fallback on older versions of Android.
99     NetworkBroadcastReceiver receiver = new NetworkBroadcastReceiver(constraints, futureTask);
100     context.registerReceiver(receiver, new IntentFilter(CONNECTIVITY_ACTION));
101     futureTask.addListener(() -> context.unregisterReceiver(receiver), directExecutor());
102     return Futures.withTimeout(futureTask, timeoutMillis, MILLISECONDS, scheduledExecutorService);
103   }
104 
105   @RequiresPermission(ACCESS_NETWORK_STATE)
connectivitySatisfied(DownloadConstraints downloadConstraints)106   private boolean connectivitySatisfied(DownloadConstraints downloadConstraints) {
107     // Special case the NONE value - if that is specified then skip all further checks.
108     if (downloadConstraints == DownloadConstraints.NONE) {
109       return true;
110     }
111 
112     NetworkType networkType;
113 
114     if (VERSION.SDK_INT >= VERSION_CODES.M) {
115       Network network = connectivityManager.getActiveNetwork();
116       if (network == null) {
117         logger.atFine().log("No current network, connectivity cannot be satisfied.");
118         return false;
119       }
120 
121       NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
122       if (networkCapabilities == null) {
123         logger.atFine().log(
124             "Can't determine network capabilities, connectivity cannot be satisfied");
125         return false;
126       }
127 
128       if (!networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
129         logger.atFine().log(
130             "Network does not have internet capabilities, connectivity cannot be satisfied.");
131         return false;
132       }
133 
134       if (downloadConstraints.requireUnmeteredNetwork()
135           && ConnectivityManagerCompat.isActiveNetworkMetered(connectivityManager)) {
136         logger.atFine().log("Network is metered, connectivity cannot be satisfied.");
137         return false;
138       }
139 
140       if (downloadConstraints.requiredNetworkTypes().contains(NetworkType.ANY)) {
141         // If the request doesn't care about the network type (by way of having NetworkType.ANY in
142         // its set of allowed network types), then stop checking now.
143         return true;
144       }
145 
146       networkType = computeNetworkType(networkCapabilities);
147     } else {
148       @SuppressLint("MissingPermission") // We just checked the permission above.
149       NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
150       if (networkInfo == null) {
151         logger.atFine().log("No current network, connectivity cannot be satisfied.");
152         return false;
153       }
154 
155       if (!networkInfo.isConnected()) {
156         // Regardless of which type of network we have right now, if it's not connected then all
157         // downloads will fail, so just queue up all downloads in this case.
158         logger.atFine().log("Network disconnected, connectivity cannot be satisfied.");
159         return false;
160       }
161 
162       if (downloadConstraints.requireUnmeteredNetwork()
163           && ConnectivityManagerCompat.isActiveNetworkMetered(connectivityManager)) {
164         logger.atFine().log("Network is metered, connectivity cannot be satisfied.");
165         return false;
166       }
167 
168       if (downloadConstraints.requiredNetworkTypes().contains(NetworkType.ANY)) {
169         // If the request doesn't care about the network type (by way of having NetworkType.ANY in
170         // its set of allowed network types), then stop checking now.
171         return true;
172       }
173 
174       networkType = computeNetworkType(networkInfo.getType());
175     }
176 
177     if (networkType == null) {
178       // If for some reason we couldn't determine the network type from Android (unexpected value?),
179       // then we can't validate it against the set of constraints, so fail the check.
180       return false;
181     }
182 
183     // Otherwise, just make sure that the current network type is allowed by this request.
184     return downloadConstraints.requiredNetworkTypes().contains(networkType);
185   }
186 
187   @Nullable
computeNetworkType(int networkType)188   private static NetworkType computeNetworkType(int networkType) {
189     if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR2
190         && networkType == ConnectivityManager.TYPE_BLUETOOTH) {
191       return NetworkType.BLUETOOTH;
192     } else if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR2
193         && networkType == ConnectivityManager.TYPE_ETHERNET) {
194       return NetworkType.ETHERNET;
195     } else if (networkType == ConnectivityManager.TYPE_MOBILE
196         || networkType == ConnectivityManager.TYPE_MOBILE_MMS
197         || networkType == ConnectivityManager.TYPE_MOBILE_SUPL
198         || networkType == ConnectivityManager.TYPE_MOBILE_DUN
199         || networkType == ConnectivityManager.TYPE_MOBILE_HIPRI) {
200       return NetworkType.CELLULAR;
201     } else if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP
202         && networkType == ConnectivityManager.TYPE_VPN) {
203       // There's no way to determine the underlying transport used by a VPN, so it's best to
204       // be conservative and treat it is a cellular network.
205       return NetworkType.CELLULAR;
206     } else if (networkType == ConnectivityManager.TYPE_WIFI) {
207       return NetworkType.WIFI;
208     } else if (networkType == ConnectivityManager.TYPE_WIMAX) {
209       // WiMAX and Cellular aren't really the same thing, but in practice they can be treated
210       // the same, as they are both typically available over long distances and are often metered.
211       return NetworkType.CELLULAR;
212     }
213 
214     return null;
215   }
216 
217   @Nullable
218   @TargetApi(VERSION_CODES.LOLLIPOP)
computeNetworkType(NetworkCapabilities networkCapabilities)219   private static NetworkType computeNetworkType(NetworkCapabilities networkCapabilities) {
220     if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
221       return NetworkType.CELLULAR;
222     } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
223       return NetworkType.WIFI;
224     } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) {
225       return NetworkType.BLUETOOTH;
226     } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
227       return NetworkType.ETHERNET;
228     } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
229       // There's no way to determine the underlying transport used by a VPN, so it's best to
230       // be conservative and treat it is a cellular network.
231       return NetworkType.CELLULAR;
232     }
233     return null;
234   }
235 
236   @VisibleForTesting
237   class NetworkBroadcastReceiver extends BroadcastReceiver {
238     private final DownloadConstraints constraints;
239     private final Runnable completionRunnable;
240 
NetworkBroadcastReceiver(DownloadConstraints constraints, Runnable completionRunnable)241     public NetworkBroadcastReceiver(DownloadConstraints constraints, Runnable completionRunnable) {
242       this.constraints = constraints;
243       this.completionRunnable = completionRunnable;
244     }
245 
246     @Override
247     @RequiresPermission(ACCESS_NETWORK_STATE)
onReceive(Context context, Intent intent)248     public void onReceive(Context context, Intent intent) {
249       if (!CONNECTIVITY_ACTION.equals(intent.getAction())) {
250         logger.atSevere().log(
251             "NetworkBroadcastReceiver received an unexpected intent action: %s",
252             intent.getAction());
253         return;
254       }
255 
256       if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) {
257         logger.atInfo().log("NetworkBroadcastReceiver updated but NO_CONNECTIVITY extra set");
258         return;
259       }
260 
261       logger.atInfo().log(
262           "NetworkBroadcastReceiver received intent: %s %s",
263           intent.getAction(), intent.getExtras());
264 
265       if (connectivitySatisfied(constraints)) {
266         logger.atInfo().log("Connectivity satisfied in BroadcastReceiver, running completion");
267         completionRunnable.run();
268       } else {
269         logger.atInfo().log("Connectivity NOT satisfied in BroadcastReceiver");
270       }
271     }
272   }
273 }
274