1 /* 2 * Copyright 2022 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.libraries.mobiledatadownload.monitor; 17 18 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 19 import static java.util.concurrent.TimeUnit.SECONDS; 20 21 import android.content.Context; 22 import android.net.ConnectivityManager; 23 import android.net.NetworkInfo; 24 import android.net.Uri; 25 import android.os.Build; 26 import androidx.annotation.GuardedBy; 27 import androidx.annotation.VisibleForTesting; 28 import com.google.android.libraries.mobiledatadownload.TimeSource; 29 import com.google.android.libraries.mobiledatadownload.file.monitors.ByteCountingOutputMonitor; 30 import com.google.android.libraries.mobiledatadownload.file.spi.Monitor; 31 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; 32 import com.google.android.libraries.mobiledatadownload.internal.logging.LoggingStateStore; 33 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; 34 import com.google.common.util.concurrent.FutureCallback; 35 import com.google.common.util.concurrent.ListenableFuture; 36 import com.google.mobiledatadownload.internal.MetadataProto.FileGroupLoggingState; 37 import com.google.mobiledatadownload.internal.MetadataProto.GroupKey; 38 import java.util.HashMap; 39 import java.util.concurrent.atomic.AtomicLong; 40 import javax.annotation.Nullable; 41 42 /** 43 * A network usage monitor that counts bytes downloaded for each FileGroup. Before monitoring an 44 * Uri, one needs to call monitorUri to record the file group that Uri belongs to at the downloading 45 * time. Failing to do so will result in no network usage being counted. 46 */ 47 public class NetworkUsageMonitor implements Monitor { 48 private static final String TAG = "NetworkUsageMonitor"; 49 50 // We will only flush counters to SharedPreference at most once in this time frame. 51 // 10 seconds were chosen arbitrarily. 52 @VisibleForTesting static final long LOG_FREQUENCY_SECONDS = 10L; 53 54 private final TimeSource timeSource; 55 private final Context context; 56 57 private final Object lock = new Object(); 58 59 // Key is FileGroupLoggingState with GroupKey, build id, version number populated. 60 @GuardedBy("lock") 61 private final HashMap<FileGroupLoggingState, ByteCountingOutputMonitor> 62 fileGroupLoggingStateToOutputMonitor = new HashMap<>(); 63 64 @GuardedBy("lock") 65 private final HashMap<Uri, ByteCountingOutputMonitor> uriToOutputMonitor = new HashMap<>(); 66 NetworkUsageMonitor(Context context, TimeSource timeSource)67 public NetworkUsageMonitor(Context context, TimeSource timeSource) { 68 this.context = context; 69 this.timeSource = timeSource; 70 } 71 72 @Override 73 @Nullable monitorRead(Uri uri)74 public Monitor.InputMonitor monitorRead(Uri uri) { 75 return null; 76 } 77 78 @Override 79 @Nullable monitorWrite(Uri uri)80 public Monitor.OutputMonitor monitorWrite(Uri uri) { 81 synchronized (lock) { 82 return uriToOutputMonitor.get(uri); 83 } 84 } 85 86 @Override 87 @Nullable monitorAppend(Uri uri)88 public Monitor.OutputMonitor monitorAppend(Uri uri) { 89 return monitorWrite(uri); 90 } 91 92 /** 93 * Record that the Uri belong to the FileGroup represented by ownerPackage and groupName. We need 94 * to record this at downloading time since the files in FileGroup could change due to config 95 * change. 96 * 97 * @param uri The Uri of the data file. 98 * @param groupKey The groupKey part of the file group. 99 * @param buildId The build id of the file group. 100 * @param variantId The variant id of the file group. 101 * @param versionNumber The version number of the file group. 102 * @param loggingStateStore The storage for the network usage logs 103 */ monitorUri( Uri uri, GroupKey groupKey, long buildId, String variantId, int versionNumber, LoggingStateStore loggingStateStore)104 public void monitorUri( 105 Uri uri, 106 GroupKey groupKey, 107 long buildId, 108 String variantId, 109 int versionNumber, 110 LoggingStateStore loggingStateStore) { 111 FileGroupLoggingState fileGroupLoggingStateKey = 112 FileGroupLoggingState.newBuilder() 113 .setGroupKey(groupKey) 114 .setBuildId(buildId) 115 .setVariantId(variantId) 116 .setFileGroupVersionNumber(versionNumber) 117 .build(); 118 119 // If we haven't seen this file group, create a output monitor for it and register it to this 120 // file group key. 121 synchronized (lock) { 122 if (!fileGroupLoggingStateToOutputMonitor.containsKey(fileGroupLoggingStateKey)) { 123 fileGroupLoggingStateToOutputMonitor.put( 124 fileGroupLoggingStateKey, 125 new ByteCountingOutputMonitor( 126 new DownloadedBytesCounter(context, loggingStateStore, fileGroupLoggingStateKey), 127 timeSource::currentTimeMillis, 128 LOG_FREQUENCY_SECONDS, 129 SECONDS)); 130 } 131 132 // Register the mapping from this uri to the output monitor we created. 133 // NOTE: It's possible the URI is associated with another monitor here (e.g. if a 134 // uri is shared by file groups), but we don't have a way to dedupe that at the moment, so we 135 // just overwrite it. 136 uriToOutputMonitor.put( 137 uri, fileGroupLoggingStateToOutputMonitor.get(fileGroupLoggingStateKey)); 138 } 139 } 140 141 // A counter for bytes downloaded on wifi and cellular. 142 // It will keep in-memory counters to reduce the write to SharedPreference. 143 // When updating the in-memory counters, it will only save and reset them if the time since the 144 // last save is at least LOG_FREQUENCY. 145 private static final class DownloadedBytesCounter implements ByteCountingOutputMonitor.Counter { 146 private final Context context; 147 private final LoggingStateStore loggingStateStore; 148 private final FileGroupLoggingState fileGroupLoggingStateKey; 149 150 // In-memory counters before saving to SharedPreference. 151 private final AtomicLong wifiCounter = new AtomicLong(); 152 private final AtomicLong cellularCounter = new AtomicLong(); 153 DownloadedBytesCounter( Context context, LoggingStateStore loggingStateStore, FileGroupLoggingState fileGroupLoggingStateKey)154 DownloadedBytesCounter( 155 Context context, 156 LoggingStateStore loggingStateStore, 157 FileGroupLoggingState fileGroupLoggingStateKey) { 158 this.context = context; 159 this.loggingStateStore = loggingStateStore; 160 this.fileGroupLoggingStateKey = fileGroupLoggingStateKey; 161 } 162 163 @Override bufferCounter(int len)164 public void bufferCounter(int len) { 165 166 boolean isCellular = isCellular(context); 167 168 if (isCellular) { 169 cellularCounter.getAndAdd(len); 170 } else { 171 wifiCounter.getAndAdd(len); 172 } 173 174 LogUtil.v( 175 "%s: Received data (%s) for fileGroup = %s, len = %d, wifiCounter = %d," 176 + " cellularCounter = %d", 177 TAG, 178 isCellular ? "cellular" : "wifi", 179 fileGroupLoggingStateKey.getGroupKey().getGroupName(), 180 len, 181 wifiCounter.get(), 182 cellularCounter.get()); 183 } 184 185 @Override flushCounter()186 public void flushCounter() { 187 ListenableFuture<Void> incrementDataUsage = 188 loggingStateStore.incrementDataUsage( 189 fileGroupLoggingStateKey.toBuilder() 190 .setCellularUsage(cellularCounter.getAndSet(0)) 191 .setWifiUsage(wifiCounter.getAndSet(0)) 192 .build()); 193 194 PropagatedFutures.addCallback( 195 incrementDataUsage, 196 new FutureCallback<Void>() { 197 @Override 198 public void onSuccess(Void unused) { 199 LogUtil.d( 200 "%s: Successfully incremented LoggingStateStore network usage for %s", 201 TAG, fileGroupLoggingStateKey.getGroupKey().getGroupName()); 202 } 203 204 @Override 205 public void onFailure(Throwable t) { 206 LogUtil.e( 207 t, 208 "%s: Unable to increment LoggingStateStore network usage for %s", 209 TAG, 210 fileGroupLoggingStateKey.getGroupKey().getGroupName()); 211 } 212 }, 213 directExecutor()); 214 } 215 } 216 217 @Nullable getConnectivityManager(Context context)218 public static ConnectivityManager getConnectivityManager(Context context) { 219 try { 220 return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 221 } catch (SecurityException e) { 222 LogUtil.e("%s: Couldn't retrieve ConnectivityManager.", TAG); 223 } 224 return null; 225 } 226 227 @Nullable getActiveNetworkInfo(Context context)228 public static NetworkInfo getActiveNetworkInfo(Context context) { 229 ConnectivityManager cm = getConnectivityManager(context); 230 return (cm == null) ? null : cm.getActiveNetworkInfo(); 231 } 232 233 /** 234 * Returns true if the current network connectivity type is cellular. If the network connectivity 235 * type is not cellular or if there is an error in getting NetworkInfo return false. 236 * 237 * <p>The logic here is similar to OffroadDownloader. 238 */ 239 @VisibleForTesting isCellular(Context context)240 public static boolean isCellular(Context context) { 241 NetworkInfo networkInfo = getActiveNetworkInfo(context); 242 if (networkInfo == null) { 243 LogUtil.e("%s: Fail to get network type ", TAG); 244 // We return false when we fail to get NetworkInfo. 245 return false; 246 } 247 248 if ((networkInfo.getType() == ConnectivityManager.TYPE_WIFI 249 || networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET 250 || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP 251 && networkInfo.getType() == ConnectivityManager.TYPE_VPN))) { 252 return false; 253 } else { 254 return true; 255 } 256 } 257 } 258