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