• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
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 
17 package android.app;
18 
19 import android.annotation.Nullable;
20 import android.annotation.SdkConstant;
21 import android.annotation.SdkConstant.SdkConstantType;
22 import android.annotation.SystemApi;
23 import android.annotation.SystemService;
24 import android.annotation.TestApi;
25 import android.annotation.UnsupportedAppUsage;
26 import android.content.ContentProviderClient;
27 import android.content.ContentResolver;
28 import android.content.ContentUris;
29 import android.content.ContentValues;
30 import android.content.Context;
31 import android.database.Cursor;
32 import android.database.CursorWrapper;
33 import android.database.DatabaseUtils;
34 import android.net.ConnectivityManager;
35 import android.net.NetworkPolicyManager;
36 import android.net.Uri;
37 import android.os.Build;
38 import android.os.Bundle;
39 import android.os.Environment;
40 import android.os.FileUtils;
41 import android.os.ParcelFileDescriptor;
42 import android.os.RemoteException;
43 import android.provider.Downloads;
44 import android.provider.MediaStore;
45 import android.provider.Settings;
46 import android.provider.Settings.SettingNotFoundException;
47 import android.text.TextUtils;
48 import android.util.Pair;
49 
50 import java.io.File;
51 import java.io.FileNotFoundException;
52 import java.util.ArrayList;
53 import java.util.List;
54 
55 /**
56  * The download manager is a system service that handles long-running HTTP downloads. Clients may
57  * request that a URI be downloaded to a particular destination file. The download manager will
58  * conduct the download in the background, taking care of HTTP interactions and retrying downloads
59  * after failures or across connectivity changes and system reboots.
60  * <p>
61  * Apps that request downloads through this API should register a broadcast receiver for
62  * {@link #ACTION_NOTIFICATION_CLICKED} to appropriately handle when the user clicks on a running
63  * download in a notification or from the downloads UI.
64  * <p>
65  * Note that the application must have the {@link android.Manifest.permission#INTERNET}
66  * permission to use this class.
67  */
68 @SystemService(Context.DOWNLOAD_SERVICE)
69 public class DownloadManager {
70 
71     /**
72      * An identifier for a particular download, unique across the system.  Clients use this ID to
73      * make subsequent calls related to the download.
74      */
75     public final static String COLUMN_ID = Downloads.Impl._ID;
76 
77     /**
78      * The client-supplied title for this download.  This will be displayed in system notifications.
79      * Defaults to the empty string.
80      */
81     public final static String COLUMN_TITLE = Downloads.Impl.COLUMN_TITLE;
82 
83     /**
84      * The client-supplied description of this download.  This will be displayed in system
85      * notifications.  Defaults to the empty string.
86      */
87     public final static String COLUMN_DESCRIPTION = Downloads.Impl.COLUMN_DESCRIPTION;
88 
89     /**
90      * URI to be downloaded.
91      */
92     public final static String COLUMN_URI = Downloads.Impl.COLUMN_URI;
93 
94     /**
95      * Internet Media Type of the downloaded file.  If no value is provided upon creation, this will
96      * initially be null and will be filled in based on the server's response once the download has
97      * started.
98      *
99      * @see <a href="http://www.ietf.org/rfc/rfc1590.txt">RFC 1590, defining Media Types</a>
100      */
101     public final static String COLUMN_MEDIA_TYPE = "media_type";
102 
103     /**
104      * Total size of the download in bytes.  This will initially be -1 and will be filled in once
105      * the download starts.
106      */
107     public final static String COLUMN_TOTAL_SIZE_BYTES = "total_size";
108 
109     /**
110      * Uri where downloaded file will be stored.  If a destination is supplied by client, that URI
111      * will be used here.  Otherwise, the value will initially be null and will be filled in with a
112      * generated URI once the download has started.
113      */
114     public final static String COLUMN_LOCAL_URI = "local_uri";
115 
116     /**
117      * Path to the downloaded file on disk.
118      * <p>
119      * Note that apps may not have filesystem permissions to directly access
120      * this path. Instead of trying to open this path directly, apps should use
121      * {@link ContentResolver#openFileDescriptor(Uri, String)} to gain access.
122      *
123      * @deprecated apps should transition to using
124      *             {@link ContentResolver#openFileDescriptor(Uri, String)}
125      *             instead.
126      */
127     @Deprecated
128     public final static String COLUMN_LOCAL_FILENAME = "local_filename";
129 
130     /**
131      * Current status of the download, as one of the STATUS_* constants.
132      */
133     public final static String COLUMN_STATUS = Downloads.Impl.COLUMN_STATUS;
134 
135     /** {@hide} */
136     public static final String COLUMN_FILE_NAME_HINT = Downloads.Impl.COLUMN_FILE_NAME_HINT;
137 
138     /**
139      * Provides more detail on the status of the download.  Its meaning depends on the value of
140      * {@link #COLUMN_STATUS}.
141      *
142      * When {@link #COLUMN_STATUS} is {@link #STATUS_FAILED}, this indicates the type of error that
143      * occurred.  If an HTTP error occurred, this will hold the HTTP status code as defined in RFC
144      * 2616.  Otherwise, it will hold one of the ERROR_* constants.
145      *
146      * When {@link #COLUMN_STATUS} is {@link #STATUS_PAUSED}, this indicates why the download is
147      * paused.  It will hold one of the PAUSED_* constants.
148      *
149      * If {@link #COLUMN_STATUS} is neither {@link #STATUS_FAILED} nor {@link #STATUS_PAUSED}, this
150      * column's value is undefined.
151      *
152      * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1">RFC 2616
153      * status codes</a>
154      */
155     public final static String COLUMN_REASON = "reason";
156 
157     /**
158      * Number of bytes download so far.
159      */
160     public final static String COLUMN_BYTES_DOWNLOADED_SO_FAR = "bytes_so_far";
161 
162     /**
163      * Timestamp when the download was last modified, in {@link System#currentTimeMillis
164      * System.currentTimeMillis()} (wall clock time in UTC).
165      */
166     public final static String COLUMN_LAST_MODIFIED_TIMESTAMP = "last_modified_timestamp";
167 
168     /**
169      * The URI to the corresponding entry in MediaProvider for this downloaded entry. It is
170      * used to delete the entries from MediaProvider database when it is deleted from the
171      * downloaded list.
172      */
173     public static final String COLUMN_MEDIAPROVIDER_URI = Downloads.Impl.COLUMN_MEDIAPROVIDER_URI;
174 
175     /** @hide */
176     @TestApi
177     public static final String COLUMN_MEDIASTORE_URI = Downloads.Impl.COLUMN_MEDIASTORE_URI;
178 
179     /** {@hide} */
180     public static final String COLUMN_DESTINATION = Downloads.Impl.COLUMN_DESTINATION;
181 
182     /**
183      * @hide
184      */
185     public final static String COLUMN_ALLOW_WRITE = Downloads.Impl.COLUMN_ALLOW_WRITE;
186 
187     /**
188      * Value of {@link #COLUMN_STATUS} when the download is waiting to start.
189      */
190     public final static int STATUS_PENDING = 1 << 0;
191 
192     /**
193      * Value of {@link #COLUMN_STATUS} when the download is currently running.
194      */
195     public final static int STATUS_RUNNING = 1 << 1;
196 
197     /**
198      * Value of {@link #COLUMN_STATUS} when the download is waiting to retry or resume.
199      */
200     public final static int STATUS_PAUSED = 1 << 2;
201 
202     /**
203      * Value of {@link #COLUMN_STATUS} when the download has successfully completed.
204      */
205     public final static int STATUS_SUCCESSFUL = 1 << 3;
206 
207     /**
208      * Value of {@link #COLUMN_STATUS} when the download has failed (and will not be retried).
209      */
210     public final static int STATUS_FAILED = 1 << 4;
211 
212     /**
213      * Value of COLUMN_ERROR_CODE when the download has completed with an error that doesn't fit
214      * under any other error code.
215      */
216     public final static int ERROR_UNKNOWN = 1000;
217 
218     /**
219      * Value of {@link #COLUMN_REASON} when a storage issue arises which doesn't fit under any
220      * other error code. Use the more specific {@link #ERROR_INSUFFICIENT_SPACE} and
221      * {@link #ERROR_DEVICE_NOT_FOUND} when appropriate.
222      */
223     public final static int ERROR_FILE_ERROR = 1001;
224 
225     /**
226      * Value of {@link #COLUMN_REASON} when an HTTP code was received that download manager
227      * can't handle.
228      */
229     public final static int ERROR_UNHANDLED_HTTP_CODE = 1002;
230 
231     /**
232      * Value of {@link #COLUMN_REASON} when an error receiving or processing data occurred at
233      * the HTTP level.
234      */
235     public final static int ERROR_HTTP_DATA_ERROR = 1004;
236 
237     /**
238      * Value of {@link #COLUMN_REASON} when there were too many redirects.
239      */
240     public final static int ERROR_TOO_MANY_REDIRECTS = 1005;
241 
242     /**
243      * Value of {@link #COLUMN_REASON} when there was insufficient storage space. Typically,
244      * this is because the SD card is full.
245      */
246     public final static int ERROR_INSUFFICIENT_SPACE = 1006;
247 
248     /**
249      * Value of {@link #COLUMN_REASON} when no external storage device was found. Typically,
250      * this is because the SD card is not mounted.
251      */
252     public final static int ERROR_DEVICE_NOT_FOUND = 1007;
253 
254     /**
255      * Value of {@link #COLUMN_REASON} when some possibly transient error occurred but we can't
256      * resume the download.
257      */
258     public final static int ERROR_CANNOT_RESUME = 1008;
259 
260     /**
261      * Value of {@link #COLUMN_REASON} when the requested destination file already exists (the
262      * download manager will not overwrite an existing file).
263      */
264     public final static int ERROR_FILE_ALREADY_EXISTS = 1009;
265 
266     /**
267      * Value of {@link #COLUMN_REASON} when the download has failed because of
268      * {@link NetworkPolicyManager} controls on the requesting application.
269      *
270      * @hide
271      */
272     public final static int ERROR_BLOCKED = 1010;
273 
274     /**
275      * Value of {@link #COLUMN_REASON} when the download is paused because some network error
276      * occurred and the download manager is waiting before retrying the request.
277      */
278     public final static int PAUSED_WAITING_TO_RETRY = 1;
279 
280     /**
281      * Value of {@link #COLUMN_REASON} when the download is waiting for network connectivity to
282      * proceed.
283      */
284     public final static int PAUSED_WAITING_FOR_NETWORK = 2;
285 
286     /**
287      * Value of {@link #COLUMN_REASON} when the download exceeds a size limit for downloads over
288      * the mobile network and the download manager is waiting for a Wi-Fi connection to proceed.
289      */
290     public final static int PAUSED_QUEUED_FOR_WIFI = 3;
291 
292     /**
293      * Value of {@link #COLUMN_REASON} when the download is paused for some other reason.
294      */
295     public final static int PAUSED_UNKNOWN = 4;
296 
297     /**
298      * Broadcast intent action sent by the download manager when a download completes.
299      */
300     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
301     public final static String ACTION_DOWNLOAD_COMPLETE = "android.intent.action.DOWNLOAD_COMPLETE";
302 
303     /**
304      * Broadcast intent action sent by the download manager when the user clicks on a running
305      * download, either from a system notification or from the downloads UI.
306      */
307     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
308     public final static String ACTION_NOTIFICATION_CLICKED =
309             "android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED";
310 
311     /**
312      * Intent action to launch an activity to display all downloads.
313      */
314     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
315     public final static String ACTION_VIEW_DOWNLOADS = "android.intent.action.VIEW_DOWNLOADS";
316 
317     /**
318      * Intent extra included with {@link #ACTION_VIEW_DOWNLOADS} to start DownloadApp in
319      * sort-by-size mode.
320      */
321     public final static String INTENT_EXTRAS_SORT_BY_SIZE =
322             "android.app.DownloadManager.extra_sortBySize";
323 
324     /**
325      * Intent extra included with {@link #ACTION_DOWNLOAD_COMPLETE} intents, indicating the ID (as a
326      * long) of the download that just completed.
327      */
328     public static final String EXTRA_DOWNLOAD_ID = "extra_download_id";
329 
330     /**
331      * When clicks on multiple notifications are received, the following
332      * provides an array of download ids corresponding to the download notification that was
333      * clicked. It can be retrieved by the receiver of this
334      * Intent using {@link android.content.Intent#getLongArrayExtra(String)}.
335      */
336     public static final String EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS = "extra_click_download_ids";
337 
338     /** {@hide} */
339     @SystemApi
340     public static final String ACTION_DOWNLOAD_COMPLETED =
341             "android.intent.action.DOWNLOAD_COMPLETED";
342 
343     /**
344      * columns to request from DownloadProvider.
345      * @hide
346      */
347     @UnsupportedAppUsage
348     public static final String[] UNDERLYING_COLUMNS = new String[] {
349         DownloadManager.COLUMN_ID,
350         DownloadManager.COLUMN_LOCAL_FILENAME,
351         DownloadManager.COLUMN_MEDIAPROVIDER_URI,
352         DownloadManager.COLUMN_DESTINATION,
353         DownloadManager.COLUMN_TITLE,
354         DownloadManager.COLUMN_DESCRIPTION,
355         DownloadManager.COLUMN_URI,
356         DownloadManager.COLUMN_STATUS,
357         DownloadManager.COLUMN_FILE_NAME_HINT,
358         DownloadManager.COLUMN_MEDIA_TYPE,
359         DownloadManager.COLUMN_TOTAL_SIZE_BYTES,
360         DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP,
361         DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR,
362         DownloadManager.COLUMN_ALLOW_WRITE,
363         DownloadManager.COLUMN_LOCAL_URI,
364         DownloadManager.COLUMN_REASON
365     };
366 
367     /**
368      * This class contains all the information necessary to request a new download. The URI is the
369      * only required parameter.
370      *
371      * Note that the default download destination is a shared volume where the system might delete
372      * your file if it needs to reclaim space for system use. If this is a problem, use a location
373      * on external storage (see {@link #setDestinationUri(Uri)}.
374      */
375     public static class Request {
376         /**
377          * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
378          * {@link ConnectivityManager#TYPE_MOBILE}.
379          */
380         public static final int NETWORK_MOBILE = 1 << 0;
381 
382         /**
383          * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
384          * {@link ConnectivityManager#TYPE_WIFI}.
385          */
386         public static final int NETWORK_WIFI = 1 << 1;
387 
388         /**
389          * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
390          * {@link ConnectivityManager#TYPE_BLUETOOTH}.
391          * @hide
392          */
393         @Deprecated
394         public static final int NETWORK_BLUETOOTH = 1 << 2;
395 
396         @UnsupportedAppUsage
397         private Uri mUri;
398         private Uri mDestinationUri;
399         private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
400         private CharSequence mTitle;
401         private CharSequence mDescription;
402         private String mMimeType;
403         private int mAllowedNetworkTypes = ~0; // default to all network types allowed
404         private boolean mRoamingAllowed = true;
405         private boolean mMeteredAllowed = true;
406         private int mFlags = 0;
407         private boolean mIsVisibleInDownloadsUi = true;
408         private boolean mScannable = false;
409         /** if a file is designated as a MediaScanner scannable file, the following value is
410          * stored in the database column {@link Downloads.Impl#COLUMN_MEDIA_SCANNED}.
411          */
412         private static final int SCANNABLE_VALUE_YES = Downloads.Impl.MEDIA_NOT_SCANNED;
413         // value of 1 is stored in the above column by DownloadProvider after it is scanned by
414         // MediaScanner
415         /** if a file is designated as a file that should not be scanned by MediaScanner,
416          * the following value is stored in the database column
417          * {@link Downloads.Impl#COLUMN_MEDIA_SCANNED}.
418          */
419         private static final int SCANNABLE_VALUE_NO = Downloads.Impl.MEDIA_NOT_SCANNABLE;
420 
421         /**
422          * This download is visible but only shows in the notifications
423          * while it's in progress.
424          */
425         public static final int VISIBILITY_VISIBLE = 0;
426 
427         /**
428          * This download is visible and shows in the notifications while
429          * in progress and after completion.
430          */
431         public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1;
432 
433         /**
434          * This download doesn't show in the UI or in the notifications.
435          */
436         public static final int VISIBILITY_HIDDEN = 2;
437 
438         /**
439          * This download shows in the notifications after completion ONLY.
440          * It is usuable only with
441          * {@link DownloadManager#addCompletedDownload(String, String,
442          * boolean, String, String, long, boolean)}.
443          */
444         public static final int VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION = 3;
445 
446         /** can take any of the following values: {@link #VISIBILITY_HIDDEN}
447          * {@link #VISIBILITY_VISIBLE_NOTIFY_COMPLETED}, {@link #VISIBILITY_VISIBLE},
448          * {@link #VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION}
449          */
450         private int mNotificationVisibility = VISIBILITY_VISIBLE;
451 
452         /**
453          * @param uri the HTTP or HTTPS URI to download.
454          */
Request(Uri uri)455         public Request(Uri uri) {
456             if (uri == null) {
457                 throw new NullPointerException();
458             }
459             String scheme = uri.getScheme();
460             if (scheme == null || (!scheme.equals("http") && !scheme.equals("https"))) {
461                 throw new IllegalArgumentException("Can only download HTTP/HTTPS URIs: " + uri);
462             }
463             mUri = uri;
464         }
465 
Request(String uriString)466         Request(String uriString) {
467             mUri = Uri.parse(uriString);
468         }
469 
470         /**
471          * Set the local destination for the downloaded file. Must be a file URI to a path on
472          * external storage, and the calling application must have the WRITE_EXTERNAL_STORAGE
473          * permission.
474          * <p>
475          * The downloaded file is not scanned by MediaScanner.
476          * But it can be made scannable by calling {@link #allowScanningByMediaScanner()}.
477          * <p>
478          * By default, downloads are saved to a generated filename in the shared download cache and
479          * may be deleted by the system at any time to reclaim space.
480          *
481          * <p> For applications targeting {@link android.os.Build.VERSION_CODES#Q} or above,
482          * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE WRITE EXTERNAL_STORAGE}
483          * permission is not needed and the {@code uri} must refer to a path within the
484          * directories owned by the application (e.g. {@link Context#getExternalFilesDir(String)})
485          * or a path within the top-level Downloads directory (as returned by
486          * {@link Environment#getExternalStoragePublicDirectory(String)} with
487          * {@link Environment#DIRECTORY_DOWNLOADS}).
488          *
489          * @param uri a file {@link Uri} indicating the destination for the downloaded file.
490          * @return this object
491          */
setDestinationUri(Uri uri)492         public Request setDestinationUri(Uri uri) {
493             mDestinationUri = uri;
494             return this;
495         }
496 
497         /**
498          * Set the local destination for the downloaded file to a path within
499          * the application's external files directory (as returned by
500          * {@link Context#getExternalFilesDir(String)}.
501          * <p>
502          * The downloaded file is not scanned by MediaScanner. But it can be
503          * made scannable by calling {@link #allowScanningByMediaScanner()}.
504          *
505          * @param context the {@link Context} to use in determining the external
506          *            files directory
507          * @param dirType the directory type to pass to
508          *            {@link Context#getExternalFilesDir(String)}
509          * @param subPath the path within the external directory, including the
510          *            destination filename
511          * @return this object
512          * @throws IllegalStateException If the external storage directory
513          *             cannot be found or created.
514          */
setDestinationInExternalFilesDir(Context context, String dirType, String subPath)515         public Request setDestinationInExternalFilesDir(Context context, String dirType,
516                 String subPath) {
517             final File file = context.getExternalFilesDir(dirType);
518             if (file == null) {
519                 throw new IllegalStateException("Failed to get external storage files directory");
520             } else if (file.exists()) {
521                 if (!file.isDirectory()) {
522                     throw new IllegalStateException(file.getAbsolutePath() +
523                             " already exists and is not a directory");
524                 }
525             } else {
526                 if (!file.mkdirs()) {
527                     throw new IllegalStateException("Unable to create directory: "+
528                             file.getAbsolutePath());
529                 }
530             }
531             setDestinationFromBase(file, subPath);
532             return this;
533         }
534 
535         /**
536          * Set the local destination for the downloaded file to a path within
537          * the public external storage directory (as returned by
538          * {@link Environment#getExternalStoragePublicDirectory(String)}).
539          * <p>
540          * The downloaded file is not scanned by MediaScanner. But it can be
541          * made scannable by calling {@link #allowScanningByMediaScanner()}.
542          *
543          * <p> For applications targeting {@link android.os.Build.VERSION_CODES#Q} or above,
544          * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE}
545          * permission is not needed and the {@code dirType} must be one of the known public
546          * directories like {@link Environment#DIRECTORY_DOWNLOADS},
547          * {@link Environment#DIRECTORY_PICTURES}, {@link Environment#DIRECTORY_MOVIES}, etc.
548          *
549          * @param dirType the directory type to pass to {@link Environment#getExternalStoragePublicDirectory(String)}
550          * @param subPath the path within the external directory, including the
551          *            destination filename
552          * @return this object
553          * @throws IllegalStateException If the external storage directory
554          *             cannot be found or created.
555          */
setDestinationInExternalPublicDir(String dirType, String subPath)556         public Request setDestinationInExternalPublicDir(String dirType, String subPath) {
557             File file = Environment.getExternalStoragePublicDirectory(dirType);
558             if (file == null) {
559                 throw new IllegalStateException("Failed to get external storage public directory");
560             }
561 
562             final Context context = AppGlobals.getInitialApplication();
563             if (context.getApplicationInfo().targetSdkVersion
564                     >= Build.VERSION_CODES.Q || !Environment.isExternalStorageLegacy()) {
565                 try (ContentProviderClient client = context.getContentResolver()
566                         .acquireContentProviderClient(Downloads.Impl.AUTHORITY)) {
567                     final Bundle extras = new Bundle();
568                     extras.putString(Downloads.DIR_TYPE, dirType);
569                     client.call(Downloads.CALL_CREATE_EXTERNAL_PUBLIC_DIR, null, extras);
570                 } catch (RemoteException e) {
571                     throw new IllegalStateException("Unable to create directory: "
572                             + file.getAbsolutePath());
573                 }
574             } else {
575                 if (file.exists()) {
576                     if (!file.isDirectory()) {
577                         throw new IllegalStateException(file.getAbsolutePath()
578                                 + " already exists and is not a directory");
579                     }
580                 } else if (!file.mkdirs()) {
581                     throw new IllegalStateException("Unable to create directory: "
582                             + file.getAbsolutePath());
583                 }
584             }
585             setDestinationFromBase(file, subPath);
586             return this;
587         }
588 
setDestinationFromBase(File base, String subPath)589         private void setDestinationFromBase(File base, String subPath) {
590             if (subPath == null) {
591                 throw new NullPointerException("subPath cannot be null");
592             }
593             mDestinationUri = Uri.withAppendedPath(Uri.fromFile(base), subPath);
594         }
595 
596         /**
597          * If the file to be downloaded is to be scanned by MediaScanner, this method
598          * should be called before {@link DownloadManager#enqueue(Request)} is called.
599          *
600          * @deprecated Starting in Q, this value is ignored. Files downloaded to
601          * directories owned by applications (e.g. {@link Context#getExternalFilesDir(String)})
602          * will not be scanned by MediaScanner and the rest will be scanned.
603          */
604         @Deprecated
allowScanningByMediaScanner()605         public void allowScanningByMediaScanner() {
606             mScannable = true;
607         }
608 
609         /**
610          * Add an HTTP header to be included with the download request.  The header will be added to
611          * the end of the list.
612          * @param header HTTP header name
613          * @param value header value
614          * @return this object
615          * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2">HTTP/1.1
616          *      Message Headers</a>
617          */
addRequestHeader(String header, String value)618         public Request addRequestHeader(String header, String value) {
619             if (header == null) {
620                 throw new NullPointerException("header cannot be null");
621             }
622             if (header.contains(":")) {
623                 throw new IllegalArgumentException("header may not contain ':'");
624             }
625             if (value == null) {
626                 value = "";
627             }
628             mRequestHeaders.add(Pair.create(header, value));
629             return this;
630         }
631 
632         /**
633          * Set the title of this download, to be displayed in notifications (if enabled).  If no
634          * title is given, a default one will be assigned based on the download filename, once the
635          * download starts.
636          * @return this object
637          */
setTitle(CharSequence title)638         public Request setTitle(CharSequence title) {
639             mTitle = title;
640             return this;
641         }
642 
643         /**
644          * Set a description of this download, to be displayed in notifications (if enabled)
645          * @return this object
646          */
setDescription(CharSequence description)647         public Request setDescription(CharSequence description) {
648             mDescription = description;
649             return this;
650         }
651 
652         /**
653          * Set the MIME content type of this download.  This will override the content type declared
654          * in the server's response.
655          * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7">HTTP/1.1
656          *      Media Types</a>
657          * @return this object
658          */
setMimeType(String mimeType)659         public Request setMimeType(String mimeType) {
660             mMimeType = mimeType;
661             return this;
662         }
663 
664         /**
665          * Control whether a system notification is posted by the download manager while this
666          * download is running. If enabled, the download manager posts notifications about downloads
667          * through the system {@link android.app.NotificationManager}. By default, a notification is
668          * shown.
669          *
670          * If set to false, this requires the permission
671          * android.permission.DOWNLOAD_WITHOUT_NOTIFICATION.
672          *
673          * @param show whether the download manager should show a notification for this download.
674          * @return this object
675          * @deprecated use {@link #setNotificationVisibility(int)}
676          */
677         @Deprecated
setShowRunningNotification(boolean show)678         public Request setShowRunningNotification(boolean show) {
679             return (show) ? setNotificationVisibility(VISIBILITY_VISIBLE) :
680                     setNotificationVisibility(VISIBILITY_HIDDEN);
681         }
682 
683         /**
684          * Control whether a system notification is posted by the download manager while this
685          * download is running or when it is completed.
686          * If enabled, the download manager posts notifications about downloads
687          * through the system {@link android.app.NotificationManager}.
688          * By default, a notification is shown only when the download is in progress.
689          *<p>
690          * It can take the following values: {@link #VISIBILITY_HIDDEN},
691          * {@link #VISIBILITY_VISIBLE},
692          * {@link #VISIBILITY_VISIBLE_NOTIFY_COMPLETED}.
693          *<p>
694          * If set to {@link #VISIBILITY_HIDDEN}, this requires the permission
695          * android.permission.DOWNLOAD_WITHOUT_NOTIFICATION.
696          *
697          * @param visibility the visibility setting value
698          * @return this object
699          */
setNotificationVisibility(int visibility)700         public Request setNotificationVisibility(int visibility) {
701             mNotificationVisibility = visibility;
702             return this;
703         }
704 
705         /**
706          * Restrict the types of networks over which this download may proceed.
707          * By default, all network types are allowed. Consider using
708          * {@link #setAllowedOverMetered(boolean)} instead, since it's more
709          * flexible.
710          * <p>
711          * As of {@link android.os.Build.VERSION_CODES#N}, setting only the
712          * {@link #NETWORK_WIFI} flag here is equivalent to calling
713          * {@link #setAllowedOverMetered(boolean)} with {@code false}.
714          *
715          * @param flags any combination of the NETWORK_* bit flags.
716          * @return this object
717          */
setAllowedNetworkTypes(int flags)718         public Request setAllowedNetworkTypes(int flags) {
719             mAllowedNetworkTypes = flags;
720             return this;
721         }
722 
723         /**
724          * Set whether this download may proceed over a roaming connection.  By default, roaming is
725          * allowed.
726          * @param allowed whether to allow a roaming connection to be used
727          * @return this object
728          */
setAllowedOverRoaming(boolean allowed)729         public Request setAllowedOverRoaming(boolean allowed) {
730             mRoamingAllowed = allowed;
731             return this;
732         }
733 
734         /**
735          * Set whether this download may proceed over a metered network
736          * connection. By default, metered networks are allowed.
737          *
738          * @see ConnectivityManager#isActiveNetworkMetered()
739          */
setAllowedOverMetered(boolean allow)740         public Request setAllowedOverMetered(boolean allow) {
741             mMeteredAllowed = allow;
742             return this;
743         }
744 
745         /**
746          * Specify that to run this download, the device needs to be plugged in.
747          * This defaults to false.
748          *
749          * @param requiresCharging Whether or not the device is plugged in.
750          * @see android.app.job.JobInfo.Builder#setRequiresCharging(boolean)
751          */
setRequiresCharging(boolean requiresCharging)752         public Request setRequiresCharging(boolean requiresCharging) {
753             if (requiresCharging) {
754                 mFlags |= Downloads.Impl.FLAG_REQUIRES_CHARGING;
755             } else {
756                 mFlags &= ~Downloads.Impl.FLAG_REQUIRES_CHARGING;
757             }
758             return this;
759         }
760 
761         /**
762          * Specify that to run, the download needs the device to be in idle
763          * mode. This defaults to false.
764          * <p>
765          * Idle mode is a loose definition provided by the system, which means
766          * that the device is not in use, and has not been in use for some time.
767          *
768          * @param requiresDeviceIdle Whether or not the device need be within an
769          *            idle maintenance window.
770          * @see android.app.job.JobInfo.Builder#setRequiresDeviceIdle(boolean)
771          */
setRequiresDeviceIdle(boolean requiresDeviceIdle)772         public Request setRequiresDeviceIdle(boolean requiresDeviceIdle) {
773             if (requiresDeviceIdle) {
774                 mFlags |= Downloads.Impl.FLAG_REQUIRES_DEVICE_IDLE;
775             } else {
776                 mFlags &= ~Downloads.Impl.FLAG_REQUIRES_DEVICE_IDLE;
777             }
778             return this;
779         }
780 
781         /**
782          * Set whether this download should be displayed in the system's Downloads UI. True by
783          * default.
784          * @param isVisible whether to display this download in the Downloads UI
785          * @return this object
786          *
787          * @deprecated Starting in Q, this value is ignored. Only files downloaded to
788          * public Downloads directory (as returned by
789          * {@link Environment#getExternalStoragePublicDirectory(String)} with
790          * {@link Environment#DIRECTORY_DOWNLOADS}) will be visible in system's Downloads UI
791          * and the rest will not be visible.
792          * (e.g. {@link Context#getExternalFilesDir(String)}) will not be visible.
793          */
794         @Deprecated
setVisibleInDownloadsUi(boolean isVisible)795         public Request setVisibleInDownloadsUi(boolean isVisible) {
796             mIsVisibleInDownloadsUi = isVisible;
797             return this;
798         }
799 
800         /**
801          * @return ContentValues to be passed to DownloadProvider.insert()
802          */
toContentValues(String packageName)803         ContentValues toContentValues(String packageName) {
804             ContentValues values = new ContentValues();
805             assert mUri != null;
806             values.put(Downloads.Impl.COLUMN_URI, mUri.toString());
807             values.put(Downloads.Impl.COLUMN_IS_PUBLIC_API, true);
808             values.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, packageName);
809 
810             if (mDestinationUri != null) {
811                 values.put(Downloads.Impl.COLUMN_DESTINATION,
812                         Downloads.Impl.DESTINATION_FILE_URI);
813                 values.put(Downloads.Impl.COLUMN_FILE_NAME_HINT,
814                         mDestinationUri.toString());
815             } else {
816                 values.put(Downloads.Impl.COLUMN_DESTINATION,
817                         Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE);
818             }
819             // is the file supposed to be media-scannable?
820             values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, (mScannable) ? SCANNABLE_VALUE_YES :
821                     SCANNABLE_VALUE_NO);
822 
823             if (!mRequestHeaders.isEmpty()) {
824                 encodeHttpHeaders(values);
825             }
826 
827             putIfNonNull(values, Downloads.Impl.COLUMN_TITLE, mTitle);
828             putIfNonNull(values, Downloads.Impl.COLUMN_DESCRIPTION, mDescription);
829             putIfNonNull(values, Downloads.Impl.COLUMN_MIME_TYPE, mMimeType);
830 
831             values.put(Downloads.Impl.COLUMN_VISIBILITY, mNotificationVisibility);
832             values.put(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, mAllowedNetworkTypes);
833             values.put(Downloads.Impl.COLUMN_ALLOW_ROAMING, mRoamingAllowed);
834             values.put(Downloads.Impl.COLUMN_ALLOW_METERED, mMeteredAllowed);
835             values.put(Downloads.Impl.COLUMN_FLAGS, mFlags);
836             values.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, mIsVisibleInDownloadsUi);
837 
838             return values;
839         }
840 
encodeHttpHeaders(ContentValues values)841         private void encodeHttpHeaders(ContentValues values) {
842             int index = 0;
843             for (Pair<String, String> header : mRequestHeaders) {
844                 String headerString = header.first + ": " + header.second;
845                 values.put(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX + index, headerString);
846                 index++;
847             }
848         }
849 
putIfNonNull(ContentValues contentValues, String key, Object value)850         private void putIfNonNull(ContentValues contentValues, String key, Object value) {
851             if (value != null) {
852                 contentValues.put(key, value.toString());
853             }
854         }
855     }
856 
857     /**
858      * This class may be used to filter download manager queries.
859      */
860     public static class Query {
861         /**
862          * Constant for use with {@link #orderBy}
863          * @hide
864          */
865         public static final int ORDER_ASCENDING = 1;
866 
867         /**
868          * Constant for use with {@link #orderBy}
869          * @hide
870          */
871         public static final int ORDER_DESCENDING = 2;
872 
873         private long[] mIds = null;
874         private Integer mStatusFlags = null;
875         private String mFilterString = null;
876         private String mOrderByColumn = Downloads.Impl.COLUMN_LAST_MODIFICATION;
877         private int mOrderDirection = ORDER_DESCENDING;
878         private boolean mOnlyIncludeVisibleInDownloadsUi = false;
879 
880         /**
881          * Include only the downloads with the given IDs.
882          * @return this object
883          */
setFilterById(long... ids)884         public Query setFilterById(long... ids) {
885             mIds = ids;
886             return this;
887         }
888 
889         /**
890          *
891          * Include only the downloads that contains the given string in its name.
892          * @return this object
893          * @hide
894          */
setFilterByString(@ullable String filter)895         public Query setFilterByString(@Nullable String filter) {
896             mFilterString = filter;
897             return this;
898         }
899 
900         /**
901          * Include only downloads with status matching any the given status flags.
902          * @param flags any combination of the STATUS_* bit flags
903          * @return this object
904          */
setFilterByStatus(int flags)905         public Query setFilterByStatus(int flags) {
906             mStatusFlags = flags;
907             return this;
908         }
909 
910         /**
911          * Controls whether this query includes downloads not visible in the system's Downloads UI.
912          * @param value if true, this query will only include downloads that should be displayed in
913          *            the system's Downloads UI; if false (the default), this query will include
914          *            both visible and invisible downloads.
915          * @return this object
916          * @hide
917          */
918         @UnsupportedAppUsage
setOnlyIncludeVisibleInDownloadsUi(boolean value)919         public Query setOnlyIncludeVisibleInDownloadsUi(boolean value) {
920             mOnlyIncludeVisibleInDownloadsUi = value;
921             return this;
922         }
923 
924         /**
925          * Change the sort order of the returned Cursor.
926          *
927          * @param column one of the COLUMN_* constants; currently, only
928          *         {@link #COLUMN_LAST_MODIFIED_TIMESTAMP} and {@link #COLUMN_TOTAL_SIZE_BYTES} are
929          *         supported.
930          * @param direction either {@link #ORDER_ASCENDING} or {@link #ORDER_DESCENDING}
931          * @return this object
932          * @hide
933          */
934         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
orderBy(String column, int direction)935         public Query orderBy(String column, int direction) {
936             if (direction != ORDER_ASCENDING && direction != ORDER_DESCENDING) {
937                 throw new IllegalArgumentException("Invalid direction: " + direction);
938             }
939 
940             if (column.equals(COLUMN_LAST_MODIFIED_TIMESTAMP)) {
941                 mOrderByColumn = Downloads.Impl.COLUMN_LAST_MODIFICATION;
942             } else if (column.equals(COLUMN_TOTAL_SIZE_BYTES)) {
943                 mOrderByColumn = Downloads.Impl.COLUMN_TOTAL_BYTES;
944             } else {
945                 throw new IllegalArgumentException("Cannot order by " + column);
946             }
947             mOrderDirection = direction;
948             return this;
949         }
950 
951         /**
952          * Run this query using the given ContentResolver.
953          * @param projection the projection to pass to ContentResolver.query()
954          * @return the Cursor returned by ContentResolver.query()
955          */
runQuery(ContentResolver resolver, String[] projection, Uri baseUri)956         Cursor runQuery(ContentResolver resolver, String[] projection, Uri baseUri) {
957             Uri uri = baseUri;
958             List<String> selectionParts = new ArrayList<String>();
959             String[] selectionArgs = null;
960 
961             int whereArgsCount = (mIds == null) ? 0 : mIds.length;
962             whereArgsCount = (mFilterString == null) ? whereArgsCount : whereArgsCount + 1;
963             selectionArgs = new String[whereArgsCount];
964 
965             if (whereArgsCount > 0) {
966                 if (mIds != null) {
967                     selectionParts.add(getWhereClauseForIds(mIds));
968                     getWhereArgsForIds(mIds, selectionArgs);
969                 }
970 
971                 if (mFilterString != null) {
972                     selectionParts.add(Downloads.Impl.COLUMN_TITLE + " LIKE ?");
973                     selectionArgs[selectionArgs.length - 1] = "%" + mFilterString + "%";
974                 }
975             }
976 
977             if (mStatusFlags != null) {
978                 List<String> parts = new ArrayList<String>();
979                 if ((mStatusFlags & STATUS_PENDING) != 0) {
980                     parts.add(statusClause("=", Downloads.Impl.STATUS_PENDING));
981                 }
982                 if ((mStatusFlags & STATUS_RUNNING) != 0) {
983                     parts.add(statusClause("=", Downloads.Impl.STATUS_RUNNING));
984                 }
985                 if ((mStatusFlags & STATUS_PAUSED) != 0) {
986                     parts.add(statusClause("=", Downloads.Impl.STATUS_PAUSED_BY_APP));
987                     parts.add(statusClause("=", Downloads.Impl.STATUS_WAITING_TO_RETRY));
988                     parts.add(statusClause("=", Downloads.Impl.STATUS_WAITING_FOR_NETWORK));
989                     parts.add(statusClause("=", Downloads.Impl.STATUS_QUEUED_FOR_WIFI));
990                 }
991                 if ((mStatusFlags & STATUS_SUCCESSFUL) != 0) {
992                     parts.add(statusClause("=", Downloads.Impl.STATUS_SUCCESS));
993                 }
994                 if ((mStatusFlags & STATUS_FAILED) != 0) {
995                     parts.add("(" + statusClause(">=", 400)
996                               + " AND " + statusClause("<", 600) + ")");
997                 }
998                 selectionParts.add(joinStrings(" OR ", parts));
999             }
1000 
1001             if (mOnlyIncludeVisibleInDownloadsUi) {
1002                 selectionParts.add(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI + " != '0'");
1003             }
1004 
1005             // only return rows which are not marked 'deleted = 1'
1006             selectionParts.add(Downloads.Impl.COLUMN_DELETED + " != '1'");
1007 
1008             String selection = joinStrings(" AND ", selectionParts);
1009             String orderDirection = (mOrderDirection == ORDER_ASCENDING ? "ASC" : "DESC");
1010             String orderBy = mOrderByColumn + " " + orderDirection;
1011 
1012             return resolver.query(uri, projection, selection, selectionArgs, orderBy);
1013         }
1014 
joinStrings(String joiner, Iterable<String> parts)1015         private String joinStrings(String joiner, Iterable<String> parts) {
1016             StringBuilder builder = new StringBuilder();
1017             boolean first = true;
1018             for (String part : parts) {
1019                 if (!first) {
1020                     builder.append(joiner);
1021                 }
1022                 builder.append(part);
1023                 first = false;
1024             }
1025             return builder.toString();
1026         }
1027 
statusClause(String operator, int value)1028         private String statusClause(String operator, int value) {
1029             return Downloads.Impl.COLUMN_STATUS + operator + "'" + value + "'";
1030         }
1031     }
1032 
1033     private final ContentResolver mResolver;
1034     private final String mPackageName;
1035 
1036     private Uri mBaseUri = Downloads.Impl.CONTENT_URI;
1037     private boolean mAccessFilename;
1038 
1039     /**
1040      * @hide
1041      */
DownloadManager(Context context)1042     public DownloadManager(Context context) {
1043         mResolver = context.getContentResolver();
1044         mPackageName = context.getPackageName();
1045 
1046         // Callers can access filename columns when targeting old platform
1047         // versions; otherwise we throw telling them it's deprecated.
1048         mAccessFilename = context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N;
1049     }
1050 
1051     /**
1052      * Makes this object access the download provider through /all_downloads URIs rather than
1053      * /my_downloads URIs, for clients that have permission to do so.
1054      * @hide
1055      */
1056     @UnsupportedAppUsage
1057     public void setAccessAllDownloads(boolean accessAllDownloads) {
1058         if (accessAllDownloads) {
1059             mBaseUri = Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI;
1060         } else {
1061             mBaseUri = Downloads.Impl.CONTENT_URI;
1062         }
1063     }
1064 
1065     /** {@hide} */
1066     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
1067     public void setAccessFilename(boolean accessFilename) {
1068         mAccessFilename = accessFilename;
1069     }
1070 
1071     /**
1072      * Enqueue a new download.  The download will start automatically once the download manager is
1073      * ready to execute it and connectivity is available.
1074      *
1075      * @param request the parameters specifying this download
1076      * @return an ID for the download, unique across the system.  This ID is used to make future
1077      * calls related to this download.
1078      */
1079     public long enqueue(Request request) {
1080         ContentValues values = request.toContentValues(mPackageName);
1081         Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
1082         long id = Long.parseLong(downloadUri.getLastPathSegment());
1083         return id;
1084     }
1085 
1086     /**
1087      * Marks the specified download as 'to be deleted'. This is done when a completed download
1088      * is to be removed but the row was stored without enough info to delete the corresponding
1089      * metadata from Mediaprovider database. Actual cleanup of this row is done in DownloadService.
1090      *
1091      * @param ids the IDs of the downloads to be marked 'deleted'
1092      * @return the number of downloads actually updated
1093      * @hide
1094      */
1095     public int markRowDeleted(long... ids) {
1096         if (ids == null || ids.length == 0) {
1097             // called with nothing to remove!
1098             throw new IllegalArgumentException("input param 'ids' can't be null");
1099         }
1100         return mResolver.delete(mBaseUri, getWhereClauseForIds(ids), getWhereArgsForIds(ids));
1101     }
1102 
1103     /**
1104      * Cancel downloads and remove them from the download manager.  Each download will be stopped if
1105      * it was running, and it will no longer be accessible through the download manager.
1106      * If there is a downloaded file, partial or complete, it is deleted.
1107      *
1108      * @param ids the IDs of the downloads to remove
1109      * @return the number of downloads actually removed
1110      */
1111     public int remove(long... ids) {
1112         return markRowDeleted(ids);
1113     }
1114 
1115     /**
1116      * Query the download manager about downloads that have been requested.
1117      * @param query parameters specifying filters for this query
1118      * @return a Cursor over the result set of downloads, with columns consisting of all the
1119      * COLUMN_* constants.
1120      */
1121     public Cursor query(Query query) {
1122         return query(query, UNDERLYING_COLUMNS);
1123     }
1124 
1125     /** @hide */
1126     public Cursor query(Query query, String[] projection) {
1127         Cursor underlyingCursor = query.runQuery(mResolver, projection, mBaseUri);
1128         if (underlyingCursor == null) {
1129             return null;
1130         }
1131         return new CursorTranslator(underlyingCursor, mBaseUri, mAccessFilename);
1132     }
1133 
1134     /**
1135      * Open a downloaded file for reading.  The download must have completed.
1136      * @param id the ID of the download
1137      * @return a read-only {@link ParcelFileDescriptor}
1138      * @throws FileNotFoundException if the destination file does not already exist
1139      */
1140     public ParcelFileDescriptor openDownloadedFile(long id) throws FileNotFoundException {
1141         return mResolver.openFileDescriptor(getDownloadUri(id), "r");
1142     }
1143 
1144     /**
1145      * Returns the {@link Uri} of the given downloaded file id, if the file is
1146      * downloaded successfully. Otherwise, null is returned.
1147      *
1148      * @param id the id of the downloaded file.
1149      * @return the {@link Uri} of the given downloaded file id, if download was
1150      *         successful. null otherwise.
1151      */
1152     public Uri getUriForDownloadedFile(long id) {
1153         // to check if the file is in cache, get its destination from the database
1154         Query query = new Query().setFilterById(id);
1155         Cursor cursor = null;
1156         try {
1157             cursor = query(query);
1158             if (cursor == null) {
1159                 return null;
1160             }
1161             if (cursor.moveToFirst()) {
1162                 int status = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_STATUS));
1163                 if (DownloadManager.STATUS_SUCCESSFUL == status) {
1164                     return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
1165                 }
1166             }
1167         } finally {
1168             if (cursor != null) {
1169                 cursor.close();
1170             }
1171         }
1172         // downloaded file not found or its status is not 'successfully completed'
1173         return null;
1174     }
1175 
1176     /**
1177      * Returns the media type of the given downloaded file id, if the file was
1178      * downloaded successfully. Otherwise, null is returned.
1179      *
1180      * @param id the id of the downloaded file.
1181      * @return the media type of the given downloaded file id, if download was successful. null
1182      * otherwise.
1183      */
1184     public String getMimeTypeForDownloadedFile(long id) {
1185         Query query = new Query().setFilterById(id);
1186         Cursor cursor = null;
1187         try {
1188             cursor = query(query);
1189             if (cursor == null) {
1190                 return null;
1191             }
1192             while (cursor.moveToFirst()) {
1193                 return cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MEDIA_TYPE));
1194             }
1195         } finally {
1196             if (cursor != null) {
1197                 cursor.close();
1198             }
1199         }
1200         // downloaded file not found or its status is not 'successfully completed'
1201         return null;
1202     }
1203 
1204     /**
1205      * Restart the given downloads, which must have already completed (successfully or not).  This
1206      * method will only work when called from within the download manager's process.
1207      * @param ids the IDs of the downloads
1208      * @hide
1209      */
1210     @UnsupportedAppUsage
1211     public void restartDownload(long... ids) {
1212         Cursor cursor = query(new Query().setFilterById(ids));
1213         try {
1214             for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1215                 int status = cursor.getInt(cursor.getColumnIndex(COLUMN_STATUS));
1216                 if (status != STATUS_SUCCESSFUL && status != STATUS_FAILED) {
1217                     throw new IllegalArgumentException("Cannot restart incomplete download: "
1218                             + cursor.getLong(cursor.getColumnIndex(COLUMN_ID)));
1219                 }
1220             }
1221         } finally {
1222             cursor.close();
1223         }
1224 
1225         ContentValues values = new ContentValues();
1226         values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
1227         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
1228         values.putNull(Downloads.Impl._DATA);
1229         values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
1230         values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, 0);
1231         mResolver.update(mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids));
1232     }
1233 
1234     /**
1235      * Force the given downloads to proceed even if their size is larger than
1236      * {@link #getMaxBytesOverMobile(Context)}.
1237      *
1238      * @hide
1239      */
1240     public void forceDownload(long... ids) {
1241         ContentValues values = new ContentValues();
1242         values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
1243         values.put(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_RUN);
1244         values.put(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT, 1);
1245         mResolver.update(mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids));
1246     }
1247 
1248     /**
1249      * Returns maximum size, in bytes, of downloads that may go over a mobile connection; or null if
1250      * there's no limit
1251      *
1252      * @param context the {@link Context} to use for accessing the {@link ContentResolver}
1253      * @return maximum size, in bytes, of downloads that may go over a mobile connection; or null if
1254      * there's no limit
1255      */
1256     public static Long getMaxBytesOverMobile(Context context) {
1257         try {
1258             return Settings.Global.getLong(context.getContentResolver(),
1259                     Settings.Global.DOWNLOAD_MAX_BYTES_OVER_MOBILE);
1260         } catch (SettingNotFoundException exc) {
1261             return null;
1262         }
1263     }
1264 
1265     /**
1266      * Rename the given download if the download has completed
1267      *
1268      * @param context the {@link Context} to use in case need to update MediaProvider
1269      * @param id the downloaded id
1270      * @param displayName the new name to rename to
1271      * @return true if rename was successful, false otherwise
1272      * @hide
1273      */
1274     public boolean rename(Context context, long id, String displayName) {
1275         if (!FileUtils.isValidFatFilename(displayName)) {
1276             throw new SecurityException(displayName + " is not a valid filename");
1277         }
1278 
1279         final String filePath;
1280         final Query query = new Query().setFilterById(id);
1281         try (Cursor cursor = query(query)) {
1282             if (cursor == null) {
1283                 throw new IllegalStateException("Missing cursor for download id=" + id);
1284             }
1285             if (cursor.moveToFirst()) {
1286                 final int status = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_STATUS));
1287                 if (status != DownloadManager.STATUS_SUCCESSFUL) {
1288                     throw new IllegalStateException("Download is not completed yet: "
1289                             + DatabaseUtils.dumpCurrentRowToString(cursor));
1290                 }
1291                 filePath = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_LOCAL_FILENAME));
1292                 if (filePath == null) {
1293                     throw new IllegalStateException("Download doesn't have a valid file path: "
1294                             + DatabaseUtils.dumpCurrentRowToString(cursor));
1295                 } else if (!new File(filePath).exists()) {
1296                     throw new IllegalStateException("Downloaded file doesn't exist anymore: "
1297                             + DatabaseUtils.dumpCurrentRowToString(cursor));
1298                 }
1299             } else {
1300                 throw new IllegalStateException("Missing download id=" + id);
1301             }
1302         }
1303 
1304         final File before = new File(filePath);
1305         final File after = new File(before.getParentFile(), displayName);
1306 
1307         if (after.exists()) {
1308             throw new IllegalStateException("File already exists: " + after);
1309         }
1310         if (!before.renameTo(after)) {
1311             throw new IllegalStateException(
1312                     "Failed to rename file from " + before + " to " + after);
1313         }
1314 
1315         // TODO: DownloadProvider.update() should take care of updating corresponding
1316         // MediaProvider entries.
1317         MediaStore.scanFile(context, before);
1318         MediaStore.scanFile(context, after);
1319 
1320         final ContentValues values = new ContentValues();
1321         values.put(Downloads.Impl.COLUMN_TITLE, displayName);
1322         values.put(Downloads.Impl._DATA, after.toString());
1323         values.putNull(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
1324         final long[] ids = { id };
1325 
1326         return mResolver.update(
1327                 mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids)) == 1;
1328     }
1329 
1330     /**
1331      * Returns recommended maximum size, in bytes, of downloads that may go over a mobile
1332      * connection; or null if there's no recommended limit.  The user will have the option to bypass
1333      * this limit.
1334      *
1335      * @param context the {@link Context} to use for accessing the {@link ContentResolver}
1336      * @return recommended maximum size, in bytes, of downloads that may go over a mobile
1337      * connection; or null if there's no recommended limit.
1338      */
1339     public static Long getRecommendedMaxBytesOverMobile(Context context) {
1340         try {
1341             return Settings.Global.getLong(context.getContentResolver(),
1342                     Settings.Global.DOWNLOAD_RECOMMENDED_MAX_BYTES_OVER_MOBILE);
1343         } catch (SettingNotFoundException exc) {
1344             return null;
1345         }
1346     }
1347 
1348     /** {@hide} */
1349     public static boolean isActiveNetworkExpensive(Context context) {
1350         // TODO: connect to NetworkPolicyManager
1351         return false;
1352     }
1353 
1354     /** {@hide} */
1355     public static long getActiveNetworkWarningBytes(Context context) {
1356         // TODO: connect to NetworkPolicyManager
1357         return -1;
1358     }
1359 
1360     /**
1361      * Adds a file to the downloads database system, so it could appear in Downloads App
1362      * (and thus become eligible for management by the Downloads App).
1363      * <p>
1364      * It is helpful to make the file scannable by MediaScanner by setting the param
1365      * isMediaScannerScannable to true. It makes the file visible in media managing
1366      * applications such as Gallery App, which could be a useful purpose of using this API.
1367      *
1368      * <p> For applications targeting {@link android.os.Build.VERSION_CODES#Q} or above,
1369      * {@code path} must be within directories owned by the application
1370      * {e.g. {@link Context#getExternalFilesDir(String)}} or if the application is running under
1371      * the legacy storage model (see
1372      * {@link android.R.styleable#AndroidManifestApplication_requestLegacyExternalStorage
1373      * android:requestLegacyExternalStorage}), {@code path} can also be within the top-level
1374      * Downloads directory (as returned by
1375      * {@link Environment#getExternalStoragePublicDirectory(String)} with
1376      * {@link Environment#DIRECTORY_DOWNLOADS}).
1377      *
1378      * @param title the title that would appear for this file in Downloads App.
1379      * @param description the description that would appear for this file in Downloads App.
1380      * @param isMediaScannerScannable true if the file is to be scanned by MediaScanner. Files
1381      * scanned by MediaScanner appear in the applications used to view media (for example,
1382      * Gallery app).
1383      * @param mimeType mimetype of the file.
1384      * @param path absolute pathname to the file. The file should be world-readable, so that it can
1385      * be managed by the Downloads App and any other app that is used to read it (for example,
1386      * Gallery app to display the file, if the file contents represent a video/image).
1387      * @param length length of the downloaded file
1388      * @param showNotification true if a notification is to be sent, false otherwise
1389      * @return  an ID for the download entry added to the downloads app, unique across the system
1390      * This ID is used to make future calls related to this download.
1391      *
1392      * @deprecated Apps should instead contribute files to
1393      * {@link android.provider.MediaStore.Downloads} collection to make them available to user
1394      * as part of Downloads.
1395      */
1396     @Deprecated
1397     public long addCompletedDownload(String title, String description,
1398             boolean isMediaScannerScannable, String mimeType, String path, long length,
1399             boolean showNotification) {
1400         return addCompletedDownload(title, description, isMediaScannerScannable, mimeType, path,
1401                 length, showNotification, false, null, null);
1402     }
1403 
1404     /**
1405      * Adds a file to the downloads database system, so it could appear in Downloads App
1406      * (and thus become eligible for management by the Downloads App).
1407      * <p>
1408      * It is helpful to make the file scannable by MediaScanner by setting the param
1409      * isMediaScannerScannable to true. It makes the file visible in media managing
1410      * applications such as Gallery App, which could be a useful purpose of using this API.
1411      *
1412      * <p> For applications targeting {@link android.os.Build.VERSION_CODES#Q} or above,
1413      * {@code path} must be within directories owned by the application
1414      * {e.g. {@link Context#getExternalFilesDir(String)}} or if the application is running under
1415      * the legacy storage model (see
1416      * {@link android.R.styleable#AndroidManifestApplication_requestLegacyExternalStorage
1417      * android:requestLegacyExternalStorage}), {@code path} can also be within the top-level
1418      * Downloads directory (as returned by
1419      * {@link Environment#getExternalStoragePublicDirectory(String)} with
1420      * {@link Environment#DIRECTORY_DOWNLOADS}).
1421      *
1422      * @param title the title that would appear for this file in Downloads App.
1423      * @param description the description that would appear for this file in Downloads App.
1424      * @param isMediaScannerScannable true if the file is to be scanned by MediaScanner. Files
1425      * scanned by MediaScanner appear in the applications used to view media (for example,
1426      * Gallery app).
1427      * @param mimeType mimetype of the file.
1428      * @param path absolute pathname to the file. The file should be world-readable, so that it can
1429      * be managed by the Downloads App and any other app that is used to read it (for example,
1430      * Gallery app to display the file, if the file contents represent a video/image).
1431      * @param length length of the downloaded file
1432      * @param showNotification true if a notification is to be sent, false otherwise
1433      * @param uri the original HTTP URI of the download
1434      * @param referer the HTTP Referer for the download
1435      * @return  an ID for the download entry added to the downloads app, unique across the system
1436      * This ID is used to make future calls related to this download.
1437      *
1438      * @deprecated Apps should instead contribute files to
1439      * {@link android.provider.MediaStore.Downloads} collection to make them available to user
1440      * as part of Downloads.
1441      */
1442     @Deprecated
1443     public long addCompletedDownload(String title, String description,
1444             boolean isMediaScannerScannable, String mimeType, String path, long length,
1445             boolean showNotification, Uri uri, Uri referer) {
1446         return addCompletedDownload(title, description, isMediaScannerScannable, mimeType, path,
1447                 length, showNotification, false, uri, referer);
1448     }
1449 
1450     /**
1451      * <p> For applications targeting {@link android.os.Build.VERSION_CODES#Q} or above,
1452      * {@code path} must be within directories owned by the application
1453      * {e.g. {@link Context#getExternalFilesDir(String)}} or if the application is running under
1454      * the legacy storage model (see
1455      * {@link android.R.styleable#AndroidManifestApplication_requestLegacyExternalStorage
1456      * android:requestLegacyExternalStorage}), {@code path} can also be within the top-level
1457      * Downloads directory (as returned by
1458      * {@link Environment#getExternalStoragePublicDirectory(String)} with
1459      * {@link Environment#DIRECTORY_DOWNLOADS}).
1460      *
1461      * @deprecated Apps should instead contribute files to
1462      * {@link android.provider.MediaStore.Downloads} collection to make them available to user
1463      * as part of Downloads.
1464      *
1465      * {@hide}
1466      */
1467     @Deprecated
1468     public long addCompletedDownload(String title, String description,
1469             boolean isMediaScannerScannable, String mimeType, String path, long length,
1470             boolean showNotification, boolean allowWrite) {
1471         return addCompletedDownload(title, description, isMediaScannerScannable, mimeType, path,
1472                 length, showNotification, allowWrite, null, null);
1473     }
1474 
1475     /**
1476      * <p> For applications targeting {@link android.os.Build.VERSION_CODES#Q} or above,
1477      * {@code path} must be within directories owned by the application
1478      * {e.g. {@link Context#getExternalFilesDir(String)}} or if the application is running under
1479      * the legacy storage model (see
1480      * {@link android.R.styleable#AndroidManifestApplication_requestLegacyExternalStorage
1481      * android:requestLegacyExternalStorage}), {@code path} can also be within the top-level
1482      * Downloads directory (as returned by
1483      * {@link Environment#getExternalStoragePublicDirectory(String)} with
1484      * {@link Environment#DIRECTORY_DOWNLOADS}).
1485      *
1486      * {@hide}
1487      *
1488      * @deprecated Apps should instead contribute files to
1489      * {@link android.provider.MediaStore.Downloads} collection to make them available to user
1490      * as part of Downloads.
1491      */
1492     @Deprecated
1493     public long addCompletedDownload(String title, String description,
1494             boolean isMediaScannerScannable, String mimeType, String path, long length,
1495             boolean showNotification, boolean allowWrite, Uri uri, Uri referer) {
1496         // make sure the input args are non-null/non-zero
1497         validateArgumentIsNonEmpty("title", title);
1498         validateArgumentIsNonEmpty("description", description);
1499         validateArgumentIsNonEmpty("path", path);
1500         validateArgumentIsNonEmpty("mimeType", mimeType);
1501         if (length < 0) {
1502             throw new IllegalArgumentException(" invalid value for param: totalBytes");
1503         }
1504 
1505         // if there is already an entry with the given path name in downloads.db, return its id
1506         Request request;
1507         if (uri != null) {
1508             request = new Request(uri);
1509         } else {
1510             request = new Request(NON_DOWNLOADMANAGER_DOWNLOAD);
1511         }
1512         request.setTitle(title)
1513                 .setDescription(description)
1514                 .setMimeType(mimeType);
1515         if (referer != null) {
1516             request.addRequestHeader("Referer", referer.toString());
1517         }
1518         ContentValues values = request.toContentValues(null);
1519         values.put(Downloads.Impl.COLUMN_DESTINATION,
1520                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD);
1521         values.put(Downloads.Impl._DATA, path);
1522         values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS);
1523         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, length);
1524         values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED,
1525                 (isMediaScannerScannable) ? Request.SCANNABLE_VALUE_YES :
1526                         Request.SCANNABLE_VALUE_NO);
1527         values.put(Downloads.Impl.COLUMN_VISIBILITY, (showNotification) ?
1528                 Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION : Request.VISIBILITY_HIDDEN);
1529         values.put(Downloads.Impl.COLUMN_ALLOW_WRITE, allowWrite ? 1 : 0);
1530         Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
1531         if (downloadUri == null) {
1532             return -1;
1533         }
1534         return Long.parseLong(downloadUri.getLastPathSegment());
1535     }
1536 
1537     private static final String NON_DOWNLOADMANAGER_DOWNLOAD =
1538             "non-dwnldmngr-download-dont-retry2download";
1539 
1540     private static void validateArgumentIsNonEmpty(String paramName, String val) {
1541         if (TextUtils.isEmpty(val)) {
1542             throw new IllegalArgumentException(paramName + " can't be null");
1543         }
1544     }
1545 
1546     /**
1547      * Get the DownloadProvider URI for the download with the given ID.
1548      *
1549      * @hide
1550      */
1551     public Uri getDownloadUri(long id) {
1552         return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
1553     }
1554 
1555     /**
1556      * Get a parameterized SQL WHERE clause to select a bunch of IDs.
1557      */
1558     @UnsupportedAppUsage
1559     static String getWhereClauseForIds(long[] ids) {
1560         StringBuilder whereClause = new StringBuilder();
1561         whereClause.append("(");
1562         for (int i = 0; i < ids.length; i++) {
1563             if (i > 0) {
1564                 whereClause.append("OR ");
1565             }
1566             whereClause.append(Downloads.Impl._ID);
1567             whereClause.append(" = ? ");
1568         }
1569         whereClause.append(")");
1570         return whereClause.toString();
1571     }
1572 
1573     /**
1574      * Get the selection args for a clause returned by {@link #getWhereClauseForIds(long[])}.
1575      */
1576     @UnsupportedAppUsage
1577     static String[] getWhereArgsForIds(long[] ids) {
1578         String[] whereArgs = new String[ids.length];
1579         return getWhereArgsForIds(ids, whereArgs);
1580     }
1581 
1582     /**
1583      * Get selection args for a clause returned by {@link #getWhereClauseForIds(long[])}
1584      * and write it to the supplied args array.
1585      */
1586     static String[] getWhereArgsForIds(long[] ids, String[] args) {
1587         assert(args.length >= ids.length);
1588         for (int i = 0; i < ids.length; i++) {
1589             args[i] = Long.toString(ids[i]);
1590         }
1591         return args;
1592     }
1593 
1594 
1595     /**
1596      * This class wraps a cursor returned by DownloadProvider -- the "underlying cursor" -- and
1597      * presents a different set of columns, those defined in the DownloadManager.COLUMN_* constants.
1598      * Some columns correspond directly to underlying values while others are computed from
1599      * underlying data.
1600      */
1601     private static class CursorTranslator extends CursorWrapper {
1602         private final Uri mBaseUri;
1603         private final boolean mAccessFilename;
1604 
1605         public CursorTranslator(Cursor cursor, Uri baseUri, boolean accessFilename) {
1606             super(cursor);
1607             mBaseUri = baseUri;
1608             mAccessFilename = accessFilename;
1609         }
1610 
1611         @Override
1612         public int getInt(int columnIndex) {
1613             return (int) getLong(columnIndex);
1614         }
1615 
1616         @Override
1617         public long getLong(int columnIndex) {
1618             if (getColumnName(columnIndex).equals(COLUMN_REASON)) {
1619                 return getReason(super.getInt(getColumnIndex(Downloads.Impl.COLUMN_STATUS)));
1620             } else if (getColumnName(columnIndex).equals(COLUMN_STATUS)) {
1621                 return translateStatus(super.getInt(getColumnIndex(Downloads.Impl.COLUMN_STATUS)));
1622             } else {
1623                 return super.getLong(columnIndex);
1624             }
1625         }
1626 
1627         @Override
1628         public String getString(int columnIndex) {
1629             final String columnName = getColumnName(columnIndex);
1630             switch (columnName) {
1631                 case COLUMN_LOCAL_URI:
1632                     return getLocalUri();
1633                 case COLUMN_LOCAL_FILENAME:
1634                     if (!mAccessFilename) {
1635                         throw new SecurityException(
1636                                 "COLUMN_LOCAL_FILENAME is deprecated;"
1637                                         + " use ContentResolver.openFileDescriptor() instead");
1638                     }
1639                 default:
1640                     return super.getString(columnIndex);
1641             }
1642         }
1643 
1644         private String getLocalUri() {
1645             long destinationType = getLong(getColumnIndex(Downloads.Impl.COLUMN_DESTINATION));
1646             if (destinationType == Downloads.Impl.DESTINATION_FILE_URI ||
1647                     destinationType == Downloads.Impl.DESTINATION_EXTERNAL ||
1648                     destinationType == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
1649                 String localPath = super.getString(getColumnIndex(COLUMN_LOCAL_FILENAME));
1650                 if (localPath == null) {
1651                     return null;
1652                 }
1653                 return Uri.fromFile(new File(localPath)).toString();
1654             }
1655 
1656             // return content URI for cache download
1657             long downloadId = getLong(getColumnIndex(Downloads.Impl._ID));
1658             return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, downloadId).toString();
1659         }
1660 
1661         private long getReason(int status) {
1662             switch (translateStatus(status)) {
1663                 case STATUS_FAILED:
1664                     return getErrorCode(status);
1665 
1666                 case STATUS_PAUSED:
1667                     return getPausedReason(status);
1668 
1669                 default:
1670                     return 0; // arbitrary value when status is not an error
1671             }
1672         }
1673 
1674         private long getPausedReason(int status) {
1675             switch (status) {
1676                 case Downloads.Impl.STATUS_WAITING_TO_RETRY:
1677                     return PAUSED_WAITING_TO_RETRY;
1678 
1679                 case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
1680                     return PAUSED_WAITING_FOR_NETWORK;
1681 
1682                 case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
1683                     return PAUSED_QUEUED_FOR_WIFI;
1684 
1685                 default:
1686                     return PAUSED_UNKNOWN;
1687             }
1688         }
1689 
1690         private long getErrorCode(int status) {
1691             if ((400 <= status && status < Downloads.Impl.MIN_ARTIFICIAL_ERROR_STATUS)
1692                     || (500 <= status && status < 600)) {
1693                 // HTTP status code
1694                 return status;
1695             }
1696 
1697             switch (status) {
1698                 case Downloads.Impl.STATUS_FILE_ERROR:
1699                     return ERROR_FILE_ERROR;
1700 
1701                 case Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE:
1702                 case Downloads.Impl.STATUS_UNHANDLED_REDIRECT:
1703                     return ERROR_UNHANDLED_HTTP_CODE;
1704 
1705                 case Downloads.Impl.STATUS_HTTP_DATA_ERROR:
1706                     return ERROR_HTTP_DATA_ERROR;
1707 
1708                 case Downloads.Impl.STATUS_TOO_MANY_REDIRECTS:
1709                     return ERROR_TOO_MANY_REDIRECTS;
1710 
1711                 case Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR:
1712                     return ERROR_INSUFFICIENT_SPACE;
1713 
1714                 case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR:
1715                     return ERROR_DEVICE_NOT_FOUND;
1716 
1717                 case Downloads.Impl.STATUS_CANNOT_RESUME:
1718                     return ERROR_CANNOT_RESUME;
1719 
1720                 case Downloads.Impl.STATUS_FILE_ALREADY_EXISTS_ERROR:
1721                     return ERROR_FILE_ALREADY_EXISTS;
1722 
1723                 default:
1724                     return ERROR_UNKNOWN;
1725             }
1726         }
1727 
1728         private int translateStatus(int status) {
1729             switch (status) {
1730                 case Downloads.Impl.STATUS_PENDING:
1731                     return STATUS_PENDING;
1732 
1733                 case Downloads.Impl.STATUS_RUNNING:
1734                     return STATUS_RUNNING;
1735 
1736                 case Downloads.Impl.STATUS_PAUSED_BY_APP:
1737                 case Downloads.Impl.STATUS_WAITING_TO_RETRY:
1738                 case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
1739                 case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
1740                     return STATUS_PAUSED;
1741 
1742                 case Downloads.Impl.STATUS_SUCCESS:
1743                     return STATUS_SUCCESSFUL;
1744 
1745                 default:
1746                     assert Downloads.Impl.isStatusError(status);
1747                     return STATUS_FAILED;
1748             }
1749         }
1750     }
1751 }
1752