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