• 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.lite;
17 
18 import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
19 import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
20 
21 import android.content.Context;
22 import androidx.annotation.VisibleForTesting;
23 import androidx.core.app.NotificationCompat;
24 import androidx.core.app.NotificationManagerCompat;
25 import com.google.android.libraries.mobiledatadownload.DownloadException;
26 import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode;
27 import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader;
28 import com.google.android.libraries.mobiledatadownload.foreground.ForegroundDownloadKey;
29 import com.google.android.libraries.mobiledatadownload.foreground.NotificationUtil;
30 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil;
31 import com.google.android.libraries.mobiledatadownload.internal.util.DownloadFutureMap;
32 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture;
33 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures;
34 import com.google.common.base.Optional;
35 import com.google.common.base.Supplier;
36 import com.google.common.util.concurrent.FutureCallback;
37 import com.google.common.util.concurrent.ListenableFuture;
38 import com.google.common.util.concurrent.ListenableFutureTask;
39 import com.google.common.util.concurrent.MoreExecutors;
40 import java.util.concurrent.Executor;
41 import org.checkerframework.checker.nullness.compatqual.NullableDecl;
42 
43 final class DownloaderImpl implements Downloader {
44   private static final String TAG = "DownloaderImp";
45 
46   private final Context context;
47   private final Optional<Class<?>> foregroundDownloadServiceClassOptional;
48   // This executor will execute tasks sequentially.
49   private final Executor sequentialControlExecutor;
50   private final Optional<SingleFileDownloadProgressMonitor> downloadMonitorOptional;
51   private final Supplier<FileDownloader> fileDownloaderSupplier;
52 
53   @VisibleForTesting final DownloadFutureMap<Void> downloadFutureMap;
54   @VisibleForTesting final DownloadFutureMap<Void> foregroundDownloadFutureMap;
55 
DownloaderImpl( Context context, Optional<Class<?>> foregroundDownloadServiceClassOptional, Executor sequentialControlExecutor, Optional<SingleFileDownloadProgressMonitor> downloadMonitorOptional, Supplier<FileDownloader> fileDownloaderSupplier)56   DownloaderImpl(
57       Context context,
58       Optional<Class<?>> foregroundDownloadServiceClassOptional,
59       Executor sequentialControlExecutor,
60       Optional<SingleFileDownloadProgressMonitor> downloadMonitorOptional,
61       Supplier<FileDownloader> fileDownloaderSupplier) {
62     this.context = context;
63     this.sequentialControlExecutor = sequentialControlExecutor;
64     this.foregroundDownloadServiceClassOptional = foregroundDownloadServiceClassOptional;
65     this.downloadMonitorOptional = downloadMonitorOptional;
66     this.fileDownloaderSupplier = fileDownloaderSupplier;
67     this.downloadFutureMap = DownloadFutureMap.create(sequentialControlExecutor);
68     this.foregroundDownloadFutureMap =
69         DownloadFutureMap.create(
70             sequentialControlExecutor,
71             createCallbacksForForegroundService(context, foregroundDownloadServiceClassOptional));
72   }
73 
74   @Override
download(DownloadRequest downloadRequest)75   public ListenableFuture<Void> download(DownloadRequest downloadRequest) {
76     LogUtil.d("%s: download for Uri = %s", TAG, downloadRequest.destinationFileUri().toString());
77     ForegroundDownloadKey foregroundDownloadKey =
78         ForegroundDownloadKey.ofSingleFile(downloadRequest.destinationFileUri());
79 
80     return PropagatedFutures.transformAsync(
81         getInProgressDownloadFuture(foregroundDownloadKey.toString()),
82         (Optional<ListenableFuture<Void>> existingDownloadFuture) -> {
83           // if there is the same on-going request, return that one.
84           if (existingDownloadFuture.isPresent()) {
85             return existingDownloadFuture.get();
86           }
87 
88           // Register listener with monitor if present
89           if (downloadRequest.listenerOptional().isPresent()) {
90             if (downloadMonitorOptional.isPresent()) {
91               downloadMonitorOptional
92                   .get()
93                   .addDownloadListener(
94                       downloadRequest.destinationFileUri(),
95                       downloadRequest.listenerOptional().get());
96             } else {
97               LogUtil.w(
98                   "%s: download request included DownloadListener, but DownloadMonitor is not"
99                       + " present! DownloadListener will only be invoked for complete/failure.",
100                   TAG);
101             }
102           }
103 
104           // Create a ListenableFutureTask to delay starting the downloadFuture until we can add the
105           // future to our map.
106           ListenableFutureTask<Void> startTask = ListenableFutureTask.create(() -> null);
107           ListenableFuture<Void> downloadFuture =
108               PropagatedFutures.transformAsync(
109                   startTask, unused -> startDownload(downloadRequest), sequentialControlExecutor);
110 
111           PropagatedFutures.addCallback(
112               downloadFuture,
113               new FutureCallback<Void>() {
114                 @Override
115                 public void onSuccess(Void result) {
116                   // Currently the MobStore monitor does not support onSuccess so we have to add
117                   // callback to the download future here.
118 
119                   // Remove download listener and remove download future from map after listener
120                   // completes
121                   if (downloadRequest.listenerOptional().isPresent()) {
122                     PropagatedFutures.addCallback(
123                         downloadRequest.listenerOptional().get().onComplete(),
124                         new FutureCallback<Void>() {
125                           @Override
126                           public void onSuccess(@NullableDecl Void result) {
127                             if (downloadMonitorOptional.isPresent()) {
128                               downloadMonitorOptional
129                                   .get()
130                                   .removeDownloadListener(downloadRequest.destinationFileUri());
131                             }
132                             ListenableFuture<Void> unused =
133                                 downloadFutureMap.remove(foregroundDownloadKey.toString());
134                           }
135 
136                           @Override
137                           public void onFailure(Throwable t) {
138                             LogUtil.e(t, "%s: Failed to run client onComplete", TAG);
139                             if (downloadMonitorOptional.isPresent()) {
140                               downloadMonitorOptional
141                                   .get()
142                                   .removeDownloadListener(downloadRequest.destinationFileUri());
143                             }
144                             ListenableFuture<Void> unused =
145                                 downloadFutureMap.remove(foregroundDownloadKey.toString());
146                           }
147                         },
148                         sequentialControlExecutor);
149                   } else {
150                     ListenableFuture<Void> unused =
151                         downloadFutureMap.remove(foregroundDownloadKey.toString());
152                   }
153                 }
154 
155                 @Override
156                 public void onFailure(Throwable t) {
157                   LogUtil.e(t, "%s: Download Future failed", TAG);
158 
159                   // Currently the MobStore monitor does not support onFailure so we have to add
160                   // callback to the download future here.
161                   if (downloadRequest.listenerOptional().isPresent()) {
162                     downloadRequest.listenerOptional().get().onFailure(t);
163                     if (downloadMonitorOptional.isPresent()) {
164                       downloadMonitorOptional
165                           .get()
166                           .removeDownloadListener(downloadRequest.destinationFileUri());
167                     }
168                   }
169                   ListenableFuture<Void> unused =
170                       downloadFutureMap.remove(foregroundDownloadKey.toString());
171                 }
172               },
173               MoreExecutors.directExecutor());
174 
175           return PropagatedFutures.transformAsync(
176               downloadFutureMap.add(foregroundDownloadKey.toString(), downloadFuture),
177               unused -> {
178                 // Now that the download future is added, start the task and return the future
179                 startTask.run();
180                 return downloadFuture;
181               },
182               sequentialControlExecutor);
183         },
184         sequentialControlExecutor);
185   }
186 
187   private ListenableFuture<Void> startDownload(DownloadRequest downloadRequest) {
188     // Translate from MDDLite DownloadRequest to MDDDownloader DownloadRequest.
189     com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest
190         fileDownloaderRequest =
191             com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest.newBuilder()
192                 .setFileUri(downloadRequest.destinationFileUri())
193                 .setDownloadConstraints(downloadRequest.downloadConstraints())
194                 .setUrlToDownload(downloadRequest.urlToDownload())
195                 .setExtraHttpHeaders(downloadRequest.extraHttpHeaders())
196                 .setTrafficTag(downloadRequest.trafficTag())
197                 .build();
198     try {
199       return fileDownloaderSupplier.get().startDownloading(fileDownloaderRequest);
200     } catch (RuntimeException e) {
201       // Catch any unchecked exceptions that prevented the download from starting.
202       return immediateFailedFuture(
203           DownloadException.builder()
204               .setDownloadResultCode(DownloadResultCode.UNKNOWN_ERROR)
205               .setCause(e)
206               .build());
207     }
208   }
209 
210   @Override
211   public ListenableFuture<Void> downloadWithForegroundService(DownloadRequest downloadRequest) {
212     LogUtil.d(
213         "%s: downloadWithForegroundService for Uri = %s",
214         TAG, downloadRequest.destinationFileUri().toString());
215     if (!downloadMonitorOptional.isPresent()) {
216       return immediateFailedFuture(
217           new IllegalStateException(
218               "downloadWithForegroundService: DownloadMonitor is not provided!"));
219     }
220     if (!foregroundDownloadServiceClassOptional.isPresent()) {
221       return immediateFailedFuture(
222           new IllegalStateException(
223               "downloadWithForegroundService: ForegroundDownloadService is not provided!"));
224     }
225 
226     ForegroundDownloadKey foregroundDownloadKey =
227         ForegroundDownloadKey.ofSingleFile(downloadRequest.destinationFileUri());
228 
229     return PropagatedFutures.transformAsync(
230         getInProgressDownloadFuture(foregroundDownloadKey.toString()),
231         (Optional<ListenableFuture<Void>> existingDownloadFuture) -> {
232           // if there is the same on-going request, return that one.
233           if (existingDownloadFuture.isPresent()) {
234             return existingDownloadFuture.get();
235           }
236 
237           // It's OK to recreate the NotificationChannel since it can also be used to restore a
238           // deleted channel and to update an existing channel's name, description, group, and/or
239           // importance.
240           NotificationUtil.createNotificationChannel(context);
241 
242           DownloadListener downloadListenerWithNotification =
243               createDownloadListenerWithNotification(downloadRequest);
244 
245           // The downloadMonitor will trigger the DownloadListener.
246           downloadMonitorOptional
247               .get()
248               .addDownloadListener(
249                   downloadRequest.destinationFileUri(), downloadListenerWithNotification);
250 
251           // Create a ListenableFutureTask to delay starting the downloadFuture until we can add the
252           // future to our map.
253           ListenableFutureTask<Void> startTask = ListenableFutureTask.create(() -> null);
254           ListenableFuture<Void> downloadFuture =
255               PropagatedFutures.transformAsync(
256                   startTask, unused -> startDownload(downloadRequest), sequentialControlExecutor);
257 
258           PropagatedFutures.addCallback(
259               downloadFuture,
260               new FutureCallback<Void>() {
261                 @Override
262                 public void onSuccess(Void result) {
263                   // Currently the MobStore monitor does not support onSuccess so we have to add
264                   // callback to the download future here.
265 
266                   PropagatedFutures.addCallback(
267                       downloadListenerWithNotification.onComplete(),
268                       new FutureCallback<Void>() {
269                         @Override
270                         public void onSuccess(@NullableDecl Void result) {}
271 
272                         @Override
273                         public void onFailure(Throwable t) {
274                           LogUtil.e(t, "%s: Failed to run client onComplete", TAG);
275                         }
276                       },
277                       sequentialControlExecutor);
278                 }
279 
280                 @Override
281                 public void onFailure(Throwable t) {
282                   // Currently the MobStore monitor does not support onFailure so we have to add
283                   // callback to the download future here.
284                   LogUtil.e(t, "%s: Download Future failed", TAG);
285                   downloadListenerWithNotification.onFailure(t);
286                 }
287               },
288               MoreExecutors.directExecutor());
289 
290           return PropagatedFutures.transformAsync(
291               foregroundDownloadFutureMap.add(foregroundDownloadKey.toString(), downloadFuture),
292               unused -> {
293                 // Now that the download future is added, start the task and return the future
294                 startTask.run();
295                 return downloadFuture;
296               },
297               sequentialControlExecutor);
298         },
299         sequentialControlExecutor);
300   }
301 
302   // Assertion: foregroundDownloadService and downloadMonitor are present
303   private DownloadListener createDownloadListenerWithNotification(DownloadRequest downloadRequest) {
304     String networkPausedMessage =
305         downloadRequest.downloadConstraints().requireUnmeteredNetwork()
306             ? NotificationUtil.getDownloadPausedWifiMessage(context)
307             : NotificationUtil.getDownloadPausedMessage(context);
308 
309     NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
310     NotificationCompat.Builder notification =
311         NotificationUtil.createNotificationBuilder(
312             context,
313             downloadRequest.fileSizeBytes(),
314             downloadRequest.notificationContentTitle(),
315             downloadRequest.notificationContentTextOptional().or(downloadRequest.urlToDownload()));
316 
317     ForegroundDownloadKey foregroundDownloadKey =
318         ForegroundDownloadKey.ofSingleFile(downloadRequest.destinationFileUri());
319 
320     int notificationKey = NotificationUtil.notificationKeyForKey(foregroundDownloadKey.toString());
321 
322     // Attach the Cancel action to the notification.
323     NotificationUtil.createCancelAction(
324         context,
325         foregroundDownloadServiceClassOptional.get(),
326         foregroundDownloadKey.toString(),
327         notification,
328         notificationKey);
329     notificationManager.notify(notificationKey, notification.build());
330 
331     return new DownloadListener() {
332       @Override
333       public void onProgress(long currentSize) {
334         // TODO(b/229123693): return this future once DownloadListener has an async api.
335         ListenableFuture<?> unused =
336             PropagatedFutures.transformAsync(
337                 foregroundDownloadFutureMap.containsKey(foregroundDownloadKey.toString()),
338                 futureInProgress -> {
339                   if (futureInProgress) {
340                     notification
341                         .setCategory(NotificationCompat.CATEGORY_PROGRESS)
342                         .setContentText(
343                             downloadRequest
344                                 .notificationContentTextOptional()
345                                 .or(downloadRequest.urlToDownload()))
346                         .setSmallIcon(android.R.drawable.stat_sys_download)
347                         .setProgress(
348                             downloadRequest.fileSizeBytes(),
349                             (int) currentSize,
350                             /* indeterminate= */ downloadRequest.fileSizeBytes() <= 0);
351                     notificationManager.notify(notificationKey, notification.build());
352                   }
353                   if (downloadRequest.listenerOptional().isPresent()) {
354                     downloadRequest.listenerOptional().get().onProgress(currentSize);
355                   }
356                   return immediateVoidFuture();
357                 },
358                 sequentialControlExecutor);
359       }
360 
361       @Override
362       public void onPausedForConnectivity() {
363         // TODO(b/229123693): return this future once DownloadListener has an async api.
364         ListenableFuture<?> unused =
365             PropagatedFutures.transformAsync(
366                 foregroundDownloadFutureMap.containsKey(foregroundDownloadKey.toString()),
367                 futureInProgress -> {
368                   if (futureInProgress) {
369                     notification
370                         .setCategory(NotificationCompat.CATEGORY_STATUS)
371                         .setContentText(networkPausedMessage)
372                         .setSmallIcon(android.R.drawable.stat_sys_download)
373                         .setOngoing(true)
374                         // hide progress bar.
375                         .setProgress(0, 0, false);
376                     notificationManager.notify(notificationKey, notification.build());
377                   }
378                   if (downloadRequest.listenerOptional().isPresent()) {
379                     downloadRequest.listenerOptional().get().onPausedForConnectivity();
380                   }
381                   return immediateVoidFuture();
382                 },
383                 sequentialControlExecutor);
384       }
385 
386       @Override
387       public ListenableFuture<Void> onComplete() {
388         // We want to keep the Foreground Download Service alive until client's onComplete finishes.
389         ListenableFuture<Void> clientOnCompleteFuture =
390             downloadRequest.listenerOptional().isPresent()
391                 ? downloadRequest.listenerOptional().get().onComplete()
392                 : immediateVoidFuture();
393 
394         // Logic to shutdown Foreground Download Service after the client's provided onComplete
395         // finished
396         return PropagatedFluentFuture.from(clientOnCompleteFuture)
397             .transformAsync(
398                 unused -> {
399                   // onComplete succeeded, show a success message
400                   notification.mActions.clear();
401 
402                   if (downloadRequest.showDownloadedNotification()) {
403                     notification
404                         .setCategory(NotificationCompat.CATEGORY_STATUS)
405                         .setContentText(NotificationUtil.getDownloadSuccessMessage(context))
406                         .setOngoing(false)
407                         .setSmallIcon(android.R.drawable.stat_sys_download_done)
408                         // hide progress bar.
409                         .setProgress(0, 0, false);
410 
411                     notificationManager.notify(notificationKey, notification.build());
412                   } else {
413                     NotificationUtil.cancelNotificationForKey(
414                         context, foregroundDownloadKey.toString());
415                   }
416                   return immediateVoidFuture();
417                 },
418                 sequentialControlExecutor)
419             .catchingAsync(
420                 Exception.class,
421                 e -> {
422                   LogUtil.w(
423                       e,
424                       "%s: Delegate onComplete failed for uri: %s, showing failure notification.",
425                       TAG,
426                       downloadRequest.destinationFileUri());
427                   notification.mActions.clear();
428 
429                   if (downloadRequest.showDownloadedNotification()) {
430                     notification
431                         .setCategory(NotificationCompat.CATEGORY_STATUS)
432                         .setContentText(NotificationUtil.getDownloadFailedMessage(context))
433                         .setOngoing(false)
434                         .setSmallIcon(android.R.drawable.stat_sys_warning)
435                         // hide progress bar.
436                         .setProgress(0, 0, false);
437 
438                     notificationManager.notify(notificationKey, notification.build());
439                   } else {
440                     NotificationUtil.cancelNotificationForKey(
441                         context, downloadRequest.destinationFileUri().toString());
442                   }
443 
444                   return immediateVoidFuture();
445                 },
446                 sequentialControlExecutor)
447             .transformAsync(
448                 unused -> {
449                   // After success or failure notification is shown, clean up
450                   downloadMonitorOptional
451                       .get()
452                       .removeDownloadListener(downloadRequest.destinationFileUri());
453 
454                   return foregroundDownloadFutureMap.remove(foregroundDownloadKey.toString());
455                 },
456                 sequentialControlExecutor);
457       }
458 
459       @Override
460       public void onFailure(Throwable t) {
461         // TODO(b/229123693): return this future once DownloadListener has an async api.
462         ListenableFuture<?> unused =
463             PropagatedFutures.submitAsync(
464                 () -> {
465                   // Clear the notification action.
466                   notification.mActions.clear();
467 
468                   // Show download failed in notification.
469                   notification
470                       .setCategory(NotificationCompat.CATEGORY_STATUS)
471                       .setContentText(NotificationUtil.getDownloadFailedMessage(context))
472                       .setOngoing(false)
473                       .setSmallIcon(android.R.drawable.stat_sys_warning)
474                       // hide progress bar.
475                       .setProgress(0, 0, false);
476 
477                   notificationManager.notify(notificationKey, notification.build());
478 
479                   if (downloadRequest.listenerOptional().isPresent()) {
480                     downloadRequest.listenerOptional().get().onFailure(t);
481                   }
482                   downloadMonitorOptional
483                       .get()
484                       .removeDownloadListener(downloadRequest.destinationFileUri());
485 
486                   return foregroundDownloadFutureMap.remove(foregroundDownloadKey.toString());
487                 },
488                 sequentialControlExecutor);
489       }
490     };
491   }
492 
493   @Override
494   public void cancelForegroundDownload(String downloadKey) {
495     LogUtil.d("%s: CancelForegroundDownload for Uri = %s", TAG, downloadKey);
496     ListenableFuture<?> unused =
497         PropagatedFutures.transformAsync(
498             getInProgressDownloadFuture(downloadKey),
499             downloadFuture -> {
500               if (downloadFuture.isPresent()) {
501                 LogUtil.v(
502                     "%s: CancelForegroundDownload future found for key = %s, cancelling...",
503                     TAG, downloadKey);
504                 downloadFuture.get().cancel(false);
505               }
506               return immediateVoidFuture();
507             },
508             sequentialControlExecutor);
509   }
510 
511   private ListenableFuture<Optional<ListenableFuture<Void>>> getInProgressDownloadFuture(
512       String key) {
513     return PropagatedFutures.transformAsync(
514         foregroundDownloadFutureMap.containsKey(key),
515         isInForeground ->
516             isInForeground ? foregroundDownloadFutureMap.get(key) : downloadFutureMap.get(key),
517         sequentialControlExecutor);
518   }
519 
520   private static DownloadFutureMap.StateChangeCallbacks createCallbacksForForegroundService(
521       Context context, Optional<Class<?>> foregroundDownloadServiceClassOptional) {
522     return new DownloadFutureMap.StateChangeCallbacks() {
523       @Override
524       public void onAdd(String key, int newSize) {
525         // Only start foreground service if this is the first future we are adding.
526         if (newSize == 1 && foregroundDownloadServiceClassOptional.isPresent()) {
527           NotificationUtil.startForegroundDownloadService(
528               context, foregroundDownloadServiceClassOptional.get(), key);
529         }
530       }
531 
532       @Override
533       public void onRemove(String key, int newSize) {
534         // Only stop foreground service if there are no more futures remaining.
535         if (newSize == 0 && foregroundDownloadServiceClassOptional.isPresent()) {
536           NotificationUtil.stopForegroundDownloadService(
537               context, foregroundDownloadServiceClassOptional.get(), key);
538         }
539       }
540     };
541   }
542 }
543