summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSudheer Shanka <sudheersai@google.com>2019-08-07 01:51:48 (GMT)
committerSudheer Shanka <sudheersai@google.com>2019-08-13 17:38:13 (GMT)
commitb02bbf988e4e7e0c2cdba7bad6ad518e82df1f33 (patch)
treea728f1462e19748acf6ac59e13c55c24e35e1dc7
parent9541cdbd3b8374a1f54dc29a2c4bb9d6170f1e29 (diff)
downloadandroid_packages_providers_DownloadProvider-b02bbf988e4e7e0c2cdba7bad6ad518e82df1f33.zip
android_packages_providers_DownloadProvider-b02bbf988e4e7e0c2cdba7bad6ad518e82df1f33.tar.gz
android_packages_providers_DownloadProvider-b02bbf988e4e7e0c2cdba7bad6ad518e82df1f33.tar.bz2
Ensure files get mediascanned after the download is completed.
Update file size in MediaStore to 0 before triggering mediascan which should force MediaScanner to scan this file. Also, add a clean-up job to trigger mediscan on already existing downloads which should have been mediascanned but haven't and got stuck in this state. Bug: 138419471 Test: manual Test: atest DownloadProviderTests Test: atest cts/tests/app/src/android/app/cts/DownloadManagerTest.java Test: atest DownloadManagerLegacy Test: atest DownloadManagerApi28 Change-Id: I813086ceba6c70ca42309fcce5f9db209eac1575 (cherry picked from commit c2b0739b08e423e3c9fcde42b095cd72315ecf1f)
-rw-r--r--AndroidManifest.xml5
-rw-r--r--src/com/android/providers/downloads/Constants.java11
-rw-r--r--src/com/android/providers/downloads/DownloadIdleService.java2
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java99
-rw-r--r--src/com/android/providers/downloads/Helpers.java24
-rw-r--r--src/com/android/providers/downloads/MediaScanTriggerJob.java122
6 files changed, 219 insertions, 44 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 45e2888..302a58e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -107,6 +107,11 @@
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
+ <service
+ android:name=".MediaScanTriggerJob"
+ android:exported="true"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
+
<receiver android:name=".DownloadReceiver" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java
index 79daeae..c822c7c 100644
--- a/src/com/android/providers/downloads/Constants.java
+++ b/src/com/android/providers/downloads/Constants.java
@@ -83,6 +83,17 @@ public class Constants {
/** The default user agent used for downloads */
public static final String DEFAULT_USER_AGENT;
+ /**
+ * Job id for the periodic service to clean-up stale and orphan downloads.
+ */
+ public static final int IDLE_JOB_ID = -100;
+
+ /**
+ * Job id for a one-time clean-up job to trigger mediascan on files which should have been
+ * mediascanned earlier when they were downloaded but didn't get scanned.
+ */
+ public static final int MEDIA_SCAN_TRIGGER_JOB_ID = -101;
+
static {
final StringBuilder builder = new StringBuilder();
diff --git a/src/com/android/providers/downloads/DownloadIdleService.java b/src/com/android/providers/downloads/DownloadIdleService.java
index ecebb0f..2a68954 100644
--- a/src/com/android/providers/downloads/DownloadIdleService.java
+++ b/src/com/android/providers/downloads/DownloadIdleService.java
@@ -16,6 +16,7 @@
package com.android.providers.downloads;
+import static com.android.providers.downloads.Constants.IDLE_JOB_ID;
import static com.android.providers.downloads.Constants.TAG;
import static com.android.providers.downloads.StorageUtils.listFilesRecursive;
@@ -53,7 +54,6 @@ import java.util.HashSet;
* deleted directly on disk.
*/
public class DownloadIdleService extends JobService {
- private static final int IDLE_JOB_ID = -100;
private class IdleRunnable implements Runnable {
private JobParameters mParams;
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index eb0313c..db98c50 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -31,6 +31,9 @@ import static android.provider.Downloads.Impl.MEDIA_SCANNED;
import static android.provider.Downloads.Impl.PERMISSION_ACCESS_ALL;
import static android.provider.Downloads.Impl._DATA;
+import static com.android.providers.downloads.Helpers.convertToMediaStoreDownloadsUri;
+import static com.android.providers.downloads.Helpers.triggerMediaScan;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
@@ -106,7 +109,7 @@ public final class DownloadProvider extends ContentProvider {
/** Database filename */
private static final String DB_NAME = "downloads.db";
/** Current database version */
- private static final int DB_VERSION = 113;
+ private static final int DB_VERSION = 114;
/** Name of table in the database */
private static final String DB_TABLE = "downloads";
/** Memory optimization - close idle connections after 30s of inactivity */
@@ -340,6 +343,11 @@ public final class DownloadProvider extends ContentProvider {
canonicalizeDataPaths(db);
break;
+ case 114:
+ nullifyMediaStoreUris(db);
+ MediaScanTriggerJob.schedule(getContext());
+ break;
+
default:
throw new IllegalStateException("Don't know how to upgrade to " + version);
}
@@ -480,6 +488,24 @@ public final class DownloadProvider extends ContentProvider {
}
/**
+ * Set mediastore uri column to null before the clean-up job and fill it again while
+ * running the job so that if the clean-up job gets preempted, we could use it
+ * as a way to know the entries which are already handled when the job gets restarted.
+ */
+ private void nullifyMediaStoreUris(SQLiteDatabase db) {
+ final String whereClause = Downloads.Impl._DATA + " IS NOT NULL"
+ + " AND (" + COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI + "=1"
+ + " OR " + COLUMN_MEDIA_SCANNED + "=" + MEDIA_SCANNED + ")"
+ + " AND (" + COLUMN_DESTINATION + "=" + Downloads.Impl.DESTINATION_EXTERNAL
+ + " OR " + COLUMN_DESTINATION + "=" + DESTINATION_FILE_URI
+ + " OR " + COLUMN_DESTINATION + "=" + DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD
+ + ")";
+ final ContentValues values = new ContentValues();
+ values.putNull(COLUMN_MEDIASTORE_URI);
+ db.update(DB_TABLE, values, whereClause, null);
+ }
+
+ /**
* Add a column to a table using ALTER TABLE.
* @param dbTable name of the table
* @param columnName name of the column to add
@@ -840,19 +866,28 @@ public final class DownloadProvider extends ContentProvider {
if (shouldBeVisibleToUser && filteredValues.getAsInteger(COLUMN_DESTINATION)
== DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
final CallingIdentity token = clearCallingIdentity();
- try (ContentProviderClient client = getContext().getContentResolver()
- .acquireContentProviderClient(MediaStore.AUTHORITY)) {
- final Uri mediaStoreUri = updateMediaProvider(client,
- convertToMediaProviderValues(filteredValues));
+ try {
+ final Uri mediaStoreUri = MediaStore.scanFile(getContext(),
+ new File(filteredValues.getAsString(Downloads.Impl._DATA)));
if (mediaStoreUri != null) {
+ final ContentValues mediaValues = new ContentValues();
+ mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI,
+ filteredValues.getAsString(Downloads.Impl.COLUMN_URI));
+ mediaValues.put(MediaStore.Downloads.REFERER_URI,
+ filteredValues.getAsString(Downloads.Impl.COLUMN_REFERER));
+ mediaValues.put(MediaStore.Downloads.OWNER_PACKAGE_NAME,
+ Helpers.getPackageForUid(getContext(),
+ filteredValues.getAsInteger(Constants.UID)));
+ getContext().getContentResolver().update(
+ convertToMediaStoreDownloadsUri(mediaStoreUri),
+ mediaValues, null, null);
+
filteredValues.put(Downloads.Impl.COLUMN_MEDIASTORE_URI,
mediaStoreUri.toString());
filteredValues.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
mediaStoreUri.toString());
filteredValues.put(COLUMN_MEDIA_SCANNED, MEDIA_SCANNED);
}
- MediaStore.scanFile(getContext(),
- new File(filteredValues.getAsString(Downloads.Impl._DATA)));
} finally {
restoreCallingIdentity(token);
}
@@ -948,48 +983,21 @@ public final class DownloadProvider extends ContentProvider {
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
+ final boolean downloadCompleted = Downloads.Impl.isStatusCompleted(info.mStatus);
final ContentValues mediaValues = new ContentValues();
mediaValues.put(MediaStore.Downloads.DATA, filePath);
- mediaValues.put(MediaStore.Downloads.SIZE, info.mTotalBytes);
+ mediaValues.put(MediaStore.Downloads.SIZE,
+ downloadCompleted ? info.mTotalBytes : info.mCurrentBytes);
mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI, info.mUri);
mediaValues.put(MediaStore.Downloads.REFERER_URI, info.mReferer);
mediaValues.put(MediaStore.Downloads.MIME_TYPE, info.mMimeType);
- mediaValues.put(MediaStore.Downloads.IS_PENDING,
- Downloads.Impl.isStatusSuccess(info.mStatus) ? 0 : 1);
+ mediaValues.put(MediaStore.Downloads.IS_PENDING, downloadCompleted ? 0 : 1);
mediaValues.put(MediaStore.Downloads.OWNER_PACKAGE_NAME,
Helpers.getPackageForUid(getContext(), info.mUid));
mediaValues.put(MediaStore.Files.FileColumns.IS_DOWNLOAD, info.mIsVisibleInDownloadsUi);
return mediaValues;
}
- private ContentValues convertToMediaProviderValues(ContentValues downloadValues) {
- final String filePath;
- try {
- filePath = new File(downloadValues.getAsString(Downloads.Impl._DATA))
- .getCanonicalPath();
- } catch (IOException e) {
- throw new IllegalArgumentException(e);
- }
- final ContentValues mediaValues = new ContentValues();
- mediaValues.put(MediaStore.Downloads.DATA, filePath);
- mediaValues.put(MediaStore.Downloads.SIZE,
- downloadValues.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES));
- mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI,
- downloadValues.getAsString(Downloads.Impl.COLUMN_URI));
- mediaValues.put(MediaStore.Downloads.REFERER_URI,
- downloadValues.getAsString(Downloads.Impl.COLUMN_REFERER));
- mediaValues.put(MediaStore.Downloads.MIME_TYPE,
- downloadValues.getAsString(Downloads.Impl.COLUMN_MIME_TYPE));
- final boolean isPending = downloadValues.getAsInteger(Downloads.Impl.COLUMN_STATUS)
- != Downloads.Impl.STATUS_SUCCESS;
- mediaValues.put(MediaStore.Downloads.IS_PENDING, isPending ? 1 : 0);
- mediaValues.put(MediaStore.Downloads.OWNER_PACKAGE_NAME,
- Helpers.getPackageForUid(getContext(), downloadValues.getAsInteger(Constants.UID)));
- mediaValues.put(MediaStore.Files.FileColumns.IS_DOWNLOAD,
- downloadValues.getAsBoolean(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI));
- return mediaValues;
- }
-
private static Uri getFileUri(String uriString) {
final Uri uri = Uri.parse(uriString);
return TextUtils.equals(uri.getScheme(), ContentResolver.SCHEME_FILE) ? uri : null;
@@ -1534,8 +1542,16 @@ public final class DownloadProvider extends ContentProvider {
|| info.mDestination == Downloads.Impl
.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
&& visibleToUser) {
- final Uri mediaStoreUri = updateMediaProvider(client,
- convertToMediaProviderValues(info));
+ final ContentValues mediaValues = convertToMediaProviderValues(info);
+ final Uri mediaStoreUri;
+ if (Downloads.Impl.isStatusCompleted(info.mStatus)) {
+ // Set size to 0 to ensure MediaScanner will scan this file.
+ mediaValues.put(MediaStore.Downloads.SIZE, 0);
+ updateMediaProvider(client, mediaValues);
+ mediaStoreUri = triggerMediaScan(client, new File(info.mFileName));
+ } else {
+ mediaStoreUri = updateMediaProvider(client, mediaValues);
+ }
if (!TextUtils.equals(info.mMediaStoreUri,
mediaStoreUri == null ? null : mediaStoreUri.toString())) {
updateValues.clear();
@@ -1553,9 +1569,6 @@ public final class DownloadProvider extends ContentProvider {
qb.update(db, updateValues, Downloads.Impl._ID + "=?",
new String[] { Long.toString(info.mId) });
}
- if (Downloads.Impl.isStatusSuccess(info.mStatus)) {
- MediaStore.scanFile(getContext(), new File(info.mFileName));
- }
}
if (updateSchedule) {
Helpers.scheduleJob(context, info);
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 565aa52..f75b627 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -34,18 +34,22 @@ import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Bundle;
import android.os.Environment;
import android.os.FileUtils;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
+import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.provider.Downloads;
+import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import android.util.LongSparseArray;
@@ -489,6 +493,26 @@ public class Helpers {
throw new IOException("Failed to generate an available filename");
}
+ public static Uri convertToMediaStoreDownloadsUri(Uri mediaStoreUri) {
+ final String volumeName = MediaStore.getVolumeName(mediaStoreUri);
+ final long id = android.content.ContentUris.parseId(mediaStoreUri);
+ return MediaStore.Downloads.getContentUri(volumeName, id);
+ }
+
+ // TODO: Move it to MediaStore.
+ public static Uri triggerMediaScan(android.content.ContentProviderClient mediaProviderClient,
+ File file) {
+ try {
+ final Bundle in = new Bundle();
+ in.putParcelable(Intent.EXTRA_STREAM, Uri.fromFile(file));
+ final Bundle out = mediaProviderClient.call(MediaStore.SCAN_FILE_CALL, null, in);
+ return out.getParcelable(Intent.EXTRA_STREAM);
+ } catch (RemoteException e) {
+ // Should not happen
+ }
+ return null;
+ }
+
public static boolean isFileInExternalAndroidDirs(String filePath) {
return PATTERN_ANDROID_DIRS.matcher(filePath).matches();
}
diff --git a/src/com/android/providers/downloads/MediaScanTriggerJob.java b/src/com/android/providers/downloads/MediaScanTriggerJob.java
new file mode 100644
index 0000000..8da2525
--- /dev/null
+++ b/src/com/android/providers/downloads/MediaScanTriggerJob.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.downloads;
+
+import static android.provider.BaseColumns._ID;
+import static android.provider.Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI;
+import static android.provider.Downloads.Impl.COLUMN_DESTINATION;
+import static android.provider.Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI;
+import static android.provider.Downloads.Impl.COLUMN_MEDIASTORE_URI;
+import static android.provider.Downloads.Impl.COLUMN_MEDIA_SCANNED;
+import static android.provider.Downloads.Impl.DESTINATION_EXTERNAL;
+import static android.provider.Downloads.Impl.DESTINATION_FILE_URI;
+import static android.provider.Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD;
+import static android.provider.Downloads.Impl.MEDIA_SCANNED;
+import static android.provider.Downloads.Impl._DATA;
+
+import static com.android.providers.downloads.Constants.MEDIA_SCAN_TRIGGER_JOB_ID;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.Downloads;
+import android.provider.MediaStore;
+
+import java.io.File;
+
+/**
+ * Clean-up job to force mediascan on downloads which should have been but didn't get mediascanned.
+ */
+public class MediaScanTriggerJob extends JobService {
+ private volatile boolean mJobStopped;
+
+ @Override
+ public boolean onStartJob(JobParameters parameters) {
+ Helpers.getAsyncHandler().post(() -> {
+ final String selection = _DATA + " IS NOT NULL"
+ + " AND (" + COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI + "=1"
+ + " OR " + COLUMN_MEDIA_SCANNED + "=" + MEDIA_SCANNED + ")"
+ + " AND (" + COLUMN_DESTINATION + "=" + DESTINATION_EXTERNAL
+ + " OR " + COLUMN_DESTINATION + "=" + DESTINATION_FILE_URI
+ + " OR " + COLUMN_DESTINATION + "=" + DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD
+ + ")";
+ try (ContentProviderClient downloadProviderClient
+ = getContentResolver().acquireContentProviderClient(Downloads.Impl.AUTHORITY);
+ ContentProviderClient mediaProviderClient
+ = getContentResolver().acquireContentProviderClient(MediaStore.AUTHORITY)) {
+ try (Cursor cursor = downloadProviderClient.query(ALL_DOWNLOADS_CONTENT_URI,
+ new String[] {_ID, _DATA, COLUMN_MEDIASTORE_URI},
+ selection, null, null)) {
+ while (cursor.moveToNext()) {
+ if (mJobStopped) {
+ return;
+ }
+ // This indicates that this entry has been handled already (perhaps when
+ // this job ran earlier and got preempted), so skip.
+ if (cursor.getString(2) != null) {
+ continue;
+ }
+ final long id = cursor.getLong(0);
+ final String filePath = cursor.getString(1);
+ final ContentValues mediaValues = new ContentValues();
+ mediaValues.put(MediaStore.Files.FileColumns.SIZE, 0);
+ mediaProviderClient.update(MediaStore.Files.getContentUriForPath(filePath),
+ mediaValues,
+ MediaStore.Files.FileColumns.DATA + "=?",
+ new String[] { filePath });
+
+ final Uri mediaStoreUri = Helpers.triggerMediaScan(mediaProviderClient,
+ new File(filePath));
+ if (mediaStoreUri != null) {
+ final ContentValues downloadValues = new ContentValues();
+ downloadValues.put(COLUMN_MEDIASTORE_URI, mediaStoreUri.toString());
+ downloadProviderClient.update(ALL_DOWNLOADS_CONTENT_URI,
+ downloadValues, _ID + "=" + id, null);
+ }
+ }
+ } catch (RemoteException e) {
+ // Should not happen
+ }
+ }
+ jobFinished(parameters, false);
+ });
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters parameters) {
+ mJobStopped = true;
+ return true;
+ }
+
+ public static void schedule(Context context) {
+ final JobScheduler scheduler = context.getSystemService(JobScheduler.class);
+ final JobInfo job = new JobInfo.Builder(MEDIA_SCAN_TRIGGER_JOB_ID,
+ new ComponentName(context, MediaScanTriggerJob.class))
+ .setRequiresCharging(true)
+ .setRequiresDeviceIdle(true)
+ .build();
+ scheduler.schedule(job);
+ }
+}