blob: 08c0b130fa1f0c20aa6a31bc2e2cd8e86204edc5 [file] [log] [blame]
/*
* Copyright (C) 2010 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.Downloads.Impl.COLUMN_DESTINATION;
import static android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE;
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._DATA;
import static android.provider.Downloads.Impl._ID;
import static com.android.internal.util.ArrayUtils.contains;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.ContentProvider;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Environment;
import android.os.Process;
import android.provider.Downloads;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.LongArray;
import android.util.LongSparseArray;
import libcore.io.IoUtils;
import java.io.File;
import java.util.Arrays;
import java.util.function.BiConsumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This test exercises methods in the {@Helpers} utility class.
*/
@SmallTest
public class HelpersTest extends AndroidTestCase {
private final static int TEST_UID1 = 11111;
private final static int TEST_UID2 = 11112;
private final static int TEST_UID3 = 11113;
private final MockitoHelper mMockitoHelper = new MockitoHelper();
@Override
protected void setUp() throws Exception {
super.setUp();
mMockitoHelper.setUp(getClass());
}
@Override
protected void tearDown() throws Exception {
mMockitoHelper.tearDown();
FsHelper.deleteContents(getContext().getFilesDir());
FsHelper.deleteContents(getContext().getCacheDir());
super.tearDown();
}
public void testGenerateSaveFile() throws Exception {
final File expected = new File(getContext().getFilesDir(), "file.mp4");
final String actual = Helpers.generateSaveFile(getContext(),
"http://example.com/file.txt", null, null, null,
"video/mp4", Downloads.Impl.DESTINATION_CACHE_PARTITION);
assertEquals(expected.getAbsolutePath(), actual);
}
public void testGenerateSaveFileDupes() throws Exception {
final File expected1 = new File(getContext().getFilesDir(), "file.txt");
final String actual1 = Helpers.generateSaveFile(getContext(), "http://example.com/file.txt",
null, null, null, null, Downloads.Impl.DESTINATION_CACHE_PARTITION);
final File expected2 = new File(getContext().getFilesDir(), "file-1.txt");
final String actual2 = Helpers.generateSaveFile(getContext(), "http://example.com/file.txt",
null, null, null, null, Downloads.Impl.DESTINATION_CACHE_PARTITION);
assertEquals(expected1.getAbsolutePath(), actual1);
assertEquals(expected2.getAbsolutePath(), actual2);
}
public void testGenerateSaveFileNoExtension() throws Exception {
final File expected = new File(getContext().getFilesDir(), "file.mp4");
final String actual = Helpers.generateSaveFile(getContext(),
"http://example.com/file", null, null, null,
"video/mp4", Downloads.Impl.DESTINATION_CACHE_PARTITION);
assertEquals(expected.getAbsolutePath(), actual);
}
public void testGenerateSaveFileHint() throws Exception {
final File expected = new File(getContext().getFilesDir(), "meow");
final String hint = Uri.fromFile(expected).toString();
// Test that we never change requested filename.
final String actual = Helpers.generateSaveFile(getContext(), "url", hint,
"dispo", "locat", "video/mp4", Downloads.Impl.DESTINATION_FILE_URI);
assertEquals(expected.getAbsolutePath(), actual);
}
public void testGenerateSaveFileDisposition() throws Exception {
final File expected = new File(getContext().getFilesDir(), "real.mp4");
final String actual = Helpers.generateSaveFile(getContext(),
"http://example.com/file.txt", null, "attachment; filename=\"subdir/real.pdf\"",
null, "video/mp4", Downloads.Impl.DESTINATION_CACHE_PARTITION);
assertEquals(expected.getAbsolutePath(), actual);
}
public void testIsFileInExternalAndroidDirs() throws Exception {
assertTrue(Helpers.isFileInExternalAndroidDirs(
"/storage/emulated/0/Android/data/com.example"));
assertTrue(Helpers.isFileInExternalAndroidDirs(
"/storage/emulated/0/Android/data/com.example/colors.txt"));
assertTrue(Helpers.isFileInExternalAndroidDirs(
"/storage/emulated/0/Android/media/com.example/file.mp4"));
assertTrue(Helpers.isFileInExternalAndroidDirs(
"/storage/AAAA-FFFF/Android/media/com.example/file.mp4"));
assertFalse(Helpers.isFileInExternalAndroidDirs(
"/storage/emulated/0/Download/foo.pdf"));
assertFalse(Helpers.isFileInExternalAndroidDirs(
"/storage/emulated/0/Download/dir/bar.html"));
assertFalse(Helpers.isFileInExternalAndroidDirs(
"/storage/AAAA-FFFF/Download/dir/bar.html"));
}
public void testIsFilenameValidinKnownPublicDir() throws Exception {
assertTrue(Helpers.isFilenameValidInKnownPublicDir(
"/storage/emulated/0/Download/dir/file.txt"));
assertTrue(Helpers.isFilenameValidInKnownPublicDir(
"/storage/emulated/0/Music/foo.mp4"));
assertTrue(Helpers.isFilenameValidInKnownPublicDir(
"/storage/emulated/0/DCIM/vacation/bar.jpg"));
assertFalse(Helpers.isFilenameValidInKnownPublicDir(
"/storage/emulated/0/Testing/foo.mp4"));
assertFalse(Helpers.isFilenameValidInKnownPublicDir(
"/storage/emulated/0/Misc/Download/bar.jpg"));
assertFalse(Helpers.isFilenameValidInKnownPublicDir(
"/storage/emulated/0/Android/data/com.example/bar.jpg"));
}
public void testHandleRemovedUidEntries() throws Exception {
// Prepare
final int[] testUids = {
TEST_UID1, TEST_UID2, TEST_UID3
};
final int[] unknownUids = {
TEST_UID1, TEST_UID2
};
final Context context = mock(Context.class);
final PackageManager packageManager = mock(PackageManager.class);
when(context.getPackageManager()).thenReturn(packageManager);
for (int uid : testUids) {
when(packageManager.getPackagesForUid(uid)).thenReturn(
contains(unknownUids, uid) ? null : new String[] {"com.example" + uid}
);
}
final LongArray idsToRemove = new LongArray();
final LongArray idsToOrphan = new LongArray();
final LongSparseArray<String> validEntries = new LongSparseArray<>();
final MatrixCursor cursor = prepareData(testUids, unknownUids,
idsToOrphan, idsToRemove, validEntries);
final ContentProvider downloadProvider = mock(ContentProvider.class);
when(downloadProvider.query(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),
any(String[].class), any(String.class),isNull(), isNull())).thenReturn(cursor);
// Call
Helpers.handleRemovedUidEntries(context, downloadProvider, Process.INVALID_UID);
// Verify
verify(downloadProvider).update(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),
argThat(values -> values.get(Constants.UID) == null),
argThat(selection -> Arrays.equals(
idsToOrphan.toArray(), extractIdsFromSelection(selection))),
isNull());
verify(downloadProvider).delete(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),
argThat(selection -> Arrays.equals(
idsToRemove.toArray(), extractIdsFromSelection(selection))),
isNull());
// Reset
idsToOrphan.clear();
idsToRemove.clear();
validEntries.clear();
reset(downloadProvider);
// Prepare
final MatrixCursor cursor2 = prepareData(new int[] {TEST_UID2}, unknownUids,
idsToOrphan, idsToRemove, validEntries);
when(downloadProvider.query(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),
any(String[].class), any(String.class),isNull(), isNull())).thenReturn(cursor2);
// Call
Helpers.handleRemovedUidEntries(context, downloadProvider, TEST_UID2);
// Verify
verify(downloadProvider).update(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),
argThat(values -> values.get(Constants.UID) == null),
argThat(selection -> Arrays.equals(
idsToOrphan.toArray(), extractIdsFromSelection(selection))),
isNull());
verify(downloadProvider).delete(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),
argThat(selection -> Arrays.equals(
idsToRemove.toArray(), extractIdsFromSelection(selection))),
isNull());
}
private MatrixCursor prepareData(int[] uids, int[] unknownUids,
final LongArray idsToOrphan, final LongArray idsToRemove,
LongSparseArray<String> validEntries) {
final MatrixCursor cursor = new MatrixCursor(
new String[] {_ID, Constants.UID, COLUMN_DESTINATION, _DATA});
final int[] destinations = {
DESTINATION_EXTERNAL,
DESTINATION_FILE_URI,
DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD,
DESTINATION_CACHE_PARTITION_PURGEABLE
};
long counter = 0;
for (int uid : uids) {
for (int destination : destinations) {
final String fileName = uid + "_" + destination + ".txt";
switch (destination) {
case DESTINATION_EXTERNAL: {
final File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS), fileName);
cursor.addRow(new Object[]{++counter, uid, destination, file.getPath()});
if (contains(unknownUids, uid)) {
idsToOrphan.add(counter);
} else {
validEntries.put(counter, "com.example" + uid);
}
} break;
case DESTINATION_FILE_URI: {
final File file1 = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOCUMENTS), fileName);
cursor.addRow(new Object[]{++counter, uid, destination, file1.getPath()});
if (contains(unknownUids, uid)) {
idsToOrphan.add(counter);
} else {
validEntries.put(counter, "com.example" + uid);
}
final File file2 = new File(getContext().getExternalFilesDir(null),
fileName);
cursor.addRow(new Object[]{++counter, uid, destination, file2.getPath()});
if (contains(unknownUids, uid)) {
idsToRemove.add(counter);
} else {
validEntries.put(counter, "com.example" + uid);
}
} break;
case DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD: {
final File file1 = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOCUMENTS), fileName);
cursor.addRow(new Object[]{++counter, uid, destination, file1.getPath()});
if (contains(unknownUids, uid)) {
idsToOrphan.add(counter);
} else {
validEntries.put(counter, "com.example" + uid);
}
final File file2 = new File(getContext().getExternalFilesDir(null),
fileName);
cursor.addRow(new Object[]{++counter, uid, destination, file2.getPath()});
if (contains(unknownUids, uid)) {
idsToRemove.add(counter);
} else {
validEntries.put(counter, "com.example" + uid);
}
} break;
case DESTINATION_CACHE_PARTITION_PURGEABLE: {
final File file = new File(getContext().getCacheDir(), fileName);
final String filePath = file.getPath().replace(
getContext().getPackageName(), "com.android.providers.downloads");
cursor.addRow(new Object[]{++counter, uid, destination, filePath});
if (contains(unknownUids, uid)) {
idsToRemove.add(counter);
} else {
validEntries.put(counter, "com.example" + uid);
}
} break;
}
}
}
return cursor;
}
private long[] extractIdsFromSelection(String selection) {
final Pattern uidsListPattern = Pattern.compile(".*\\((.+)\\)");
final Matcher matcher = uidsListPattern.matcher(selection);
assertTrue(matcher.matches());
return Arrays.stream(matcher.group(1).split(","))
.mapToLong(Long::valueOf).sorted().toArray();
}
}