• 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 android.net.Uri;
19 import androidx.annotation.VisibleForTesting;
20 import com.google.android.libraries.mobiledatadownload.DownloadListener;
21 import com.google.android.libraries.mobiledatadownload.TimeSource;
22 import com.google.android.libraries.mobiledatadownload.file.monitors.ByteCountingOutputMonitor;
23 import com.google.android.libraries.mobiledatadownload.file.spi.Monitor;
24 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil;
25 import com.google.android.libraries.mobiledatadownload.lite.SingleFileDownloadProgressMonitor;
26 import com.google.common.util.concurrent.MoreExecutors;
27 import com.google.errorprone.annotations.concurrent.GuardedBy;
28 import java.util.HashMap;
29 import java.util.concurrent.Executor;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.atomic.AtomicLong;
32 import javax.annotation.Nullable;
33 import javax.annotation.concurrent.ThreadSafe;
34 
35 /**
36  * A Download Progress Monitor to support {@link DownloadListener}.
37  *
38  * <p>Before monitoring an Uri, one needs to call monitorUri to record the file group that Uri
39  * belongs to at the downloading time.
40  *
41  * <p>Currently we only support 1 DownloadListener per File Group.
42  */
43 @ThreadSafe
44 public class DownloadProgressMonitor implements Monitor, SingleFileDownloadProgressMonitor {
45 
46   private static final String TAG = "DownloadProgressMonitor";
47 
48   private final TimeSource timeSource;
49   private final Executor sequentialControlExecutor;
50   private final com.google.android.libraries.mobiledatadownload.lite.DownloadProgressMonitor
51       liteDownloadProgressMonitor;
52 
DownloadProgressMonitor(TimeSource timeSource, Executor controlExecutor)53   public DownloadProgressMonitor(TimeSource timeSource, Executor controlExecutor) {
54     this.timeSource = timeSource;
55 
56     // We want onProgress to be executed in order otherwise clients will observe out of order
57     // updates (bigger current size update appears before smaller current size update).
58     // We use Sequential Executor to ensure the onProgress will be processed sequentially.
59     this.sequentialControlExecutor = MoreExecutors.newSequentialExecutor(controlExecutor);
60 
61     // Construct internal instance of MDD Lite's DownloadProgressMonitor. methods of
62     // SingleFileDownloadProgressMonitor will delegate to this instance.
63     this.liteDownloadProgressMonitor =
64         com.google.android.libraries.mobiledatadownload.lite.DownloadProgressMonitor.create(
65             this.timeSource, controlExecutor);
66   }
67 
68   // We will only broadcast on progress notification at most once in this time frame.
69   // Currently MobStore Monitor notify every 8KB of downloaded bytes. This may be too chatty on
70   // fast network.
71   // 1 second was chosen arbitrarily.
72   @VisibleForTesting static final long LOG_FREQUENCY = 1000L;
73 
74   // MobStore Monitor works at file level. We want to monitor at FileGroup level. This map will
75   // help to map from a fileUri to a file group.
76   @GuardedBy("DownloadProgressMonitor.class")
77   private final HashMap<Uri, String> uriToFileGroup = new HashMap<>();
78 
79   @GuardedBy("DownloadProgressMonitor.class")
80   private final HashMap<String, ByteCountingOutputMonitor> fileGroupToByteCountingOutputMonitor =
81       new HashMap<>();
82 
83   @Override
84   @Nullable
monitorRead(Uri uri)85   public Monitor.InputMonitor monitorRead(Uri uri) {
86     return null;
87   }
88 
89   @Override
90   @Nullable
monitorWrite(Uri uri)91   public Monitor.OutputMonitor monitorWrite(Uri uri) {
92     synchronized (DownloadProgressMonitor.class) {
93       String groupName = uriToFileGroup.get(uri);
94       if (groupName == null) {
95         // MobStore will call all monitors for all files it handles.
96         // In this case, we receive a call from MobStore for an Uri that we did not register for.
97         //
98         // This may be for a single file download, so delegate to liteDownloadProgressMonitor. This
99         // will check its internal map and return null if not found.
100         return liteDownloadProgressMonitor.monitorWrite(uri);
101       }
102 
103       if (fileGroupToByteCountingOutputMonitor.get(groupName) == null) {
104         // This is a real error. We register for this Uri but does ot have a counter for it.
105         LogUtil.e("%s: Can't find file group for uri: %s", TAG, uri);
106         return null;
107       }
108       return fileGroupToByteCountingOutputMonitor.get(groupName);
109     }
110   }
111 
112   @Override
113   @Nullable
monitorAppend(Uri uri)114   public Monitor.OutputMonitor monitorAppend(Uri uri) {
115     return monitorWrite(uri);
116   }
117 
pausedForConnectivity()118   public void pausedForConnectivity() {
119     synchronized (DownloadProgressMonitor.class) {
120       for (ByteCountingOutputMonitor byteCountingOutputMonitor :
121           fileGroupToByteCountingOutputMonitor.values()) {
122         DownloadedBytesCounter counter =
123             (DownloadedBytesCounter) byteCountingOutputMonitor.getCounter();
124         counter.pausedForConnectivity();
125       }
126 
127       // Delegate to liteDownloadProgressMonitor as well so single file downloads can be updated
128       liteDownloadProgressMonitor.pausedForConnectivity();
129     }
130   }
131 
132   /**
133    * Add a File Group DownloadListener that client can use to receive download progress update.
134    *
135    * <p>Currently we only support 1 listener per file group. Calling addDownloadListener to add
136    * another listener would be no-op.
137    *
138    * @param groupName The groupName that the DownloadListener will receive download progress update.
139    * @param downloadListener the DownloadListener to add.
140    */
addDownloadListener(String groupName, DownloadListener downloadListener)141   public void addDownloadListener(String groupName, DownloadListener downloadListener) {
142     synchronized (DownloadProgressMonitor.class) {
143       if (!fileGroupToByteCountingOutputMonitor.containsKey(groupName)) {
144         fileGroupToByteCountingOutputMonitor.put(
145             groupName,
146             new ByteCountingOutputMonitor(
147                 new DownloadedBytesCounter(groupName, downloadListener),
148                 timeSource::currentTimeMillis,
149                 LOG_FREQUENCY,
150                 TimeUnit.MILLISECONDS));
151       }
152     }
153   }
154 
155   /**
156    * Add a Single File DownloadListener.
157    *
158    * <p>This listener allows clients to receive on progress updates for single file downloads.
159    *
160    * @param uri the uri for which the DownloadListener should receive updates
161    * @param downloadListener the MDD Lite DownloadListener to add
162    */
163   @Override
addDownloadListener( Uri uri, com.google.android.libraries.mobiledatadownload.lite.DownloadListener downloadListener)164   public void addDownloadListener(
165       Uri uri,
166       com.google.android.libraries.mobiledatadownload.lite.DownloadListener downloadListener) {
167     liteDownloadProgressMonitor.addDownloadListener(uri, downloadListener);
168   }
169 
170   /**
171    * Remove a File Group DownloadListener.
172    *
173    * @param groupName The groupName that the DownloadListener receive download progress update.
174    */
removeDownloadListener(String groupName)175   public void removeDownloadListener(String groupName) {
176     synchronized (DownloadProgressMonitor.class) {
177       fileGroupToByteCountingOutputMonitor.remove(groupName);
178     }
179   }
180 
181   /**
182    * Remove a Single File DownloadListener.
183    *
184    * @param uri the uri which should be cleared of any registered DownloadListener
185    */
186   @Override
removeDownloadListener(Uri uri)187   public void removeDownloadListener(Uri uri) {
188     liteDownloadProgressMonitor.removeDownloadListener(uri);
189   }
190 
191   /**
192    * Record that the Uri belong to the FileGroup represented by groupName. We need to record this at
193    * downloading time since the files in FileGroup could change due to config change. monitorUri
194    * should only be called for files that have not been downloaded.
195    *
196    * @param uri the FileUri to be monitored.
197    * @param groupName the group name to be monitored.
198    */
monitorUri(Uri uri, String groupName)199   public void monitorUri(Uri uri, String groupName) {
200     synchronized (DownloadProgressMonitor.class) {
201       uriToFileGroup.put(uri, groupName);
202     }
203   }
204 
205   /**
206    * Add the current size of a downloaded or partially downloaded file to a file group counter.
207    *
208    * @param groupName the group name to add the current size.
209    * @param currentSize the current size of the file.
210    */
notifyCurrentFileSize(String groupName, long currentSize)211   public void notifyCurrentFileSize(String groupName, long currentSize) {
212     synchronized (DownloadProgressMonitor.class) {
213       // Update the counter with the current size.
214       if (fileGroupToByteCountingOutputMonitor.containsKey(groupName)) {
215         fileGroupToByteCountingOutputMonitor
216             .get(groupName)
217             .getCounter()
218             .bufferCounter((int) currentSize);
219       }
220     }
221   }
222 
223   // A counter for bytes downloaded.
224   private final class DownloadedBytesCounter implements ByteCountingOutputMonitor.Counter {
225     private final String groupName;
226     private final DownloadListener downloadListener;
227 
228     private final AtomicLong byteCounter = new AtomicLong();
229 
DownloadedBytesCounter(String groupName, DownloadListener downloadListener)230     DownloadedBytesCounter(String groupName, DownloadListener downloadListener) {
231       this.groupName = groupName;
232       this.downloadListener = downloadListener;
233     }
234 
235     @Override
bufferCounter(int len)236     public void bufferCounter(int len) {
237       byteCounter.getAndAdd(len);
238       LogUtil.v(
239           "%s: Received data for groupName = %s, len = %d, Counter = %d",
240           TAG, groupName, len, byteCounter.get());
241     }
242 
243     @Override
flushCounter()244     public void flushCounter() {
245       // Check if the DownloadListener is still being used before calling its onProgress.
246       synchronized (DownloadProgressMonitor.class) {
247         if (fileGroupToByteCountingOutputMonitor.containsKey(groupName)) {
248           sequentialControlExecutor.execute(() -> downloadListener.onProgress(byteCounter.get()));
249         }
250       }
251     }
252 
pausedForConnectivity()253     public void pausedForConnectivity() {
254       downloadListener.pausedForConnectivity();
255     }
256   }
257 }
258