26
26
import android .os .Build .VERSION ;
27
27
import android .os .Build .VERSION_CODES ;
28
28
import android .os .ParcelFileDescriptor ;
29
+ import android .util .Log ;
29
30
import android .util .LongSparseArray ;
30
31
import androidx .annotation .GuardedBy ;
31
32
import androidx .annotation .NonNull ;
57
58
*/
58
59
public class ModelFileDownloadService {
59
60
61
+ private static final String TAG = "ModelFileDownloadSer" ;
62
+ private static final int COMPLETION_BUFFER_IN_MS = 60 * 5 * 1000 ;
63
+
60
64
private final DownloadManager downloadManager ;
61
65
private final Context context ;
62
66
private final ModelFileManager fileManager ;
@@ -125,10 +129,30 @@ public Task<Void> download(
125
129
126
130
@ VisibleForTesting
127
131
Task <Void > ensureModelDownloaded (CustomModel customModel ) {
128
- // todo check model not already in progress of being downloaded
129
-
130
- // todo remove any failed download attempts
131
-
132
+ // todo add logging for explicitly requested
133
+ // check model download already in progress
134
+ CustomModel downloadingModel =
135
+ sharedPreferencesUtil .getDownloadingCustomModelDetails (customModel .getName ());
136
+ if (downloadingModel != null && downloadingModel .getDownloadId () != 0 ) {
137
+ Integer statusCode = getDownloadingModelStatusCode (downloadingModel .getDownloadId ());
138
+ Date now = new Date ();
139
+
140
+ // check if download has completed or still has time to finish.
141
+ // Give a buffer above url expiry to continue if in progress.
142
+ if (statusCode != null
143
+ && (statusCode == DownloadManager .STATUS_SUCCESSFUL
144
+ || statusCode == DownloadManager .STATUS_FAILED
145
+ || (customModel .getDownloadUrlExpiry ()
146
+ > (now .getTime () - COMPLETION_BUFFER_IN_MS )))) {
147
+ // download in progress for this hash already - return this result.
148
+ return registerReceiverForDownloadId (
149
+ downloadingModel .getDownloadId (), customModel .getName ());
150
+ }
151
+ }
152
+ if (downloadingModel != null ) {
153
+ // remove failed download attempts
154
+ removeOrCancelDownload (downloadingModel .getName (), downloadingModel .getDownloadId ());
155
+ }
132
156
// schedule new download of model file
133
157
Long newDownloadId = scheduleModelDownload (customModel );
134
158
if (newDownloadId == null ) {
@@ -138,18 +162,30 @@ Task<Void> ensureModelDownloaded(CustomModel customModel) {
138
162
return registerReceiverForDownloadId (newDownloadId , customModel .getName ());
139
163
}
140
164
165
+ /** Removes or cancels the downloading model if exists. */
166
+ synchronized void removeOrCancelDownload (String modelName , Long downloadId ) {
167
+ if (downloadManager != null && downloadId != 0 ) {
168
+ downloadManager .remove (downloadId );
169
+ }
170
+ sharedPreferencesUtil .setFailedUploadedCustomModelDetails (modelName );
171
+ }
172
+
141
173
private synchronized DownloadBroadcastReceiver getReceiverInstance (
142
174
long downloadId , String modelName ) {
143
- DownloadBroadcastReceiver receiver = receiverMaps .get (downloadId );
175
+ DownloadBroadcastReceiver receiver = this . receiverMaps .get (downloadId );
144
176
if (receiver == null ) {
145
177
receiver =
146
178
new DownloadBroadcastReceiver (
147
179
downloadId , modelName , getTaskCompletionSourceInstance (downloadId ));
148
- receiverMaps .put (downloadId , receiver );
180
+ this . receiverMaps .put (downloadId , receiver );
149
181
}
150
182
return receiver ;
151
183
}
152
184
185
+ private synchronized void removeReceiverInstance (long downloadId ) {
186
+ this .receiverMaps .remove (downloadId );
187
+ }
188
+
153
189
private Task <Void > registerReceiverForDownloadId (long downloadId , String modelName ) {
154
190
BroadcastReceiver broadcastReceiver = getReceiverInstance (downloadId , modelName );
155
191
// It is okay to always register here. Since the broadcast receiver is the same via the lookup
@@ -162,15 +198,25 @@ private Task<Void> registerReceiverForDownloadId(long downloadId, String modelNa
162
198
163
199
@ VisibleForTesting
164
200
synchronized TaskCompletionSource <Void > getTaskCompletionSourceInstance (long downloadId ) {
165
- TaskCompletionSource <Void > taskCompletionSource = taskCompletionSourceMaps .get (downloadId );
201
+ TaskCompletionSource <Void > taskCompletionSource = this . taskCompletionSourceMaps .get (downloadId );
166
202
if (taskCompletionSource == null ) {
167
203
taskCompletionSource = new TaskCompletionSource <>();
168
- taskCompletionSourceMaps .put (downloadId , taskCompletionSource );
204
+ this . taskCompletionSourceMaps .put (downloadId , taskCompletionSource );
169
205
}
170
206
171
207
return taskCompletionSource ;
172
208
}
173
209
210
+ @ VisibleForTesting
211
+ synchronized boolean existTaskCompletionSourceInstance (long downloadId ) {
212
+ TaskCompletionSource <Void > taskCompletionSource = this .taskCompletionSourceMaps .get (downloadId );
213
+ return (taskCompletionSource != null );
214
+ }
215
+
216
+ private synchronized void removeTaskCompletionSourceInstance (long downloadId ) {
217
+ this .taskCompletionSourceMaps .remove (downloadId );
218
+ }
219
+
174
220
@ VisibleForTesting
175
221
synchronized Long scheduleModelDownload (@ NonNull CustomModel customModel ) {
176
222
if (downloadManager == null ) {
@@ -253,8 +299,8 @@ private synchronized ParcelFileDescriptor getDownloadedFile(Long downloadingId)
253
299
try {
254
300
fileDescriptor = downloadManager .openDownloadedFile (downloadingId );
255
301
} catch (FileNotFoundException e ) {
256
- // todo(annz)
257
- System . out . println ( "Downloaded file is not found" );
302
+ // todo replace with FirebaseMlException
303
+ Log . d ( TAG , "Downloaded file is not found: " + e );
258
304
}
259
305
return fileDescriptor ;
260
306
}
@@ -286,12 +332,18 @@ public File loadNewlyDownloadedModelFile(CustomModel model) throws FirebaseMlExc
286
332
String downloadingModelHash = model .getModelHash ();
287
333
288
334
if (downloadingId == 0 || downloadingModelHash .isEmpty ()) {
289
- // no downloading model file or incomplete info.
335
+ // Clear the downloading info completely.
336
+ // It handles the case: developer clear the app cache but downloaded model file in
337
+ // DownloadManager's cache would not be cleared.
338
+ removeOrCancelDownload (model .getName (), model .getDownloadId ());
290
339
return null ;
291
340
}
292
341
293
342
Integer statusCode = getDownloadingModelStatusCode (downloadingId );
294
343
if (statusCode == null ) {
344
+ Log .d (TAG , "Download failed - no download status available." );
345
+ // No status code, it may mean no such download or no download manager.
346
+ removeOrCancelDownload (model .getName (), model .getDownloadId ());
295
347
return null ;
296
348
}
297
349
@@ -301,16 +353,21 @@ public File loadNewlyDownloadedModelFile(CustomModel model) throws FirebaseMlExc
301
353
if (fileDescriptor == null ) {
302
354
// reset original model - removing download id.
303
355
sharedPreferencesUtil .setFailedUploadedCustomModelDetails (model .getName ());
304
- // todo call the download register?
356
+ removeOrCancelDownload (model .getName (), model .getDownloadId ());
357
+ // todo log this?
305
358
return null ;
306
359
}
307
360
308
361
// Try to move it to destination folder.
309
- File newModelFile = fileManager .moveModelToDestinationFolder (model , fileDescriptor );
362
+ File newModelFile ;
363
+ try {
364
+ // TODO add logging
365
+ newModelFile = fileManager .moveModelToDestinationFolder (model , fileDescriptor );
366
+ } finally {
367
+ removeOrCancelDownload (model .getName (), model .getDownloadId ());
368
+ }
310
369
311
370
if (newModelFile == null ) {
312
- // reset original model - removing download id.
313
- // todo call the download register?
314
371
sharedPreferencesUtil .setFailedUploadedCustomModelDetails (model .getName ());
315
372
return null ;
316
373
}
@@ -323,9 +380,9 @@ public File loadNewlyDownloadedModelFile(CustomModel model) throws FirebaseMlExc
323
380
// todo(annzimmer) Cleans up the old files if it is the initial creation.
324
381
return newModelFile ;
325
382
} else if (statusCode == DownloadManager .STATUS_FAILED ) {
326
- // reset original model - removing download id .
383
+ // reset original model - removing downloading details .
327
384
sharedPreferencesUtil .setFailedUploadedCustomModelDetails (model .getName ());
328
- // todo - determine if the temp files need to be clean up? Does one exist?
385
+ removeOrCancelDownload ( model . getName (), model . getDownloadId ());
329
386
}
330
387
// Other cases, return as null and wait for download finish.
331
388
return null ;
@@ -398,6 +455,13 @@ public void onReceive(Context context, Intent intent) {
398
455
}
399
456
400
457
Integer statusCode = getDownloadingModelStatusCode (downloadId );
458
+ // check to prevent DuplicateTaskCompletionException - this was already updated and removed.
459
+ // Just return.
460
+ if (!existTaskCompletionSourceInstance (downloadId )) {
461
+ removeReceiverInstance (downloadId );
462
+ return ;
463
+ }
464
+
401
465
synchronized (ModelFileDownloadService .this ) {
402
466
try {
403
467
context .getApplicationContext ().unregisterReceiver (this );
@@ -409,31 +473,45 @@ public void onReceive(Context context, Intent intent) {
409
473
// move on.
410
474
}
411
475
412
- receiverMaps . remove (downloadId );
413
- taskCompletionSourceMaps . remove (downloadId );
476
+ removeReceiverInstance (downloadId );
477
+ removeTaskCompletionSourceInstance (downloadId );
414
478
}
415
479
416
480
if (statusCode != null ) {
417
481
if (statusCode == DownloadManager .STATUS_FAILED ) {
418
- eventLogger .logDownloadFailureWithReason (
419
- sharedPreferencesUtil .getDownloadingCustomModelDetails (modelName ),
420
- false ,
421
- getFailureReason (id ));
422
- if (checkErrorCausedByExpiry (id , modelName )) {
423
- // retry as a new download
424
- // todo change to FirebaseMlException retry error.
425
- taskCompletionSource .setException (new Exception ("Retry: Expired URL" ));
426
- return ;
482
+ int failureReason = getFailureReason (id );
483
+ CustomModel downloadingModel =
484
+ sharedPreferencesUtil .getDownloadingCustomModelDetails (modelName );
485
+ if (downloadingModel != null ) {
486
+ eventLogger .logDownloadFailureWithReason (downloadingModel , false , failureReason );
487
+ if (checkErrorCausedByExpiry (downloadingModel .getDownloadUrlExpiry (), failureReason )) {
488
+ // retry as a new download
489
+ // todo change to FirebaseMlException retry error - or whatever we decide is
490
+ // appropriate.
491
+ taskCompletionSource .setException (new Exception ("Retry: Expired URL" ));
492
+ return ;
493
+ }
427
494
}
428
495
taskCompletionSource .setException (getExceptionAccordingToDownloadManager (id ));
429
496
return ;
430
497
}
431
498
432
499
if (statusCode == DownloadManager .STATUS_SUCCESSFUL ) {
500
+ CustomModel model = sharedPreferencesUtil .getDownloadingCustomModelDetails (modelName );
501
+ if (model == null ) {
502
+ // model might have been updated already get the downloaded model.
503
+ model = sharedPreferencesUtil .getCustomModelDetails (modelName );
504
+ if (model == null ) {
505
+ // todo add logging here.
506
+ taskCompletionSource .setException (
507
+ new FirebaseMlException (
508
+ "No model associated with name: " + modelName ,
509
+ FirebaseMlException .INVALID_ARGUMENT ));
510
+ return ;
511
+ }
512
+ }
433
513
eventLogger .logDownloadEventWithExactDownloadTime (
434
- sharedPreferencesUtil .getDownloadingCustomModelDetails (modelName ),
435
- ErrorCode .NO_ERROR ,
436
- DownloadStatus .SUCCEEDED );
514
+ model , ErrorCode .NO_ERROR , DownloadStatus .SUCCEEDED );
437
515
taskCompletionSource .setResult (null );
438
516
return ;
439
517
}
@@ -443,25 +521,11 @@ public void onReceive(Context context, Intent intent) {
443
521
taskCompletionSource .setException (new Exception ("Model downloading failed" ));
444
522
}
445
523
446
- private boolean checkErrorCausedByExpiry (Long downloadId , String modelName ) {
447
- CustomModel model = sharedPreferencesUtil .getCustomModelDetails (modelName );
448
-
449
- if (model == null ) {
450
- return false ;
451
- }
452
-
524
+ private boolean checkErrorCausedByExpiry (long downloadUrlExpiry , int failureReason ) {
453
525
final Date time = new Date ();
454
526
455
- if (model .getDownloadUrlExpiry () < time .getTime ()) {
456
- Cursor cursor =
457
- (downloadManager == null || downloadId == null )
458
- ? null
459
- : downloadManager .query (new Query ().setFilterById (downloadId ));
460
- if (cursor != null && cursor .moveToFirst ()) {
461
- int reason = cursor .getInt (cursor .getColumnIndex (DownloadManager .COLUMN_REASON ));
462
- // 400 implies possibility of url expiry
463
- return (reason == 400 );
464
- }
527
+ if (failureReason == 400 && downloadUrlExpiry < time .getTime ()) {
528
+ return true ;
465
529
}
466
530
return false ;
467
531
}
0 commit comments