• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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