summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/Android.bp6
-rw-r--r--tests/AndroidManifest.xml16
-rw-r--r--tests/client/src/com/android/providers/media/client/DownloadProviderTest.java16
-rw-r--r--tests/hostsidetests/photopicker/TEST_MAPPING4
-rw-r--r--tests/hostsidetests/photopicker/src/android/tests/photopicker/CloudProviderHostSideTest.kt36
-rw-r--r--tests/res/raw/android_mime_types166
-rw-r--r--tests/res/raw/corrupted_mime_types2320
-rw-r--r--tests/res/raw/mime_types846
-rw-r--r--tests/src/com/android/providers/media/IsolatedContext.java32
-rw-r--r--tests/src/com/android/providers/media/LocalUriMatcherTest.java2
-rw-r--r--tests/src/com/android/providers/media/MaliciousAppCheckerTest.java139
-rw-r--r--tests/src/com/android/providers/media/MediaIndexingDatabaseOperationsTest.java220
-rw-r--r--tests/src/com/android/providers/media/MediaProviderForFuseTest.java29
-rw-r--r--tests/src/com/android/providers/media/MediaProviderTest.java485
-rw-r--r--tests/src/com/android/providers/media/PermissionActivityTest.java9
-rw-r--r--tests/src/com/android/providers/media/PhotoPickerTranscodeHelperTest.java287
-rw-r--r--tests/src/com/android/providers/media/PickerUriResolverTest.java19
-rw-r--r--tests/src/com/android/providers/media/SearchTestingUtils.java116
-rw-r--r--tests/src/com/android/providers/media/TestDatabaseBackupAndRecovery.java5
-rw-r--r--tests/src/com/android/providers/media/backupandrestore/BackupAndRestoreTestUtils.java175
-rw-r--r--tests/src/com/android/providers/media/backupandrestore/BackupExecutorTest.java74
-rw-r--r--tests/src/com/android/providers/media/backupandrestore/MediaBackupAgentTest.java189
-rw-r--r--tests/src/com/android/providers/media/backupandrestore/RestoreExecutorTest.java73
-rw-r--r--tests/src/com/android/providers/media/cloudproviders/SearchProvider.java218
-rw-r--r--tests/src/com/android/providers/media/oemmetadataservices/OemMetadataServiceTest.java12
-rw-r--r--tests/src/com/android/providers/media/photopicker/CategoriesStateTest.java109
-rw-r--r--tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java47
-rw-r--r--tests/src/com/android/providers/media/photopicker/PhotoPickerCloudTestUtils.java7
-rw-r--r--tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java3
-rw-r--r--tests/src/com/android/providers/media/photopicker/PickerSearchUtils.java33
-rw-r--r--tests/src/com/android/providers/media/photopicker/SearchStateTest.java120
-rw-r--r--tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java2
-rw-r--r--tests/src/com/android/providers/media/photopicker/data/model/ItemTest.java20
-rw-r--r--tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java7
-rw-r--r--tests/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorkerTest.java577
-rw-r--r--tests/src/com/android/providers/media/photopicker/sync/MediaSetsResetWorkerTest.java208
-rw-r--r--tests/src/com/android/providers/media/photopicker/sync/MediaSetsSyncWorkerTest.java398
-rw-r--r--tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java650
-rw-r--r--tests/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorkerTest.java1002
-rw-r--r--tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java13
-rw-r--r--tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java35
-rw-r--r--tests/src/com/android/providers/media/photopicker/util/CloudProviderUtilsTest.java1
-rw-r--r--tests/src/com/android/providers/media/photopicker/util/PickerDbTestUtils.java52
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java812
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2Test.java37
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/SearchSuggestionsProviderTest.java288
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaGroupCursorUtilsTest.java230
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsDatabaseUtilTest.java664
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaSetsDatabaseUtilsTest.java278
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/sqlite/SearchRequestDatabaseUtilTest.java572
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/sqlite/SearchResultsDatabaseUtilTest.java1004
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/sqlite/SearchSuggestionsDatabaseUtilTest.java908
-rw-r--r--tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelPaginationTest.java7
-rw-r--r--tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java6
-rw-r--r--tests/src/com/android/providers/media/photopickersearch/CloudMediaProviderSearch.java134
-rw-r--r--tests/src/com/android/providers/media/photopickersearch/PickerSearchProviderClientTest.java138
-rw-r--r--tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java23
-rw-r--r--tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java1
-rw-r--r--tests/src/com/android/providers/media/util/FileUtilsTest.java23
-rw-r--r--tests/src/com/android/providers/media/util/MimeTypeFixHandlerTest.java222
-rw-r--r--tests/src/com/android/providers/media/util/PermissionUtilsTest.java70
-rw-r--r--tests/src/com/android/providers/media/util/SyntheticPathUtilsTest.java10
62 files changed, 13918 insertions, 287 deletions
diff --git a/tests/Android.bp b/tests/Android.bp
index 61a0e8dcb..88a9708d0 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -226,6 +226,12 @@ android_test {
"SettingsLibSelectorWithWidgetPreference",
"mediaprovider_flags_java_lib",
"flag-junit",
+ "androidx.media3.media3-common",
+ "androidx.media3.media3-transformer",
+ "junit",
+ "android-support-test",
+ "mockito-target-minus-junit4",
+ "platform-compat-test-rules",
],
certificate: "media",
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index e785e929d..acd1a21af 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -22,6 +22,10 @@
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
+ <!-- Permissions required for reading and logging compat changes -->
+ <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
+ <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
+
<uses-permission
android:name="com.android.providers.media.permission.BIND_MEDIA_COGNITION_SERVICE"/>
<uses-permission
@@ -97,6 +101,12 @@
</intent-filter>
</provider>
+ <provider android:name="com.android.providers.media.cloudproviders.SearchProvider"
+ android:authorities="com.android.providers.media.photopicker.tests.cloud_search_provider"
+ android:permission="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS"
+ android:exported="true">
+ </provider>
+
<provider android:name="com.android.providers.media.cloudproviders.CloudProviderPrimary"
android:authorities="com.android.providers.media.photopicker.tests.cloud_primary"
android:permission="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS"
@@ -129,6 +139,12 @@
android:exported="true">
</provider>
+ <provider android:name="com.android.providers.media.photopickersearch.CloudMediaProviderSearch"
+ android:permission="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS"
+ android:authorities="com.android.providers.media.photopicker.tests.cloud_provider_for_search_client"
+ android:exported="true">
+ </provider>
+
<service
android:name=
"com.android.providers.media.stableuris.job.StableUriIdleMaintenanceService"
diff --git a/tests/client/src/com/android/providers/media/client/DownloadProviderTest.java b/tests/client/src/com/android/providers/media/client/DownloadProviderTest.java
index 3deb2593e..ec83e196e 100644
--- a/tests/client/src/com/android/providers/media/client/DownloadProviderTest.java
+++ b/tests/client/src/com/android/providers/media/client/DownloadProviderTest.java
@@ -65,6 +65,16 @@ public class DownloadProviderTest {
deletePublicVolumes();
}
+ @Test
+ public void canCreateOtherPackageExternalFilesDir() {
+ try {
+ // Verifies that downloadProvider can create files dir for other packages.
+ createOtherPackageExternalFilesDir();
+ } catch (Exception e) {
+ throw new UnsupportedOperationException(
+ "Unable to create files dir: \n" + e.getMessage());
+ }
+ }
@Test
public void testCanReadWriteOtherAppPrivateFiles() throws Exception {
@@ -104,11 +114,11 @@ public class DownloadProviderTest {
for (String dir: otherPackageDirsOnSameVolume) {
otherPackageDirs.add(new File(dir));
- final String otherPackageExternalFilesDir = dir + "/files";
- executeShellCommand("mkdir -p " + otherPackageExternalFilesDir + " -m 2770");
+ File otherPackageExternalFilesDir = new File(dir, "/files");
+ otherPackageExternalFilesDir.mkdirs();
// Need to wait for the directory to be created, as the rest of the test depends on
// the dir to be created. A race condition can cause the test to be flaky.
- pollForDirectoryToBeCreated(new File(otherPackageExternalFilesDir));
+ pollForDirectoryToBeCreated(otherPackageExternalFilesDir);
}
}
return otherPackageDirs;
diff --git a/tests/hostsidetests/photopicker/TEST_MAPPING b/tests/hostsidetests/photopicker/TEST_MAPPING
index 2dfcf6c61..7a8de2095 100644
--- a/tests/hostsidetests/photopicker/TEST_MAPPING
+++ b/tests/hostsidetests/photopicker/TEST_MAPPING
@@ -1,7 +1,7 @@
{
- "postsubmit": [
+ "presubmit": [
{
"name": "PhotoPickerHostTestCases"
}
]
-} \ No newline at end of file
+}
diff --git a/tests/hostsidetests/photopicker/src/android/tests/photopicker/CloudProviderHostSideTest.kt b/tests/hostsidetests/photopicker/src/android/tests/photopicker/CloudProviderHostSideTest.kt
index 7dad80ea7..61d54b2a3 100644
--- a/tests/hostsidetests/photopicker/src/android/tests/photopicker/CloudProviderHostSideTest.kt
+++ b/tests/hostsidetests/photopicker/src/android/tests/photopicker/CloudProviderHostSideTest.kt
@@ -39,6 +39,8 @@ import org.junit.runner.RunWith
class CloudProviderHostSideTest : IDeviceTest {
private lateinit var mDevice: ITestDevice
+ private var mInitialCloudProvider: String? = null
+
companion object {
/** The package name of the test APK. */
private const val TEST_PACKAGE = "com.android.photopicker.testcloudmediaproviderapp"
@@ -68,6 +70,15 @@ class CloudProviderHostSideTest : IDeviceTest {
fun setUp() {
// ensure the test APK is enabled before each test by setting it explicitly.
mDevice.executeShellCommand(COMMAND_ENABLE_TEST_APK)
+ // find the initial cloud provider to be reset at the end of the test execution.
+ mInitialCloudProvider = null
+ val result: String = mDevice.executeShellCommand(COMMAND_GET_CLOUD_PROVIDER)
+ val regex = Regex("get_cloud_provider_result=(.*?)}]")
+ val matchResult = regex.find(result)
+ val initialCloudProvider = matchResult?.groupValues?.get(1)
+ if (initialCloudProvider != "null") {
+ mInitialCloudProvider = initialCloudProvider
+ }
}
/**
@@ -87,8 +98,8 @@ class CloudProviderHostSideTest : IDeviceTest {
// Add the test package authority to the allowlist for cloud providers.
mDevice.executeShellCommand(
- "device_config put mediaprovider allowed_cloud_providers "
- + "\"$TEST_CLOUD_PROVIDER_AUTHORITY\""
+ "device_config put mediaprovider allowed_cloud_providers "
+ + "\"$TEST_CLOUD_PROVIDER_AUTHORITY\""
)
// Set the test cloud provider as the current provider.
@@ -126,8 +137,10 @@ class CloudProviderHostSideTest : IDeviceTest {
COMMAND_GET_CLOUD_PROVIDER
)
assertWithMessage("Unexpected cloud provider, expected : null")
- .that(resultForGetCloudProvider
- .contains("{get_cloud_provider_result=null}"))
+ .that(
+ resultForGetCloudProvider
+ .contains("{get_cloud_provider_result=null}")
+ )
.isTrue()
isCloudProviderReset = true
break // Condition met, exit the loop
@@ -144,5 +157,20 @@ class CloudProviderHostSideTest : IDeviceTest {
@Throws(Exception::class)
fun tearDown() {
mDevice.executeShellCommand(COMMAND_ENABLE_TEST_APK)
+
+ // reset initial cloud provider.
+ val setCloudProvider: String
+ if (mInitialCloudProvider != null) {
+ setCloudProvider = " content call --uri content://media --method set_cloud_provider" +
+ " --extra cloud_provider:s:$mInitialCloudProvider"
+ } else {
+ // set to null if no cloud provider was set earlier.
+ setCloudProvider =
+ " content call --uri content://media --method set_cloud_provider"
+ }
+ mDevice.executeShellCommand(setCloudProvider)
+
+ // To enable syncs after test is completed
+ mDevice.executeShellCommand("device_config set_sync_disabled_for_tests none")
}
} \ No newline at end of file
diff --git a/tests/res/raw/android_mime_types b/tests/res/raw/android_mime_types
new file mode 100644
index 000000000..d8a2ce8f5
--- /dev/null
+++ b/tests/res/raw/android_mime_types
@@ -0,0 +1,166 @@
+
+###############################################################################
+#
+# Android-specific MIME type <-> extension mappings
+#
+# Each line below defines a mapping from one MIME type to the first of the
+# listed extensions, and from listed extension back to the MIME type.
+# A mapping overrides any previous mapping _from_ that same MIME type or
+# extension (put() semantics), unless that MIME type / extension is prefixed with '?'
+# (putIfAbsent() semantics).
+#
+#
+###############################################################################
+#
+# EXAMPLES
+#
+# A line of the form:
+#
+# ?mime ext1 ?ext2 ext3
+#
+# affects the current mappings along the lines of the following pseudo code:
+#
+# mimeToExt.putIfAbsent("mime", "ext1");
+# extToMime.put("ext1", "mime");
+# extToMime.putIfAbsent("ext2", "mime");
+# extToMime.put("ext3", "mime");
+#
+# The line:
+#
+# ?text/plain txt
+#
+# leaves any earlier mapping for "text/plain" untouched, or maps that MIME type
+# to the file extension ".txt" if there is no earlier mapping. The line also
+# sets the mapping from file extension ".txt" to be the MIME type "text/plain",
+# regardless of whether a previous mapping existed.
+#
+###############################################################################
+
+
+# File extensions that Android wants to override to point to the given MIME type.
+#
+# After processing a line of the form:
+# ?<mimeType> <extension1> <extension2>
+# If <mimeType> was not already mapped to an extension then it will be
+# mapped to <extension1>.
+# <extension1> and <extension2> are mapped (or remapped) to <mimeType>.
+
+?application/epub+zip epub
+?application/lrc lrc
+?application/pkix-cert cer
+?application/rss+xml rss
+?application/sdp sdp
+?application/smil+xml smil
+?application/ttml+xml ttml dfxp
+?application/vnd.android.haptics.vibration+xml ahv
+?application/vnd.android.ota ota
+?application/vnd.apple.mpegurl m3u8
+?application/vnd.apple.pkpass pkpass
+?application/vnd.apple.pkpasses pkpasses
+?application/vnd.ms-pki.stl stl
+?application/vnd.ms-powerpoint pot
+?application/vnd.ms-wpl wpl
+?application/vnd.stardivision.impress sdp
+?application/vnd.stardivision.writer vor
+?application/vnd.youtube.yt yt
+?application/x-android-drm-fl fl
+?application/x-flac flac
+?application/x-font pcf
+?application/x-mobipocket-ebook prc mobi
+?application/x-mpegurl m3u m3u8
+?application/x-pem-file pem
+?application/x-pkcs12 p12 pfx
+?application/x-subrip srt
+?application/x-webarchive webarchive
+?application/x-webarchive-xml webarchivexml
+?application/x-x509-server-cert crt
+?application/x-x509-user-cert crt
+
+?audio/3gpp 3ga 3gpp
+?audio/aac-adts aac
+?audio/ac3 ac3 a52
+?audio/amr amr
+?audio/x-gsm gsm
+?audio/imelody imy
+?audio/midi rtttl xmf
+?audio/mobile-xmf mxmf
+?audio/mp4 m4a m4b m4p f4a f4b f4p
+?audio/mpegurl m3u
+?audio/sp-midi smf
+?audio/x-matroska mka
+?audio/x-pn-realaudio ra
+?audio/x-mpeg mp3
+?audio/mp3 mp3
+
+?image/bmp bmp
+?image/gif gif
+?image/heic heic
+?image/heic-sequence heics
+?image/heif heif hif
+?image/heif-sequence heifs
+?image/avif avif
+?image/ico cur
+?image/webp webp
+?image/x-adobe-dng dng
+?image/x-fuji-raf raf
+?image/x-icon ico
+?image/x-jg art
+?image/x-nikon-nrw nrw
+?image/x-panasonic-rw2 rw2
+?image/x-pentax-pef pef
+?image/x-samsung-srw srw
+?image/x-sony-arw arw
+
+?text/comma-separated-values csv
+?text/plain diff po
+?text/rtf rtf
+?text/text phps
+?text/xml xml
+?text/x-vcard vcf
+
+?video/3gpp2 3gpp2 3gp2 3g2
+?video/3gpp 3gpp 3gp
+?video/avi avi
+?video/m4v m4v
+?video/mp4 m4v f4v mp4v mpeg4
+?video/mp2p mpeg
+?video/mp2t m2ts mts
+?video/mp2ts ts
+?video/vnd.youtube.yt yt
+?video/x-webex wrf
+
+# Optional additions that should not override any previous mapping.
+
+?application/x-wifi-config ?xml
+?multipart/related mht
+
+# Special cases where Android has a strong opinion about mappings, so we
+# define them very last and make them override in both directions (no "?").
+#
+# Lines here are of the form:
+# <mimeType> <extension1> <extension2> ...
+#
+# After processing each line,
+# <mimeType> is mapped to <extension1>
+# <extension1>, <extension2>, ... are all mapped to <mimeType>
+# This overrides any mappings for this <mimeType> / for these extensions
+# that may have been defined earlier.
+
+application/pgp-signature pgp
+application/x-x509-ca-cert crt der
+audio/aac aac adts adt
+audio/basic snd
+audio/flac flac
+audio/midi rtx
+audio/mpeg mp3 mp2 mp1 mpa m4a m4r
+audio/x-mpegurl m3u m3u8
+image/jpeg jpg
+image/x-ms-bmp bmp
+image/x-photoshop psd
+text/plain txt
+text/x-c++hdr hpp
+text/x-c++src cpp
+video/3gpp 3gpp
+video/mpeg mpeg mpeg2 mpv2 mp2v m2v m2t mpeg1 mpv1 mp1v m1v
+video/quicktime mov
+video/x-matroska mkv \ No newline at end of file
diff --git a/tests/res/raw/corrupted_mime_types b/tests/res/raw/corrupted_mime_types
new file mode 100644
index 000000000..dc34a542a
--- /dev/null
+++ b/tests/res/raw/corrupted_mime_types
@@ -0,0 +1,2320 @@
+###############################################################################
+#
+# Media (MIME) types and the extensions that represent them.
+#
+# The format of this file is a media type on the left and zero or more
+# filename extensions on the right. Programs using this file will map
+# files ending with those extensions to the associated type.
+#
+# This file is part of the "media-types" package. Please report a bug using
+# the "reportbug" command of the "reportbug" package if you would like new
+# types or extensions to be added.
+#
+# The reason that all types are managed by the media-types package instead
+# allowing individual packages to install types in much the same way as they
+# add entries in to the mailcap file is so these types can be referenced by
+# other programs (such as a web server) even if the specific support package
+# for that type is not installed.
+#
+# Users can add their own types if they wish by creating a ".mime.types"
+# file in their home directory. Definitions included there will take
+# precedence over those listed here.
+#
+###############################################################################
+
+application/1d-interleaved-parityfec
+application/3gpdash-qoe-report+xml
+application/3gpp-ims+xml
+application/3gppHal+json
+application/3gppHalForms+json
+application/A2L a2l
+application/ace+cbor
+application/ace+json
+application/activemessage
+application/activity+json
+application/aif+cbor
+application/aif+json
+application/alto-cdni+json
+application/alto-cdnifilter+json
+application/alto-costmap+json
+application/alto-costmapfilter+json
+application/alto-directory+json
+application/alto-endpointcost+json
+application/alto-endpointcostparams+json
+application/alto-endpointprop+json
+application/alto-endpointpropparams+json
+application/alto-error+json
+application/alto-networkmap+json
+application/alto-networkmapfilter+json
+application/alto-propmap+json
+application/alto-propmapparams+json
+application/alto-updatestreamcontrol+json
+application/alto-updatestreamparams+json
+application/AML aml
+application/andrew-inset ez
+application/annodex anx
+application/applefile
+application/at+jwt
+application/ATF atf
+application/ATFX atfx
+application/atom+xml atom
+application/atomcat+xml atomcat
+application/atomdeleted+xml atomdeleted
+application/atomicmail
+application/atomserv+xml atomsrv
+application/atomsvc+xml atomsvc
+application/atsc-dwd+xml dwd
+application/atsc-dynamic-event-message
+application/atsc-held+xml held
+application/atsc-rdt+json
+application/atsc-rsat+xml rsat
+application/ATXML atxml
+application/auth-policy+xml apxml
+application/automationml-aml+xml
+application/automationml-amlx+zip amlx
+application/bacnet-xdd+zip xdd
+application/batch-SMTP
+application/bbolin lin
+application/beep+xml
+application/calendar+json
+application/calendar+xml xcs
+application/call-completion
+application/CALS-1840
+application/captive+json
+application/cbor cbor
+application/cbor-seq
+application/cccex c3ex
+application/ccmp+xml ccmp
+application/ccxml+xml ccxml
+application/cda+xml
+application/CDFX+XML cdfx
+application/cdmi-capability cdmia
+application/cdmi-container cdmic
+application/cdmi-domain cdmid
+application/cdmi-object cdmio
+application/cdmi-queue cdmiq
+application/cdni
+application/CEA cea
+application/cea-2018+xml
+application/cellml+xml cellml cml
+application/cfw
+application/city+json
+application/clr 1clr
+application/clue+xml
+application/clue_info+xml clue
+application/cms cmsc
+application/cnrp+xml
+application/coap-group+json
+application/coap-payload
+application/commonground
+application/concise-problem-details+cbor
+application/conference-info+xml
+application/cose
+application/cose-key
+application/cose-key-set
+application/cose-x509
+application/cpl+xml cpl
+application/csrattrs csrattrs
+application/csta+xml
+application/CSTAdata+xml
+application/csvm+json
+application/cu-seeme cu
+application/cwl cwl
+application/cwl+json cwl.json
+application/cwt
+application/cybercash
+application/dash+xml mpd
+application/dash-patch+xml
+application/dashdelta mpdd
+application/davmount+xml davmount
+application/dca-rft
+application/DCD dcd
+application/dec-dx
+application/dialog-info+xml
+application/dicom dcm
+application/dicom+json
+application/dicom+xml
+application/DII dii
+application/DIT dit
+application/dns
+application/dns+json
+application/dns-message
+application/dots+cbor
+application/dpop+jwt
+application/dskpp+xml xmls
+application/dsptype tsp
+application/dssc+der dssc
+application/dssc+xml xdssc
+application/dvcs dvc
+application/EDI-consent
+application/EDI-X12
+application/EDIFACT
+application/efi efi
+application/elm+json
+application/elm+xml
+application/EmergencyCallData.cap+xml
+application/EmergencyCallData.Comment+xml
+application/EmergencyCallData.Control+xml
+application/EmergencyCallData.DeviceInfo+xml
+application/EmergencyCallData.eCall.MSD
+application/EmergencyCallData.LegacyESN+json
+application/EmergencyCallData.ProviderInfo+xml
+application/EmergencyCallData.ServiceInfo+xml
+application/EmergencyCallData.SubscriberInfo+xml
+application/EmergencyCallData.VEDS+xml
+application/emma+xml emma
+application/emotionml+xml emotionml
+application/encaprtp
+application/epp+xml
+application/epub+zip epub
+application/eshop
+application/example
+application/exi exi
+application/expect-ct-report+json
+application/express exp
+application/fastinfoset finf
+application/fastsoap
+application/fdf fdf
+application/fdt+xml fdt
+application/fhir+json
+application/fhir+xml
+application/fits
+application/flexfec
+application/font-tdpfr pfr
+application/framework-attributes+xml
+application/futuresplash spl
+application/geo+json geojson
+application/geo+json-seq
+application/geopackage+sqlite3 gpkg
+application/geoxacml+xml
+application/gltf-buffer glbin glbuf
+application/gml+xml gml
+application/gzip gz
+application/H224
+application/held+xml
+application/hl7v2+xml
+application/hta hta
+application/http
+application/hyperstudio stk
+application/ibe-key-request+xml
+application/ibe-pkg-reply+xml
+application/ibe-pp-data
+application/iges
+application/im-iscomposing+xml
+application/index
+application/index.cmd
+application/index.obj
+application/index.response
+application/index.vnd
+application/inkml+xml ink inkml
+application/IOTP
+application/ipfix ipfix
+application/ipp
+application/ISUP
+application/its+xml its
+application/java-archive jar
+application/java-serialized-object ser
+application/java-vm class
+application/jf2feed+json
+application/jose
+application/jose+json
+application/jrd+json jrd
+application/jscalendar+json
+application/json json
+application/json-patch+json json-patch
+application/json-seq
+application/jwk+json
+application/jwk-set+json
+application/jwt
+application/kpml-request+xml
+application/kpml-response+xml
+application/ld+json jsonld
+application/lgr+xml lgr
+application/link-format wlnk
+application/linkset
+application/linkset+json
+application/load-control+xml
+application/logout+jwt
+application/lost+xml lostxml
+application/lostsync+xml lostsyncxml
+application/lpf+zip lpf
+application/LXF lxf
+application/m3g m3g
+application/mac-binhex40 hqx
+application/mac-compactpro cpt
+application/macwriteii
+application/mads+xml mads
+application/manifest+json webmanifest
+application/marc mrc
+application/marcxml+xml mrcx
+application/mathematica ma mb
+application/mathml+xml mml
+application/mathml-content+xml
+application/mathml-presentation+xml
+application/mbms-associated-procedure-description+xml
+application/mbms-deregister+xml
+application/mbms-envelope+xml
+application/mbms-msk+xml
+application/mbms-msk-response+xml
+application/mbms-protection-description+xml
+application/mbms-reception-report+xml
+application/mbms-register+xml
+application/mbms-register-response+xml
+application/mbms-schedule+xml
+application/mbms-user-service-description+xml
+application/mbox mbox
+application/media-policy-dataset+xml
+application/mediaservercontrol+xml
+application/media_control+xml
+application/merge-patch+json
+application/metalink4+xml meta4
+application/mets+xml mets
+application/MF4 mf4
+application/mikey
+application/mipc
+application/missing-blocks+cbor-seq
+application/mmt-aei+xml maei
+application/mmt-usd+xml musd
+application/mods+xml mods
+application/moss-keys
+application/moss-signature
+application/mosskey-data
+application/mosskey-request
+application/mp21 m21 mp21
+application/mp4
+application/mpeg4-generic
+application/mpeg4-iod
+application/mpeg4-iod-xmt
+application/mrb-consumer+xml
+application/mrb-publish+xml
+application/msaccess mdb
+application/msc-ivr+xml
+application/msc-mixer+xml
+application/msword doc
+application/mud+json
+application/multipart-core
+application/mxf mxf
+application/n-quads nq
+application/n-triples nt
+application/nasdata
+application/news-checkgroups
+application/news-groupinfo
+application/news-transmission
+application/nlsml+xml
+application/node
+application/nss
+application/oauth-authz-req+jwt
+application/oblivious-dns-message
+application/ocsp-request orq
+application/ocsp-response ors
+application/octet-stream bin deploy msu msp
+application/ODA oda
+application/odm+xml
+application/ODX odx
+application/oebps-package+xml opf
+application/ogg ogx
+application/ohttp-keys
+application/onenote one onetoc2 onetmp onepkg
+application/opc-nodeset+xml
+application/oscore
+application/oxps oxps
+application/p21 p21 stpnc 210 ifc
+application/p21+zip
+application/p2p-overlay+xml relo
+application/parityfec
+application/passport
+application/patch-ops-error+xml
+application/pdf pdf
+application/PDX pdx
+application/pem-certificate-chain pem
+application/pgp-encrypted pgp
+application/pgp-keys asc key
+application/pgp-signature sig
+application/pics-rules prf
+application/pidf+xml
+application/pidf-diff+xml
+application/pkcs10 p10
+application/pkcs12 p12 pfx
+application/pkcs7-mime p7m p7c p7z
+application/pkcs7-signature p7s
+application/pkcs8 p8
+application/pkcs8-encrypted p8e
+application/pkix-attr-cert ac
+application/pkix-cert cer
+application/pkix-crl crl
+application/pkix-pkipath pkipath
+application/pkixcmp pki
+application/pls+xml
+application/poc-settings+xml
+application/postscript ps ai eps epsi epsf eps2 eps3
+application/ppsp-tracker+json
+application/problem+json
+application/problem+xml
+application/provenance+xml provx
+application/prs.alvestrand.titrax-sheet
+application/prs.cww cw cww
+application/prs.cyn
+application/prs.hpub+zip hpub
+application/prs.implied-document+xml
+application/prs.implied-executable
+application/prs.implied-structure
+application/prs.nprend rnd rct
+application/prs.plucker
+application/prs.rdf-xml-crypt rdf-crypt
+application/prs.xsf+xml xsf
+application/pskc+xml pskcxml
+application/pvd+json
+application/QSIG
+application/raptorfec
+application/rdap+json
+application/rdf+xml rdf
+application/reginfo+xml rif
+application/relax-ng-compact-syntax rnc
+application/reputon+json
+application/resource-lists+xml rl
+application/resource-lists-diff+xml rld
+application/rfc+xml rfcxml
+application/riscos
+application/rlmi+xml
+application/rls-services+xml rs
+application/route-apd+xml rapd
+application/route-s-tsid+xml sls
+application/route-usd+xml rusd
+application/rpki-checklist
+application/rpki-ghostbusters gbr
+application/rpki-manifest mft
+application/rpki-publication
+application/rpki-roa roa
+application/rpki-updown
+application/rtf rtf
+application/rtploopback
+application/rtx
+application/samlassertion+xml
+application/samlmetadata+xml
+application/sarif+json sarif sarif.json
+application/sarif-external-properties+json sarif-external-properties sarif-external-properties.json
+application/sbe
+application/sbml+xml
+application/scaip+xml
+application/scim+json scim
+application/scvp-cv-request scq
+application/scvp-cv-response scs
+application/scvp-vp-request spq
+application/scvp-vp-response spp
+application/sdp sdp
+application/secevent+jwt
+application/senml+cbor senmlc
+application/senml+json senml
+application/senml+xml senmlx
+application/senml-etch+cbor senml-etchc
+application/senml-etch+json senml-etchj
+application/senml-exi senmle
+application/sensml+cbor sensmlc
+application/sensml+json sensml
+application/sensml+xml sensmlx
+application/sensml-exi sensmle
+application/sep+xml
+application/sep-exi
+application/session-info
+application/set-payment
+application/set-payment-initiation
+application/set-registration
+application/set-registration-initiation
+application/SGML
+application/sgml-open-catalog soc
+application/shf+xml shf
+application/sieve siv sieve
+application/simple-filter+xml cl
+application/simple-message-summary
+application/simpleSymbolContainer
+application/sipc
+application/slate
+application/smil+xml smil smi sml
+application/smpte336m
+application/soap+fastinfoset
+application/soap+xml
+application/sparql-query rq
+application/sparql-results+xml srx
+application/spdx+json spdx.json
+application/spirits-event+xml
+application/sql sql
+application/srgs gram
+application/srgs+xml grxml
+application/sru+xml sru
+application/ssml+xml ssml
+application/stix+json stix
+application/swid+cbor coswid
+application/swid+xml swidtag
+application/tamp-apex-update tau
+application/tamp-apex-update-confirm auc
+application/tamp-community-update tcu
+application/tamp-community-update-confirm cuc
+application/tamp-error ter
+application/tamp-sequence-adjust tsa
+application/tamp-sequence-adjust-confirm sac
+application/tamp-status-query
+application/tamp-status-response
+application/tamp-update tur
+application/tamp-update-confirm tuc
+application/taxii+json
+application/td+json jsontd
+application/tei+xml tei teiCorpus odd
+application/TETRA_ISI
+application/thraud+xml tfi
+application/timestamp-query tsq
+application/timestamp-reply tsr
+application/timestamped-data tsd
+application/tlsrpt+gzip
+application/tlsrpt+json
+application/tm+json tm.jsonld tm.json jsontm
+application/tnauthlist
+application/token-introspection+jwt
+application/trickle-ice-sdpfrag
+application/trig trig
+application/ttml+xml ttml
+application/tve-trigger
+application/tzif
+application/tzif-leap
+application/ulpfec
+application/urc-grpsheet+xml gsheet
+application/urc-ressheet+xml rsheet
+application/urc-targetdesc+xml td
+application/urc-uisocketdesc+xml uis
+application/vcard+json
+application/vcard+xml
+application/vemmi
+application/vnd.1000minds.decision-model+xml 1km
+application/vnd.1ob ob
+application/vnd.3gpp-prose+xml
+application/vnd.3gpp-prose-pc3a+xml
+application/vnd.3gpp-prose-pc3ach+xml
+application/vnd.3gpp-prose-pc3ch+xml
+application/vnd.3gpp-prose-pc8+xml
+application/vnd.3gpp-v2x-local-service-information
+application/vnd.3gpp.5gnas
+application/vnd.3gpp.access-transfer-events+xml
+application/vnd.3gpp.bsf+xml
+application/vnd.3gpp.crs+xml
+application/vnd.3gpp.current-location-discovery+xml
+application/vnd.3gpp.GMOP+xml
+application/vnd.3gpp.gtpc
+application/vnd.3gpp.interworking-data
+application/vnd.3gpp.lpp
+application/vnd.3gpp.mc-signalling-ear
+application/vnd.3gpp.mcdata-affiliation-command+xml
+application/vnd.3gpp.mcdata-info+xml
+application/vnd.3gpp.mcdata-msgstore-ctrl-request+xml
+application/vnd.3gpp.mcdata-payload
+application/vnd.3gpp.mcdata-regroup+xml
+application/vnd.3gpp.mcdata-service-config+xml
+application/vnd.3gpp.mcdata-signalling
+application/vnd.3gpp.mcdata-ue-config+xml
+application/vnd.3gpp.mcdata-user-profile+xml
+application/vnd.3gpp.mcptt-affiliation-command+xml
+application/vnd.3gpp.mcptt-floor-request+xml
+application/vnd.3gpp.mcptt-info+xml
+application/vnd.3gpp.mcptt-location-info+xml
+application/vnd.3gpp.mcptt-mbms-usage-info+xml
+application/vnd.3gpp.mcptt-regroup+xml
+application/vnd.3gpp.mcptt-service-config+xml
+application/vnd.3gpp.mcptt-signed+xml
+application/vnd.3gpp.mcptt-ue-config+xml
+application/vnd.3gpp.mcptt-ue-init-config+xml
+application/vnd.3gpp.mcptt-user-profile+xml
+application/vnd.3gpp.mcvideo-affiliation-command+xml
+application/vnd.3gpp.mcvideo-info+xml
+application/vnd.3gpp.mcvideo-location-info+xml
+application/vnd.3gpp.mcvideo-mbms-usage-info+xml
+application/vnd.3gpp.mcvideo-regroup+xml
+application/vnd.3gpp.mcvideo-service-config+xml
+application/vnd.3gpp.mcvideo-transmission-request+xml
+application/vnd.3gpp.mcvideo-ue-config+xml
+application/vnd.3gpp.mcvideo-user-profile+xml
+application/vnd.3gpp.mid-call+xml
+application/vnd.3gpp.ngap
+application/vnd.3gpp.pfcp
+application/vnd.3gpp.pic-bw-large plb
+application/vnd.3gpp.pic-bw-small psb
+application/vnd.3gpp.pic-bw-var pvb
+application/vnd.3gpp.s1ap
+application/vnd.3gpp.seal-group-doc+xml
+application/vnd.3gpp.seal-info+xml
+application/vnd.3gpp.seal-location-info+xml
+application/vnd.3gpp.seal-mbms-usage-info+xml
+application/vnd.3gpp.seal-network-QoS-management-info+xml
+application/vnd.3gpp.seal-ue-config-info+xml
+application/vnd.3gpp.seal-unicast-info+xml
+application/vnd.3gpp.seal-user-profile-info+xml
+application/vnd.3gpp.sms
+application/vnd.3gpp.sms+xml
+application/vnd.3gpp.srvcc-ext+xml
+application/vnd.3gpp.SRVCC-info+xml
+application/vnd.3gpp.state-and-event-info+xml
+application/vnd.3gpp.ussd+xml
+application/vnd.3gpp.v2x
+application/vnd.3gpp.vae-info+xml
+application/vnd.3gpp2.bcmcsinfo+xml
+application/vnd.3gpp2.sms sms
+application/vnd.3gpp2.tcap tcap
+application/vnd.3lightssoftware.imagescal imgcal
+application/vnd.3M.Post-it-Notes pwn
+application/vnd.accpac.simply.aso aso
+application/vnd.accpac.simply.imp imp
+application/vnd.acm.addressxfer+json
+application/vnd.acucobol acu
+application/vnd.acucorp atc acutc
+application/vnd.adobe.flash.movie swf
+application/vnd.adobe.formscentral.fcdt fcdt
+application/vnd.adobe.fxp fxp fxpl
+application/vnd.adobe.partial-upload
+application/vnd.adobe.xdp+xml xdp
+application/vnd.aether.imp
+application/vnd.afpc.afplinedata
+application/vnd.afpc.afplinedata-pagedef
+application/vnd.afpc.cmoca-cmresource
+application/vnd.afpc.foca-charset
+application/vnd.afpc.foca-codedfont
+application/vnd.afpc.foca-codepage
+application/vnd.afpc.modca list3820 listafp afp pseg3820
+application/vnd.afpc.modca-formdef
+application/vnd.afpc.modca-mediummap
+application/vnd.afpc.modca-objectcontainer
+application/vnd.afpc.modca-overlay ovl
+application/vnd.afpc.modca-pagesegment psg
+application/vnd.age age
+application/vnd.ah-barcode
+application/vnd.ahead.space ahead
+application/vnd.airzip.filesecure.azf azf
+application/vnd.airzip.filesecure.azs azs
+application/vnd.amadeus+json
+application/vnd.amazon.mobi8-ebook azw3
+application/vnd.americandynamics.acc acc
+application/vnd.amiga.ami ami
+application/vnd.amundsen.maze+xml
+application/vnd.android.ota ota
+application/vnd.android.package-archive apk
+application/vnd.anki apkg
+application/vnd.anser-web-certificate-issue-initiation cii
+application/vnd.anser-web-funds-transfer-initiation fti
+application/vnd.antix.game-component
+application/vnd.apache.arrow.file arrow
+application/vnd.apache.arrow.stream arrows
+application/vnd.apache.thrift.binary
+application/vnd.apache.thrift.compact
+application/vnd.apache.thrift.json
+application/vnd.apexlang apexlang apex
+application/vnd.api+json
+application/vnd.aplextor.warrp+json
+application/vnd.apothekende.reservation+json
+application/vnd.apple.installer+xml dist distz pkg mpkg
+application/vnd.apple.keynote keynote
+application/vnd.apple.mpegurl m3u8
+application/vnd.apple.numbers numbers
+application/vnd.apple.pages pages
+application/vnd.aristanetworks.swi swi
+application/vnd.artisan+json artisan
+application/vnd.artsquare
+application/vnd.astraea-software.iota iota
+application/vnd.audiograph aep
+application/vnd.autopackage package
+application/vnd.avalon+json
+application/vnd.avistar+xml
+application/vnd.balsamiq.bmml+xml bmml
+application/vnd.balsamiq.bmpr bmpr
+application/vnd.banana-accounting ac2
+application/vnd.bbf.usp.error
+application/vnd.bbf.usp.msg
+application/vnd.bbf.usp.msg+json
+application/vnd.bekitzur-stech+json
+application/vnd.belightsoft.lhzd+zip lhzd
+application/vnd.belightsoft.lhzl+zip lhzl
+application/vnd.bint.med-content
+application/vnd.biopax.rdf+xml
+application/vnd.blink-idb-value-wrapper
+application/vnd.blueice.multipass mpm
+application/vnd.bluetooth.ep.oob ep
+application/vnd.bluetooth.le.oob le
+application/vnd.bmi bmi
+application/vnd.bpf
+application/vnd.bpf3
+application/vnd.businessobjects rep
+application/vnd.byu.uapi+json
+application/vnd.cab-jscript
+application/vnd.canon-cpdl
+application/vnd.canon-lips
+application/vnd.capasystems-pg+json
+application/vnd.cendio.thinlinc.clientconf tlclient
+application/vnd.century-systems.tcp_stream
+application/vnd.chemdraw+xml cdxml
+application/vnd.chess-pgn pgn
+application/vnd.chipnuts.karaoke-mmd mmd
+application/vnd.ciedi
+application/vnd.cinderella cdy
+application/vnd.cirpack.isdn-ext
+application/vnd.citationstyles.style+xml csl
+application/vnd.claymore cla
+application/vnd.cloanto.rp9 rp9
+application/vnd.clonk.c4group c4g c4d c4f c4p c4u
+application/vnd.cluetrust.cartomobile-config c11amc
+application/vnd.cluetrust.cartomobile-config-pkg c11amz
+application/vnd.cncf.helm.chart.content.v1.tar+gzip
+application/vnd.cncf.helm.chart.provenance.v1.prov
+application/vnd.cncf.helm.config.v1+json
+application/vnd.coffeescript coffee
+application/vnd.collabio.xodocuments.document xodt
+application/vnd.collabio.xodocuments.document-template xott
+application/vnd.collabio.xodocuments.presentation xodp
+application/vnd.collabio.xodocuments.presentation-template xotp
+application/vnd.collabio.xodocuments.spreadsheet xods
+application/vnd.collabio.xodocuments.spreadsheet-template xots
+application/vnd.collection+json
+application/vnd.collection.doc+json
+application/vnd.collection.next+json
+application/vnd.comicbook+zip cbz
+application/vnd.comicbook-rar cbr
+application/vnd.commerce-battelle icf icd ic0 ic1 ic2 ic3 ic4 ic5 ic6 ic7 ic8
+application/vnd.commonspace csp cst
+application/vnd.contact.cmsg cdbcmsg
+application/vnd.coreos.ignition+json ign ignition
+application/vnd.cosmocaller cmc
+application/vnd.crick.clicker clkx
+application/vnd.crick.clicker.keyboard clkk
+application/vnd.crick.clicker.palette clkp
+application/vnd.crick.clicker.template clkt
+application/vnd.crick.clicker.wordbank clkw
+application/vnd.criticaltools.wbs+xml wbs
+application/vnd.cryptii.pipe+json
+application/vnd.crypto-shade-file ssvc
+application/vnd.cryptomator.encrypted c9r c9s
+application/vnd.cryptomator.vault cryptomator
+application/vnd.ctc-posml pml
+application/vnd.ctct.ws+xml
+application/vnd.cups-pdf
+application/vnd.cups-postscript
+application/vnd.cups-ppd ppd
+application/vnd.cups-raster
+application/vnd.cups-raw
+application/vnd.curl
+application/vnd.cyan.dean.root+xml
+application/vnd.cybank
+application/vnd.cyclonedx+json
+application/vnd.cyclonedx+xml
+application/vnd.d2l.coursepackage1p0+zip
+application/vnd.d3m-dataset
+application/vnd.d3m-problem
+application/vnd.dart dart
+application/vnd.data-vision.rdz rdz
+application/vnd.datalog dl
+application/vnd.datapackage+json
+application/vnd.dataresource+json
+application/vnd.dbf dbf
+application/vnd.debian.binary-package deb ddeb udeb
+application/vnd.dece.data uvf uvvf uvd uvvd
+application/vnd.dece.ttml+xml uvt uvvt
+application/vnd.dece.unspecified uvx uvvx
+application/vnd.dece.zip uvz uvvz
+application/vnd.denovo.fcselayout-link fe_launch
+application/vnd.desmume.movie dsm
+application/vnd.dir-bi.plate-dl-nosuffix
+application/vnd.dm.delegation+xml
+application/vnd.dna dna
+application/vnd.document+json docjson
+application/vnd.dolby.mobile.1
+application/vnd.dolby.mobile.2
+application/vnd.doremir.scorecloud-binary-document scld
+application/vnd.dpgraph dpg mwc dpgraph
+application/vnd.dreamfactory dfac
+application/vnd.drive+json
+application/vnd.dtg.local
+application/vnd.dtg.local.flash fla
+application/vnd.dtg.local.html
+application/vnd.dvb.ait ait
+application/vnd.dvb.dvbisl+xml
+application/vnd.dvb.dvbj
+application/vnd.dvb.esgcontainer
+application/vnd.dvb.ipdcdftnotifaccess
+application/vnd.dvb.ipdcesgaccess
+application/vnd.dvb.ipdcesgaccess2
+application/vnd.dvb.ipdcesgpdd
+application/vnd.dvb.ipdcroaming
+application/vnd.dvb.iptv.alfec-base
+application/vnd.dvb.iptv.alfec-enhancement
+application/vnd.dvb.notif-aggregate-root+xml
+application/vnd.dvb.notif-container+xml
+application/vnd.dvb.notif-generic+xml
+application/vnd.dvb.notif-ia-msglist+xml
+application/vnd.dvb.notif-ia-registration-request+xml
+application/vnd.dvb.notif-ia-registration-response+xml
+application/vnd.dvb.notif-init+xml
+application/vnd.dvb.pfr
+application/vnd.dvb.service svc
+application/vnd.dxr
+application/vnd.dynageo geo
+application/vnd.dzr dzr
+application/vnd.easykaraoke.cdgdownload
+application/vnd.ecdis-update
+application/vnd.ecip.rlp
+application/vnd.eclipse.ditto+json
+application/vnd.ecowin.chart mag
+application/vnd.ecowin.filerequest
+application/vnd.ecowin.fileupdate
+application/vnd.ecowin.series
+application/vnd.ecowin.seriesrequest
+application/vnd.ecowin.seriesupdate
+application/vnd.efi.img
+application/vnd.efi.iso
+application/vnd.eln+zip ELN
+application/vnd.emclient.accessrequest+xml
+application/vnd.enliven nml
+application/vnd.enphase.envoy
+application/vnd.eprints.data+xml
+application/vnd.epson.esf esf
+application/vnd.epson.msf msf
+application/vnd.epson.quickanime qam
+application/vnd.epson.salt slt
+application/vnd.epson.ssf ssf
+application/vnd.ericsson.quickcall qcall qca
+application/vnd.espass-espass+zip espass
+application/vnd.eszigno3+xml es3 et3
+application/vnd.etsi.aoc+xml
+application/vnd.etsi.asic-e+zip asice sce
+application/vnd.etsi.asic-s+zip asics
+application/vnd.etsi.cug+xml
+application/vnd.etsi.iptvcommand+xml
+application/vnd.etsi.iptvdiscovery+xml
+application/vnd.etsi.iptvprofile+xml
+application/vnd.etsi.iptvsad-bc+xml
+application/vnd.etsi.iptvsad-cod+xml
+application/vnd.etsi.iptvsad-npvr+xml
+application/vnd.etsi.iptvservice+xml
+application/vnd.etsi.iptvsync+xml
+application/vnd.etsi.iptvueprofile+xml
+application/vnd.etsi.mcid+xml
+application/vnd.etsi.mheg5
+application/vnd.etsi.overload-control-policy-dataset+xml
+application/vnd.etsi.pstn+xml
+application/vnd.etsi.sci+xml
+application/vnd.etsi.simservs+xml
+application/vnd.etsi.timestamp-token tst
+application/vnd.etsi.tsl+xml
+application/vnd.etsi.tsl.der
+application/vnd.eu.kasparian.car+json carjson
+application/vnd.eudora.data
+application/vnd.evolv.ecig.profile ecigprofile
+application/vnd.evolv.ecig.settings ecig
+application/vnd.evolv.ecig.theme ecigtheme
+application/vnd.exstream-empower+zip mpw
+application/vnd.exstream-package pub
+application/vnd.ezpix-album ez2
+application/vnd.ezpix-package ez3
+application/vnd.f-secure.mobile
+application/vnd.familysearch.gedcom+zip gdz
+application/vnd.fastcopy-disk-image dim
+application/vnd.fdsn.mseed msd mseed
+application/vnd.fdsn.seed seed dataless
+application/vnd.ffsns
+application/vnd.ficlab.flb+zip flb
+application/vnd.filmit.zfc zfc
+application/vnd.fints
+application/vnd.firemonkeys.cloudcell
+application/vnd.FloGraphIt gph
+application/vnd.fluxtime.clip ftc
+application/vnd.font-fontforge-sfd sfd
+application/vnd.framemaker fm
+application/vnd.freelog.comic
+application/vnd.fsc.weblaunch fsc
+application/vnd.fujifilm.fb.docuworks
+application/vnd.fujifilm.fb.docuworks.binder
+application/vnd.fujifilm.fb.docuworks.container
+application/vnd.fujifilm.fb.jfi+xml
+application/vnd.fujitsu.oasys oas
+application/vnd.fujitsu.oasys2 oa2
+application/vnd.fujitsu.oasys3 oa3
+application/vnd.fujitsu.oasysgp fg5
+application/vnd.fujitsu.oasysprs bh2
+application/vnd.fujixerox.ART-EX
+application/vnd.fujixerox.ART4
+application/vnd.fujixerox.ddd ddd
+application/vnd.fujixerox.HBPL
+application/vnd.fut-misnet
+application/vnd.futoin+cbor
+application/vnd.futoin+json
+application/vnd.fuzzysheet fzs
+application/vnd.genomatix.tuxedo txd
+application/vnd.genozip genozip
+application/vnd.gentics.grd+json grd
+application/vnd.gentoo.catmetadata+xml
+application/vnd.gentoo.ebuild ebuild
+application/vnd.gentoo.eclass eclass
+application/vnd.gentoo.gpkg gpkg.tar
+application/vnd.gentoo.manifest
+application/vnd.gentoo.pkgmetadata+xml
+application/vnd.gentoo.xpak xpak
+application/vnd.geogebra.file ggb
+application/vnd.geogebra.slides ggs
+application/vnd.geogebra.tool ggt
+application/vnd.geometry-explorer gex gre
+application/vnd.geonext gxt
+application/vnd.geoplan g2w
+application/vnd.geospace g3w
+application/vnd.gerber
+application/vnd.globalplatform.card-content-mgt
+application/vnd.globalplatform.card-content-mgt-response
+application/vnd.gnu.taler.exchange+json
+application/vnd.gnu.taler.merchant+json
+application/vnd.google-earth.kml+xml kml
+application/vnd.google-earth.kmz kmz
+application/vnd.gov.sk.e-form+xml
+application/vnd.gov.sk.e-form+zip
+application/vnd.gov.sk.xmldatacontainer+xml
+application/vnd.gpxsee.map+xml
+application/vnd.grafeq gqf gqs
+application/vnd.gridmp
+application/vnd.groove-account gac
+application/vnd.groove-help ghf
+application/vnd.groove-identity-message gim
+application/vnd.groove-injector grv
+application/vnd.groove-tool-message gtm
+application/vnd.groove-tool-template tpl
+application/vnd.groove-vcard vcg
+application/vnd.hal+json
+application/vnd.hal+xml hal
+application/vnd.HandHeld-Entertainment+xml zmm
+application/vnd.hbci hbci hbc kom upa pkd bpd
+application/vnd.hc+json
+application/vnd.hcl-bireports
+application/vnd.hdt hdt
+application/vnd.heroku+json
+application/vnd.hhe.lesson-player les
+application/vnd.hp-HPGL hpgl
+application/vnd.hp-hpid hpi hpid
+application/vnd.hp-hps hps
+application/vnd.hp-jlyt jlt
+application/vnd.hp-PCL pcl
+application/vnd.hp-PCLXL
+application/vnd.hsl hsl
+application/vnd.httphone
+application/vnd.hydrostatix.sof-data sfd-hdstx
+application/vnd.hyper+json
+application/vnd.hyper-item+json
+application/vnd.hyperdrive+json
+application/vnd.hzn-3d-crossword
+application/vnd.ibm.electronic-media emm
+application/vnd.ibm.MiniPay mpy
+application/vnd.ibm.rights-management irm
+application/vnd.ibm.secure-container sc
+application/vnd.iccprofile icc icm
+application/vnd.ieee.1905 1905.1
+application/vnd.igloader igl
+application/vnd.imagemeter.folder+zip imf
+application/vnd.imagemeter.image+zip imi
+application/vnd.immervision-ivp ivp
+application/vnd.immervision-ivu ivu
+application/vnd.ims.imsccv1p1 imscc
+application/vnd.ims.imsccv1p2
+application/vnd.ims.imsccv1p3
+application/vnd.ims.lis.v2.result+json
+application/vnd.ims.lti.v2.toolconsumerprofile+json
+application/vnd.ims.lti.v2.toolproxy+json
+application/vnd.ims.lti.v2.toolproxy.id+json
+application/vnd.ims.lti.v2.toolsettings+json
+application/vnd.ims.lti.v2.toolsettings.simple+json
+application/vnd.informedcontrol.rms+xml
+application/vnd.infotech.project
+application/vnd.infotech.project+xml
+application/vnd.innopath.wamp.notification
+application/vnd.insors.igm igm
+application/vnd.intercon.formnet xpw xpx
+application/vnd.intergeo i2g
+application/vnd.intertrust.digibox
+application/vnd.intertrust.nncp
+application/vnd.intu.qbo qbo
+application/vnd.intu.qfx qfx
+application/vnd.ipfs.ipns-record ipns-record
+application/vnd.ipld.car car
+application/vnd.ipld.dag-cbor
+application/vnd.ipld.dag-json
+application/vnd.ipld.raw
+application/vnd.iptc.g2.catalogitem+xml
+application/vnd.iptc.g2.conceptitem+xml
+application/vnd.iptc.g2.knowledgeitem+xml
+application/vnd.iptc.g2.newsitem+xml
+application/vnd.iptc.g2.newsmessage+xml
+application/vnd.iptc.g2.packageitem+xml
+application/vnd.iptc.g2.planningitem+xml
+application/vnd.ipunplugged.rcprofile rcprofile
+application/vnd.irepository.package+xml irp
+application/vnd.is-xpr xpr
+application/vnd.isac.fcs fcs
+application/vnd.iso11783-10+zip
+application/vnd.jam jam
+application/vnd.japannet-directory-service
+application/vnd.japannet-jpnstore-wakeup
+application/vnd.japannet-payment-wakeup
+application/vnd.japannet-registration
+application/vnd.japannet-registration-wakeup
+application/vnd.japannet-setstore-wakeup
+application/vnd.japannet-verification
+application/vnd.japannet-verification-wakeup
+application/vnd.jcp.javame.midlet-rms rms
+application/vnd.jisp jisp
+application/vnd.joost.joda-archive joda
+application/vnd.jsk.isdn-ngn
+application/vnd.kahootz ktz ktr
+application/vnd.kde.karbon karbon
+application/vnd.kde.kchart chrt
+application/vnd.kde.kformula kfo
+application/vnd.kde.kivio flw
+application/vnd.kde.kontour kon
+application/vnd.kde.kpresenter kpr kpt
+application/vnd.kde.kspread ksp
+application/vnd.kde.kword kwd kwt
+application/vnd.kenameaapp htke
+application/vnd.kidspiration kia
+application/vnd.Kinar kne knp sdf
+application/vnd.koan skp skd skm skt
+application/vnd.kodak-descriptor sse
+application/vnd.las las
+application/vnd.las.las+json lasjson
+application/vnd.las.las+xml lasxml
+application/vnd.laszip
+application/vnd.leap+json
+application/vnd.liberty-request+xml
+application/vnd.llamagraphics.life-balance.desktop lbd
+application/vnd.llamagraphics.life-balance.exchange+xml lbe
+application/vnd.logipipe.circuit+zip lcs lca
+application/vnd.loom loom
+application/vnd.lotus-1-2-3 123 wk4 wk3 wk1
+application/vnd.lotus-approach apr vew
+application/vnd.lotus-freelance prz pre
+application/vnd.lotus-notes nsf ntf ndl ns4 ns3 ns2 nsh nsg
+application/vnd.lotus-organizer or3 or2 org
+application/vnd.lotus-screencam scm
+application/vnd.lotus-wordpro lwp sam
+application/vnd.macports.portpkg portpkg
+application/vnd.mapbox-vector-tile mvt
+application/vnd.marlin.drm.actiontoken+xml
+application/vnd.marlin.drm.conftoken+xml
+application/vnd.marlin.drm.license+xml
+application/vnd.marlin.drm.mdcf mdc
+application/vnd.mason+json
+application/vnd.maxar.archive.3tz+zip 3tz
+application/vnd.maxmind.maxmind-db mmdb
+application/vnd.mcd mcd
+application/vnd.mdl mdl
+application/vnd.mdl-mbsdf mbsdf
+application/vnd.medcalcdata mc1
+application/vnd.mediastation.cdkey cdkey
+application/vnd.medicalholodeck.recordxr rxt
+application/vnd.meridian-slingshot
+application/vnd.MFER mwf
+application/vnd.mfmp mfm
+application/vnd.micro+json
+application/vnd.micrografx.flo flo
+application/vnd.micrografx.igx igx
+application/vnd.microsoft.portable-executable
+application/vnd.microsoft.windows.thumbnail-cache
+application/vnd.miele+json
+application/vnd.mif mif
+application/vnd.minisoft-hp3000-save
+application/vnd.mitsubishi.misty-guard.trustweb
+application/vnd.Mobius.DAF daf
+application/vnd.Mobius.DIS dis
+application/vnd.Mobius.MBK mbk
+application/vnd.Mobius.MQY mqy
+application/vnd.Mobius.MSL msl
+application/vnd.Mobius.PLC plc
+application/vnd.Mobius.TXF txf
+application/vnd.modl modl
+application/vnd.mophun.application mpn
+application/vnd.mophun.certificate mpc
+application/vnd.motorola.flexsuite
+application/vnd.motorola.flexsuite.adsi
+application/vnd.motorola.flexsuite.fis
+application/vnd.motorola.flexsuite.gotap
+application/vnd.motorola.flexsuite.kmr
+application/vnd.motorola.flexsuite.ttc
+application/vnd.motorola.flexsuite.wem
+application/vnd.motorola.iprm
+application/vnd.mozilla.xul+xml xul
+application/vnd.ms-3mfdocument 3mf
+application/vnd.ms-artgalry cil
+application/vnd.ms-asf asf
+application/vnd.ms-cab-compressed cab
+application/vnd.ms-excel xls xlm xla xlc xlt xlw
+application/vnd.ms-excel.addin.macroEnabled.12 xlam
+application/vnd.ms-excel.sheet.binary.macroEnabled.12 xlsb
+application/vnd.ms-excel.sheet.macroEnabled.12 xlsm
+application/vnd.ms-excel.template.macroEnabled.12 xltm
+application/vnd.ms-fontobject eot
+application/vnd.ms-htmlhelp chm
+application/vnd.ms-ims ims
+application/vnd.ms-lrm lrm
+application/vnd.ms-office.activeX+xml
+application/vnd.ms-officetheme thmx
+application/vnd.ms-pki.seccat cat
+application/vnd.ms-playready.initiator+xml
+application/vnd.ms-powerpoint ppt pps
+application/vnd.ms-powerpoint.addin.macroEnabled.12 ppam
+application/vnd.ms-powerpoint.presentation.macroEnabled.12 pptm
+application/vnd.ms-powerpoint.slide.macroEnabled.12 sldm
+application/vnd.ms-powerpoint.slideshow.macroEnabled.12 ppsm
+application/vnd.ms-powerpoint.template.macroEnabled.12 potm
+application/vnd.ms-PrintDeviceCapabilities+xml
+application/vnd.ms-PrintSchemaTicket+xml
+application/vnd.ms-project mpp mpt
+application/vnd.ms-tnef tnef tnf
+application/vnd.ms-windows.devicepairing
+application/vnd.ms-windows.nwprinting.oob
+application/vnd.ms-windows.printerpairing
+application/vnd.ms-windows.wsd.oob
+application/vnd.ms-wmdrm.lic-chlg-req
+application/vnd.ms-wmdrm.lic-resp
+application/vnd.ms-wmdrm.meter-chlg-req
+application/vnd.ms-wmdrm.meter-resp
+application/vnd.ms-word.document.macroEnabled.12 docm
+application/vnd.ms-word.template.macroEnabled.12 dotm
+application/vnd.ms-works wcm wdb wks wps
+application/vnd.ms-wpl wpl
+application/vnd.ms-xpsdocument xps
+application/vnd.msa-disk-image msa
+application/vnd.mseq mseq
+application/vnd.msign
+application/vnd.multiad.creator crtr
+application/vnd.multiad.creator.cif cif
+application/vnd.music-niff
+application/vnd.musician mus
+application/vnd.muvee.style msty
+application/vnd.mynfc taglet
+application/vnd.nacamar.ybrid+json
+application/vnd.ncd.control
+application/vnd.ncd.reference
+application/vnd.nearst.inv+json
+application/vnd.nebumind.line nebul line
+application/vnd.nervana entity request bkm kcm
+application/vnd.netfpx
+application/vnd.neurolanguage.nlu nlu
+application/vnd.nimn nimn
+application/vnd.nintendo.nitro.rom nds
+application/vnd.nintendo.snes.rom sfc smc
+application/vnd.nitf nitf
+application/vnd.noblenet-directory nnd
+application/vnd.noblenet-sealer nns
+application/vnd.noblenet-web nnw
+application/vnd.nokia.catalogs
+application/vnd.nokia.conml+wbxml
+application/vnd.nokia.conml+xml
+application/vnd.nokia.iptv.config+xml
+application/vnd.nokia.iSDS-radio-presets
+application/vnd.nokia.landmark+wbxml
+application/vnd.nokia.landmark+xml
+application/vnd.nokia.landmarkcollection+xml
+application/vnd.nokia.n-gage.ac+xml
+application/vnd.nokia.n-gage.data ngdat
+application/vnd.nokia.ncd
+application/vnd.nokia.pcd+wbxml
+application/vnd.nokia.pcd+xml
+application/vnd.nokia.radio-preset rpst
+application/vnd.nokia.radio-presets rpss
+application/vnd.novadigm.EDM edm
+application/vnd.novadigm.EDX edx
+application/vnd.novadigm.EXT ext
+application/vnd.ntt-local.content-share
+application/vnd.ntt-local.file-transfer
+application/vnd.ntt-local.ogw_remote-access
+application/vnd.ntt-local.sip-ta_remote
+application/vnd.ntt-local.sip-ta_tcp_stream
+application/vnd.oasis.opendocument.base odb
+application/vnd.oasis.opendocument.chart odc
+application/vnd.oasis.opendocument.chart-template otc
+application/vnd.oasis.opendocument.formula odf
+application/vnd.oasis.opendocument.formula-template
+application/vnd.oasis.opendocument.graphics odg
+application/vnd.oasis.opendocument.graphics-template otg
+application/vnd.oasis.opendocument.image odi
+application/vnd.oasis.opendocument.image-template oti
+application/vnd.oasis.opendocument.presentation odp
+application/vnd.oasis.opendocument.presentation-template otp
+application/vnd.oasis.opendocument.spreadsheet ods
+application/vnd.oasis.opendocument.spreadsheet-template ots
+application/vnd.oasis.opendocument.text odt
+application/vnd.oasis.opendocument.text-master odm
+application/vnd.oasis.opendocument.text-master-template otm
+application/vnd.oasis.opendocument.text-template ott
+application/vnd.oasis.opendocument.text-web oth
+application/vnd.obn
+application/vnd.ocf+cbor
+application/vnd.oci.image.manifest.v1+json
+application/vnd.oftn.l10n+json
+application/vnd.oipf.contentaccessdownload+xml
+application/vnd.oipf.contentaccessstreaming+xml
+application/vnd.oipf.cspg-hexbinary
+application/vnd.oipf.dae.svg+xml
+application/vnd.oipf.dae.xhtml+xml
+application/vnd.oipf.mippvcontrolmessage+xml
+application/vnd.oipf.pae.gem
+application/vnd.oipf.spdiscovery+xml
+application/vnd.oipf.spdlist+xml
+application/vnd.oipf.ueprofile+xml
+application/vnd.oipf.userprofile+xml
+application/vnd.olpc-sugar xo
+application/vnd.oma-scws-config
+application/vnd.oma-scws-http-request
+application/vnd.oma-scws-http-response
+application/vnd.oma.bcast.associated-procedure-parameter+xml
+application/vnd.oma.bcast.drm-trigger+xml
+application/vnd.oma.bcast.imd+xml
+application/vnd.oma.bcast.ltkm
+application/vnd.oma.bcast.notification+xml
+application/vnd.oma.bcast.provisioningtrigger
+application/vnd.oma.bcast.sgboot
+application/vnd.oma.bcast.sgdd+xml
+application/vnd.oma.bcast.sgdu
+application/vnd.oma.bcast.simple-symbol-container
+application/vnd.oma.bcast.smartcard-trigger+xml
+application/vnd.oma.bcast.sprov+xml
+application/vnd.oma.bcast.stkm
+application/vnd.oma.cab-address-book+xml
+application/vnd.oma.cab-feature-handler+xml
+application/vnd.oma.cab-pcc+xml
+application/vnd.oma.cab-subs-invite+xml
+application/vnd.oma.cab-user-prefs+xml
+application/vnd.oma.dcd
+application/vnd.oma.dcdc
+application/vnd.oma.dd2+xml dd2
+application/vnd.oma.drm.risd+xml
+application/vnd.oma.group-usage-list+xml
+application/vnd.oma.lwm2m+cbor
+application/vnd.oma.lwm2m+json
+application/vnd.oma.lwm2m+tlv
+application/vnd.oma.pal+xml
+application/vnd.oma.poc.detailed-progress-report+xml
+application/vnd.oma.poc.final-report+xml
+application/vnd.oma.poc.groups+xml
+application/vnd.oma.poc.invocation-descriptor+xml
+application/vnd.oma.poc.optimized-progress-report+xml
+application/vnd.oma.push
+application/vnd.oma.scidm.messages+xml
+application/vnd.oma.xcap-directory+xml
+application/vnd.omads-email+xml
+application/vnd.omads-file+xml
+application/vnd.omads-folder+xml
+application/vnd.omaloc-supl-init
+application/vnd.onepager tam
+application/vnd.onepagertamp tamp
+application/vnd.onepagertamx tamx
+application/vnd.onepagertat tat
+application/vnd.onepagertatp tatp
+application/vnd.onepagertatx tatx
+application/vnd.onvif.metadata
+application/vnd.openblox.game+xml obgx
+application/vnd.openblox.game-binary obg
+application/vnd.openeye.oeb oeb
+application/vnd.openofficeorg.extension oxt
+application/vnd.openstreetmap.data+xml osm
+application/vnd.opentimestamps.ots
+application/vnd.openxmlformats-officedocument.custom-properties+xml
+application/vnd.openxmlformats-officedocument.customXmlProperties+xml
+application/vnd.openxmlformats-officedocument.drawing+xml
+application/vnd.openxmlformats-officedocument.drawingml.chart+xml
+application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml
+application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml
+application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml
+application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml
+application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml
+application/vnd.openxmlformats-officedocument.extended-properties+xml
+application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml
+application/vnd.openxmlformats-officedocument.presentationml.comments+xml
+application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml
+application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml
+application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml
+application/vnd.openxmlformats-officedocument.presentationml.presentation pptx
+application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml
+application/vnd.openxmlformats-officedocument.presentationml.presProps+xml
+application/vnd.openxmlformats-officedocument.presentationml.slide sldx
+application/vnd.openxmlformats-officedocument.presentationml.slide+xml
+application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml
+application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml
+application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx
+application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml
+application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo+xml
+application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml
+application/vnd.openxmlformats-officedocument.presentationml.tags+xml
+application/vnd.openxmlformats-officedocument.presentationml.template potx
+application/vnd.openxmlformats-officedocument.presentationml.template.main+xml
+application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx
+application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx
+application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependencies+xml
+application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml
+application/vnd.openxmlformats-officedocument.theme+xml
+application/vnd.openxmlformats-officedocument.themeOverride+xml
+application/vnd.openxmlformats-officedocument.vmlDrawing
+application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml
+application/vnd.openxmlformats-officedocument.wordprocessingml.document docx
+application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml
+application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml
+application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml
+application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml
+application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml
+application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml
+application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml
+application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml
+application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml
+application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx
+application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml
+application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml
+application/vnd.openxmlformats-package.core-properties+xml
+application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml
+application/vnd.openxmlformats-package.relationships+xml
+application/vnd.oracle.resource+json
+application/vnd.orange.indata
+application/vnd.osa.netdeploy ndc
+application/vnd.osgeo.mapguide.package mgp
+application/vnd.osgi.bundle
+application/vnd.osgi.dp dp
+application/vnd.osgi.subsystem esa
+application/vnd.otps.ct-kip+xml
+application/vnd.oxli.countgraph oxlicg
+application/vnd.pagerduty+json
+application/vnd.palm pdb pqa oprc
+application/vnd.panoply plp
+application/vnd.paos.xml
+application/vnd.patentdive dive
+application/vnd.patientecommsdoc
+application/vnd.pawaafile paw
+application/vnd.pcos
+application/vnd.pg.format str
+application/vnd.pg.osasli ei6
+application/vnd.piaccess.application-licence pil
+application/vnd.picsel efif
+application/vnd.pmi.widget wg
+application/vnd.poc.group-advertisement+xml
+application/vnd.pocketlearn plf
+application/vnd.powerbuilder6 pbd
+application/vnd.powerbuilder6-s
+application/vnd.powerbuilder7
+application/vnd.powerbuilder7-s
+application/vnd.powerbuilder75
+application/vnd.powerbuilder75-s
+application/vnd.preminet preminet
+application/vnd.previewsystems.box box vbox
+application/vnd.proteus.magazine mgz
+application/vnd.psfs psfs
+application/vnd.pt.mundusmundi
+application/vnd.publishare-delta-tree qps
+application/vnd.pvi.ptid1 ptid
+application/vnd.pwg-multiplexed
+application/vnd.pwg-xhtml-print+xml
+application/vnd.qualcomm.brew-app-res bar
+application/vnd.quarantainenet
+application/vnd.Quark.QuarkXPress qxd qxt qwd qwt qxl qxb
+application/vnd.quobject-quoxdocument quox quiz
+application/vnd.radisys.moml+xml
+application/vnd.radisys.msml+xml
+application/vnd.radisys.msml-audit+xml
+application/vnd.radisys.msml-audit-conf+xml
+application/vnd.radisys.msml-audit-conn+xml
+application/vnd.radisys.msml-audit-dialog+xml
+application/vnd.radisys.msml-audit-stream+xml
+application/vnd.radisys.msml-conf+xml
+application/vnd.radisys.msml-dialog+xml
+application/vnd.radisys.msml-dialog-base+xml
+application/vnd.radisys.msml-dialog-fax-detect+xml
+application/vnd.radisys.msml-dialog-fax-sendrecv+xml
+application/vnd.radisys.msml-dialog-group+xml
+application/vnd.radisys.msml-dialog-speech+xml
+application/vnd.radisys.msml-dialog-transform+xml
+application/vnd.rainstor.data tree
+application/vnd.rapid
+application/vnd.rar rar
+application/vnd.realvnc.bed bed
+application/vnd.recordare.musicxml mxl
+application/vnd.recordare.musicxml+xml
+application/vnd.RenLearn.rlprint
+application/vnd.resilient.logic rlm reload
+application/vnd.restful+json
+application/vnd.rig.cryptonote cryptonote
+application/vnd.rim.cod cod
+application/vnd.route66.link66+xml link66
+application/vnd.rs-274x
+application/vnd.ruckus.download
+application/vnd.s3sms
+application/vnd.sailingtracker.track st
+application/vnd.sar SAR
+application/vnd.sbm.cid
+application/vnd.sbm.mid2
+application/vnd.scribus scd sla slaz
+application/vnd.sealed.3df s3df
+application/vnd.sealed.csf scsf
+application/vnd.sealed.doc sdoc sdo s1w
+application/vnd.sealed.eml seml sem
+application/vnd.sealed.mht smht smh
+application/vnd.sealed.net
+application/vnd.sealed.ppt sppt s1p
+application/vnd.sealed.tiff stif
+application/vnd.sealed.xls sxls sxl s1e
+application/vnd.sealedmedia.softseal.html stml s1h
+application/vnd.sealedmedia.softseal.pdf spdf spd s1a
+application/vnd.seemail see
+application/vnd.seis+json
+application/vnd.sema sema
+application/vnd.semd semd
+application/vnd.semf semf
+application/vnd.shade-save-file ssv
+application/vnd.shana.informed.formdata ifm
+application/vnd.shana.informed.formtemplate itp
+application/vnd.shana.informed.interchange iif
+application/vnd.shana.informed.package ipk
+application/vnd.shootproof+json
+application/vnd.shopkick+json
+application/vnd.shp shp
+application/vnd.shx shx
+application/vnd.sigrok.session sr
+application/vnd.SimTech-MindMapper twd twds
+application/vnd.siren+json
+application/vnd.smaf mmf
+application/vnd.smart.notebook notebook
+application/vnd.smart.teacher teacher
+application/vnd.smintio.portals.archive sipa
+application/vnd.snesdev-page-table ptrom pt
+application/vnd.software602.filler.form+xml fo
+application/vnd.software602.filler.form-xml-zip zfo
+application/vnd.solent.sdkm+xml sdkm sdkd
+application/vnd.spotfire.dxp dxp
+application/vnd.spotfire.sfs sfs
+application/vnd.sqlite3 sqlite sqlite3
+application/vnd.sss-cod
+application/vnd.sss-dtf
+application/vnd.sss-ntf
+application/vnd.stardivision.calc sdc
+application/vnd.stardivision.chart sds
+application/vnd.stardivision.draw sda
+application/vnd.stardivision.impress sdd
+application/vnd.stardivision.math smf
+application/vnd.stardivision.writer sdw
+application/vnd.stardivision.writer-global sgl
+application/vnd.stepmania.package smzip
+application/vnd.stepmania.stepchart sm
+application/vnd.street-stream
+application/vnd.sun.wadl+xml wadl
+application/vnd.sun.xml.calc sxc
+application/vnd.sun.xml.calc.template stc
+application/vnd.sun.xml.draw sxd
+application/vnd.sun.xml.draw.template std
+application/vnd.sun.xml.impress sxi
+application/vnd.sun.xml.impress.template sti
+application/vnd.sun.xml.math sxm
+application/vnd.sun.xml.writer sxw
+application/vnd.sun.xml.writer.global sxg
+application/vnd.sun.xml.writer.template stw
+application/vnd.sus-calendar sus susp
+application/vnd.svd
+application/vnd.swiftview-ics
+application/vnd.sybyl.mol2 ml2 mol2 sy2
+application/vnd.sycle+xml scl
+application/vnd.syft+json syft.json
+application/vnd.symbian.install sis
+application/vnd.syncml+xml xsm
+application/vnd.syncml.dm+wbxml bdm
+application/vnd.syncml.dm+xml xdm
+application/vnd.syncml.dm.notification
+application/vnd.syncml.dmddf+wbxml
+application/vnd.syncml.dmddf+xml ddf
+application/vnd.syncml.dmtnds+wbxml
+application/vnd.syncml.dmtnds+xml
+application/vnd.syncml.ds.notification
+application/vnd.tableschema+json
+application/vnd.tao.intent-module-archive tao
+application/vnd.tcpdump.pcap pcap cap dmp
+application/vnd.theqvd qvd
+application/vnd.think-cell.ppttc+json ppttc
+application/vnd.tmd.mediaflex.api+xml
+application/vnd.tml vfr viaframe
+application/vnd.tmobile-livetv tmo
+application/vnd.tri.onesource
+application/vnd.trid.tpt tpt
+application/vnd.triscape.mxs mxs
+application/vnd.trueapp tra
+application/vnd.truedoc
+application/vnd.ubisoft.webplayer
+application/vnd.ufdl ufdl ufd frm
+application/vnd.uiq.theme utz
+application/vnd.umajin umj
+application/vnd.unity unityweb
+application/vnd.uoml+xml uoml uo
+application/vnd.uplanet.alert
+application/vnd.uplanet.alert-wbxml
+application/vnd.uplanet.bearer-choice
+application/vnd.uplanet.bearer-choice-wbxml
+application/vnd.uplanet.cacheop
+application/vnd.uplanet.cacheop-wbxml
+application/vnd.uplanet.channel
+application/vnd.uplanet.channel-wbxml
+application/vnd.uplanet.list
+application/vnd.uplanet.list-wbxml
+application/vnd.uplanet.listcmd
+application/vnd.uplanet.listcmd-wbxml
+application/vnd.uplanet.signal
+application/vnd.uri-map urim urimap
+application/vnd.valve.source.material vmt
+application/vnd.vcx vcx
+application/vnd.vd-study mxi study-inter model-inter
+application/vnd.vectorworks vwx
+application/vnd.vel+json
+application/vnd.verimatrix.vcas
+application/vnd.veritone.aion+json aion vtnstd
+application/vnd.veryant.thin istc isws
+application/vnd.ves.encrypted VES
+application/vnd.vidsoft.vidconference vsc
+application/vnd.visio vsd vst vsw vss
+application/vnd.visionary vis
+application/vnd.vividence.scriptfile
+application/vnd.vsf vsf
+application/vnd.wap.sic sic
+application/vnd.wap.slc slc
+application/vnd.wap.wbxml wbxml
+application/vnd.wap.wmlc wmlc
+application/vnd.wap.wmlscriptc wmlsc
+application/vnd.wasmflow.wafl wafl
+application/vnd.webturbo wtb
+application/vnd.wfa.dpp
+application/vnd.wfa.p2p p2p
+application/vnd.wfa.wsc wsc
+application/vnd.windows.devicepairing
+application/vnd.wmc wmc
+application/vnd.wmf.bootstrap
+application/vnd.wolfram.mathematica nb
+application/vnd.wolfram.mathematica.package m
+application/vnd.wolfram.player nbp
+application/vnd.wordlift
+application/vnd.wordperfect wpd
+application/vnd.wqd wqd
+application/vnd.wrq-hp3000-labelled
+application/vnd.wt.stf stf
+application/vnd.wv.csp+wbxml wv
+application/vnd.wv.csp+xml
+application/vnd.wv.ssp+xml
+application/vnd.xacml+json
+application/vnd.xara xar
+application/vnd.xfdl xfdl xfd
+application/vnd.xfdl.webform
+application/vnd.xmi+xml
+application/vnd.xmpie.cpkg cpkg
+application/vnd.xmpie.dpkg dpkg
+application/vnd.xmpie.plan
+application/vnd.xmpie.ppkg ppkg
+application/vnd.xmpie.xlim xlim
+application/vnd.yamaha.hv-dic hvd
+application/vnd.yamaha.hv-script hvs
+application/vnd.yamaha.hv-voice hvp
+application/vnd.yamaha.openscoreformat osf
+application/vnd.yamaha.openscoreformat.osfpvg+xml
+application/vnd.yamaha.remote-setup
+application/vnd.yamaha.smaf-audio saf
+application/vnd.yamaha.smaf-phrase spf
+application/vnd.yamaha.through-ngn
+application/vnd.yamaha.tunnel-udpencap
+application/vnd.yaoweme yme
+application/vnd.yellowriver-custom-menu cmp
+application/vnd.zul zir zirz
+application/vnd.zzazz.deck+xml zaz
+application/voicexml+xml vxml
+application/voucher-cms+json vcj
+application/vq-rtcpxr
+application/wasm wasm
+application/watcherinfo+xml wif
+application/webpush-options+json
+application/whoispp-query
+application/whoispp-response
+application/widget wgt
+application/wita
+application/wordperfect5.1
+application/wsdl+xml wsdl
+application/wspolicy+xml wspolicy
+application/x-123 wk
+application/x-7z-compressed 7z
+application/x-abiword abw
+application/x-apple-diskimage dmg
+application/x-bcpio bcpio
+application/x-bittorrent torrent
+application/x-cdf cdf cda
+application/x-cdlink vcd
+application/x-comsol mph
+application/x-cpio cpio
+application/x-csh csh
+application/x-director dcr dir dxr
+application/x-doom wad
+application/x-dvi dvi
+application/x-font pfa pfb gsf
+application/x-font-pcf pcf pcf.Z
+application/x-freemind mm
+application/x-ganttproject gan
+application/x-gnumeric gnumeric
+application/x-go-sgf sgf
+application/x-graphing-calculator gcf
+application/x-gtar gtar
+application/x-gtar-compressed tgz taz
+application/x-hdf hdf
+application/x-hwp hwp
+application/x-ica ica
+application/x-info info
+application/x-internet-signup ins isp
+application/x-iphone iii
+application/x-iso9660-image iso
+application/x-java-jnlp-file jnlp
+application/x-jmol jmz
+application/x-killustrator kil
+application/x-latex latex
+application/x-lha lha
+application/x-lyx lyx
+application/x-lzh lzh
+application/x-lzx lzx
+application/x-maker frm maker frame fm fb book fbdoc
+application/x-ms-wmd wmd
+application/x-ms-wmz wmz
+application/x-msdos-program com exe bat dll
+application/x-msi msi
+application/x-netcdf nc
+application/x-ns-proxy-autoconfig pac
+application/x-nwc nwc
+application/x-object o
+application/x-oz-application oza
+application/x-pkcs7-certreqresp p7r
+application/x-pki-message
+application/x-python-code pyc pyo
+application/x-qgis qgs shp shx
+application/x-quicktimeplayer qtl
+application/x-rdp rdp
+application/x-redhat-package-manager rpm
+application/x-rss+xml rss
+application/x-ruby rb
+application/x-scilab sci sce
+application/x-scilab-xcos xcos
+application/x-sh sh
+application/x-shar shar
+application/x-silverlight scr
+application/x-stuffit sit sitx
+application/x-sv4cpio sv4cpio
+application/x-sv4crc sv4crc
+application/x-tar tar
+application/x-tcl tcl
+application/x-tex-gf gf
+application/x-tex-pk pk
+application/x-texinfo texinfo texi
+application/x-trash ~ % bak old sik
+application/x-troff-man man
+application/x-troff-me me
+application/x-troff-ms ms
+application/x-ustar ustar
+application/x-wais-source src
+application/x-wingz wz
+application/x-www-form-urlencoded
+application/x-x509-ca-cert crt
+application/x-x509-ca-ra-cert
+application/x-x509-next-ca-cert
+application/x-xfig fig
+application/x-xpinstall xpi
+application/x-xz xz
+application/x400-bp
+application/xacml+xml
+application/xcap-att+xml xav
+application/xcap-caps+xml xca
+application/xcap-diff+xml xdf
+application/xcap-el+xml xel
+application/xcap-error+xml xer
+application/xcap-ns+xml xns
+application/xcon-conference-info+xml
+application/xcon-conference-info-diff+xml
+application/xenc+xml
+application/xfdf xfdf
+application/xhtml+xml xhtml xhtm xht
+application/xliff+xml xlf
+application/xml xml
+application/xml-dtd dtd mod
+application/xml-external-parsed-entity ent
+application/xml-patch+xml
+application/xmpp+xml
+application/xop+xml xop
+application/xslt+xml xsl xslt
+application/xspf+xml xspf
+application/xv+xml mxml xhvml xvml xvm
+application/yaml yaml yml
+application/yang yang
+application/yang-data+cbor
+application/yang-data+json
+application/yang-data+xml
+application/yang-patch+json
+application/yang-patch+xml
+application/yin+xml yin
+application/zip zip
+application/zlib
+application/zstd zst
+
+audio/1d-interleaved-parityfec
+audio/32kadpcm 726
+audio/3gpp
+audio/3gpp2
+audio/aac adts aac ass
+audio/ac3 ac3
+audio/AMR amr AMR
+audio/AMR-WB awb AWB
+audio/amr-wb+
+audio/annodex axa
+audio/aptx
+audio/asc acn
+audio/ATRAC-ADVANCED-LOSSLESS aal
+audio/ATRAC-X atx
+audio/ATRAC3 at3 aa3 omg
+audio/basic au snd
+audio/BV16
+audio/BV32
+audio/clearmode
+audio/CN
+audio/csound csd orc sco
+audio/DAT12
+audio/dls dls
+audio/dsr-es201108
+audio/dsr-es202050
+audio/dsr-es202211
+audio/dsr-es202212
+audio/DV
+audio/DVI4
+audio/eac3
+audio/encaprtp
+audio/EVRC evc
+audio/EVRC-QCP qcp QCP
+audio/EVRC0
+audio/EVRC1
+audio/EVRCB evb
+audio/EVRCB0
+audio/EVRCB1
+audio/EVRCNW enw
+audio/EVRCNW0
+audio/EVRCNW1
+audio/EVRCWB evw
+audio/EVRCWB0
+audio/EVRCWB1
+audio/EVS
+audio/example
+audio/flac flac
+audio/flexfec
+audio/fwdred
+audio/G711-0
+audio/G719
+audio/G722
+audio/G7221
+audio/G723
+audio/G726-16
+audio/G726-24
+audio/G726-32
+audio/G726-40
+audio/G728
+audio/G729
+audio/G7291
+audio/G729D
+audio/G729E
+audio/GSM
+audio/GSM-EFR
+audio/GSM-HR-08
+audio/iLBC lbc
+audio/ip-mr_v2.5
+audio/L16 l16
+audio/L20
+audio/L24
+audio/L8
+audio/LPC
+audio/MELP
+audio/MELP1200
+audio/MELP2400
+audio/MELP600
+audio/mhas mhas
+audio/mobile-xmf mxmf
+audio/mp4 m4a
+audio/MP4A-LATM
+audio/MPA
+audio/mpa-robust
+audio/mpeg mpga mpega mp1 mp2 mp3
+audio/mpeg4-generic
+audio/mpegurl m3u
+audio/ogg oga ogg opus spx
+audio/opus
+audio/parityfec
+audio/PCMA
+audio/PCMA-WB
+audio/PCMU
+audio/PCMU-WB
+audio/prs.sid sid psid
+audio/QCELP
+audio/raptorfec
+audio/RED
+audio/rtp-enc-aescm128
+audio/rtp-midi
+audio/rtploopback
+audio/rtx
+audio/scip
+audio/SMV smv
+audio/SMV-QCP
+audio/SMV0
+audio/sofa sofa
+audio/sp-midi mid
+audio/speex
+audio/t140c
+audio/t38
+audio/telephone-event
+audio/TETRA_ACELP
+audio/TETRA_ACELP_BB
+audio/tone
+audio/TSVCIS
+audio/UEMCLIP
+audio/ulpfec
+audio/usac loas xhe
+audio/VDVI
+audio/VMR-WB
+audio/vnd.3gpp.iufp
+audio/vnd.4SB
+audio/vnd.audiokoz koz
+audio/vnd.CELP
+audio/vnd.cisco.nse
+audio/vnd.cmles.radio-events
+audio/vnd.cns.anp1
+audio/vnd.cns.inf1
+audio/vnd.dece.audio uva uvva
+audio/vnd.digital-winds eol
+audio/vnd.dlna.adts
+audio/vnd.dolby.heaac.1
+audio/vnd.dolby.heaac.2
+audio/vnd.dolby.mlp mlp
+audio/vnd.dolby.mps
+audio/vnd.dolby.pl2
+audio/vnd.dolby.pl2x
+audio/vnd.dolby.pl2z
+audio/vnd.dolby.pulse.1
+audio/vnd.dra
+audio/vnd.dts dts
+audio/vnd.dts.hd dtshd
+audio/vnd.dts.uhd
+audio/vnd.dvb.file
+audio/vnd.everad.plj plj
+audio/vnd.hns.audio
+audio/vnd.lucent.voice lvp
+audio/vnd.ms-playready.media.pya pya
+audio/vnd.nokia.mobile-xmf
+audio/vnd.nortel.vbk vbk
+audio/vnd.nuera.ecelp4800 ecelp4800
+audio/vnd.nuera.ecelp7470 ecelp7470
+audio/vnd.nuera.ecelp9600 ecelp9600
+audio/vnd.octel.sbc
+audio/vnd.presonus.multitrack multitrack
+audio/vnd.rhetorex.32kadpcm
+audio/vnd.rip rip
+audio/vnd.sealedmedia.softseal.mpeg smp3 smp s1m
+audio/vnd.vmx.cvsd
+audio/vorbis
+audio/vorbis-config
+audio/x-aiff aif aiff aifc
+audio/x-gsm gsm
+audio/x-ms-wax wax
+audio/x-ms-wma wma
+audio/x-pn-realaudio ra rm ram
+audio/x-scpls pls
+audio/x-sd2 sd2
+audio/x-wav wav
+
+chemical/x-alchemy alc
+chemical/x-cache cac cache
+chemical/x-cache-csf csf
+chemical/x-cactvs-binary cbin cascii ctab
+chemical/x-cdx cdx
+chemical/x-cerius
+chemical/x-chem3d c3d
+chemical/x-chemdraw chm
+chemical/x-cif cif
+chemical/x-cmdf cmdf
+chemical/x-cml cml
+chemical/x-compass cpa
+chemical/x-crossfire bsd
+chemical/x-csml csml csm
+chemical/x-ctx ctx
+chemical/x-cxf cxf cef
+#chemical/x-daylight-smiles smi
+chemical/x-embl-dl-nucleotide emb embl
+chemical/x-galactic-spc spc
+chemical/x-gamess-input inp gam gamin
+chemical/x-gaussian-checkpoint fch fchk
+chemical/x-gaussian-cube cub
+chemical/x-gaussian-input gau gjc gjf
+chemical/x-gaussian-log gal
+chemical/x-gcg8-sequence gcg
+chemical/x-genbank gen
+chemical/x-hin hin
+chemical/x-isostar istr ist
+chemical/x-jcamp-dx jdx dx
+chemical/x-kinemage kin
+chemical/x-macmolecule mcm
+chemical/x-macromodel-input mmod
+chemical/x-mdl-molfile mol
+chemical/x-mdl-rdfile rd
+chemical/x-mdl-rxnfile rxn
+chemical/x-mdl-sdfile sd sdf
+chemical/x-mdl-tgf tgf
+#chemical/x-mif mif
+chemical/x-mmcif mcif
+chemical/x-molconn-Z b
+chemical/x-mopac-graph gpt
+chemical/x-mopac-input mop mopcrt mpc zmt
+chemical/x-mopac-out moo
+chemical/x-mopac-vib mvb
+chemical/x-ncbi-asn1 asn
+chemical/x-ncbi-asn1-ascii prt
+chemical/x-ncbi-asn1-binary val aso
+chemical/x-ncbi-asn1-spec asn
+chemical/x-pdb pdb
+chemical/x-rosdal ros
+chemical/x-swissprot sw
+chemical/x-vamas-iso14976 vms
+chemical/x-vmd vmd
+chemical/x-xtel xtel
+chemical/x-xyz xyz
+
+font/collection ttc
+font/otf otf
+font/sfnt
+font/ttf ttf
+font/woff woff
+font/woff2 woff2
+
+image/aces exr
+image/apng apng
+image/avci avci
+image/avcs avcs
+image/avif avif hif
+image/bmp bmp
+image/cgm cgm
+image/dicom-rle drle
+image/dpx dpx
+image/emf emf
+image/example
+image/fits fits fit fts
+image/g3fax
+image/gif gif
+image/heic heic
+image/heic-sequence heics
+image/heif heif
+image/heif-sequence heifs
+image/hej2k hej2
+image/hsj2 hsj2
+image/ief ief
+image/j2c j2c J2C j2k J2K
+image/jls jls
+image/jp2 jp2 jpg2
+image/jpeg jpeg jpg jpe jfif
+image/jph jph
+image/jphc jhc jphc
+image/jpm jpm jpgm
+image/jpx jpx jpf
+image/jxl jxl
+image/jxr jxr
+image/jxrA jxra
+image/jxrS jxrs
+image/jxs jxs
+image/jxsc jxsc
+image/jxsi jxsi
+image/jxss jxss
+image/ktx ktx
+image/ktx2 ktx2
+image/naplps
+image/png png
+image/prs.btif btif btf
+image/prs.pti pti
+image/pwg-raster
+image/svg+xml svg svgz
+image/t38
+image/tiff tiff tif
+image/tiff-fx tfx
+image/vnd.adobe.photoshop psd
+image/vnd.airzip.accelerator.azv azv
+image/vnd.cns.inf2
+image/vnd.dece.graphic uvi uvvi uvg uvvg
+image/vnd.djvu djvu djv
+image/vnd.dvb.subtitle
+image/vnd.dwg dwg
+image/vnd.dxf dxf
+image/vnd.fastbidsheet fbs
+image/vnd.fpx fpx
+image/vnd.fst fst
+image/vnd.fujixerox.edmics-mmr mmr
+image/vnd.fujixerox.edmics-rlc rlc
+image/vnd.globalgraphics.pgb PGB pgb
+image/vnd.microsoft.icon ico
+image/vnd.mix
+image/vnd.ms-modi mdi
+image/vnd.net-fpx
+image/vnd.pco.b16 b16
+image/vnd.radiance hdr rgbe xyze
+image/vnd.sealed.png spng spn s1n
+image/vnd.sealedmedia.softseal.gif sgif sgi s1g
+image/vnd.sealedmedia.softseal.jpg sjpg sjp s1j
+image/vnd.svf
+image/vnd.tencent.tap tap
+image/vnd.valve.source.texture vtf
+image/vnd.wap.wbmp wbmp
+image/vnd.xiff xif
+image/vnd.zbrush.pcx pcx
+image/webp webp
+image/wmf wmf
+image/x-canon-cr2 cr2
+image/x-canon-crw crw
+image/x-cmu-raster ras
+image/x-coreldraw cdr
+image/x-coreldrawpattern pat
+image/x-coreldrawtemplate cdt
+image/x-corelphotopaint cpt
+image/x-epson-erf erf
+image/x-jg art
+image/x-jng jng
+image/x-nikon-nef nef
+image/x-olympus-orf orf
+image/x-portable-anymap pnm
+image/x-portable-bitmap pbm
+image/x-portable-graymap pgm
+image/x-portable-pixmap ppm
+image/x-rgb rgb
+image/x-xbitmap xbm
+image/x-xcf xcf
+image/x-xpixmap xpm
+image/x-xwindowdump xwd
+
+inode/blockdevice
+inode/chardevice
+inode/directory
+inode/directory-locked
+inode/fifo
+inode/socket
+
+message/bhttp
+message/CPIM
+message/delivery-status
+message/disposition-notification
+message/example
+message/external-body
+message/feedback-report
+message/global u8msg
+message/global-delivery-status u8dsn
+message/global-disposition-notification u8mdn
+message/global-headers u8hdr
+message/http
+message/imdn+xml
+message/mls
+message/ohttp-req
+message/ohttp-res
+message/partial
+message/rfc822 eml mail art
+message/s-http
+message/sip
+message/sipfrag
+message/tracking-status
+message/vnd.wfa.wsc
+
+model/3mf
+model/e57
+model/example
+model/gltf+json gltf
+model/gltf-binary glb
+model/iges igs iges
+model/JT jt
+model/mesh msh mesh silo
+model/mtl mtl
+model/obj obj
+model/prc prc
+model/step stp step
+model/step+xml stpx
+model/step+zip stpz
+model/step-xml+zip stpxz
+model/stl stl
+model/u3d u3d
+model/vnd.bary bary
+model/vnd.cld cld
+model/vnd.collada+xml dae
+model/vnd.dwf dwf
+model/vnd.flatland.3dml
+model/vnd.gdl gdl gsm win dor lmp rsm msm ism
+model/vnd.gs-gdl
+model/vnd.gtw gtw
+model/vnd.moml+xml moml
+model/vnd.mts mts
+model/vnd.opengex ogex
+model/vnd.parasolid.transmit.binary x_b xmt_bin
+model/vnd.parasolid.transmit.text x_t xmt_txt
+model/vnd.pytha.pyox pyox
+model/vnd.rosette.annotated-data-model
+model/vnd.sap.vds vds
+model/vnd.usda usda
+model/vnd.usdz+zip usdz
+model/vnd.valve.source.compiled-map bsp
+model/vnd.vtu vtu
+model/vrml wrl vrm vrml
+model/x3d+fastinfoset x3db
+model/x3d+xml x3d x3dz
+model/x3d-vrml x3dv x3dvz
+
+multipart/alternative
+multipart/appledouble
+multipart/byteranges
+multipart/digest
+multipart/encrypted
+multipart/example
+multipart/form-data
+multipart/header-set
+multipart/mixed
+multipart/multilingual
+multipart/parallel
+multipart/related
+multipart/report
+multipart/signed
+multipart/vnd.bint.med-plus bmed
+multipart/voice-message vpm
+multipart/x-mixed-replace
+
+text/1d-interleaved-parityfec
+text/cache-manifest appcache manifest
+text/calendar ics ifb
+text/cql CQL
+text/cql-extension
+text/cql-identifier
+text/css css
+text/csv csv
+text/csv-schema csvs
+text/dns soa zone
+text/encaprtp
+text/enriched
+text/example
+text/fhirpath
+text/flexfec
+text/fwdred
+text/gff3 gff3
+text/grammar-ref-list
+text/hl7v2
+text/html html htm shtml
+text/javascript es js mjs
+text/jcr-cnd cnd
+text/markdown md markdown
+text/mizar miz
+text/n3 n3
+text/parameters
+text/parityfec
+text/plain txt text pot brf srt
+text/provenance-notation provn
+text/prs.fallenstein.rst rst
+text/prs.lines.tag tag dsc
+text/prs.prop.logic
+text/raptorfec
+text/RED
+text/rfc822-headers
+text/rtf
+text/rtp-enc-aescm128
+text/rtploopback
+text/rtx
+text/SGML sgml sgm
+text/shaclc shaclc shc
+text/shex shex
+text/spdx spdx
+text/strings
+text/t140
+text/tab-separated-values tsv
+text/texmacs tm
+text/troff t tr roff
+text/turtle ttl
+text/ulpfec
+text/uri-list uris uri
+text/vcard vcf vcard
+text/vnd.a a
+text/vnd.abc abc
+text/vnd.ascii-art ascii
+text/vnd.curl curl
+text/vnd.debian.copyright copyright
+text/vnd.DMClientScript dms
+text/vnd.dvb.subtitle
+text/vnd.esmertec.theme-descriptor jtd
+text/vnd.exchangeable VFK
+text/vnd.familysearch.gedcom ged
+text/vnd.ficlab.flt flt
+text/vnd.fly fly
+text/vnd.fmi.flexstor flx
+text/vnd.gml
+text/vnd.graphviz gv dot
+text/vnd.hans hans
+text/vnd.hgl hgl
+text/vnd.in3d.3dml 3dml 3dm
+text/vnd.in3d.spot spot spo
+text/vnd.IPTC.NewsML
+text/vnd.IPTC.NITF
+text/vnd.latex-z
+text/vnd.motorola.reflex
+text/vnd.ms-mediapackage mpf
+text/vnd.net2phone.commcenter.command ccc
+text/vnd.radisys.msml-basic-layout
+text/vnd.senx.warpscript mc2
+text/vnd.sosi sos
+text/vnd.sun.j2me.app-descriptor jad
+text/vnd.trolltech.linguist ts
+text/vnd.wap.si si
+text/vnd.wap.sl sl
+text/vnd.wap.wml wml
+text/vnd.wap.wmlscript wmls
+text/vtt vtt
+text/wgsl wgsl
+text/x-bibtex bib
+text/x-boo boo
+text/x-c++hdr h++ hpp hxx hh
+text/x-c++src c++ cpp cxx cc
+text/x-chdr h
+text/x-component htc
+text/x-csh csh
+text/x-csrc c
+text/x-diff diff patch
+text/x-dsrc d
+text/x-haskell hs
+text/x-java java
+text/x-lilypond ly
+text/x-literate-haskell lhs
+text/x-moc moc
+text/x-pascal p pas
+text/x-pcs-gcd gcd
+text/x-perl pl pm
+text/x-python py
+text/x-scala scala
+text/x-setext etx
+text/x-sfv sfv
+text/x-sh sh
+text/x-tcl tcl tk
+text/x-tex tex ltx sty cls
+text/x-vcalendar vcs
+text/xml
+text/xml-dtd
+text/xml-external-parsed-entity
+
+video/1d-interleaved-parityfec
+video/3gpp
+video/3gpp-tt
+video/3gpp2
+video/annodex axv
+video/AV1
+video/BMPEG
+video/BT656
+video/CelB
+video/DV
+video/dv dif dv
+video/encaprtp
+video/example
+video/FFV1
+video/flexfec
+video/fli fli
+video/gl gl
+video/H261
+video/H263
+video/H263-1998
+video/H263-2000
+video/H264
+video/H264-RCDO
+video/H264-SVC
+video/H265
+video/H266
+video/iso.segment m4s
+video/JPEG
+video/jpeg2000
+video/jxsv
+video/mj2 mj2 mjp2
+video/MP1S
+video/MP2P
+video/MP2T
+video/mp4 mp4 mpg4 m4v
+video/MP4V-ES
+video/mpeg mpeg mpg mpe m1v m2v
+video/mpeg4-generic
+video/MPV
+video/nv
+video/ogg ogv
+video/parityfec
+video/pointer
+video/quicktime qt mov
+video/raptorfec
+video/raw
+video/rtp-enc-aescm128
+video/rtploopback
+video/rtx
+video/scip
+video/smpte291
+video/SMPTE292M
+video/ulpfec
+video/vc1
+video/vc2
+video/vnd.CCTV
+video/vnd.dece.hd uvh uvvh
+video/vnd.dece.mobile uvm uvvm
+video/vnd.dece.mp4 uvu uvvu
+video/vnd.dece.pd uvp uvvp
+video/vnd.dece.sd uvs uvvs
+video/vnd.dece.video uvv uvvv
+video/vnd.directv.mpeg
+video/vnd.directv.mpeg-tts
+video/vnd.dlna.mpeg-tts
+video/vnd.dvb.file dvb
+video/vnd.fvt fvt
+video/vnd.hns.video
+video/vnd.iptvforum.1dparityfec-1010
+video/vnd.iptvforum.1dparityfec-2005
+video/vnd.iptvforum.2dparityfec-1010
+video/vnd.iptvforum.2dparityfec-2005
+video/vnd.iptvforum.ttsavc
+video/vnd.iptvforum.ttsmpeg2
+video/vnd.motorola.video
+video/vnd.motorola.videop
+video/vnd.mpegurl mxu m4u
+video/vnd.ms-playready.media.pyv pyv
+video/vnd.nokia.interleaved-multimedia nim
+video/vnd.nokia.mp4vr
+video/vnd.nokia.videovoip
+video/vnd.objectvideo
+video/vnd.radgamettools.bink bik bk2
+video/vnd.radgamettools.smacker smk
+video/vnd.sealed.mpeg1 smpg s11
+video/vnd.sealed.mpeg4 s14
+video/vnd.sealed.swf sswf ssw
+video/vnd.sealedmedia.softseal.mov smov smo s1q
+video/vnd.uvvu.mp4
+video/vnd.vivo viv
+video/vnd.youtube.yt yt
+video/VP8
+video/VP9
+video/webm webm
+video/x-flv flv
+video/x-la-asf lsf lsx
+video/x-matroska mpv mkv
+video/x-mng mng
+video/x-ms-wm wm
+video/x-ms-wmv wmv
+video/x-ms-wmx wmx
+video/x-ms-wvx wvx
+video/x-msvideo avi
+video/x-sgi-movie movie \ No newline at end of file
diff --git a/tests/res/raw/mime_types b/tests/res/raw/mime_types
new file mode 100644
index 000000000..57fa013c2
--- /dev/null
+++ b/tests/res/raw/mime_types
@@ -0,0 +1,846 @@
+###############################################################################
+#
+# MIME media types and the extensions that represent them.
+#
+# The format of this file is a media type on the left and zero or more
+# filename extensions on the right. Programs using this file will map
+# files ending with those extensions to the associated type.
+#
+# This file is part of the "mime-support" package. Please report a bug using
+# the "reportbug" command of the "reportbug" package if you would like new
+# types or extensions to be added.
+#
+# The reason that all types are managed by the mime-support package instead
+# allowing individual packages to install types in much the same way as they
+# add entries in to the mailcap file is so these types can be referenced by
+# other programs (such as a web server) even if the specific support package
+# for that type is not installed.
+#
+# Users can add their own types if they wish by creating a ".mime.types"
+# file in their home directory. Definitions included there will take
+# precedence over those listed here.
+#
+###############################################################################
+
+
+application/activemessage
+application/andrew-inset ez
+application/annodex anx
+application/applefile
+application/atom+xml atom
+application/atomcat+xml atomcat
+application/atomicmail
+application/atomserv+xml atomsrv
+application/batch-SMTP
+application/bbolin lin
+application/beep+xml
+application/cals-1840
+application/commonground
+application/cu-seeme cu
+application/cybercash
+application/davmount+xml davmount
+application/dca-rft
+application/dec-dx
+application/dicom dcm
+application/docbook+xml
+application/dsptype tsp
+application/dvcs
+application/ecmascript es
+application/edi-consent
+application/edi-x12
+application/edifact
+application/epub+zip epub
+application/eshop
+application/font-sfnt otf ttf
+application/font-tdpfr pfr
+application/font-woff woff
+application/futuresplash spl
+application/ghostview
+application/gzip gz
+application/hta hta
+application/http
+application/hyperstudio
+application/iges
+application/index
+application/index.cmd
+application/index.obj
+application/index.response
+application/index.vnd
+application/iotp
+application/ipp
+application/isup
+application/java-archive jar
+application/java-serialized-object ser
+application/java-vm class
+application/javascript js mjs
+application/json json
+application/ld+json jsonld
+application/m3g m3g
+application/mac-binhex40 hqx
+application/mac-compactpro cpt
+application/macwriteii
+application/marc
+application/mathematica nb nbp
+application/mbox mbox
+application/ms-tnef
+application/msaccess mdb
+application/msword doc dot
+application/mxf mxf
+application/news-message-id
+application/news-transmission
+application/ocsp-request
+application/ocsp-response
+application/octet-stream bin deploy msu msp
+application/oda oda
+application/oebps-package+xml opf
+application/ogg ogx
+application/onenote one onetoc2 onetmp onepkg
+application/parityfec
+application/pdf pdf
+application/pgp-encrypted pgp
+application/pgp-keys key
+application/pgp-signature sig
+application/pics-rules prf
+application/pkcs10
+application/pkcs7-mime
+application/pkcs7-signature
+application/pkix-cert
+application/pkix-crl
+application/pkixcmp
+application/postscript ps ai eps epsi epsf eps2 eps3
+application/prs.alvestrand.titrax-sheet
+application/prs.cww
+application/prs.nprend
+application/qsig
+application/rar rar
+application/rdf+xml rdf
+application/remote-printing
+application/riscos
+application/rtf rtf
+application/sdp
+application/set-payment
+application/set-payment-initiation
+application/set-registration
+application/set-registration-initiation
+application/sgml
+application/sgml-open-catalog
+application/sieve
+application/sla stl
+application/slate
+application/smil+xml smi smil
+application/timestamp-query
+application/timestamp-reply
+application/vemmi
+application/wasm wasm
+application/whoispp-query
+application/whoispp-response
+application/wita
+application/x400-bp
+application/xhtml+xml xhtml xht
+application/xml xml xsd
+application/xml-dtd
+application/xml-external-parsed-entity
+application/xslt+xml xsl xslt
+application/xspf+xml xspf
+application/zip zip
+application/vnd.3M.Post-it-Notes
+application/vnd.accpac.simply.aso
+application/vnd.accpac.simply.imp
+application/vnd.acucobol
+application/vnd.aether.imp
+application/vnd.android.package-archive apk
+application/vnd.anser-web-certificate-issue-initiation
+application/vnd.anser-web-funds-transfer-initiation
+application/vnd.audiograph
+application/vnd.bmi
+application/vnd.businessobjects
+application/vnd.canon-cpdl
+application/vnd.canon-lips
+application/vnd.cinderella cdy
+application/vnd.claymore
+application/vnd.commerce-battelle
+application/vnd.commonspace
+application/vnd.comsocaller
+application/vnd.contact.cmsg
+application/vnd.cosmocaller
+application/vnd.ctc-posml
+application/vnd.cups-postscript
+application/vnd.cups-raster
+application/vnd.cups-raw
+application/vnd.cybank
+application/vnd.debian.binary-package deb ddeb udeb
+application/vnd.dna
+application/vnd.dpgraph
+application/vnd.dxr
+application/vnd.ecdis-update
+application/vnd.ecowin.chart
+application/vnd.ecowin.filerequest
+application/vnd.ecowin.fileupdate
+application/vnd.ecowin.series
+application/vnd.ecowin.seriesrequest
+application/vnd.ecowin.seriesupdate
+application/vnd.enliven
+application/vnd.epson.esf
+application/vnd.epson.msf
+application/vnd.epson.quickanime
+application/vnd.epson.salt
+application/vnd.epson.ssf
+application/vnd.ericsson.quickcall
+application/vnd.eudora.data
+application/vnd.fdf
+application/vnd.ffsns
+application/vnd.flographit
+application/vnd.font-fontforge-sfd sfd
+application/vnd.framemaker
+application/vnd.fsc.weblaunch
+application/vnd.fujitsu.oasys
+application/vnd.fujitsu.oasys2
+application/vnd.fujitsu.oasys3
+application/vnd.fujitsu.oasysgp
+application/vnd.fujitsu.oasysprs
+application/vnd.fujixerox.ddd
+application/vnd.fujixerox.docuworks
+application/vnd.fujixerox.docuworks.binder
+application/vnd.fut-misnet
+application/vnd.google-earth.kml+xml kml
+application/vnd.google-earth.kmz kmz
+application/vnd.grafeq
+application/vnd.groove-account
+application/vnd.groove-identity-message
+application/vnd.groove-injector
+application/vnd.groove-tool-message
+application/vnd.groove-tool-template
+application/vnd.groove-vcard
+application/vnd.hhe.lesson-player
+application/vnd.hp-HPGL
+application/vnd.hp-PCL
+application/vnd.hp-PCLXL
+application/vnd.hp-hpid
+application/vnd.hp-hps
+application/vnd.httphone
+application/vnd.hzn-3d-crossword
+application/vnd.ibm.MiniPay
+application/vnd.ibm.afplinedata
+application/vnd.ibm.modcap
+application/vnd.informix-visionary
+application/vnd.intercon.formnet
+application/vnd.intertrust.digibox
+application/vnd.intertrust.nncp
+application/vnd.intu.qbo
+application/vnd.intu.qfx
+application/vnd.irepository.package+xml
+application/vnd.is-xpr
+application/vnd.japannet-directory-service
+application/vnd.japannet-jpnstore-wakeup
+application/vnd.japannet-payment-wakeup
+application/vnd.japannet-registration
+application/vnd.japannet-registration-wakeup
+application/vnd.japannet-setstore-wakeup
+application/vnd.japannet-verification
+application/vnd.japannet-verification-wakeup
+application/vnd.koan
+application/vnd.lotus-1-2-3
+application/vnd.lotus-approach
+application/vnd.lotus-freelance
+application/vnd.lotus-notes
+application/vnd.lotus-organizer
+application/vnd.lotus-screencam
+application/vnd.lotus-wordpro
+application/vnd.mcd
+application/vnd.mediastation.cdkey
+application/vnd.meridian-slingshot
+application/vnd.mif
+application/vnd.minisoft-hp3000-save
+application/vnd.mitsubishi.misty-guard.trustweb
+application/vnd.mobius.daf
+application/vnd.mobius.dis
+application/vnd.mobius.msl
+application/vnd.mobius.plc
+application/vnd.mobius.txf
+application/vnd.motorola.flexsuite
+application/vnd.motorola.flexsuite.adsi
+application/vnd.motorola.flexsuite.fis
+application/vnd.motorola.flexsuite.gotap
+application/vnd.motorola.flexsuite.kmr
+application/vnd.motorola.flexsuite.ttc
+application/vnd.motorola.flexsuite.wem
+application/vnd.mozilla.xul+xml xul
+application/vnd.ms-artgalry
+application/vnd.ms-asf
+application/vnd.ms-excel xls xlb xlt
+application/vnd.ms-excel.addin.macroEnabled.12 xlam
+application/vnd.ms-excel.sheet.binary.macroEnabled.12 xlsb
+application/vnd.ms-excel.sheet.macroEnabled.12 xlsm
+application/vnd.ms-excel.template.macroEnabled.12 xltm
+application/vnd.ms-fontobject eot
+application/vnd.ms-lrm
+application/vnd.ms-officetheme thmx
+application/vnd.ms-pki.seccat cat
+#application/vnd.ms-pki.stl stl
+application/vnd.ms-powerpoint ppt pps
+application/vnd.ms-powerpoint.addin.macroEnabled.12 ppam
+application/vnd.ms-powerpoint.presentation.macroEnabled.12 pptm
+application/vnd.ms-powerpoint.slide.macroEnabled.12 sldm
+application/vnd.ms-powerpoint.slideshow.macroEnabled.12 ppsm
+application/vnd.ms-powerpoint.template.macroEnabled.12 potm
+application/vnd.ms-project
+application/vnd.ms-tnef
+application/vnd.ms-word.document.macroEnabled.12 docm
+application/vnd.ms-word.template.macroEnabled.12 dotm
+application/vnd.ms-works
+application/vnd.mseq
+application/vnd.msign
+application/vnd.music-niff
+application/vnd.musician
+application/vnd.netfpx
+application/vnd.noblenet-directory
+application/vnd.noblenet-sealer
+application/vnd.noblenet-web
+application/vnd.novadigm.EDM
+application/vnd.novadigm.EDX
+application/vnd.novadigm.EXT
+application/vnd.oasis.opendocument.chart odc
+application/vnd.oasis.opendocument.database odb
+application/vnd.oasis.opendocument.formula odf
+application/vnd.oasis.opendocument.graphics odg
+application/vnd.oasis.opendocument.graphics-template otg
+application/vnd.oasis.opendocument.image odi
+application/vnd.oasis.opendocument.presentation odp
+application/vnd.oasis.opendocument.presentation-template otp
+application/vnd.oasis.opendocument.spreadsheet ods
+application/vnd.oasis.opendocument.spreadsheet-template ots
+application/vnd.oasis.opendocument.text odt
+application/vnd.oasis.opendocument.text-master odm
+application/vnd.oasis.opendocument.text-template ott
+application/vnd.oasis.opendocument.text-web oth
+application/vnd.openxmlformats-officedocument.presentationml.presentation pptx
+application/vnd.openxmlformats-officedocument.presentationml.slide sldx
+application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx
+application/vnd.openxmlformats-officedocument.presentationml.template potx
+application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx
+application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx
+application/vnd.openxmlformats-officedocument.wordprocessingml.document docx
+application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx
+application/vnd.osa.netdeploy
+application/vnd.palm
+application/vnd.pg.format
+application/vnd.pg.osasli
+application/vnd.powerbuilder6
+application/vnd.powerbuilder6-s
+application/vnd.powerbuilder7
+application/vnd.powerbuilder7-s
+application/vnd.powerbuilder75
+application/vnd.powerbuilder75-s
+application/vnd.previewsystems.box
+application/vnd.publishare-delta-tree
+application/vnd.pvi.ptid1
+application/vnd.pwg-xhtml-print+xml
+application/vnd.rapid
+application/vnd.rim.cod cod
+application/vnd.s3sms
+application/vnd.seemail
+application/vnd.shana.informed.formdata
+application/vnd.shana.informed.formtemplate
+application/vnd.shana.informed.interchange
+application/vnd.shana.informed.package
+application/vnd.smaf mmf
+application/vnd.sss-cod
+application/vnd.sss-dtf
+application/vnd.sss-ntf
+application/vnd.stardivision.calc sdc
+application/vnd.stardivision.chart sds
+application/vnd.stardivision.draw sda
+application/vnd.stardivision.impress sdd
+application/vnd.stardivision.math sdf
+application/vnd.stardivision.writer sdw
+application/vnd.stardivision.writer-global sgl
+application/vnd.street-stream
+application/vnd.sun.xml.calc sxc
+application/vnd.sun.xml.calc.template stc
+application/vnd.sun.xml.draw sxd
+application/vnd.sun.xml.draw.template std
+application/vnd.sun.xml.impress sxi
+application/vnd.sun.xml.impress.template sti
+application/vnd.sun.xml.math sxm
+application/vnd.sun.xml.writer sxw
+application/vnd.sun.xml.writer.global sxg
+application/vnd.sun.xml.writer.template stw
+application/vnd.svd
+application/vnd.swiftview-ics
+application/vnd.symbian.install sis
+application/vnd.tcpdump.pcap cap pcap
+application/vnd.triscape.mxs
+application/vnd.trueapp
+application/vnd.truedoc
+application/vnd.tve-trigger
+application/vnd.ufdl
+application/vnd.uplanet.alert
+application/vnd.uplanet.alert-wbxml
+application/vnd.uplanet.bearer-choice
+application/vnd.uplanet.bearer-choice-wbxml
+application/vnd.uplanet.cacheop
+application/vnd.uplanet.cacheop-wbxml
+application/vnd.uplanet.channel
+application/vnd.uplanet.channel-wbxml
+application/vnd.uplanet.list
+application/vnd.uplanet.list-wbxml
+application/vnd.uplanet.listcmd
+application/vnd.uplanet.listcmd-wbxml
+application/vnd.uplanet.signal
+application/vnd.vcx
+application/vnd.vectorworks
+application/vnd.vidsoft.vidconference
+application/vnd.visio vsd vst vsw vss
+application/vnd.vividence.scriptfile
+application/vnd.wap.sic
+application/vnd.wap.slc
+application/vnd.wap.wbxml wbxml
+application/vnd.wap.wmlc wmlc
+application/vnd.wap.wmlscriptc wmlsc
+application/vnd.webturbo
+application/vnd.wordperfect wpd
+application/vnd.wordperfect5.1 wp5
+application/vnd.wrq-hp3000-labelled
+application/vnd.wt.stf
+application/vnd.xara
+application/vnd.xfdl
+application/vnd.yellowriver-custom-menu
+application/zlib
+application/x-123 wk
+application/x-7z-compressed 7z
+application/x-abiword abw
+application/x-apple-diskimage dmg
+application/x-bcpio bcpio
+application/x-bittorrent torrent
+application/x-cab cab
+application/x-cbr cbr
+application/x-cbz cbz
+application/x-cdf cdf cda
+application/x-cdlink vcd
+application/x-chess-pgn pgn
+application/x-comsol mph
+application/x-core
+application/x-cpio cpio
+application/x-csh csh
+application/x-debian-package deb udeb
+application/x-director dcr dir dxr
+application/x-dms dms
+application/x-doom wad
+application/x-dvi dvi
+application/x-executable
+application/x-font pfa pfb gsf
+application/x-font-pcf pcf pcf.Z
+application/x-freemind mm
+application/x-futuresplash spl
+application/x-ganttproject gan
+application/x-gnumeric gnumeric
+application/x-go-sgf sgf
+application/x-graphing-calculator gcf
+application/x-gtar gtar
+application/x-gtar-compressed tgz taz
+application/x-hdf hdf
+#application/x-httpd-eruby rhtml
+#application/x-httpd-php phtml pht php
+#application/x-httpd-php-source phps
+#application/x-httpd-php3 php3
+#application/x-httpd-php3-preprocessed php3p
+#application/x-httpd-php4 php4
+#application/x-httpd-php5 php5
+application/x-hwp hwp
+application/x-ica ica
+application/x-info info
+application/x-internet-signup ins isp
+application/x-iphone iii
+application/x-iso9660-image iso
+application/x-jam jam
+application/x-java-applet
+application/x-java-bean
+application/x-java-jnlp-file jnlp
+application/x-jmol jmz
+application/x-kchart chrt
+application/x-kdelnk
+application/x-killustrator kil
+application/x-koan skp skd skt skm
+application/x-kpresenter kpr kpt
+application/x-kspread ksp
+application/x-kword kwd kwt
+application/x-latex latex
+application/x-lha lha
+application/x-lyx lyx
+application/x-lzh lzh
+application/x-lzx lzx
+application/x-maker frm maker frame fm fb book fbdoc
+application/x-mif mif
+application/x-mpegURL m3u8
+application/x-ms-application application
+application/x-ms-manifest manifest
+application/x-ms-wmd wmd
+application/x-ms-wmz wmz
+application/x-msdos-program com exe bat dll
+application/x-msi msi
+application/x-netcdf nc
+application/x-ns-proxy-autoconfig pac
+application/x-nwc nwc
+application/x-object o
+application/x-oz-application oza
+application/x-pkcs7-certreqresp p7r
+application/x-pkcs7-crl crl
+application/x-python-code pyc pyo
+application/x-qgis qgs shp shx
+application/x-quicktimeplayer qtl
+application/x-rdp rdp
+application/x-redhat-package-manager rpm
+application/x-rss+xml rss
+application/x-ruby rb
+application/x-rx
+application/x-scilab sci sce
+application/x-scilab-xcos xcos
+application/x-sh sh
+application/x-shar shar
+application/x-shellscript
+application/x-shockwave-flash swf swfl
+application/x-silverlight scr
+application/x-sql sql
+application/x-stuffit sit sitx
+application/x-sv4cpio sv4cpio
+application/x-sv4crc sv4crc
+application/x-tar tar
+application/x-tcl tcl
+application/x-tex-gf gf
+application/x-tex-pk pk
+application/x-texinfo texinfo texi
+application/x-trash ~ % bak old sik
+application/x-troff t tr roff
+application/x-troff-man man
+application/x-troff-me me
+application/x-troff-ms ms
+application/x-ustar ustar
+application/x-videolan
+application/x-wais-source src
+application/x-wingz wz
+application/x-x509-ca-cert crt
+application/x-xcf xcf
+application/x-xfig fig
+application/x-xpinstall xpi
+application/x-xz xz
+
+audio/32kadpcm
+audio/3gpp
+audio/amr amr
+audio/amr-wb awb
+audio/annodex axa
+audio/basic au snd
+audio/csound csd orc sco
+audio/flac flac
+audio/g.722.1
+audio/l16
+audio/midi mid midi kar
+audio/mp4a-latm
+audio/mpa-robust
+audio/mpeg mpga mpega mp2 mp3 m4a
+audio/mpegurl m3u
+audio/ogg oga ogg opus spx
+audio/parityfec
+audio/prs.sid sid
+audio/telephone-event
+audio/tone
+audio/vnd.cisco.nse
+audio/vnd.cns.anp1
+audio/vnd.cns.inf1
+audio/vnd.digital-winds
+audio/vnd.everad.plj
+audio/vnd.lucent.voice
+audio/vnd.nortel.vbk
+audio/vnd.nuera.ecelp4800
+audio/vnd.nuera.ecelp7470
+audio/vnd.nuera.ecelp9600
+audio/vnd.octel.sbc
+audio/vnd.qcelp
+audio/vnd.rhetorex.32kadpcm
+audio/vnd.vmx.cvsd
+audio/x-aiff aif aiff aifc
+audio/x-gsm gsm
+audio/x-mpegurl m3u
+audio/x-ms-wma wma
+audio/x-ms-wax wax
+audio/x-pn-realaudio-plugin
+audio/x-pn-realaudio ra rm ram
+audio/x-realaudio ra
+audio/x-scpls pls
+audio/x-sd2 sd2
+audio/x-wav wav
+
+chemical/x-alchemy alc
+chemical/x-cache cac cache
+chemical/x-cache-csf csf
+chemical/x-cactvs-binary cbin cascii ctab
+chemical/x-cdx cdx
+chemical/x-cerius cer
+chemical/x-chem3d c3d
+chemical/x-chemdraw chm
+chemical/x-cif cif
+chemical/x-cmdf cmdf
+chemical/x-cml cml
+chemical/x-compass cpa
+chemical/x-crossfire bsd
+chemical/x-csml csml csm
+chemical/x-ctx ctx
+chemical/x-cxf cxf cef
+#chemical/x-daylight-smiles smi
+chemical/x-embl-dl-nucleotide emb embl
+chemical/x-galactic-spc spc
+chemical/x-gamess-input inp gam gamin
+chemical/x-gaussian-checkpoint fch fchk
+chemical/x-gaussian-cube cub
+chemical/x-gaussian-input gau gjc gjf
+chemical/x-gaussian-log gal
+chemical/x-gcg8-sequence gcg
+chemical/x-genbank gen
+chemical/x-hin hin
+chemical/x-isostar istr ist
+chemical/x-jcamp-dx jdx dx
+chemical/x-kinemage kin
+chemical/x-macmolecule mcm
+chemical/x-macromodel-input mmd mmod
+chemical/x-mdl-molfile mol
+chemical/x-mdl-rdfile rd
+chemical/x-mdl-rxnfile rxn
+chemical/x-mdl-sdfile sd sdf
+chemical/x-mdl-tgf tgf
+#chemical/x-mif mif
+chemical/x-mmcif mcif
+chemical/x-mol2 mol2
+chemical/x-molconn-Z b
+chemical/x-mopac-graph gpt
+chemical/x-mopac-input mop mopcrt mpc zmt
+chemical/x-mopac-out moo
+chemical/x-mopac-vib mvb
+chemical/x-ncbi-asn1 asn
+chemical/x-ncbi-asn1-ascii prt ent
+chemical/x-ncbi-asn1-binary val aso
+chemical/x-ncbi-asn1-spec asn
+chemical/x-pdb pdb ent
+chemical/x-rosdal ros
+chemical/x-swissprot sw
+chemical/x-vamas-iso14976 vms
+chemical/x-vmd vmd
+chemical/x-xtel xtel
+chemical/x-xyz xyz
+
+font/collection ttc
+font/otf ttf otf
+font/sfnt ttf otf
+font/ttf ttf otf
+font/woff woff
+font/woff2 woff2
+
+image/cgm
+image/g3fax
+image/gif gif
+image/ief ief
+image/jp2 jp2 jpg2
+image/jpeg jpeg jpg jpe
+image/jpm jpm
+image/jpx jpx jpf
+image/naplps
+image/pcx pcx
+image/png png
+image/prs.btif
+image/prs.pti
+image/svg+xml svg svgz
+image/tiff tiff tif
+image/vnd.cns.inf2
+image/vnd.djvu djvu djv
+image/vnd.dwg
+image/vnd.dxf
+image/vnd.fastbidsheet
+image/vnd.fpx
+image/vnd.fst
+image/vnd.fujixerox.edmics-mmr
+image/vnd.fujixerox.edmics-rlc
+image/vnd.microsoft.icon ico
+image/vnd.mix
+image/vnd.net-fpx
+image/vnd.svf
+image/vnd.wap.wbmp wbmp
+image/vnd.xiff
+image/x-canon-cr2 cr2
+image/x-canon-crw crw
+image/x-cmu-raster ras
+image/x-coreldraw cdr
+image/x-coreldrawpattern pat
+image/x-coreldrawtemplate cdt
+image/x-corelphotopaint cpt
+image/x-epson-erf erf
+image/x-icon
+image/x-jg art
+image/x-jng jng
+image/x-ms-bmp bmp
+image/x-nikon-nef nef
+image/x-olympus-orf orf
+image/x-photoshop psd
+image/x-portable-anymap pnm
+image/x-portable-bitmap pbm
+image/x-portable-graymap pgm
+image/x-portable-pixmap ppm
+image/x-rgb rgb
+image/x-xbitmap xbm
+image/x-xpixmap xpm
+image/x-xwindowdump xwd
+
+inode/chardevice
+inode/blockdevice
+inode/directory-locked
+inode/directory
+inode/fifo
+inode/socket
+
+message/delivery-status
+message/disposition-notification
+message/external-body
+message/http
+message/s-http
+message/news
+message/partial
+message/rfc822 eml
+
+model/iges igs iges
+model/mesh msh mesh silo
+model/vnd.dwf
+model/vnd.flatland.3dml
+model/vnd.gdl
+model/vnd.gs-gdl
+model/vnd.gtw
+model/vnd.mts
+model/vnd.vtu
+model/vrml wrl vrml
+model/x3d+vrml x3dv
+model/x3d+xml x3d
+model/x3d+binary x3db
+
+multipart/alternative
+multipart/appledouble
+multipart/byteranges
+multipart/digest
+multipart/encrypted
+multipart/form-data
+multipart/header-set
+multipart/mixed
+multipart/parallel
+multipart/related
+multipart/report
+multipart/signed
+multipart/voice-message
+
+text/cache-manifest appcache
+text/calendar ics icz
+text/css css
+text/csv csv
+text/directory
+text/english
+text/enriched
+text/h323 323
+text/html html htm shtml
+text/iuls uls
+text/mathml mml
+text/markdown md markdown
+text/parityfec
+text/plain asc txt text pot brf srt
+text/prs.lines.tag
+text/rfc822-headers
+text/richtext rtx
+text/rtf
+text/scriptlet sct wsc
+text/t140
+text/texmacs tm
+text/tab-separated-values tsv
+text/turtle ttl
+text/uri-list
+text/vcard vcf vcard
+text/vnd.abc
+text/vnd.curl
+text/vnd.debian.copyright
+text/vnd.DMClientScript
+text/vnd.flatland.3dml
+text/vnd.fly
+text/vnd.fmi.flexstor
+text/vnd.in3d.3dml
+text/vnd.in3d.spot
+text/vnd.IPTC.NewsML
+text/vnd.IPTC.NITF
+text/vnd.latex-z
+text/vnd.motorola.reflex
+text/vnd.ms-mediapackage
+text/vnd.sun.j2me.app-descriptor jad
+text/vnd.wap.si
+text/vnd.wap.sl
+text/vnd.wap.wml wml
+text/vnd.wap.wmlscript wmls
+text/x-bibtex bib
+text/x-boo boo
+text/x-c++hdr h++ hpp hxx hh
+text/x-c++src c++ cpp cxx cc
+text/x-chdr h
+text/x-component htc
+text/x-crontab
+text/x-csh csh
+text/x-csrc c
+text/x-dsrc d
+text/x-diff diff patch
+text/x-haskell hs
+text/x-java java
+text/x-lilypond ly
+text/x-literate-haskell lhs
+text/x-makefile
+text/x-moc moc
+text/x-pascal p pas
+text/x-pcs-gcd gcd
+text/x-perl pl pm
+text/x-python py
+text/x-scala scala
+text/x-server-parsed-html
+text/x-setext etx
+text/x-sfv sfv
+text/x-sh sh
+text/x-tcl tcl tk
+text/x-tex tex ltx sty cls
+text/x-vcalendar vcs
+
+video/3gpp 3gp
+video/annodex axv
+video/dl dl
+video/dv dif dv
+video/fli fli
+video/gl gl
+video/mpeg mpeg mpg mpe
+video/MP2T ts
+video/mp4 mp4
+video/quicktime qt mov
+video/mp4v-es
+video/ogg ogv
+video/parityfec
+video/pointer
+video/webm webm
+video/vnd.fvt
+video/vnd.motorola.video
+video/vnd.motorola.videop
+video/vnd.mpegurl mxu
+video/vnd.mts
+video/vnd.nokia.interleaved-multimedia
+video/vnd.vivo
+video/x-flv flv
+video/x-la-asf lsf lsx
+video/x-mng mng
+video/x-ms-asf asf asx
+video/x-ms-wm wm
+video/x-ms-wmv wmv
+video/x-ms-wmx wmx
+video/x-ms-wvx wvx
+video/x-msvideo avi
+video/x-sgi-movie movie
+video/x-matroska mpv mkv
+
+x-conference/x-cooltalk ice
+
+x-epoc/x-sisx-app sisx
+x-world/x-vrml vrm vrml wrl \ No newline at end of file
diff --git a/tests/src/com/android/providers/media/IsolatedContext.java b/tests/src/com/android/providers/media/IsolatedContext.java
index d0c1001c7..fa9a103ba 100644
--- a/tests/src/com/android/providers/media/IsolatedContext.java
+++ b/tests/src/com/android/providers/media/IsolatedContext.java
@@ -35,6 +35,7 @@ import androidx.annotation.VisibleForTesting;
import com.android.providers.media.cloudproviders.CloudProviderPrimary;
import com.android.providers.media.cloudproviders.FlakyCloudProvider;
import com.android.providers.media.dao.FileRow;
+import com.android.providers.media.flags.Flags;
import com.android.providers.media.photopicker.PhotoPickerProvider;
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.util.FileUtils;
@@ -63,6 +64,17 @@ public class IsolatedContext extends ContextWrapper {
public IsolatedContext(Context base, String tag, boolean asFuseThread,
UserHandle userHandle, ConfigStore configStore) {
+ this(base, tag, asFuseThread, userHandle, configStore, new MaliciousAppDetector(base));
+ }
+
+ public IsolatedContext(Context base, String tag, boolean asFuseThread,
+ MaliciousAppDetector maliciousAppDetector) {
+ this(base, tag, asFuseThread, base.getUser(), new TestConfigStore(), maliciousAppDetector);
+ }
+
+ public IsolatedContext(Context base, String tag, boolean asFuseThread,
+ UserHandle userHandle, ConfigStore configStore,
+ MaliciousAppDetector maliciousAppDetector) {
super(base);
mDir = new File(base.getFilesDir(), tag);
mDir.mkdirs();
@@ -71,7 +83,7 @@ public class IsolatedContext extends ContextWrapper {
mResolver = new MockContentResolver(this);
mUserHandle = userHandle;
- mMediaProvider = getMockedMediaProvider(asFuseThread, configStore);
+ mMediaProvider = getMockedMediaProvider(asFuseThread, configStore, maliciousAppDetector);
attachInfoAndAddProvider(base, mMediaProvider, MediaStore.AUTHORITY);
MediaDocumentsProvider documentsProvider = new MediaDocumentsProvider();
@@ -98,7 +110,7 @@ public class IsolatedContext extends ContextWrapper {
}
private MediaProvider getMockedMediaProvider(boolean asFuseThread,
- ConfigStore configStore) {
+ ConfigStore configStore, MaliciousAppDetector maliciousAppDetector) {
return new MediaProvider() {
@Override
public boolean isFuseThread() {
@@ -124,6 +136,22 @@ public class IsolatedContext extends ContextWrapper {
protected void updateQuotaTypeForUri(@NonNull FileRow row) {
return;
}
+
+ @Override
+ boolean shouldLockdownMediaStoreVersion() {
+ // TODO(b/370999570): Set to true once Baklava is in dev
+ return false;
+ }
+
+ @Override
+ protected MaliciousAppDetector createMaliciousAppDetector() {
+ return maliciousAppDetector;
+ }
+
+ @Override
+ protected boolean shouldCheckForMaliciousActivity() {
+ return Flags.enableMaliciousAppDetector();
+ }
};
}
diff --git a/tests/src/com/android/providers/media/LocalUriMatcherTest.java b/tests/src/com/android/providers/media/LocalUriMatcherTest.java
index aceaa9b45..04b5a288f 100644
--- a/tests/src/com/android/providers/media/LocalUriMatcherTest.java
+++ b/tests/src/com/android/providers/media/LocalUriMatcherTest.java
@@ -52,6 +52,8 @@ public class LocalUriMatcherTest {
LocalUriMatcher.PICKER_GET_CONTENT_ID,
assembleTestUri(
new String[]{"picker_get_content", "0", "anything", "media", "anything"}));
+ assertMatchesPublic(LocalUriMatcher.PICKER_TRANSCODED_ID, assembleTestUri(
+ new String[]{"picker_transcoded", "0", "anything", "media", "anything"}));
assertMatchesPublic(LocalUriMatcher.CLI, assembleTestUri(new String[] {"cli"}));
diff --git a/tests/src/com/android/providers/media/MaliciousAppCheckerTest.java b/tests/src/com/android/providers/media/MaliciousAppCheckerTest.java
new file mode 100644
index 000000000..6014bd60b
--- /dev/null
+++ b/tests/src/com/android/providers/media/MaliciousAppCheckerTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2024 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.media;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.Manifest;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.SystemClock;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.MediaStore;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.flags.Flags;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class MaliciousAppCheckerTest {
+ private static Context sIsolatedContext;
+ private static ContentResolver sIsolatedResolver;
+ private static final int FILE_CREATION_THRESHOLD_LIMIT = 5;
+ private static final int FREQUENCY_OF_MALICIOUS_INSERTION_CHECK = 1;
+ private static MaliciousAppDetector sMaliciousAppDetector;
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ @BeforeClass
+ public static void setUpBeforeClass() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ Manifest.permission.READ_DEVICE_CONFIG,
+ // Adding this to use getUserHandles() api of UserManagerService which
+ // requires either MANAGE_USERS or CREATE_USERS. Since shell does not have
+ // MANAGER_USERS permissions, using CREATE_USERS in test. This works with
+ // MANAGE_USERS permission for MediaProvider module.
+ Manifest.permission.CREATE_USERS,
+ Manifest.permission.INTERACT_ACROSS_USERS);
+ }
+
+ @Before
+ public void setUp() {
+ resetIsolatedContext();
+ sMaliciousAppDetector = new MaliciousAppDetector(sIsolatedContext,
+ FILE_CREATION_THRESHOLD_LIMIT, FREQUENCY_OF_MALICIOUS_INSERTION_CHECK);
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation().dropShellPermissionIdentity();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MALICIOUS_APP_DETECTOR)
+ public void testCannotCreateFileLimitExceeded() throws Exception {
+ resetIsolatedContext();
+ sMaliciousAppDetector.clearSharedPref();
+
+ createTempFilesInDownloadFolder(FILE_CREATION_THRESHOLD_LIMIT);
+ // add sleep to wait for the background process
+ SystemClock.sleep(1000);
+ int uid = android.os.Process.myUid();
+ boolean isAllowedToCreateFile = sMaliciousAppDetector.isAppAllowedToCreateFiles(uid);
+
+ assertFalse("File should not be allowed to create after limit exceeded",
+ isAllowedToCreateFile);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MALICIOUS_APP_DETECTOR)
+ public void testCreateFileWithinLimit() throws Exception {
+ resetIsolatedContext();
+ sMaliciousAppDetector.clearSharedPref();
+
+ // create files less than the threshold limit
+ createTempFilesInDownloadFolder(FILE_CREATION_THRESHOLD_LIMIT - 1);
+ // add sleep to wait for the background process
+ SystemClock.sleep(1000);
+ int uid = android.os.Process.myUid();
+ boolean isAllowedToCreateFile = sMaliciousAppDetector.isAppAllowedToCreateFiles(uid);
+
+ assertTrue("File should be allowed to create within limit", isAllowedToCreateFile);
+ }
+
+ private void createTempFilesInDownloadFolder(int numberOfFilesToCreate) {
+ final Uri downloadUri = MediaStore.Downloads
+ .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+ for (int index = 0; index < numberOfFilesToCreate; index++) {
+ final ContentValues values = new ContentValues();
+ values.put(MediaStore.Images.Media.DISPLAY_NAME, "test_" + index + ".txt");
+ values.put(MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME,
+ sIsolatedContext.getPackageName());
+ sIsolatedResolver.insert(downloadUri, values);
+ }
+ }
+
+ private static void resetIsolatedContext() {
+ if (sIsolatedResolver != null) {
+ // This is necessary, we wait for all unfinished tasks to finish before we create a
+ // new IsolatedContext.
+ MediaStore.waitForIdle(sIsolatedResolver);
+ }
+
+ Context context = InstrumentationRegistry.getTargetContext();
+ sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false,
+ sMaliciousAppDetector);
+ sIsolatedResolver = sIsolatedContext.getContentResolver();
+ }
+}
diff --git a/tests/src/com/android/providers/media/MediaIndexingDatabaseOperationsTest.java b/tests/src/com/android/providers/media/MediaIndexingDatabaseOperationsTest.java
new file mode 100644
index 000000000..8048ec2c2
--- /dev/null
+++ b/tests/src/com/android/providers/media/MediaIndexingDatabaseOperationsTest.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2024 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.media;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.Manifest;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.providers.media.search.SearchUtilConstants;
+import com.android.providers.media.search.exceptions.SqliteCheckedException;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class MediaIndexingDatabaseOperationsTest {
+
+ private Context mContext;
+ private IsolatedContext mIsolatedContext;
+ private MediaIndexingDatabaseOperationsTestExtension mIndexingDatabaseOperations;
+ private DatabaseHelper mDatabaseHelper;
+ private SearchTestingUtils mSearchTestingUtils;
+ private SearchUtilConstants mSearchUtilConstants;
+
+ @BeforeClass
+ public static void setUpBeforeClass() {
+ // Permissions needed to insert files via the content resolver
+ androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity(
+ Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ Manifest.permission.WRITE_MEDIA_STORAGE);
+ }
+
+ @AfterClass
+ public static void tearDownAfterClass() {
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation().dropShellPermissionIdentity();
+ }
+
+ @Before
+ public void setup() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ mIsolatedContext = new IsolatedContext(mContext,
+ /* tag */"MediaItemsIndexingDatabaseHelperTest", /*asFuseThread*/ false);
+ mIndexingDatabaseOperations = new MediaIndexingDatabaseOperationsTestExtension(
+ mIsolatedContext);
+ mDatabaseHelper = mIsolatedContext.getExternalDatabase();
+ mSearchTestingUtils = new SearchTestingUtils(mDatabaseHelper);
+ mSearchUtilConstants = new SearchUtilConstants();
+ }
+
+ @Test
+ public void testInsertMediaItemIntoMediaIndexingTable()
+ throws SqliteCheckedException {
+ String mediaIdToInsert = "1000";
+
+ // Insert the mediaItemId into the status table
+ ContentValues insertValues = new ContentValues();
+ insertValues.put(mSearchUtilConstants.MEDIA_ID_COLUMN, mediaIdToInsert);
+
+ mIndexingDatabaseOperations.insertIntoMediaStatusTable(
+ mDatabaseHelper, List.of(insertValues));
+
+ // Assert that the value was inserted and its processing status to be 0
+ Cursor cursor = mSearchTestingUtils.getMediaItemDataFromStatusTable(
+ Long.parseLong(mediaIdToInsert));
+ if (cursor.moveToFirst()) {
+ int metadataStatusIndex = cursor.getColumnIndexOrThrow(
+ mSearchUtilConstants.METADATA_PROCESSING_STATUS_COLUMN);
+ int labelStatusIndex = cursor.getColumnIndexOrThrow(
+ mSearchUtilConstants.LABEL_PROCESSING_STATUS_COLUMN);
+ int ocrStatusIndex = cursor.getColumnIndexOrThrow(
+ mSearchUtilConstants.OCR_PROCESSING_STATUS_COLUMN);
+ int locationStatusIndex = cursor.getColumnIndexOrThrow(
+ mSearchUtilConstants.LOCATION_PROCESSING_STATUS_COLUMN);
+ int metadataStatus = cursor.getInt(metadataStatusIndex);
+ int locationStatus = cursor.getInt(locationStatusIndex);
+ int ocrStatus = cursor.getInt(ocrStatusIndex);
+ int labelStatus = cursor.getInt(labelStatusIndex);
+ assertEquals(/* expectedProcessingStatus */ 0, metadataStatus);
+ assertEquals(/* expectedProcessingStatus */ 0, locationStatus);
+ assertEquals(/* expectedProcessingStatus */ 0, ocrStatus);
+ assertEquals(/* expectedProcessingStatus */ 0, labelStatus);
+ }
+ }
+
+ @Test
+ public void testQueryExternalDatabaseTable() throws SqliteCheckedException {
+ String mediaIdToQuery = "1000";
+ ContentValues insertValues = new ContentValues();
+ insertValues.put(mSearchUtilConstants.MEDIA_ID_COLUMN, mediaIdToQuery);
+
+ String testSelection = mSearchUtilConstants.MEDIA_ID_COLUMN + " = ?";
+ String[] testSelectionArgs = new String[]{ mediaIdToQuery };
+ String[] columns = new String[] { mSearchUtilConstants.METADATA_PROCESSING_STATUS_COLUMN };
+ // Insert the mediaItemId into the status table
+ mIndexingDatabaseOperations.insertIntoMediaStatusTable(
+ mDatabaseHelper, List.of(insertValues));
+
+ // Query media indexing table
+ Cursor cursor = mIndexingDatabaseOperations.queryExternalDatabaseTable(
+ mDatabaseHelper, /* tableName */ mSearchUtilConstants.MEDIA_STATUS_TABLE,
+ columns, testSelection, testSelectionArgs, null, null);
+
+ // Assert on the retrieved cursor
+ assertNotNull("Cursor was found to be null", cursor);
+ assertEquals("Expected cursor size did not match",
+ /* expected cursorSize */ 1, cursor.getCount());
+ // Assert the status to be 0
+ if (cursor.moveToFirst()) {
+ assertEquals("testId processing status was expected to be 0",
+ /* expected processingStatus */ 0, cursor.getInt(0));
+ }
+ }
+
+ @Test
+ public void testUpdateStatusTableValues() throws SqliteCheckedException {
+ String mediaIdToUpdate = "1000";
+ ContentValues insertValues = new ContentValues();
+ insertValues.put(mSearchUtilConstants.MEDIA_ID_COLUMN, mediaIdToUpdate);
+
+ // Insert the mediaItemId into the status table
+ mIndexingDatabaseOperations.insertIntoMediaStatusTable(
+ mDatabaseHelper, List.of(insertValues));
+
+ // Update the metadata indexing status column
+ ContentValues updateValues = new ContentValues();
+ updateValues.put(mSearchUtilConstants.METADATA_PROCESSING_STATUS_COLUMN, 1);
+ String[] updateIds = new String[] { mediaIdToUpdate };
+ mIndexingDatabaseOperations.updateStatusTableValues(
+ mDatabaseHelper, updateIds, updateValues);
+
+ // Assert that the value was updated to 1
+ Cursor cursor = mSearchTestingUtils.getMediaItemDataFromStatusTable(
+ Long.parseLong(mediaIdToUpdate));
+ if (cursor.moveToFirst()) {
+ int metadataStatusIndex = cursor.getColumnIndexOrThrow(
+ mSearchUtilConstants.METADATA_PROCESSING_STATUS_COLUMN);
+ int metadataStatus = cursor.getInt(metadataStatusIndex);
+ assertEquals(/* expected value after update */ 1, metadataStatus);
+ }
+ }
+
+ @Test
+ public void testDeleteMediaItemFromMediaIndexingTable()
+ throws SqliteCheckedException {
+ String mediaItemToDelete = "1000";
+ String testSelection = mSearchUtilConstants.MEDIA_ID_COLUMN + " = ?";
+ String[] testSelectionArgs = new String[]{ mediaItemToDelete };
+ String[] columns = new String[] { mSearchUtilConstants.METADATA_PROCESSING_STATUS_COLUMN };
+ ContentValues insertValues = new ContentValues();
+ insertValues.put(mSearchUtilConstants.MEDIA_ID_COLUMN, mediaItemToDelete);
+
+ // Insert the mediaItemId into the status table
+ mIndexingDatabaseOperations.insertIntoMediaStatusTable(
+ mDatabaseHelper, List.of(insertValues));
+
+ // Delete the item from the indexing table
+ mIndexingDatabaseOperations.deleteMediaItemFromMediaStatusTable(
+ mDatabaseHelper, mediaItemToDelete);
+
+ // Assert that the item does not exist in the indexing table
+ Cursor cursor = mIndexingDatabaseOperations.queryExternalDatabaseTable(
+ mDatabaseHelper, /* tableName */ mSearchUtilConstants.MEDIA_STATUS_TABLE,
+ columns, testSelection, testSelectionArgs, null, null);
+ assertEquals(/* expected cursorSize */ 0, cursor.getCount());
+ }
+
+ @Test
+ public void testGetDatabaseHelper() {
+ DatabaseHelper testHelper = mIndexingDatabaseOperations.getDatabaseHelper(mContext);
+ assertNotNull(testHelper);
+ assertTrue(testHelper.isExternal());
+ }
+
+
+ private static class MediaIndexingDatabaseOperationsTestExtension extends
+ MediaIndexingDatabaseOperations {
+ IsolatedContext mIsolatedContext;
+
+ MediaIndexingDatabaseOperationsTestExtension(IsolatedContext context) {
+ mIsolatedContext = context;
+ }
+
+ @Override
+ public DatabaseHelper getDatabaseHelper(Context context) {
+ return mIsolatedContext.getExternalDatabase();
+ }
+
+ }
+}
diff --git a/tests/src/com/android/providers/media/MediaProviderForFuseTest.java b/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
index 8cbdb9c7b..ab3c4969f 100644
--- a/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
@@ -31,6 +31,7 @@ import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Environment;
+import android.os.UserHandle;
import android.provider.MediaStore;
import android.system.OsConstants;
import android.util.Log;
@@ -52,6 +53,7 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.Arrays;
+import java.util.Locale;
/**
* Unit tests for {@link MediaProvider} forFuse methods. {@code CtsScopedStorageHostTest} (and
@@ -205,15 +207,20 @@ public class MediaProviderForFuseTest {
@Test
public void testRenameDirectory_WhenParentDirectoryIsHidden() throws Exception {
// Create parent dir with nomedia file
- final File parent = new File(sTestDir, "hidden" + System.nanoTime());
- parent.mkdirs();
- createNomediaFile(parent);
+ // Choosing the base dir to be a public directory so the file can be created by the test
+ // app context without need of shell or root privilege.
+ File parentDir = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DOWNLOADS);
+ File dir = new File(parentDir, "hidden" + System.nanoTime());
+ dir.mkdirs();
+ createNomediaFile(dir);
+
// Create dir in hidden parent dir
- File file = createSubdirWithOneFile(parent);
+ File file = createSubdirWithOneFile(dir);
File oldDir = file.getParentFile();
// Rename dir within hidden parent.
- final File renamedDir = new File(parent, "renamed" + System.nanoTime());
+ final File renamedDir = new File(dir, "renamed" + System.nanoTime());
Truth.assertThat(sMediaProvider.renameForFuse(
oldDir.getPath(), renamedDir.getPath(), sTestUid)).isEqualTo(0);
@@ -237,8 +244,14 @@ public class MediaProviderForFuseTest {
// the process that is, mContext.checkUriPermission and should throw a security
// exception.
sMediaProvider.onFileLookupForFuse(
- "/storage/emulated/0/.transforms/synthetic/picker/0/com.android.providers"
- + ".media.photopicker/media/1000000.jpg", sTestUid /* uid */,
+ String.format(
+ Locale.ROOT,
+ "/storage/emulated/%d/.transforms/synthetic/picker/%d/com.android.providers"
+ + ".media.photopicker/media/1000000.jpg",
+ UserHandle.myUserId(),
+ UserHandle.myUserId()
+ ),
+ sTestUid /* uid */,
0 /* tid */);
fail("This test should throw a security exception");
} catch (SecurityException se) {
@@ -248,7 +261,7 @@ public class MediaProviderForFuseTest {
private @NonNull File createNomediaFile(@NonNull File dir) throws IOException {
final File nomediaFile = new File(dir, ".nomedia");
- executeShellCommand("touch " + nomediaFile.getAbsolutePath());
+ nomediaFile.createNewFile();
Truth.assertWithMessage("cannot create nomedia file: " + nomediaFile.getAbsolutePath())
.that(nomediaFile.exists())
.isTrue();
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 1a07fbda0..21cb56e48 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -16,8 +16,11 @@
package com.android.providers.media;
+import static android.content.ContentResolver.QUERY_ARG_SQL_GROUP_BY;
+import static android.content.ContentResolver.QUERY_ARG_SQL_HAVING;
import static android.provider.MediaStore.getGeneration;
+import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME;
import static com.android.providers.media.scan.MediaScannerTest.stage;
import static com.android.providers.media.util.FileUtils.extractDisplayName;
import static com.android.providers.media.util.FileUtils.extractRelativePath;
@@ -46,6 +49,7 @@ import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.res.AssetFileDescriptor;
@@ -59,6 +63,11 @@ import android.os.Environment;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.Files.FileColumns;
@@ -79,19 +88,22 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.providers.media.MediaProvider.FallbackException;
import com.android.providers.media.MediaProvider.VolumeArgumentException;
import com.android.providers.media.MediaProvider.VolumeNotFoundException;
+import com.android.providers.media.flags.Flags;
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.ItemsProvider;
+import com.android.providers.media.scan.MediaScanner;
+import com.android.providers.media.scan.ModernMediaScanner;
import com.android.providers.media.util.FileUtils;
import com.android.providers.media.util.FileUtilsTest;
import com.android.providers.media.util.SQLiteQueryBuilder;
import com.android.providers.media.util.UserCache;
-import org.junit.AfterClass;
+import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
-import org.junit.BeforeClass;
import org.junit.Ignore;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
@@ -107,10 +119,15 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
+import java.util.Optional;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
@RunWith(AndroidJUnit4.class)
public class MediaProviderTest {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
static final String TAG = "MediaProviderTest";
// The test app without permissions
@@ -122,8 +139,8 @@ public class MediaProviderTest {
private static Context sContext;
private static ContentResolver sIsolatedResolver;
- @BeforeClass
- public static void setUpBeforeClass() {
+ @Before
+ public void setUp() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
@@ -134,15 +151,11 @@ public class MediaProviderTest {
// MANAGE_USERS permission for MediaProvider module.
Manifest.permission.CREATE_USERS,
Manifest.permission.INTERACT_ACROSS_USERS);
- }
-
- @Before
- public void setUp() {
resetIsolatedContext();
}
- @AfterClass
- public static void tearDown() {
+ @After
+ public void tearDown() {
InstrumentationRegistry.getInstrumentation()
.getUiAutomation().dropShellPermissionIdentity();
}
@@ -651,13 +664,19 @@ public class MediaProviderTest {
values);
final ContentValues newValues = new ContentValues();
- newValues.put(MediaStore.MediaColumns.DATA, "/storage/emulated/0/../../../data/media/");
+ newValues.put(
+ MediaStore.MediaColumns.DATA,
+ String.format(Locale.ROOT,
+ "/storage/emulated/%d/../../../data/media/",
+ UserHandle.myUserId()));
IllegalArgumentException illegalArgumentException = Assert.assertThrows(
IllegalArgumentException.class,
() -> sIsolatedResolver.update(uri, newValues, null));
assertThat(illegalArgumentException).hasMessageThat().contains(
- "Requested path /data/media doesn't appear under [/storage/emulated/0]");
+ String.format(Locale.ROOT,
+ "Requested path /data/media doesn't appear under [/storage/emulated/%d]",
+ UserHandle.myUserId()));
}
/**
@@ -904,6 +923,276 @@ public class MediaProviderTest {
buildFile(uri, null, "", ""));
}
+ private void setSharedPreference(String preference, int value) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
+ sIsolatedContext);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(preference, value);
+ editor.commit();
+ }
+
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_EXCLUSION_LIST_FOR_DEFAULT_FOLDERS)
+ public void testEnsureDefaultFolders_EmptyExclusionList() throws Exception {
+ // Put the fake "default" folders in Documents as they can easily be created and
+ // deleted here.
+ String[] defaultFolderList =
+ new String[]{"Documents/A", "Documents/B", "Documents/C", "Documents/D"};
+ final MediaProvider provider = new MediaProvider() {
+ @Override
+ protected String[] getDefaultFolderNames() {
+ return defaultFolderList;
+ }
+
+ @Override
+ protected List<String> getFoldersToSkipInDefaultCreation() {
+ // Set an empty exclusion list.
+ return Arrays.asList();
+ }
+
+ @Override
+ protected void storageNativeBootPropertyChangeListener() {
+ // Ignore this as test app cannot read device config
+ }
+ };
+
+ // Get the external primary volume.
+ final ProviderInfo info = sIsolatedContext.getPackageManager()
+ .resolveContentProvider(MediaStore.AUTHORITY, PackageManager.GET_META_DATA);
+ provider.attachInfo(sIsolatedContext, info);
+ MediaVolume externalPrimary = provider.getVolume(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+
+ // Set this preference to ensure that the default folders are actually created.
+ setSharedPreference("created_default_folders_" + externalPrimary.getId(), 0);
+
+ // Make sure none of the folders exist already.
+ for (String folderName : defaultFolderList) {
+ final File folder = new File(externalPrimary.getPath(), folderName);
+ if (folder.exists()) {
+ assertTrue(folder.delete());
+ }
+ }
+
+ // Create the default folders for the external primary volume.
+ Optional<DatabaseHelper> maybeDatabaseHelper = provider.getDatabaseHelper(
+ EXTERNAL_DATABASE_NAME);
+ assertTrue(maybeDatabaseHelper.isPresent());
+ provider.ensureDefaultFolders(externalPrimary,
+ maybeDatabaseHelper.get().getWritableDatabaseForTest());
+
+ // Make sure all of the folders were created.
+ for (String folderName : defaultFolderList) {
+ final File folder = new File(externalPrimary.getPath(), folderName);
+ try {
+ assertTrue(folder.exists());
+ } finally {
+ // Clean up.
+ folder.delete();
+ }
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_EXCLUSION_LIST_FOR_DEFAULT_FOLDERS)
+ public void testEnsureDefaultFolders_WithInvalidExclusionList() throws Exception {
+ // Put the fake "default" folders in Documents as they can easily be created and
+ // deleted here.
+ String[] defaultFolderList =
+ new String[]{"Documents/FolderA", "Documents/FolderB", "Documents/FolderC",
+ "Documents/FolderD"};
+ // The exclusion list contains more items than there are on the default list. It should
+ // be ignored.
+ List<String> exclusionList = Arrays.asList("Documents/FolderA", "Documents/FolderB",
+ "Documents/FolderC",
+ "Documents/FolderD", "Documents/FolderE");
+ final MediaProvider provider = new MediaProvider() {
+ @Override
+ protected String[] getDefaultFolderNames() {
+ return defaultFolderList;
+ }
+
+ @Override
+ protected List<String> getFoldersToSkipInDefaultCreation() {
+ return exclusionList;
+ }
+
+ @Override
+ protected void storageNativeBootPropertyChangeListener() {
+ // Ignore this as test app cannot read device config
+ }
+ };
+
+ // Get the external primary volume.
+ final ProviderInfo info = sIsolatedContext.getPackageManager()
+ .resolveContentProvider(MediaStore.AUTHORITY, PackageManager.GET_META_DATA);
+ provider.attachInfo(sIsolatedContext, info);
+ MediaVolume externalPrimary = provider.getVolume(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+
+ // Set this preference to ensure that the default folders are actually created.
+ setSharedPreference("created_default_folders_" + externalPrimary.getId(), 0);
+
+ // Make sure none of the folders exist already.
+ for (String folderName : defaultFolderList) {
+ final File folder = new File(externalPrimary.getPath(), folderName);
+ if (folder.exists()) {
+ assertTrue(folder.delete());
+ }
+ }
+
+ // Create the default folders for the external primary volume.
+ Optional<DatabaseHelper> maybeDatabaseHelper = provider.getDatabaseHelper(
+ EXTERNAL_DATABASE_NAME);
+ assertTrue(maybeDatabaseHelper.isPresent());
+ provider.ensureDefaultFolders(externalPrimary,
+ maybeDatabaseHelper.get().getWritableDatabaseForTest());
+
+ // Make sure all of the folders were created.
+ for (String folderName : defaultFolderList) {
+ final File folder = new File(externalPrimary.getPath(), folderName);
+ try {
+ assertTrue(folder.exists());
+ } finally {
+ // Clean up.
+ folder.delete();
+ }
+ }
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_EXCLUSION_LIST_FOR_DEFAULT_FOLDERS)
+ public void testEnsureDefaultFolders_FlagDisabled() throws Exception {
+ // Put the fake "default" folders in Documents as they can easily be created and
+ // deleted here.
+ String[] defaultFolderList =
+ new String[]{"Documents/FolderA", "Documents/FolderB", "Documents/FolderC",
+ "Documents/FolderD"};
+ List<String> exclusionList = Arrays.asList("Documents/FolderA", "Documents/FolderC");
+ final MediaProvider provider = new MediaProvider() {
+ @Override
+ protected String[] getDefaultFolderNames() {
+ return defaultFolderList;
+ }
+
+ @Override
+ protected List<String> getFoldersToSkipInDefaultCreation() {
+ // This should be ignored as the flag is disabled.
+ return exclusionList;
+ }
+
+ @Override
+ protected void storageNativeBootPropertyChangeListener() {
+ // Ignore this as test app cannot read device config
+ }
+ };
+
+ // Get the external primary volume.
+ final ProviderInfo info = sIsolatedContext.getPackageManager()
+ .resolveContentProvider(MediaStore.AUTHORITY, PackageManager.GET_META_DATA);
+ provider.attachInfo(sIsolatedContext, info);
+ MediaVolume externalPrimary = provider.getVolume(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+
+ // Set this preference to ensure that the default folders are actually created.
+ setSharedPreference("created_default_folders_" + externalPrimary.getId(), 0);
+
+ // Make sure none of the folders exist already.
+ for (String folderName : defaultFolderList) {
+ final File folder = new File(externalPrimary.getPath(), folderName);
+ if (folder.exists()) {
+ assertTrue(folder.delete());
+ }
+ }
+
+ // Create the default folders for the external primary volume.
+ Optional<DatabaseHelper> maybeDatabaseHelper = provider.getDatabaseHelper(
+ EXTERNAL_DATABASE_NAME);
+ assertTrue(maybeDatabaseHelper.isPresent());
+ provider.ensureDefaultFolders(externalPrimary,
+ maybeDatabaseHelper.get().getWritableDatabaseForTest());
+
+ // Make sure all of the folders were created.
+ for (String folderName : defaultFolderList) {
+ final File folder = new File(externalPrimary.getPath(), folderName);
+ try {
+ assertTrue(folder.exists());
+ } finally {
+ // Clean up.
+ folder.delete();
+ }
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_EXCLUSION_LIST_FOR_DEFAULT_FOLDERS)
+ public void testEnsureDefaultFolders_WithExclusionList() throws Exception {
+ // Put the fake "default" folders in Documents as they can easily be created and
+ // deleted here.
+ String[] defaultFolderList =
+ new String[]{"Documents/FolderA", "Documents/FolderB", "Documents/FolderC",
+ "Documents/FolderD"};
+ // The exclusion list is case insensitive.
+ List<String> exclusionList = Arrays.asList("Documents/foldera", "Documents/FOLDERC");
+ final MediaProvider provider = new MediaProvider() {
+ @Override
+ protected String[] getDefaultFolderNames() {
+ return defaultFolderList;
+ }
+
+ @Override
+ protected List<String> getFoldersToSkipInDefaultCreation() {
+ return exclusionList;
+ }
+
+ @Override
+ protected void storageNativeBootPropertyChangeListener() {
+ // Ignore this as test app cannot read device config
+ }
+ };
+
+ // Get the external primary volume.
+ final ProviderInfo info = sIsolatedContext.getPackageManager()
+ .resolveContentProvider(MediaStore.AUTHORITY, PackageManager.GET_META_DATA);
+ provider.attachInfo(sIsolatedContext, info);
+ MediaVolume externalPrimary = provider.getVolume(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+
+ // Set this preference to ensure that the default folders are actually created.
+ setSharedPreference("created_default_folders_" + externalPrimary.getId(), 0);
+
+ // Make sure none of the folders exist already.
+ for (String folderName : defaultFolderList) {
+ final File folder = new File(externalPrimary.getPath(), folderName);
+ if (folder.exists()) {
+ assertTrue(folder.delete());
+ }
+ }
+
+ // Create the default folders for the external primary volume.
+ Optional<DatabaseHelper> maybeDatabaseHelper = provider.getDatabaseHelper(
+ EXTERNAL_DATABASE_NAME);
+ assertTrue(maybeDatabaseHelper.isPresent());
+ provider.ensureDefaultFolders(externalPrimary,
+ maybeDatabaseHelper.get().getWritableDatabaseForTest());
+
+ // Make sure that the folders on exclusion list were not created.
+ List<String> exclusionListCaseInsensitive = exclusionList.stream().map(
+ String::toLowerCase).collect(
+ Collectors.toList());
+ for (String folderName : defaultFolderList) {
+ final File folder = new File(externalPrimary.getPath(), folderName);
+ try {
+ if (exclusionListCaseInsensitive.contains(
+ folderName.toLowerCase(Locale.ROOT))) {
+ assertFalse(folder.exists());
+ } else {
+ assertTrue(folder.exists());
+ }
+ } finally {
+ // Clean up.
+ folder.delete();
+ }
+ }
+ }
+
@Test
public void testEnsureFileColumns_InvalidMimeType_targetSdkQ() throws Exception {
final MediaProvider provider = new MediaProvider() {
@@ -1030,7 +1319,10 @@ public class MediaProviderTest {
final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
final ContentValues reverse = new ContentValues();
reverse.put(MediaColumns.DATA,
- "/storage/emulated/0/DCIM/My Vacation/.pending-1577836800-IMG1024.JPG");
+ String.format(
+ Locale.ROOT,
+ "/storage/emulated/%d/DCIM/My Vacation/.pending-1577836800-IMG1024.JPG",
+ UserHandle.myUserId()));
ensureFileColumns(uri, reverse);
assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH));
@@ -1078,7 +1370,10 @@ public class MediaProviderTest {
final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
final ContentValues reverse = new ContentValues();
reverse.put(MediaColumns.DATA,
- "/storage/emulated/0/DCIM/My Vacation/.trashed-1577836800-IMG1024.JPG");
+ String.format(
+ Locale.ROOT,
+ "/storage/emulated/%d/DCIM/My Vacation/.trashed-1577836800-IMG1024.JPG",
+ UserHandle.myUserId()));
ensureFileColumns(uri, reverse);
assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH));
@@ -1943,6 +2238,148 @@ public class MediaProviderTest {
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_INDEX_MEDIA_LATITUDE_LONGITUDE)
+ public void testQueryingMediaGeolocationDataInProjectionShouldReturnNull() throws Exception {
+ // Check with both upper and lower case column names
+ String[][] projections = new String[][] {
+ new String[] {
+ ImageColumns.DISPLAY_NAME,
+ ImageColumns.LATITUDE,
+ ImageColumns.LONGITUDE
+ },
+ new String[] {
+ ImageColumns.DISPLAY_NAME,
+ "LATITUDE",
+ "LONGITUDE"
+ },
+ new String[] {
+ ImageColumns.DISPLAY_NAME,
+ ImageColumns.LATITUDE + " AS LAT",
+ ImageColumns.LONGITUDE + " AS LONG"
+ }
+ };
+
+ for (int i = 0; i < projections.length; i++) {
+ String[] projection = projections[i];
+ String testFileName = "test_file";
+ final File downloads = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DOWNLOADS);
+ File file = stage(R.raw.lg_g4_iso_800_jpg, new File(downloads, testFileName));
+ ModernMediaScanner modernMediaScanner = new ModernMediaScanner(sIsolatedContext,
+ new TestConfigStore());
+ Uri testFileUri = modernMediaScanner.scanFile(file, MediaScanner.REASON_UNKNOWN);
+ try (Cursor cursor = sIsolatedContext.getContentResolver()
+ .query(testFileUri, projection, null, null, null);) {
+ assertNotNull(cursor);
+ int nameIndex = cursor.getColumnIndex(ImageColumns.DISPLAY_NAME);
+ int latitudeIndex = cursor.getColumnIndex(ImageColumns.LATITUDE);
+ int longitudeIndex = cursor.getColumnIndex(ImageColumns.LONGITUDE);
+
+ assertThat(cursor.getCount()).isEqualTo(1);
+ cursor.moveToFirst();
+ // Assert name column accessed is non-null and valid
+ assertTrue(cursor.getString(nameIndex).contains(testFileName));
+ // Geolocation data fields should be NULL
+ assertTrue("Latitude is not null", cursor.isNull(latitudeIndex));
+ assertTrue("Longitude is not null", cursor.isNull(longitudeIndex));
+ } finally {
+ // Cleanup
+ file.delete();
+ }
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_INDEX_MEDIA_LATITUDE_LONGITUDE)
+ public void testQueryingMediaGeolocationDataInSelectionShouldReturnEmptyCursor()
+ throws Exception {
+ final File downloads = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DOWNLOADS);
+ File file = stage(R.raw.lg_g4_iso_800_jpg, new File(downloads, "test"));
+ ModernMediaScanner modernMediaScanner = new ModernMediaScanner(sIsolatedContext,
+ new TestConfigStore());
+ Uri testFileUri = modernMediaScanner.scanFile(file, MediaScanner.REASON_UNKNOWN);
+
+ String[] projection = new String[] {
+ ImageColumns._ID,
+ ImageColumns.DISPLAY_NAME
+ };
+ String selection = ImageColumns.LATITUDE + " = ?";
+ String[] selectionArgs = new String[] { "67.8" };
+ try (Cursor cursor = sIsolatedContext.getContentResolver()
+ .query(testFileUri, projection, selection, selectionArgs, null);) {
+ assertNotNull(cursor);
+ // Should no return any results
+ assertThat(cursor.getCount()).isEqualTo(0);
+ } finally {
+ // Clean up
+ file.delete();
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_INDEX_MEDIA_LATITUDE_LONGITUDE)
+ public void testQueryingMediaGeolocationDataInOrderByShouldReturnNonEmptyCursor()
+ throws Exception {
+ String testFileName = "test";
+ final File downloads = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DOWNLOADS);
+ File file = stage(R.raw.lg_g4_iso_800_jpg, new File(downloads, testFileName));
+ ModernMediaScanner modernMediaScanner = new ModernMediaScanner(sIsolatedContext,
+ new TestConfigStore());
+ Uri testFileUri = modernMediaScanner.scanFile(file, MediaScanner.REASON_UNKNOWN);
+
+ String[] projection = new String[] {
+ ImageColumns._ID,
+ ImageColumns.DISPLAY_NAME
+ };
+ try (Cursor cursor = sIsolatedContext.getContentResolver()
+ .query(testFileUri, projection, null, null, ImageColumns.LONGITUDE);) {
+ assertNotNull(cursor);
+ // Should return non-empty results
+ assertThat(cursor.getCount()).isEqualTo(1);
+ int nameIndex = cursor.getColumnIndex(ImageColumns.DISPLAY_NAME);
+ cursor.moveToFirst();
+ // Assert name column accessed is non-null and valid
+ assertTrue(cursor.getString(nameIndex).contains(testFileName));
+ } finally {
+ // Clean up
+ file.delete();
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_INDEX_MEDIA_LATITUDE_LONGITUDE)
+ public void testQueryingMediaGeolocationDataInGroupByAndHavingShouldReturnEmptyCursor()
+ throws Exception {
+ String testFileName = "test";
+ final File downloads = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DOWNLOADS);
+ File file = stage(R.raw.lg_g4_iso_800_jpg, new File(downloads, testFileName));
+ ModernMediaScanner modernMediaScanner = new ModernMediaScanner(sIsolatedContext,
+ new TestConfigStore());
+ Uri testFileUri = modernMediaScanner.scanFile(file, MediaScanner.REASON_UNKNOWN);
+
+ String[] projection = new String[] {
+ ImageColumns._ID,
+ ImageColumns.DISPLAY_NAME
+ };
+ Bundle queryArgs = new Bundle();
+ queryArgs.putString(QUERY_ARG_SQL_GROUP_BY, ImageColumns.LATITUDE);
+ queryArgs.putString(QUERY_ARG_SQL_HAVING, ImageColumns.LONGITUDE + " > 100");
+ try (Cursor cursor = sIsolatedContext.getContentResolver()
+ .query(testFileUri, projection, queryArgs, null);) {
+ assertNotNull(cursor);
+ // Should not return any results
+ assertThat(cursor.getCount()).isEqualTo(0);
+ } finally {
+ // Clean up
+ file.delete();
+ }
+ }
+
+
private void testRedactionForFileExtension(int resId, String extension) throws Exception {
final File dir = Environment
.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
@@ -1985,4 +2422,24 @@ public class MediaProviderTest {
sIsolatedResolver = sIsolatedContext.getContentResolver();
sItemsProvider = new ItemsProvider(sIsolatedContext);
}
+
+ @Test
+ public void testGetType() throws Exception {
+ final File dir = Environment
+ .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ final File file = new File(dir, "test" + System.nanoTime() + ".jpg");
+ stage(R.raw.lg_g4_iso_800_jpg, file);
+ final Uri uri = MediaStore.scanFile(sIsolatedResolver, file);
+ try (ContentProviderClient cpc = sIsolatedResolver
+ .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+ final MediaProvider mp = (MediaProvider) cpc.getLocalContentProvider();
+ final Uri redactedUri = mp.getRedactedUri(uri);
+
+ final String actualType = mp.getType(redactedUri);
+
+ assertEquals("image/dng", actualType);
+ } finally {
+ file.delete();
+ }
+ }
}
diff --git a/tests/src/com/android/providers/media/PermissionActivityTest.java b/tests/src/com/android/providers/media/PermissionActivityTest.java
index 1e8b1cd10..984c9c2ca 100644
--- a/tests/src/com/android/providers/media/PermissionActivityTest.java
+++ b/tests/src/com/android/providers/media/PermissionActivityTest.java
@@ -529,13 +529,16 @@ public class PermissionActivityTest {
/* attributionTag= */ null);
} else if (TextUtils.equals(op, OP_READ_MEDIA_IMAGES)) {
return expected == checkPermissionReadImages(
- context, pid, uid, packageName, /* attributionTag= */ null, /* isAtleastT */ true);
+ context, pid, uid, packageName, /* attributionTag= */ null, /* isAtleastT */ true,
+ /* forDataDelivery */ true);
} else if (TextUtils.equals(op, OP_READ_MEDIA_AUDIO)) {
return expected == checkPermissionReadAudio(
- context, pid, uid, packageName, /* attributionTag= */ null, /* isAtleastT */ true);
+ context, pid, uid, packageName, /* attributionTag= */ null, /* isAtleastT */ true,
+ /* forDataDelivery */ true);
} else if (TextUtils.equals(op, OP_READ_MEDIA_VIDEO)) {
return expected == checkPermissionReadVideo(
- context, pid, uid, packageName, /* attributionTag= */ null, /* isAtleastT */ true);
+ context, pid, uid, packageName, /* attributionTag= */ null, /* isAtleastT */ true,
+ /* forDataDelivery */ true);
} else if (TextUtils.equals(op, OP_MANAGE_EXTERNAL_STORAGE)) {
return expected == checkPermissionManager(context, pid, uid, packageName,
/* attributionTag= */ null);
diff --git a/tests/src/com/android/providers/media/PhotoPickerTranscodeHelperTest.java b/tests/src/com/android/providers/media/PhotoPickerTranscodeHelperTest.java
new file mode 100644
index 000000000..7b01f57f2
--- /dev/null
+++ b/tests/src/com/android/providers/media/PhotoPickerTranscodeHelperTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2024 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.media;
+
+import static android.os.Environment.DIRECTORY_DOWNLOADS;
+import static android.os.Environment.getExternalStorageDirectory;
+
+import static androidx.test.InstrumentationRegistry.getTargetContext;
+
+import static com.android.providers.media.PhotoPickerTranscodeHelper.Media3Transcoder;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class PhotoPickerTranscodeHelperTest {
+
+ private final int mTestUserId = UserHandle.myUserId();
+ private final String mTestHost = "test";
+ private final String mTestMediaId = "123";
+ private final Uri mTestUri = Uri.parse("content://media/picker/" + mTestUserId + "/"
+ + mTestHost + "/media/" + mTestMediaId);
+ private PhotoPickerTranscodeHelper mHelper;
+ private File mTestDirectory;
+
+ @Before
+ public void setUp() {
+ mTestDirectory = new File(getExternalStorageDirectory(),
+ DIRECTORY_DOWNLOADS + "/.picker_transcoded");
+ mHelper = new PhotoPickerTranscodeHelper(mTestDirectory);
+ }
+
+ @After
+ public void tearDown() {
+ mTestDirectory.delete();
+ }
+
+ @Test
+ public void transcode() throws IOException {
+ // Act & Assert.
+ PhotoPickerTranscodeHelper helper = getTranscodeHelper(new Media3Transcoder() {
+ @Override
+ int transcode(@NonNull Context context, @NonNull Uri sourceUri,
+ @NonNull String destinationPath, int timeoutSec, boolean useOpenGl) {
+ return Media3Transcoder.TRANSCODING_SUCCESS;
+ }
+ });
+ assertThat(helper.transcode(getTargetContext(), mTestUri)).isTrue();
+ }
+
+ @Test
+ public void transcode_doNotUseTranscoder_whenCacheFileExists() throws IOException {
+ // Pre-create corresponded cached transcoded file.
+ final File cachedFile = new File(mTestDirectory, mTestHost + "_" + mTestMediaId);
+ if (cachedFile.createNewFile()) {
+ assertThat(cachedFile.exists()).isTrue();
+ }
+
+ // Act & Assert.
+ PhotoPickerTranscodeHelper helper = getTranscodeHelper(new Media3Transcoder() {
+ @Override
+ int transcode(@NonNull Context context, @NonNull Uri sourceUri,
+ @NonNull String destinationPath, int timeoutSec, boolean useOpenGl) {
+ return PhotoPickerTranscodeHelper.Media3Transcoder.TRANSCODING_OTHER_EXCEPTION;
+ }
+ });
+ assertThat(helper.transcode(getTargetContext(), mTestUri)).isTrue();
+
+ // Cleanup.
+ cachedFile.delete();
+ }
+
+ @Test
+ public void transcode_canRetry_whenOpenGlExportFailed() throws IOException {
+ // Act & Assert.
+ PhotoPickerTranscodeHelper helper = getTranscodeHelper(new Media3Transcoder() {
+
+ private boolean mHasTriedWithOpenGl = false;
+
+ @Override
+ int transcode(@NonNull Context context, @NonNull Uri sourceUri,
+ @NonNull String destinationPath, int timeoutSec, boolean useOpenGl) {
+ if (useOpenGl) {
+ mHasTriedWithOpenGl = true;
+ return Media3Transcoder.TRANSCODING_EXPORT_EXCEPTION;
+ } else if (mHasTriedWithOpenGl) {
+ return Media3Transcoder.TRANSCODING_SUCCESS;
+ } else {
+ return Media3Transcoder.TRANSCODING_OTHER_EXCEPTION;
+ }
+ }
+ });
+ assertThat(helper.transcode(getTargetContext(), mTestUri)).isTrue();
+ }
+
+ @Test
+ public void transcode_returnFalse_whenTranscoderFailed() throws IOException {
+ // Act & Assert.
+ PhotoPickerTranscodeHelper helper = getTranscodeHelper(new Media3Transcoder() {
+ @Override
+ int transcode(@NonNull Context context, @NonNull Uri sourceUri,
+ @NonNull String destinationPath, int timeoutSec, boolean useOpenGl) {
+ return Media3Transcoder.TRANSCODING_EXPORT_EXCEPTION;
+ }
+ });
+ assertThat(helper.transcode(getTargetContext(), mTestUri)).isFalse();
+ }
+
+ @Test
+ public void transcode_returnFalse_whenTimeout() throws IOException {
+ // Act & Assert.
+ PhotoPickerTranscodeHelper helper = getTranscodeHelper(new Media3Transcoder() {
+ @Override
+ int transcode(@NonNull Context context, @NonNull Uri sourceUri,
+ @NonNull String destinationPath, int timeoutSec, boolean useOpenGl) {
+ return Media3Transcoder.TRANSCODING_TIMEOUT_EXCEPTION;
+ }
+ });
+ assertThat(helper.transcode(getTargetContext(), mTestUri)).isFalse();
+ }
+
+ @Test
+ public void openTranscodedFile() throws IOException {
+ // Pre-create corresponded cached transcoded file.
+ final File cachedFile = new File(mTestDirectory, mTestHost + "_" + mTestMediaId);
+ if (cachedFile.createNewFile()) {
+ assertThat(cachedFile.exists()).isTrue();
+ }
+
+ // Act & Assert.
+ ParcelFileDescriptor pfd = mHelper.openTranscodedFile(mTestHost, mTestMediaId);
+ assertThat(pfd).isNotNull();
+
+ // Cleanup.
+ cachedFile.delete();
+ }
+
+ @Test
+ public void getTranscodedFileSize() throws IOException {
+ // Pre-create corresponded cached transcoded file.
+ final File cachedFile = new File(mTestDirectory, mTestHost + "_" + mTestMediaId);
+ if (cachedFile.createNewFile()) {
+ assertThat(cachedFile.exists()).isTrue();
+ }
+ final RandomAccessFile raf = new RandomAccessFile(cachedFile, "rw");
+ raf.setLength(3);
+
+ // Act & Assert.
+ long fileSize = mHelper.getTranscodedFileSize(mTestHost, mTestMediaId);
+ assertThat(fileSize).isEqualTo(3);
+
+ // Cleanup.
+ cachedFile.delete();
+ }
+
+ @Test
+ public void freeCache() throws IOException {
+ final List<String> fileNamesToBeFreed = Arrays.asList("100", "101", "102");
+ createTestFiles(fileNamesToBeFreed);
+
+ // Act.
+ mHelper.freeCache(1024);
+
+ // Assert.
+ for (String fileName : fileNamesToBeFreed) {
+ final File file = new File(mTestDirectory, fileName);
+ assertThat(file.exists()).isFalse();
+ }
+ }
+
+ @Test
+ public void freeCache_returnCorrectBytesFreed() throws IOException {
+ // Pre-create corresponded cached transcoded file.
+ final File cachedFile = new File(mTestDirectory, mTestHost + "_" + mTestMediaId);
+ if (cachedFile.createNewFile()) {
+ assertThat(cachedFile.exists()).isTrue();
+ }
+ final RandomAccessFile raf = new RandomAccessFile(cachedFile, "rw");
+ raf.setLength(17);
+
+ // Act.
+ final long bytesFreed = mHelper.freeCache(1024);
+
+ // Assert.
+ assertThat(cachedFile.exists()).isFalse();
+ assertThat(bytesFreed).isEqualTo(17);
+ }
+
+ @Test
+ public void cleanAllTranscodedFiles() throws IOException {
+ final List<String> fileNamesToBeFreed = Arrays.asList("100", "101", "102");
+ createTestFiles(fileNamesToBeFreed);
+
+ // Act.
+ mHelper.cleanAllTranscodedFiles(null);
+
+ // Assert.
+ for (String fileName : fileNamesToBeFreed) {
+ final File file = new File(mTestDirectory, fileName);
+ assertThat(file.exists()).isFalse();
+ }
+ }
+
+ @Test
+ public void cleanAllTranscodedFiles_notPerform_whenCancellationsSignalSet() throws IOException {
+ final List<String> fileNamesToBeFreed = Arrays.asList("100", "101", "102");
+ createTestFiles(fileNamesToBeFreed);
+
+ // Act.
+ CancellationSignal signal = new CancellationSignal();
+ signal.cancel();
+ mHelper.cleanAllTranscodedFiles(signal);
+
+ // Assert.
+ for (String fileName : fileNamesToBeFreed) {
+ final File file = new File(mTestDirectory, fileName);
+ assertThat(file.exists()).isTrue();
+
+ // Cleanup.
+ file.delete();
+ }
+ }
+
+ @Test
+ public void deleteCachedTranscodedFile() throws IOException {
+ final long localId = 123;
+ final File fileToBeDeleted = new File(mTestDirectory, mTestHost + "_" + "123");
+ if (fileToBeDeleted.createNewFile()) {
+ assertThat(fileToBeDeleted.exists()).isTrue();
+ }
+
+ // Act.
+ mHelper.deleteCachedTranscodedFile(mTestHost, localId);
+
+ // Assert.
+ assertThat(fileToBeDeleted.exists()).isFalse();
+ }
+
+ @NonNull
+ private PhotoPickerTranscodeHelper getTranscodeHelper(@NonNull Media3Transcoder transcoder) {
+ PhotoPickerTranscodeHelper helper = new PhotoPickerTranscodeHelper(mTestDirectory);
+ helper.setTranscoder(transcoder);
+ return helper;
+ }
+
+ private void createTestFiles(@NonNull List<String> fileNames) throws IOException {
+ for (String fileName : fileNames) {
+ File file = new File(mTestDirectory, fileName);
+ if (file.createNewFile()) {
+ assertThat(file.exists()).isTrue();
+ }
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/media/PickerUriResolverTest.java b/tests/src/com/android/providers/media/PickerUriResolverTest.java
index 3f6641b53..c0504d0de 100644
--- a/tests/src/com/android/providers/media/PickerUriResolverTest.java
+++ b/tests/src/com/android/providers/media/PickerUriResolverTest.java
@@ -80,6 +80,7 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
@RunWith(AndroidJUnit4.class)
public class PickerUriResolverTest {
@@ -283,6 +284,24 @@ public class PickerUriResolverTest {
}
@Test
+ public void testOpenPickerTranscodedFile() throws Exception {
+ final Uri transcodedUri = Uri.parse(
+ String.format(
+ Locale.ROOT,
+ "content://media/picker_transcoded/%d/com.android.providers.media"
+ + ".photopicker/media/",
+ UserHandle.myUserId()) + TEST_ID);
+ updateReadUriPermission(transcodedUri, /* grant */ true);
+
+ // Act & Assert.
+ try (ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(transcodedUri,
+ "r", /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */0))) {
+ assertThat(pfd).isNotNull();
+ }
+ }
+
+ @Test
public void testProcessUrisForSelection_withoutPermissionOrAuthorityChecks() {
sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
TEST_USER, ACTION_PICK_IMAGES);
diff --git a/tests/src/com/android/providers/media/SearchTestingUtils.java b/tests/src/com/android/providers/media/SearchTestingUtils.java
new file mode 100644
index 000000000..bd8ada470
--- /dev/null
+++ b/tests/src/com/android/providers/media/SearchTestingUtils.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 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.media;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+
+import com.android.providers.media.search.SearchUtilConstants;
+
+/**
+ * Holds utility functions for testing picker search workers
+ */
+public class SearchTestingUtils {
+
+ private final DatabaseHelper mDatabaseHelper;
+ private final SearchUtilConstants mSearchUtilConstants;
+
+ public SearchTestingUtils(DatabaseHelper databaseHelper) {
+ mDatabaseHelper = databaseHelper;
+ mSearchUtilConstants = new SearchUtilConstants();
+ }
+
+ /**
+ * Returns the processing statuses from the status table for the test file id
+ */
+ public Cursor getMediaItemDataFromStatusTable(Long testFileId) {
+ return mDatabaseHelper.runWithoutTransaction(database -> {
+ return database.query(
+ /* tableName */ mSearchUtilConstants.MEDIA_STATUS_TABLE,
+ /* columns */ new String[] {
+ mSearchUtilConstants.METADATA_PROCESSING_STATUS_COLUMN,
+ mSearchUtilConstants.LABEL_PROCESSING_STATUS_COLUMN,
+ mSearchUtilConstants.LOCATION_PROCESSING_STATUS_COLUMN,
+ mSearchUtilConstants.OCR_PROCESSING_STATUS_COLUMN,
+ mSearchUtilConstants.DISPLAY_NAME_COLUMN
+ },
+ /* selection */ mSearchUtilConstants.MEDIA_ID_COLUMN + " = ?",
+ /* selectionArgs */ new String[] { String.valueOf(testFileId) },
+ /* groupBy */ null, /* having */ null, /*orderBy*/ null, /* limit */null);
+ });
+
+ }
+
+ /**
+ * Marks the testFileId as trashed in the files table
+ */
+ public void trashMediaItem(String testFileId) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(MediaStore.Files.FileColumns.IS_TRASHED, 1);
+ String updateColumn = MediaStore.Files.FileColumns._ID + " = ?";
+ String[] updateArguments = new String[] { testFileId };
+ mDatabaseHelper.runWithTransaction(database -> {
+ return database.update(
+ MediaStore.Files.TABLE, contentValues, updateColumn, updateArguments);
+ });
+ }
+
+ /**
+ * Inserts the given values into the status table
+ */
+ public void insertMediaItemIntoMediaStatusTable(
+ DatabaseHelper databaseHelper, ContentValues insertValues) {
+ databaseHelper.runWithTransaction(database -> {
+ return database.insert(
+ mSearchUtilConstants.MEDIA_STATUS_TABLE,
+ /* nullColumnHack */ null,
+ insertValues);
+ });
+ }
+
+ /**
+ * Updates the given media item data in the specified table with the given values
+ */
+ public void updateMediaItem(String tableName, String mediaId, ContentValues updateValues) {
+ String updateColumn = getUpdateColumn(tableName);
+ String[] updateArguments = new String[] { mediaId };
+ mDatabaseHelper.runWithTransaction(database -> {
+ return database.update(tableName, updateValues, updateColumn, updateArguments);
+ });
+ }
+
+ private String getUpdateColumn(String tableName) {
+ if (tableName.equals(mSearchUtilConstants.MEDIA_STATUS_TABLE)) {
+ return mSearchUtilConstants.MEDIA_ID_COLUMN + " = ?";
+ } else if (tableName.equals(MediaStore.Files.TABLE)) {
+ return MediaStore.Files.FileColumns._ID + " = ?";
+ }
+ throw new IllegalArgumentException("Invalid table name");
+ }
+
+ /**
+ * Deletes the created test file after the tests have executed
+ */
+ public void deleteTestFile(ContentResolver isolatedResolver, long testFileId) {
+ Uri testFileUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL, testFileId);
+ isolatedResolver.delete(testFileUri, Bundle.EMPTY);
+ }
+}
diff --git a/tests/src/com/android/providers/media/TestDatabaseBackupAndRecovery.java b/tests/src/com/android/providers/media/TestDatabaseBackupAndRecovery.java
index 5a2f2017c..259d5caea 100644
--- a/tests/src/com/android/providers/media/TestDatabaseBackupAndRecovery.java
+++ b/tests/src/com/android/providers/media/TestDatabaseBackupAndRecovery.java
@@ -16,7 +16,6 @@
package com.android.providers.media;
-import android.content.Context;
import android.provider.MediaStore;
import com.android.providers.media.fuse.FuseDaemon;
@@ -98,10 +97,10 @@ public class TestDatabaseBackupAndRecovery extends DatabaseBackupAndRecovery {
}
@Override
- protected void waitForVolumeToBeAttached(Set<String> setupCompleteVolumes) {
+ protected void waitForVolumeToBeAttached(String volumeName) {
}
@Override
- protected void queuePublicVolumeRecovery(Context context) {
+ protected void markPublicVolumesRecovery() {
}
}
diff --git a/tests/src/com/android/providers/media/backupandrestore/BackupAndRestoreTestUtils.java b/tests/src/com/android/providers/media/backupandrestore/BackupAndRestoreTestUtils.java
new file mode 100644
index 000000000..228be1b40
--- /dev/null
+++ b/tests/src/com/android/providers/media/backupandrestore/BackupAndRestoreTestUtils.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2025 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.media.backupandrestore;
+
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.FIELD_SEPARATOR;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.KEY_VALUE_SEPARATOR;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.RESTORE_COMPLETED;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.SHARED_PREFERENCE_NAME;
+
+import android.content.Context;
+import android.provider.MediaStore;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class BackupAndRestoreTestUtils {
+
+ /**
+ * Map used to store column name for given key id.
+ */
+ private static Map<String, String> sColumnIdToKeyMap;
+
+ /**
+ * Map used to store key id for given column name.
+ */
+ private static Map<String, String> sColumnNameToIdMap;
+
+ static void createKeyToColumnNameMap() {
+ sColumnIdToKeyMap = new HashMap<>();
+ sColumnIdToKeyMap.put("0", MediaStore.Files.FileColumns.IS_FAVORITE);
+ sColumnIdToKeyMap.put("1", MediaStore.Files.FileColumns.MEDIA_TYPE);
+ sColumnIdToKeyMap.put("2", MediaStore.Files.FileColumns.MIME_TYPE);
+ sColumnIdToKeyMap.put("3", MediaStore.Files.FileColumns._USER_ID);
+ sColumnIdToKeyMap.put("4", MediaStore.Files.FileColumns.SIZE);
+ sColumnIdToKeyMap.put("5", MediaStore.MediaColumns.DATE_TAKEN);
+ sColumnIdToKeyMap.put("6", MediaStore.MediaColumns.CD_TRACK_NUMBER);
+ sColumnIdToKeyMap.put("7", MediaStore.MediaColumns.ALBUM);
+ sColumnIdToKeyMap.put("8", MediaStore.MediaColumns.ARTIST);
+ sColumnIdToKeyMap.put("9", MediaStore.MediaColumns.AUTHOR);
+ sColumnIdToKeyMap.put("10", MediaStore.MediaColumns.COMPOSER);
+ sColumnIdToKeyMap.put("11", MediaStore.MediaColumns.GENRE);
+ sColumnIdToKeyMap.put("12", MediaStore.MediaColumns.TITLE);
+ sColumnIdToKeyMap.put("13", MediaStore.MediaColumns.YEAR);
+ sColumnIdToKeyMap.put("14", MediaStore.MediaColumns.DURATION);
+ sColumnIdToKeyMap.put("15", MediaStore.MediaColumns.NUM_TRACKS);
+ sColumnIdToKeyMap.put("16", MediaStore.MediaColumns.WRITER);
+ sColumnIdToKeyMap.put("17", MediaStore.MediaColumns.ALBUM_ARTIST);
+ sColumnIdToKeyMap.put("18", MediaStore.MediaColumns.DISC_NUMBER);
+ sColumnIdToKeyMap.put("19", MediaStore.MediaColumns.COMPILATION);
+ sColumnIdToKeyMap.put("20", MediaStore.MediaColumns.BITRATE);
+ sColumnIdToKeyMap.put("21", MediaStore.MediaColumns.CAPTURE_FRAMERATE);
+ sColumnIdToKeyMap.put("22", MediaStore.Audio.AudioColumns.TRACK);
+ sColumnIdToKeyMap.put("23", MediaStore.MediaColumns.DOCUMENT_ID);
+ sColumnIdToKeyMap.put("24", MediaStore.MediaColumns.INSTANCE_ID);
+ sColumnIdToKeyMap.put("25", MediaStore.MediaColumns.ORIGINAL_DOCUMENT_ID);
+ sColumnIdToKeyMap.put("26", MediaStore.MediaColumns.RESOLUTION);
+ sColumnIdToKeyMap.put("27", MediaStore.MediaColumns.ORIENTATION);
+ sColumnIdToKeyMap.put("28", MediaStore.Video.VideoColumns.COLOR_STANDARD);
+ sColumnIdToKeyMap.put("29", MediaStore.Video.VideoColumns.COLOR_TRANSFER);
+ sColumnIdToKeyMap.put("30", MediaStore.Video.VideoColumns.COLOR_RANGE);
+ sColumnIdToKeyMap.put("31", MediaStore.Files.FileColumns._VIDEO_CODEC_TYPE);
+ sColumnIdToKeyMap.put("32", MediaStore.MediaColumns.WIDTH);
+ sColumnIdToKeyMap.put("33", MediaStore.MediaColumns.HEIGHT);
+ sColumnIdToKeyMap.put("34", MediaStore.Images.ImageColumns.DESCRIPTION);
+ sColumnIdToKeyMap.put("35", MediaStore.Images.ImageColumns.EXPOSURE_TIME);
+ sColumnIdToKeyMap.put("36", MediaStore.Images.ImageColumns.F_NUMBER);
+ sColumnIdToKeyMap.put("37", MediaStore.Images.ImageColumns.ISO);
+ sColumnIdToKeyMap.put("38", MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE);
+ sColumnIdToKeyMap.put("39", MediaStore.Files.FileColumns._SPECIAL_FORMAT);
+ sColumnIdToKeyMap.put("40", MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME);
+ // Adding number gap to allow addition of new values
+ sColumnIdToKeyMap.put("80", MediaStore.MediaColumns.XMP);
+ }
+
+ static void createColumnNameToKeyMap() {
+ sColumnNameToIdMap = new HashMap<>();
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.IS_FAVORITE, "0");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.MEDIA_TYPE, "1");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.MIME_TYPE, "2");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns._USER_ID, "3");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.SIZE, "4");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.DATE_TAKEN, "5");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.CD_TRACK_NUMBER, "6");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ALBUM, "7");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ARTIST, "8");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.AUTHOR, "9");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.COMPOSER, "10");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.GENRE, "11");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.TITLE, "12");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.YEAR, "13");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.DURATION, "14");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.NUM_TRACKS, "15");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.WRITER, "16");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ALBUM_ARTIST, "17");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.DISC_NUMBER, "18");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.COMPILATION, "19");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.BITRATE, "20");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.CAPTURE_FRAMERATE, "21");
+ sColumnNameToIdMap.put(MediaStore.Audio.AudioColumns.TRACK, "22");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.DOCUMENT_ID, "23");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.INSTANCE_ID, "24");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ORIGINAL_DOCUMENT_ID, "25");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.RESOLUTION, "26");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ORIENTATION, "27");
+ sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_STANDARD, "28");
+ sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_TRANSFER, "29");
+ sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_RANGE, "30");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns._VIDEO_CODEC_TYPE, "31");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.WIDTH, "32");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.HEIGHT, "33");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.DESCRIPTION, "34");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.EXPOSURE_TIME, "35");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.F_NUMBER, "36");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.ISO, "37");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE, "38");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns._SPECIAL_FORMAT, "39");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME, "40");
+ // Adding number gap to allow addition of new values
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.XMP, "80");
+ }
+
+ static Map<String, String> deSerialiseValueString(String valueString) {
+ if (sColumnIdToKeyMap == null) {
+ createKeyToColumnNameMap();
+ }
+
+ String[] values = valueString.split(":::");
+ Map<String, String> map = new HashMap<>();
+ for (String value : values) {
+ if (value == null || value.isEmpty()) {
+ continue;
+ }
+
+ String[] keyValue = value.split("=", 2);
+ map.put(sColumnIdToKeyMap.get(keyValue[0]), keyValue[1]);
+ }
+
+ return map;
+ }
+
+ static String createSerialisedValue(Map<String, String> entries) {
+ if (sColumnNameToIdMap == null) {
+ createColumnNameToKeyMap();
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (String backupColumn : sColumnNameToIdMap.keySet()) {
+ if (entries.containsKey(backupColumn)) {
+ sb.append(sColumnNameToIdMap.get(backupColumn)).append(KEY_VALUE_SEPARATOR).append(
+ entries.get(backupColumn));
+ sb.append(FIELD_SEPARATOR);
+ }
+ }
+ return sb.toString();
+ }
+
+ static boolean getSharedPreferenceValue(Context context) {
+ return context.getSharedPreferences(SHARED_PREFERENCE_NAME,
+ Context.MODE_PRIVATE).getBoolean(RESTORE_COMPLETED, false);
+ }
+}
diff --git a/tests/src/com/android/providers/media/backupandrestore/BackupExecutorTest.java b/tests/src/com/android/providers/media/backupandrestore/BackupExecutorTest.java
index d464dffc3..70c7eb373 100644
--- a/tests/src/com/android/providers/media/backupandrestore/BackupExecutorTest.java
+++ b/tests/src/com/android/providers/media/backupandrestore/BackupExecutorTest.java
@@ -16,6 +16,7 @@
package com.android.providers.media.backupandrestore;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreTestUtils.deSerialiseValueString;
import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.BACKUP_COLUMNS;
import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN;
import static com.android.providers.media.scan.MediaScannerTest.stage;
@@ -51,7 +52,6 @@ import com.android.providers.media.util.FileUtils;
import org.junit.After;
import org.junit.Before;
-import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -75,11 +75,6 @@ public final class BackupExecutorTest {
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
- /**
- * Map used to store key id for given column and vice versa.
- */
- private static Map<String, String> sColumnIdToKeyMap;
-
private Set<File> mStagedFiles = new HashSet<>();
private Context mIsolatedContext;
@@ -90,11 +85,6 @@ public final class BackupExecutorTest {
private File mDownloadsDir;
- @BeforeClass
- public static void setupBeforeClass() {
- createColumnToKeyMap();
- }
-
private String mLevelDbPath;
@Before
@@ -216,71 +206,9 @@ public final class BackupExecutorTest {
}
}
- static Map<String, String> deSerialiseValueString(String valueString) {
- String[] values = valueString.split(":::");
- Map<String, String> map = new HashMap<>();
- for (String value : values) {
- if (value == null || value.isEmpty()) {
- continue;
- }
-
- String[] keyValue = value.split("=", 2);
- map.put(sColumnIdToKeyMap.get(keyValue[0]), keyValue[1]);
- }
-
- return map;
- }
-
private void stageNewFile(int resId, File file) throws IOException {
file.createNewFile();
mStagedFiles.add(file);
stage(resId, file);
}
-
- static void createColumnToKeyMap() {
- sColumnIdToKeyMap = new HashMap<>();
- sColumnIdToKeyMap.put("0", MediaStore.Files.FileColumns.IS_FAVORITE);
- sColumnIdToKeyMap.put("1", MediaStore.Files.FileColumns.MEDIA_TYPE);
- sColumnIdToKeyMap.put("2", MediaStore.Files.FileColumns.MIME_TYPE);
- sColumnIdToKeyMap.put("3", MediaStore.Files.FileColumns._USER_ID);
- sColumnIdToKeyMap.put("4", MediaStore.Files.FileColumns.SIZE);
- sColumnIdToKeyMap.put("5", MediaStore.MediaColumns.DATE_TAKEN);
- sColumnIdToKeyMap.put("6", MediaStore.MediaColumns.CD_TRACK_NUMBER);
- sColumnIdToKeyMap.put("7", MediaStore.MediaColumns.ALBUM);
- sColumnIdToKeyMap.put("8", MediaStore.MediaColumns.ARTIST);
- sColumnIdToKeyMap.put("9", MediaStore.MediaColumns.AUTHOR);
- sColumnIdToKeyMap.put("10", MediaStore.MediaColumns.COMPOSER);
- sColumnIdToKeyMap.put("11", MediaStore.MediaColumns.GENRE);
- sColumnIdToKeyMap.put("12", MediaStore.MediaColumns.TITLE);
- sColumnIdToKeyMap.put("13", MediaStore.MediaColumns.YEAR);
- sColumnIdToKeyMap.put("14", MediaStore.MediaColumns.DURATION);
- sColumnIdToKeyMap.put("15", MediaStore.MediaColumns.NUM_TRACKS);
- sColumnIdToKeyMap.put("16", MediaStore.MediaColumns.WRITER);
- sColumnIdToKeyMap.put("17", MediaStore.MediaColumns.ALBUM_ARTIST);
- sColumnIdToKeyMap.put("18", MediaStore.MediaColumns.DISC_NUMBER);
- sColumnIdToKeyMap.put("19", MediaStore.MediaColumns.COMPILATION);
- sColumnIdToKeyMap.put("20", MediaStore.MediaColumns.BITRATE);
- sColumnIdToKeyMap.put("21", MediaStore.MediaColumns.CAPTURE_FRAMERATE);
- sColumnIdToKeyMap.put("22", MediaStore.Audio.AudioColumns.TRACK);
- sColumnIdToKeyMap.put("23", MediaStore.MediaColumns.DOCUMENT_ID);
- sColumnIdToKeyMap.put("24", MediaStore.MediaColumns.INSTANCE_ID);
- sColumnIdToKeyMap.put("25", MediaStore.MediaColumns.ORIGINAL_DOCUMENT_ID);
- sColumnIdToKeyMap.put("26", MediaStore.MediaColumns.RESOLUTION);
- sColumnIdToKeyMap.put("27", MediaStore.MediaColumns.ORIENTATION);
- sColumnIdToKeyMap.put("28", MediaStore.Video.VideoColumns.COLOR_STANDARD);
- sColumnIdToKeyMap.put("29", MediaStore.Video.VideoColumns.COLOR_TRANSFER);
- sColumnIdToKeyMap.put("30", MediaStore.Video.VideoColumns.COLOR_RANGE);
- sColumnIdToKeyMap.put("31", MediaStore.Files.FileColumns._VIDEO_CODEC_TYPE);
- sColumnIdToKeyMap.put("32", MediaStore.MediaColumns.WIDTH);
- sColumnIdToKeyMap.put("33", MediaStore.MediaColumns.HEIGHT);
- sColumnIdToKeyMap.put("34", MediaStore.Images.ImageColumns.DESCRIPTION);
- sColumnIdToKeyMap.put("35", MediaStore.Images.ImageColumns.EXPOSURE_TIME);
- sColumnIdToKeyMap.put("36", MediaStore.Images.ImageColumns.F_NUMBER);
- sColumnIdToKeyMap.put("37", MediaStore.Images.ImageColumns.ISO);
- sColumnIdToKeyMap.put("38", MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE);
- sColumnIdToKeyMap.put("39", MediaStore.Files.FileColumns._SPECIAL_FORMAT);
- sColumnIdToKeyMap.put("40", MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME);
- // Adding number gap to allow addition of new values
- sColumnIdToKeyMap.put("80", MediaStore.MediaColumns.XMP);
- }
}
diff --git a/tests/src/com/android/providers/media/backupandrestore/MediaBackupAgentTest.java b/tests/src/com/android/providers/media/backupandrestore/MediaBackupAgentTest.java
new file mode 100644
index 000000000..5f892f126
--- /dev/null
+++ b/tests/src/com/android/providers/media/backupandrestore/MediaBackupAgentTest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2025 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.media.backupandrestore;
+
+import static com.android.providers.media.backupandrestore.BackupAndRestoreTestUtils.deSerialiseValueString;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreTestUtils.getSharedPreferenceValue;
+import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN;
+import static com.android.providers.media.scan.MediaScannerTest.stage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.Manifest;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.MediaStore;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import com.android.providers.media.IsolatedContext;
+import com.android.providers.media.R;
+import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.flags.Flags;
+import com.android.providers.media.leveldb.LevelDBInstance;
+import com.android.providers.media.leveldb.LevelDBManager;
+import com.android.providers.media.leveldb.LevelDBResult;
+import com.android.providers.media.scan.ModernMediaScanner;
+import com.android.providers.media.util.FileUtils;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+@EnableFlags(Flags.FLAG_ENABLE_BACKUP_AND_RESTORE)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class MediaBackupAgentTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private Context mIsolatedContext;
+
+ private ContentResolver mIsolatedResolver;
+
+ private ModernMediaScanner mModern;
+
+ private File mDownloadsDir;
+
+ private File mRestoreDir;
+ private File mBackupDir;
+
+ private String mLevelDbPath;
+ private MediaBackupAgent mMediaBackupAgent;
+
+ @Before
+ public void setUp() {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ Manifest.permission.DUMP,
+ Manifest.permission.READ_DEVICE_CONFIG);
+
+ mIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false);
+ mIsolatedResolver = mIsolatedContext.getContentResolver();
+ mModern = new ModernMediaScanner(mIsolatedContext, new TestConfigStore());
+ mRestoreDir = new File(mIsolatedContext.getFilesDir(), "restore");
+ mBackupDir = new File(mIsolatedContext.getFilesDir(), "backup");
+ mDownloadsDir = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DOWNLOADS);
+ mLevelDbPath =
+ mIsolatedContext.getFilesDir().getAbsolutePath() + "/backup/external_primary/";
+ FileUtils.deleteContents(mDownloadsDir);
+
+ mMediaBackupAgent = new MediaBackupAgent();
+ mMediaBackupAgent.attach(mIsolatedContext);
+ }
+
+ @Test
+ public void testCompleteFlow() throws Exception {
+ //create new test file & stage it
+ File file = new File(mDownloadsDir, "testImage_"
+ + SystemClock.elapsedRealtimeNanos() + ".jpg");
+ file.createNewFile();
+ stage(R.raw.test_image, file);
+
+ try {
+ String path = file.getAbsolutePath();
+ // scan directory to have entry in files table
+ mModern.scanDirectory(mDownloadsDir, REASON_UNKNOWN);
+
+ // set is_favorite value to 1. We will check this value later after restoration.
+ updateFavoritesValue(path, 1);
+
+ // run idle maintenance, this will save file's metadata in leveldb with is_favorite = 1
+ MediaStore.runIdleMaintenance(mIsolatedResolver);
+ assertTrue(mBackupDir.exists());
+
+ assertLevelDbExistsAndHasLatestValues(path);
+
+ // run the backup agent. This will copy over backup directory to restore directory and
+ // set shared preference.
+ mMediaBackupAgent.onRestoreFinished();
+ assertTrue(getSharedPreferenceValue(mIsolatedContext));
+ assertTrue(mRestoreDir.exists());
+ assertFalse(mBackupDir.exists());
+
+ //delete existing external db database having old values
+ mIsolatedContext.deleteDatabase("external.db");
+
+ // run media scan, this will populate db and read value from backup
+ mModern.scanDirectory(mDownloadsDir, REASON_UNKNOWN);
+ assertEquals(1, queryFavoritesValue(path));
+
+ // on idle maintenance, clean up is called. It should delete restore directory and set
+ // shared preference to false
+ MediaStore.runIdleMaintenance(mIsolatedResolver);
+ assertFalse(getSharedPreferenceValue(mIsolatedContext));
+ assertFalse(mRestoreDir.exists());
+ } finally {
+ file.delete();
+ }
+ }
+
+ private void assertLevelDbExistsAndHasLatestValues(String path) {
+ // check that entry is created in level db for the file
+ LevelDBInstance levelDBInstance = LevelDBManager.getInstance(mLevelDbPath);
+ assertNotNull(levelDBInstance);
+
+ // check that entry created in level db has latest value(is_favorite = 1)
+ LevelDBResult levelDBResult = levelDBInstance.query(path);
+ assertNotNull(levelDBResult);
+ Map<String, String> actualResultMap = deSerialiseValueString(levelDBResult.getValue());
+ assertEquals(1,
+ Integer.parseInt(actualResultMap.get(MediaStore.MediaColumns.IS_FAVORITE)));
+ }
+
+ private void updateFavoritesValue(String path, int value) {
+ Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
+ String selection = MediaStore.Files.FileColumns.DATA + " LIKE ?";
+ String[] selectionArgs = new String[]{path};
+
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.Files.FileColumns.IS_FAVORITE, value);
+ values.put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis());
+
+ mIsolatedResolver.update(uri, values, selection, selectionArgs);
+ }
+
+ private int queryFavoritesValue(String path) {
+ Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
+ String selection = MediaStore.Files.FileColumns.DATA + " LIKE ?";
+ String[] selectionArgs = new String[]{path};
+
+ Cursor cursor = mIsolatedResolver.query(uri, null, selection, selectionArgs, null);
+ cursor.moveToFirst();
+ return cursor.getInt(cursor.getColumnIndex(MediaStore.Files.FileColumns.IS_FAVORITE));
+ }
+}
diff --git a/tests/src/com/android/providers/media/backupandrestore/RestoreExecutorTest.java b/tests/src/com/android/providers/media/backupandrestore/RestoreExecutorTest.java
index ebffecc13..9f0b41a21 100644
--- a/tests/src/com/android/providers/media/backupandrestore/RestoreExecutorTest.java
+++ b/tests/src/com/android/providers/media/backupandrestore/RestoreExecutorTest.java
@@ -16,8 +16,7 @@
package com.android.providers.media.backupandrestore;
-import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.FIELD_SEPARATOR;
-import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.KEY_VALUE_SEPARATOR;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreTestUtils.createSerialisedValue;
import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.RESTORE_COMPLETED;
import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN;
import static com.android.providers.media.scan.MediaScannerTest.stage;
@@ -53,7 +52,6 @@ import com.android.providers.media.util.FileUtils;
import org.junit.After;
import org.junit.Before;
-import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -73,11 +71,6 @@ public final class RestoreExecutorTest {
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
- /**
- * Map used to store key id for given column and vice versa.
- */
- private static Map<String, String> sColumnNameToIdMap;
-
private Context mIsolatedContext;
private ContentResolver mIsolatedResolver;
@@ -86,11 +79,6 @@ public final class RestoreExecutorTest {
private File mDownloadsDir;
- @BeforeClass
- public static void setupBeforeClass() {
- createColumnToKeyMap();
- }
-
@Before
public void setUp() {
final Context context = InstrumentationRegistry.getTargetContext();
@@ -347,63 +335,4 @@ public final class RestoreExecutorTest {
file.createNewFile();
stage(resId, file);
}
-
- private String createSerialisedValue(Map<String, String> entries) {
- StringBuilder sb = new StringBuilder();
- for (String backupColumn : sColumnNameToIdMap.keySet()) {
- if (entries.containsKey(backupColumn)) {
- sb.append(sColumnNameToIdMap.get(backupColumn)).append(KEY_VALUE_SEPARATOR).append(
- entries.get(backupColumn));
- sb.append(FIELD_SEPARATOR);
- }
- }
- return sb.toString();
- }
-
- private static void createColumnToKeyMap() {
- sColumnNameToIdMap = new HashMap<>();
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns.IS_FAVORITE, "0");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns.MEDIA_TYPE, "1");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns.MIME_TYPE, "2");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns._USER_ID, "3");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns.SIZE, "4");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.DATE_TAKEN, "5");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.CD_TRACK_NUMBER, "6");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.ALBUM, "7");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.ARTIST, "8");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.AUTHOR, "9");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.COMPOSER, "10");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.GENRE, "11");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.TITLE, "12");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.YEAR, "13");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.DURATION, "14");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.NUM_TRACKS, "15");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.WRITER, "16");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.ALBUM_ARTIST, "17");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.DISC_NUMBER, "18");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.COMPILATION, "19");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.BITRATE, "20");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.CAPTURE_FRAMERATE, "21");
- sColumnNameToIdMap.put(MediaStore.Audio.AudioColumns.TRACK, "22");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.DOCUMENT_ID, "23");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.INSTANCE_ID, "24");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.ORIGINAL_DOCUMENT_ID, "25");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.RESOLUTION, "26");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.ORIENTATION, "27");
- sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_STANDARD, "28");
- sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_TRANSFER, "29");
- sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_RANGE, "30");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns._VIDEO_CODEC_TYPE, "31");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.WIDTH, "32");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.HEIGHT, "33");
- sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.DESCRIPTION, "34");
- sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.EXPOSURE_TIME, "35");
- sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.F_NUMBER, "36");
- sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.ISO, "37");
- sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE, "38");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns._SPECIAL_FORMAT, "39");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME, "40");
- // Adding number gap to allow addition of new values
- sColumnNameToIdMap.put(MediaStore.MediaColumns.XMP, "80");
- }
}
diff --git a/tests/src/com/android/providers/media/cloudproviders/SearchProvider.java b/tests/src/com/android/providers/media/cloudproviders/SearchProvider.java
new file mode 100644
index 000000000..d0da49abe
--- /dev/null
+++ b/tests/src/com/android/providers/media/cloudproviders/SearchProvider.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2024 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.media.cloudproviders;
+
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_1;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_2;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_3;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_4;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_1;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_2;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getAlbumCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getCloudMediaCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getLocalMediaCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getMediaCategoriesCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getSuggestionCursor;
+
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MergeCursor;
+import android.graphics.Point;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.provider.CloudMediaProvider;
+import android.provider.CloudMediaProviderContract;
+
+import java.io.FileNotFoundException;
+import java.util.List;
+
+public class SearchProvider extends CloudMediaProvider {
+ public static final String AUTHORITY =
+ "com.android.providers.media.photopicker.tests.cloud_search_provider";
+
+ public static final MergeCursor DEFAULT_CLOUD_MEDIA = new MergeCursor(List.of(
+ getCloudMediaCursor(CLOUD_ID_1, LOCAL_ID_1, 0),
+ getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0),
+ getCloudMediaCursor(CLOUD_ID_3, null, 0),
+ getCloudMediaCursor(CLOUD_ID_4, null, 0)
+ ).toArray(new Cursor[0]));
+
+ public static final MergeCursor DEFAULT_SUGGESTION_RESULTS = new MergeCursor(List.of(
+ getSuggestionCursor(CLOUD_ID_1),
+ getSuggestionCursor(CLOUD_ID_2)
+ ).toArray(new Cursor[0]));
+
+ private static Cursor sSearchSuggestions = DEFAULT_SUGGESTION_RESULTS;
+
+ public static final MergeCursor DEFAULT_CATEGORY_RESULTS = new MergeCursor(List.of(
+ getMediaCategoriesCursor("people_and_pets")
+ ).toArray(new Cursor[0]));
+
+ private static Cursor sMediaCategories = DEFAULT_CATEGORY_RESULTS;
+
+ public static final MergeCursor DEFAULT_ALBUM_RESULTS = new MergeCursor(List.of(
+ getAlbumCursor("cloud_album", 0L, /* coverId */ CLOUD_ID_1, AUTHORITY)
+ ).toArray(new Cursor[0]));
+
+ private static Cursor sAlbums = DEFAULT_ALBUM_RESULTS;
+
+ private static Cursor sSearchResults = getDefaultCloudSearchResults();
+
+ public static Cursor sMediaSets = getDefaultCursorForMediaSetSyncTest();
+
+ public static Cursor sMediaSetContents = getDefaultCloudSearchResults();
+
+ @Override
+ public Cursor onSearchMedia(String mediaSetId, String fallbackSearchText,
+ Bundle extras, CancellationSignal cancellationSignal) {
+ return sSearchResults;
+ }
+
+ @Override
+ public Cursor onSearchMedia(String searchText,
+ Bundle extras, CancellationSignal cancellationSignal) {
+ return sSearchResults;
+ }
+
+ @Override
+ public Cursor onQueryMediaInMediaSet(String mediaSetId,
+ Bundle extras, CancellationSignal cancellationSignal) {
+ return sMediaSetContents;
+ }
+
+ @Override
+ public Cursor onQuerySearchSuggestions(String prefixText, Bundle extras,
+ CancellationSignal cancellationSignal) {
+ return sSearchSuggestions;
+ }
+
+ @Override
+ public Cursor onQueryMediaSets(String mediaCategoryId,
+ Bundle extras, CancellationSignal cancellationSignal) {
+ return sMediaSets;
+ }
+
+ @Override
+ public Cursor onQueryMediaCategories(String parentCategoryId, Bundle extras,
+ CancellationSignal cancellationSignal) {
+ return sMediaCategories;
+ }
+
+ @Override
+ public Cursor onQueryAlbums(Bundle extras) {
+ return sAlbums;
+ }
+
+ @Override
+ public CloudMediaProviderContract.Capabilities onGetCapabilities() {
+ return new CloudMediaProviderContract.Capabilities.Builder()
+ .setSearchEnabled(true)
+ .setMediaCategoriesEnabled(true)
+ .build();
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor onQueryMedia(Bundle extras) {
+ throw new UnsupportedOperationException("onQueryMedia not supported");
+ }
+
+ @Override
+ public Cursor onQueryDeletedMedia(Bundle extras) {
+ throw new UnsupportedOperationException("onQueryDeletedMedia not supported");
+ }
+
+ @Override
+ public AssetFileDescriptor onOpenPreview(
+ String mediaId,
+ Point size,
+ Bundle extras,
+ CancellationSignal signal) throws FileNotFoundException {
+ throw new UnsupportedOperationException("onOpenPreview not supported");
+ }
+
+ @Override
+ public ParcelFileDescriptor onOpenMedia(
+ String mediaId,
+ Bundle extras,
+ CancellationSignal signal) throws FileNotFoundException {
+ throw new UnsupportedOperationException("onOpenMedia not supported");
+ }
+
+ @Override
+ public Bundle onGetMediaCollectionInfo(Bundle extras) {
+ throw new UnsupportedOperationException("onGetMediaCollectionInfo not supported");
+ }
+
+ public static void setSearchResults(Cursor searchResults) {
+ sSearchResults = searchResults;
+ }
+
+ public static Cursor getSearchResults() {
+ return sSearchResults;
+ }
+
+ public static void setMediaSets(Cursor mediaSets) {
+ sMediaSets = mediaSets;
+ }
+
+ public static void setMediaSetContents(Cursor mediaSetContents) {
+ sMediaSetContents = mediaSetContents;
+ }
+
+ /**
+ Returns a default media set data cursor for tests
+ */
+ public static Cursor getDefaultCursorForMediaSetSyncTest() {
+ String[] columns = new String[]{
+ CloudMediaProviderContract.MediaSetColumns.ID,
+ CloudMediaProviderContract.MediaSetColumns.DISPLAY_NAME,
+ CloudMediaProviderContract.MediaSetColumns.MEDIA_COVER_ID
+ };
+
+ MatrixCursor cursor = new MatrixCursor(columns);
+ cursor.addRow(new Object[] { "mediaSetId", "name", "id" });
+
+ return cursor;
+ }
+
+ /**
+ * Returns a default set of cloud search results for tests.
+ */
+ public static final MergeCursor getDefaultCloudSearchResults() {
+ return new MergeCursor(List.of(
+ getCloudMediaCursor(CLOUD_ID_1, LOCAL_ID_1, 1),
+ getCloudMediaCursor(CLOUD_ID_3, null, 0)
+ ).toArray(new Cursor[0]));
+ }
+
+ /**
+ * Returns a default set of local search results for tests.
+ */
+ public static final MergeCursor getDefaultLocalSearchResults() {
+ return new MergeCursor(List.of(
+ getLocalMediaCursor(LOCAL_ID_1, 1),
+ getLocalMediaCursor(LOCAL_ID_2, 0)
+ ).toArray(new Cursor[0]));
+ }
+}
diff --git a/tests/src/com/android/providers/media/oemmetadataservices/OemMetadataServiceTest.java b/tests/src/com/android/providers/media/oemmetadataservices/OemMetadataServiceTest.java
index c01e06f00..774635be4 100644
--- a/tests/src/com/android/providers/media/oemmetadataservices/OemMetadataServiceTest.java
+++ b/tests/src/com/android/providers/media/oemmetadataservices/OemMetadataServiceTest.java
@@ -36,15 +36,13 @@ import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.IOemMetadataService;
import android.provider.MediaStore;
import android.provider.MediaStore.Files.FileColumns;
import android.provider.OemMetadataService;
import android.provider.OemMetadataServiceWrapper;
-import android.provider.media.internal.flags.Flags;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
@@ -73,12 +71,11 @@ import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
@RunWith(AndroidJUnit4.class)
-@RequiresFlagsEnabled(com.android.providers.media.flags.Flags.FLAG_ENABLE_OEM_METADATA)
+@EnableFlags(com.android.providers.media.flags.Flags.FLAG_ENABLE_OEM_METADATA)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class OemMetadataServiceTest {
- @Rule
- public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
private static final long POLLING_SLEEP_MILLIS = 100;
@@ -179,6 +176,7 @@ public class OemMetadataServiceTest {
}
} finally {
audioFile.delete();
+ isolatedContext.unbindService(modernMediaScanner.getOemMetadataServiceConnection());
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/CategoriesStateTest.java b/tests/src/com/android/providers/media/photopicker/CategoriesStateTest.java
new file mode 100644
index 000000000..6b6e06387
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/CategoriesStateTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.os.Build;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.cloudproviders.SearchProvider;
+import com.android.providers.media.flags.Flags;
+
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+public class CategoriesStateTest {
+
+ private Context mContext;
+
+ @ClassRule
+ public static final SetFlagsRule.ClassRule mSetFlagsClassRule = new SetFlagsRule.ClassRule();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = mSetFlagsClassRule.createSetFlagsRule();
+
+ @Before
+ public void setup() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH,
+ Flags.FLAG_ENABLE_CLOUD_MEDIA_PROVIDER_CAPABILITIES})
+ public void testAreMediaCategoriesEnabledWithValidAuthority() {
+ final TestConfigStore configStore = new TestConfigStore();
+ configStore.setIsModernPickerEnabled(true);
+
+ final CategoriesState categoriesState = new CategoriesState(configStore);
+ assertTrue(categoriesState.areCategoriesEnabled(mContext, SearchProvider.AUTHORITY));
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH,
+ Flags.FLAG_ENABLE_CLOUD_MEDIA_PROVIDER_CAPABILITIES})
+ public void testAreMediaCategoriesEnabledWithVInvalidAuthority() {
+ final TestConfigStore configStore = new TestConfigStore();
+ configStore.setIsModernPickerEnabled(true);
+
+ final CategoriesState categoriesState = new CategoriesState(configStore);
+ assertFalse(categoriesState.areCategoriesEnabled(mContext, "invalid"));
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH,
+ Flags.FLAG_ENABLE_CLOUD_MEDIA_PROVIDER_CAPABILITIES})
+ public void testAreMediaCategoriesEnabledWithModernPickerDisabled() {
+ final TestConfigStore configStore = new TestConfigStore();
+ configStore.setIsModernPickerEnabled(false);
+
+ final CategoriesState categoriesState = new CategoriesState(configStore);
+ assertFalse(categoriesState.areCategoriesEnabled(mContext, SearchProvider.AUTHORITY));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CLOUD_MEDIA_PROVIDER_CAPABILITIES)
+ @DisableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
+ public void testAreMediaCategoriesEnabledWithPhotopickerSearchFlagDisabled() {
+ final TestConfigStore configStore = new TestConfigStore();
+ configStore.setIsModernPickerEnabled(true);
+
+ final CategoriesState categoriesState = new CategoriesState(configStore);
+ assertFalse(categoriesState.areCategoriesEnabled(mContext, "invalid"));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
+ @DisableFlags(Flags.FLAG_ENABLE_CLOUD_MEDIA_PROVIDER_CAPABILITIES)
+ public void testAreMediaCategoriesEnabledWithCapabilitiesFlagDisabled() {
+ final TestConfigStore configStore = new TestConfigStore();
+ configStore.setIsModernPickerEnabled(true);
+
+ final CategoriesState categoriesState = new CategoriesState(configStore);
+ assertFalse(categoriesState.areCategoriesEnabled(mContext, "invalid"));
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
index f39da9c09..47ab3df69 100644
--- a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
+++ b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
@@ -33,6 +33,8 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
import android.Manifest;
import android.app.Instrumentation;
@@ -49,6 +51,7 @@ import android.os.CancellationSignal;
import android.os.Environment;
import android.os.OperationCanceledException;
import android.os.ParcelFileDescriptor;
+import android.os.UserManager;
import android.provider.CloudMediaProviderContract;
import android.provider.MediaStore;
import android.util.Log;
@@ -216,6 +219,7 @@ public class ItemsProviderTest {
*/
@Test
public void testGetCategories_screenshots() throws Exception {
+ assumeFalse("Not testable in HSUM device", UserManager.isHeadlessSystemUserMode());
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
/* cancellationSignal*/ null);
assertThat(c.getCount()).isEqualTo(0);
@@ -254,6 +258,46 @@ public class ItemsProviderTest {
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
* correct info about {@link AlbumColumns#ALBUM_ID_SCREENSHOTS}.
+ * This test is specifically for HSUM devices since creating a top-level screenshots directory
+ * is not possible.
+ */
+ @Test
+ public void testGetCategoriesForHSUM_screenshots() throws Exception {
+ assumeTrue("Test only for HSUM device", UserManager.isHeadlessSystemUserMode());
+ Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+ /* cancellationSignal*/ null);
+ assertThat(c.getCount()).isEqualTo(0);
+
+ // Create 1 image file in Screenshots dir to test
+ final File screenshotsDir = getScreenshotsDir();
+ File imageFile = assertCreateNewImage(screenshotsDir);
+ // Create 1 image file in Screenshots dir of Downloads dir
+ final File screenshotsDirInDownloadsDir = getScreenshotsDirFromDownloadsDir();
+ File imageFileInScreenshotDirInDownloads =
+ assertCreateNewImage(screenshotsDirInDownloadsDir);
+
+ // This file should not be included since it's not a valid screenshot directory, even though
+ // it looks like one.
+ final File myAlbumScreenshotsDir =
+ new File(getPicturesDir(), "MyAlbum" + Environment.DIRECTORY_SCREENSHOTS);
+ final File myAlbumScreenshotsImg = assertCreateNewImage(myAlbumScreenshotsDir);
+
+ try {
+ assertGetCategoriesMatchMultiple(Arrays.asList(
+ Pair.create(ALBUM_ID_SCREENSHOTS, 2),
+ Pair.create(ALBUM_ID_DOWNLOADS, 1)
+ ));
+ } finally {
+ imageFile.delete();
+ imageFileInScreenshotDirInDownloads.delete();
+ myAlbumScreenshotsImg.delete();
+ myAlbumScreenshotsDir.delete();
+ }
+ }
+
+ /**
+ * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
+ * correct info about {@link AlbumColumns#ALBUM_ID_SCREENSHOTS}.
*/
@Test
public void testGetCategories_not_screenshots() throws Exception {
@@ -1423,7 +1467,8 @@ public class ItemsProviderTest {
private Uri prepareFileAndGetUri(File file, long lastModifiedTime) throws IOException {
ensureParentExists(file.getParentFile());
- assertThat(file.createNewFile()).isTrue();
+ file.createNewFile();
+ assertThat(file.exists()).isTrue();
// Write 1 byte because 0byte files are not valid in the picker db
try (FileOutputStream fos = new FileOutputStream(file)) {
diff --git a/tests/src/com/android/providers/media/photopicker/PhotoPickerCloudTestUtils.java b/tests/src/com/android/providers/media/photopicker/PhotoPickerCloudTestUtils.java
index 6839a8b20..f013e8587 100644
--- a/tests/src/com/android/providers/media/photopicker/PhotoPickerCloudTestUtils.java
+++ b/tests/src/com/android/providers/media/photopicker/PhotoPickerCloudTestUtils.java
@@ -17,6 +17,7 @@
package com.android.providers.media.photopicker;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -108,7 +109,8 @@ public class PhotoPickerCloudTestUtils {
private static void writeDeviceConfigProp(
@NonNull String namespace, @NonNull String name, @NonNull String value) {
- getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG);
+ getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG,
+ WRITE_ALLOWLISTED_DEVICE_CONFIG);
try {
DeviceConfig.setProperty(namespace, name, value, /* makeDefault= */ false);
@@ -118,7 +120,8 @@ public class PhotoPickerCloudTestUtils {
}
private static void deleteDeviceConfigProp(@NonNull String namespace, @NonNull String name) {
- getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG);
+ getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG,
+ WRITE_ALLOWLISTED_DEVICE_CONFIG);
try {
if (SdkLevel.isAtLeastU()) {
DeviceConfig.deleteProperty(namespace, name);
diff --git a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
index d1cfb8e7e..234c1c90c 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
@@ -144,8 +144,7 @@ public class PickerDataLayerTest {
initializeTestWorkManager(mContext);
final WorkManager workManager = WorkManager.getInstance(mContext);
- final PickerSyncManager syncManager = new PickerSyncManager(
- workManager, mContext, mConfigStore, /* schedulePeriodicSyncs */ false);
+ final PickerSyncManager syncManager = new PickerSyncManager(workManager, mContext);
mDataLayer = new PickerDataLayer(mContext, mFacade, mController, mConfigStore, syncManager);
// Set cloud provider to null to discard
diff --git a/tests/src/com/android/providers/media/photopicker/PickerSearchUtils.java b/tests/src/com/android/providers/media/photopicker/PickerSearchUtils.java
new file mode 100644
index 000000000..4a1607858
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/PickerSearchUtils.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import android.content.pm.PackageManager;
+
+public class PickerSearchUtils {
+ /**
+ * @return true if search feature is supported on the given device type, otherwise return false.
+ */
+ public static boolean isHardwareSupportedForSearch() {
+ // The Search feature in Picker is disabled for Watches and IoT devices.
+ final PackageManager pm = getInstrumentation().getContext().getPackageManager();
+ return !pm.hasSystemFeature(PackageManager.FEATURE_EMBEDDED)
+ && !pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/SearchStateTest.java b/tests/src/com/android/providers/media/photopicker/SearchStateTest.java
new file mode 100644
index 000000000..3719f0282
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/SearchStateTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker;
+
+import static com.android.providers.media.photopicker.PickerSearchUtils.isHardwareSupportedForSearch;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.Context;
+import android.os.Build;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.cloudproviders.SearchProvider;
+import com.android.providers.media.flags.Flags;
+
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+
+// SetFlagsRule.ClassRule is not available in lower Android versions and Search feature will only
+// be enabled for Android T+ devices.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+public class SearchStateTest {
+ @Mock
+ private PickerSyncController mMockSyncController;
+ private Context mContext;
+ private TestConfigStore mConfigStore;
+
+
+ @ClassRule
+ public static final SetFlagsRule.ClassRule mSetFlagsClassRule = new SetFlagsRule.ClassRule();
+ @Rule public final SetFlagsRule mSetFlagsRule = mSetFlagsClassRule.createSetFlagsRule();
+
+ @Before
+ public void setup() {
+ initMocks(this);
+
+ Assume.assumeTrue(isHardwareSupportedForSearch());
+ PickerSyncController.setInstance(mMockSyncController);
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ mConfigStore = new TestConfigStore();
+ mConfigStore.setIsModernPickerEnabled(true);
+ }
+
+ @EnableFlags({
+ Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH,
+ Flags.FLAG_ENABLE_CLOUD_MEDIA_PROVIDER_CAPABILITIES
+ })
+ @DisableFlags(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH)
+ @Test
+ public void testSearchAPIFlagIsDisabled() {
+ doReturn(SearchProvider.AUTHORITY)
+ .when(mMockSyncController).getCloudProviderOrDefault(any());
+
+ final SearchState searchState = new SearchState(mConfigStore);
+ final boolean isCloudSearchEnabled = searchState.isCloudSearchEnabled(mContext);
+
+ assertThat(isCloudSearchEnabled).isFalse();
+ }
+
+ @EnableFlags({
+ Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH,
+ Flags.FLAG_ENABLE_CLOUD_MEDIA_PROVIDER_CAPABILITIES
+ })
+ @DisableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
+ @Test
+ public void testSearchFeatureFlagIsDisabled() {
+ doReturn(SearchProvider.AUTHORITY)
+ .when(mMockSyncController).getCloudProviderOrDefault(any());
+
+ final SearchState searchState = new SearchState(mConfigStore);
+ final boolean isCloudSearchEnabled = searchState.isCloudSearchEnabled(mContext);
+
+ assertThat(isCloudSearchEnabled).isFalse();
+ }
+
+ @EnableFlags({
+ Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH,
+ Flags.FLAG_ENABLE_CLOUD_MEDIA_PROVIDER_CAPABILITIES,
+ Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH
+ })
+ @Test
+ public void testProviderCloudSearchIsEnabled() {
+ doReturn(SearchProvider.AUTHORITY)
+ .when(mMockSyncController).getCloudProviderOrDefault(any());
+
+ final SearchState searchState = new SearchState(mConfigStore);
+ assertThat(searchState.isCloudSearchEnabled(mContext)).isTrue();
+ assertThat(searchState.isCloudSearchEnabled(mContext, SearchProvider.AUTHORITY)).isTrue();
+ assertThat(searchState.isCloudSearchEnabled(mContext, "random")).isFalse();
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java b/tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java
index 2807da353..f8ea7ca08 100644
--- a/tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java
@@ -42,7 +42,7 @@ import java.util.List;
@SdkSuppress(minSdkVersion = 31, codeName = "S")
public class UserManagerStateTest {
- private final UserHandle mPersonalUser = UserHandle.SYSTEM;
+ private final UserHandle mPersonalUser = UserHandle.of(UserHandle.myUserId());
private final UserHandle mManagedUser = UserHandle.of(100); // like a managed profile
private final UserHandle mOtherUser1 = UserHandle.of(101); // like a private profile
private final UserHandle mOtherUser2 = UserHandle.of(102); // like a clone profile
diff --git a/tests/src/com/android/providers/media/photopicker/data/model/ItemTest.java b/tests/src/com/android/providers/media/photopicker/data/model/ItemTest.java
index b13db5fba..1cd26f9ea 100644
--- a/tests/src/com/android/providers/media/photopicker/data/model/ItemTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/model/ItemTest.java
@@ -45,6 +45,7 @@ import org.junit.runner.RunWith;
import java.time.LocalDate;
import java.time.ZoneId;
+import java.util.Locale;
@RunWith(AndroidJUnit4.class)
public class ItemTest {
@@ -62,7 +63,7 @@ public class ItemTest {
final String mimeType = "image/png";
final long duration = 1000;
final Cursor cursor = generateCursorForItem(id, mimeType, dateTaken, generationModified,
- duration, _SPECIAL_FORMAT_NONE);
+ duration, _SPECIAL_FORMAT_NONE, UserHandle.USER_CURRENT);
cursor.moveToFirst();
final Item item = new Item(cursor, UserId.CURRENT_USER);
@@ -88,10 +89,12 @@ public class ItemTest {
final long generationModified = 1L;
final String mimeType = "image/png";
final long duration = 1000;
+ // Some userId other than current user, hence adding 10 to the current user.
+ final int testUser = UserHandle.USER_CURRENT + 10;
final Cursor cursor = generateCursorForItem(id, mimeType, dateTaken, generationModified,
- duration, _SPECIAL_FORMAT_NONE);
+ duration, _SPECIAL_FORMAT_NONE, testUser);
cursor.moveToFirst();
- final UserId userId = UserId.of(UserHandle.of(10));
+ final UserId userId = UserId.of(UserHandle.of(testUser));
final Item item = new Item(cursor, userId);
@@ -101,7 +104,10 @@ public class ItemTest {
assertThat(item.getMimeType()).isEqualTo(mimeType);
assertThat(item.getDuration()).isEqualTo(duration);
assertThat(item.getContentUri()).isEqualTo(
- Uri.parse("content://10@com.android.providers.media.photopicker/media/1"));
+ Uri.parse(String.format(
+ Locale.ROOT,
+ "content://%d@com.android.providers.media.photopicker/media/1",
+ testUser)));
assertThat(item.isImage()).isTrue();
@@ -306,7 +312,7 @@ public class ItemTest {
}
private static Cursor generateCursorForItem(String id, String mimeType, long dateTaken,
- long generationModified, long duration, int specialFormat) {
+ long generationModified, long duration, int specialFormat, int userId) {
final MatrixCursor cursor = new MatrixCursor(MediaColumns.ALL_PROJECTION);
cursor.addRow(new Object[] {
id,
@@ -322,7 +328,9 @@ public class ItemTest {
500, // height
0, // orientation
"/storage/emulated/0/foo", // data
- PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY
+ PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY,
+ null, //owner_package_name
+ userId
}
);
return cursor;
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java
index 502623520..3443b0c1e 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java
@@ -28,6 +28,7 @@ import com.android.internal.logging.UiEventLogger;
import com.android.providers.media.TestConfigStore;
import com.android.providers.media.photopicker.PhotoPickerActivity;
import com.android.providers.media.photopicker.data.ItemsProvider;
+import com.android.providers.media.photopicker.data.UserIdManager;
import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger;
import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
@@ -53,6 +54,12 @@ public class PhotoPickerTestActivity extends PhotoPickerActivity {
return pickerViewModel;
}
+ @Override
+ @NonNull
+ protected UserIdManager getUserIdManager() {
+ return PhotoPickerBaseTest.getMockUserIdManager();
+ }
+
TestConfigStore getConfigStore() {
return mConfigStore;
}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorkerTest.java b/tests/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorkerTest.java
new file mode 100644
index 000000000..78ca6fc46
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorkerTest.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker.sync;
+
+import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+import static com.android.providers.media.photopicker.sync.MediaInMediaSetsSyncWorker.SYNC_COMPLETE_RESUME_KEY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_AUTHORITY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.initializeTestWorkManager;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Bundle;
+import android.platform.test.annotations.EnableFlags;
+import android.provider.CloudMediaProviderContract;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.work.Data;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkInfo;
+import androidx.work.WorkManager;
+
+import com.android.providers.media.cloudproviders.SearchProvider;
+import com.android.providers.media.flags.Flags;
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.v2.model.MediaSetsSyncRequestParams;
+import com.android.providers.media.photopicker.v2.sqlite.MediaSetsDatabaseUtil;
+import com.android.providers.media.photopicker.v2.sqlite.PickerSQLConstants;
+import com.android.providers.media.photopicker.v2.sqlite.SelectSQLiteQueryBuilder;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+@EnableFlags(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH)
+public class MediaInMediaSetsSyncWorkerTest {
+ @Mock
+ private PickerSyncController mMockSyncController;
+ @Mock
+ private SyncTracker mMockLocalMediaInMediaSetTracker;
+ @Mock
+ private SyncTracker mMockCloudMediaInMediaSetTracker;
+ private Context mContext;
+ private SQLiteDatabase mDatabase;
+ private PickerDbFacade mFacade;
+
+ @Before
+ public void setup() {
+ initMocks(this);
+
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ initializeTestWorkManager(mContext);
+
+ SyncTrackerRegistry.setLocalMediaInMediaSetTracker(mMockLocalMediaInMediaSetTracker);
+ SyncTrackerRegistry.setCloudMediaInMediaSetTracker(mMockCloudMediaInMediaSetTracker);
+ PickerSyncController.setInstance(mMockSyncController);
+
+ final File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ final PickerDatabaseHelper helper = new PickerDatabaseHelper(mContext);
+ mDatabase = helper.getWritableDatabase();
+ mFacade = new PickerDbFacade(
+ mContext, new PickerSyncLockManager(), LOCAL_PICKER_PROVIDER_AUTHORITY);
+ mFacade.setCloudProvider(SearchProvider.AUTHORITY);
+
+ doReturn(LOCAL_PICKER_PROVIDER_AUTHORITY).when(mMockSyncController).getLocalProvider();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController).getCloudProvider();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController)
+ .getCloudProviderOrDefault(any());
+ doReturn(mFacade).when(mMockSyncController).getDbFacade();
+ doReturn(new PickerSyncLockManager()).when(mMockSyncController).getPickerSyncLockManager();
+ }
+
+ @After
+ public void teardown() {
+ mDatabase.close();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ }
+
+ @Test
+ public void testMediaInMediaSetSyncWithInvalidSyncSource() throws
+ ExecutionException, InterruptedException {
+ // Setup
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaInMediaSetsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, 56,
+ SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID, 1L,
+ SYNC_WORKER_INPUT_AUTHORITY, SearchProvider.AUTHORITY)))
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+ }
+
+ @Test
+ public void testMediaInMediaSetSyncWithInvalidMediaSetPickerId() throws
+ ExecutionException, InterruptedException {
+ // Setup
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaInMediaSetsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_ONLY,
+ SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID, "",
+ SYNC_WORKER_INPUT_AUTHORITY, SearchProvider.AUTHORITY)))
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+ }
+
+ @Test
+ public void testMediaInMediaSetSyncWithInvalidMediaSetAuthority() throws
+ ExecutionException, InterruptedException {
+ // Setup
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaInMediaSetsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_ONLY,
+ SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID, 1L,
+ SYNC_WORKER_INPUT_AUTHORITY, "")))
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+ }
+
+ @Test
+ public void testMediaInMediaSetSyncWithCloudProvider() throws
+ ExecutionException, InterruptedException {
+
+ String categoryId = "categoryId";
+ String auth = String.valueOf(SYNC_CLOUD_ONLY);
+ long mediaSetPickerId = 1L;
+ Cursor c = getCursorForMediaSetInsertionTest();
+ List<String> mimeTypes = new ArrayList<>();
+ mimeTypes.add("img");
+
+ int mediaSetsInserted = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, categoryId, auth, mimeTypes);
+ assertEquals("Count of inserted media sets should be equal to the cursor size",
+ /*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
+ Bundle extras = new Bundle();
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, auth);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, categoryId);
+ extras.putStringArrayList(
+ MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ new ArrayList<String>(mimeTypes));
+ MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
+ Cursor fetchMediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
+ mDatabase, requestParams);
+ if (fetchMediaSetCursor.moveToFirst()) {
+ mediaSetPickerId = fetchMediaSetCursor.getLong(
+ fetchMediaSetCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaSetsTableColumns.PICKER_ID.getColumnName()));
+ }
+
+ final Cursor inputCursor = SearchProvider.getDefaultCloudSearchResults();
+ SearchProvider.setMediaSetContents(inputCursor);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaInMediaSetsSyncWorker.class)
+ .setInputData(new Data(
+ Map.of(
+ SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_CLOUD_ONLY,
+ SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID, mediaSetPickerId,
+ SYNC_WORKER_INPUT_AUTHORITY, SearchProvider.AUTHORITY)))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor mediaInMediaSetsTableCursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.MEDIA_IN_MEDIA_SETS.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(mediaInMediaSetsTableCursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(mediaInMediaSetsTableCursor.getCount())
+ .isEqualTo(inputCursor.getCount());
+
+ if (mediaInMediaSetsTableCursor.moveToFirst() && inputCursor.moveToFirst()) {
+
+ do {
+
+ assertEquals(mediaInMediaSetsTableCursor.getString(
+ mediaInMediaSetsTableCursor.getColumnIndex(
+ PickerSQLConstants.MediaInMediaSetsTableColumns.CLOUD_ID
+ .getColumnName())),
+ inputCursor.getString(
+ inputCursor.getColumnIndex(
+ CloudMediaProviderContract.MediaColumns.ID
+ ))
+ );
+
+ assertEquals((long) mediaInMediaSetsTableCursor.getLong(
+ mediaInMediaSetsTableCursor.getColumnIndex(
+ PickerSQLConstants.MediaInMediaSetsTableColumns
+ .MEDIA_SETS_PICKER_ID.getColumnName())),
+ mediaSetPickerId
+ );
+
+ String mediaStoreUri = inputCursor.getString(inputCursor.getColumnIndex(
+ CloudMediaProviderContract.MediaColumns.MEDIA_STORE_URI
+ ));
+
+ if (mediaStoreUri == null) {
+ assertTrue(mediaInMediaSetsTableCursor.isNull(
+ mediaInMediaSetsTableCursor.getColumnIndex(
+ PickerSQLConstants.MediaInMediaSetsTableColumns
+ .LOCAL_ID.getColumnName()))
+ );
+ } else {
+ String localId = String.valueOf(
+ ContentUris.parseId(Uri.parse(mediaStoreUri)));
+ assertEquals(mediaInMediaSetsTableCursor.getString(
+ mediaInMediaSetsTableCursor.getColumnIndex(
+ PickerSQLConstants.MediaInMediaSetsTableColumns
+ .LOCAL_ID.getColumnName())), localId
+ );
+ }
+
+ } while (mediaInMediaSetsTableCursor.moveToNext() && inputCursor.moveToNext());
+ }
+ }
+
+ verify(mMockLocalMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testMediaInMediaSetsSyncLocalProvider() throws
+ ExecutionException, InterruptedException {
+
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController).getLocalProvider();
+
+ String categoryId = "categoryId";
+ String auth = String.valueOf(SYNC_LOCAL_ONLY);
+ long mediaSetPickerId = 1L;
+ Cursor c = getCursorForMediaSetInsertionTest();
+ List<String> mimeTypes = new ArrayList<>();
+ mimeTypes.add("img");
+
+ int mediaSetsInserted = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, categoryId, auth, mimeTypes);
+ assertEquals("Count of inserted media sets should be equal to the cursor size",
+ /*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
+ Bundle extras = new Bundle();
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, auth);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, categoryId);
+ extras.putStringArrayList(MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ new ArrayList<String>(mimeTypes));
+ MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
+ Cursor fetchMediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
+ mDatabase, requestParams);
+ if (fetchMediaSetCursor.moveToFirst()) {
+ mediaSetPickerId = fetchMediaSetCursor.getLong(
+ fetchMediaSetCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaSetsTableColumns.PICKER_ID.getColumnName()));
+ }
+
+ final Cursor inputCursor = SearchProvider.getDefaultLocalSearchResults();
+ SearchProvider.setMediaSetContents(inputCursor);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaInMediaSetsSyncWorker.class)
+ .setInputData(new Data(
+ Map.of(
+ SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_ONLY,
+ SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID, mediaSetPickerId,
+ SYNC_WORKER_INPUT_AUTHORITY, SearchProvider.AUTHORITY)))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor mediaInMediaSetsTableCursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.MEDIA_IN_MEDIA_SETS.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(mediaInMediaSetsTableCursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(mediaInMediaSetsTableCursor.getCount())
+ .isEqualTo(inputCursor.getCount());
+
+ if (mediaInMediaSetsTableCursor.moveToFirst() && inputCursor.moveToFirst()) {
+
+ do {
+
+ assertEquals(mediaInMediaSetsTableCursor.getString(
+ mediaInMediaSetsTableCursor.getColumnIndex(
+ PickerSQLConstants.MediaInMediaSetsTableColumns.LOCAL_ID
+ .getColumnName())),
+ inputCursor.getString(
+ inputCursor.getColumnIndex(
+ CloudMediaProviderContract.MediaColumns.ID
+ ))
+ );
+
+ assertEquals((long) mediaInMediaSetsTableCursor.getLong(
+ mediaInMediaSetsTableCursor.getColumnIndex(
+ PickerSQLConstants.MediaInMediaSetsTableColumns
+ .MEDIA_SETS_PICKER_ID.getColumnName())),
+ mediaSetPickerId
+ );
+
+ assertTrue(mediaInMediaSetsTableCursor.isNull(
+ mediaInMediaSetsTableCursor.getColumnIndex(
+ PickerSQLConstants.MediaInMediaSetsTableColumns
+ .CLOUD_ID.getColumnName()))
+ );
+
+ } while (mediaInMediaSetsTableCursor.moveToNext() && inputCursor.moveToNext());
+ }
+ }
+ verify(mMockLocalMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ @Ignore("Enable when b/391639613 is fixed")
+ public void testMediaSetContentsSyncLoop() throws
+ ExecutionException, InterruptedException {
+
+ String categoryId = "categoryId";
+ String auth = String.valueOf(SYNC_CLOUD_ONLY);
+ long mediaSetPickerId = 1L;
+ Cursor c = getCursorForMediaSetInsertionTest();
+ List<String> mimeTypes = new ArrayList<>();
+ mimeTypes.add("img");
+
+ int mediaSetsInserted = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, categoryId, auth, mimeTypes);
+ assertEquals("Count of inserted media sets should be equal to the cursor size",
+ /*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
+ Bundle extras = new Bundle();
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, auth);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, categoryId);
+ extras.putStringArrayList(
+ MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ new ArrayList<String>(mimeTypes));
+ MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
+ Cursor fetchMediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
+ mDatabase, requestParams);
+ if (fetchMediaSetCursor.moveToFirst()) {
+ mediaSetPickerId = fetchMediaSetCursor.getLong(
+ fetchMediaSetCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaSetsTableColumns.PICKER_ID.getColumnName()));
+ }
+
+ final Cursor inputCursor = SearchProvider.getDefaultCloudSearchResults();
+ final String repeatPageToken = "LOOP";
+ final Bundle bundle = new Bundle();
+ bundle.putString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN, repeatPageToken);
+ inputCursor.setExtras(bundle);
+ SearchProvider.setMediaSetContents(inputCursor);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaInMediaSetsSyncWorker.class)
+ .setInputData(new Data(
+ Map.of(
+ SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_CLOUD_ONLY,
+ SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID, mediaSetPickerId,
+ SYNC_WORKER_INPUT_AUTHORITY, SearchProvider.AUTHORITY)))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor mediaInMediaSetsTableCursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.MEDIA_IN_MEDIA_SETS.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(mediaInMediaSetsTableCursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(mediaInMediaSetsTableCursor.getCount())
+ .isEqualTo(inputCursor.getCount());
+ }
+
+ verify(mMockLocalMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+
+ private Cursor getCursorForMediaSetInsertionTest() {
+ String[] columns = new String[]{
+ CloudMediaProviderContract.MediaSetColumns.ID,
+ CloudMediaProviderContract.MediaSetColumns.DISPLAY_NAME,
+ CloudMediaProviderContract.MediaSetColumns.MEDIA_COVER_ID
+ };
+
+ final String mediaSetId = "mediaSetId";
+ final String displayName = "name";
+ final String coverId = "coverId";
+ MatrixCursor cursor = new MatrixCursor(columns);
+ cursor.addRow(new Object[] { mediaSetId, displayName, coverId });
+
+ return cursor;
+ }
+
+ @Test
+ public void testMediaInMediaSetSyncComplete() throws
+ ExecutionException, InterruptedException {
+
+ String categoryId = "categoryId";
+ String auth = String.valueOf(SYNC_CLOUD_ONLY);
+ long mediaSetPickerId = 1L;
+ Cursor c = getCursorForMediaSetInsertionTest();
+ List<String> mimeTypes = new ArrayList<>();
+ mimeTypes.add("img");
+
+ int mediaSetsInserted = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, categoryId, auth, mimeTypes);
+ assertEquals("Count of inserted media sets should be equal to the cursor size",
+ /*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
+ Bundle extras = new Bundle();
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, auth);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, categoryId);
+ extras.putStringArrayList(MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ new ArrayList<String>(mimeTypes));
+ MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
+ Cursor fetchMediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
+ mDatabase, requestParams);
+ if (fetchMediaSetCursor.moveToFirst()) {
+ mediaSetPickerId = fetchMediaSetCursor.getLong(
+ fetchMediaSetCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaSetsTableColumns.PICKER_ID.getColumnName()));
+ }
+ MediaSetsDatabaseUtil.updateMediaInMediaSetSyncResumeKey(
+ mDatabase, mediaSetPickerId, SYNC_COMPLETE_RESUME_KEY);
+
+ final Cursor inputCursor = SearchProvider.getDefaultCloudSearchResults();
+ SearchProvider.setMediaSetContents(inputCursor);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaInMediaSetsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_CLOUD_ONLY,
+ SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID, mediaSetPickerId,
+ SYNC_WORKER_INPUT_AUTHORITY, SearchProvider.AUTHORITY)))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor mediaInMediaSetsTableCursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.MEDIA_IN_MEDIA_SETS.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(mediaInMediaSetsTableCursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(mediaInMediaSetsTableCursor.getCount())
+ .isEqualTo(0);
+
+ verify(mMockLocalMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudMediaInMediaSetTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/MediaSetsResetWorkerTest.java b/tests/src/com/android/providers/media/photopicker/sync/MediaSetsResetWorkerTest.java
new file mode 100644
index 000000000..17678b08b
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/MediaSetsResetWorkerTest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker.sync;
+
+import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.initializeTestWorkManager;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_1;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_2;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_3;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_PROVIDER;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_1;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_2;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_3;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddMediaOperation;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getCloudMediaCursor;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.provider.CloudMediaProviderContract;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkInfo;
+import androidx.work.WorkManager;
+
+import com.android.providers.media.cloudproviders.SearchProvider;
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.v2.sqlite.MediaInMediaSetsDatabaseUtil;
+import com.android.providers.media.photopicker.v2.sqlite.MediaSetsDatabaseUtil;
+import com.android.providers.media.photopicker.v2.sqlite.PickerSQLConstants;
+import com.android.providers.media.photopicker.v2.sqlite.SelectSQLiteQueryBuilder;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public class MediaSetsResetWorkerTest {
+
+ private SQLiteDatabase mDatabase;
+ private PickerDbFacade mFacade;
+ private Context mContext;
+ private final String mMediaSetId = "mediaSetId";
+ private final String mCategoryId = "categoryId";
+ private final String mAuthority = "auth";
+ private final String mMimeType = "img";
+ private final String mDisplayName = "name";
+ private final String mCoverId = "id";
+ @Mock
+ private PickerSyncController mMockSyncController;
+ @Mock
+ private SyncTracker mLocalMediaSetsSyncTracker;
+ @Mock
+ private SyncTracker mCloudMediaSetsSyncTracker;
+
+ @Before
+ public void setup() {
+ initMocks(this);
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ PickerSyncController.setInstance(mMockSyncController);
+ SyncTrackerRegistry.setCloudMediaSetsSyncTracker(mCloudMediaSetsSyncTracker);
+ SyncTrackerRegistry.setLocalMediaSetsSyncTracker(mLocalMediaSetsSyncTracker);
+ initializeTestWorkManager(mContext);
+ final File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ final PickerDatabaseHelper helper = new PickerDatabaseHelper(mContext);
+ mDatabase = helper.getWritableDatabase();
+ mFacade = new PickerDbFacade(
+ mContext, new PickerSyncLockManager(), LOCAL_PICKER_PROVIDER_AUTHORITY);
+ mFacade.setCloudProvider(CLOUD_PROVIDER);
+ doReturn(mFacade).when(mMockSyncController).getDbFacade();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController).getLocalProvider();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController).getCloudProvider();
+ }
+
+ @After
+ public void teardown() {
+ mDatabase.close();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ }
+
+ @Test
+ public void testMediaSetsAndMediaSetsContentCacheReset() throws
+ ExecutionException, InterruptedException {
+ Cursor c = getCursorForMediaSetInsertionTest();
+ List<String> mimeTypes = new ArrayList<>();
+ mimeTypes.add(mMimeType);
+
+ int mediaSetsInserted = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, mCategoryId, mAuthority, mimeTypes);
+ assertEquals("Count of inserted media sets should be equal to the cursor size",
+ /*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
+
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ Long mediaSetPickerId = 1L;
+
+ final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(null, CLOUD_ID_3, mediaSetPickerId),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, mediaSetPickerId),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, mediaSetPickerId)
+ ), CLOUD_PROVIDER);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ // Setup
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaSetsResetWorker.class)
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor cursorFromMediaSetTable = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.MEDIA_SETS.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursorFromMediaSetTable)
+ .isNotNull();
+ assertEquals(/*expected*/ 0, /*actual*/ cursorFromMediaSetTable.getCount());
+ }
+
+ try (Cursor cursorFromMediaInMediaSetTable = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.MEDIA_IN_MEDIA_SETS.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursorFromMediaInMediaSetTable)
+ .isNotNull();
+ assertEquals(/*expected */ 0, /*actual*/ cursorFromMediaInMediaSetTable.getCount());
+ }
+ }
+
+ private Cursor getCursorForMediaSetInsertionTest() {
+ String[] columns = new String[]{
+ CloudMediaProviderContract.MediaSetColumns.ID,
+ CloudMediaProviderContract.MediaSetColumns.DISPLAY_NAME,
+ CloudMediaProviderContract.MediaSetColumns.MEDIA_COVER_ID
+ };
+
+ MatrixCursor cursor = new MatrixCursor(columns);
+ cursor.addRow(new Object[] { mMediaSetId, mDisplayName, mCoverId });
+
+ return cursor;
+ }
+
+ private ContentValues getContentValues(
+ String localId, String cloudId, Long mediaSetPickerId) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(
+ PickerSQLConstants.MediaInMediaSetsTableColumns.CLOUD_ID.getColumnName(), cloudId);
+ contentValues.put(
+ PickerSQLConstants.MediaInMediaSetsTableColumns.LOCAL_ID.getColumnName(), localId);
+ contentValues.put(
+ PickerSQLConstants.MediaInMediaSetsTableColumns.MEDIA_SETS_PICKER_ID
+ .getColumnName(),
+ mediaSetPickerId);
+ return contentValues;
+ }
+
+}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/MediaSetsSyncWorkerTest.java b/tests/src/com/android/providers/media/photopicker/sync/MediaSetsSyncWorkerTest.java
new file mode 100644
index 000000000..3733cc453
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/MediaSetsSyncWorkerTest.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker.sync;
+
+import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.EXTRA_MIME_TYPES;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_AUTHORITY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_CATEGORY_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.initializeTestWorkManager;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_PROVIDER;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.platform.test.annotations.EnableFlags;
+import android.provider.CloudMediaProviderContract;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.work.Data;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkInfo;
+import androidx.work.WorkManager;
+
+import com.android.providers.media.cloudproviders.SearchProvider;
+import com.android.providers.media.flags.Flags;
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.v2.sqlite.PickerSQLConstants;
+import com.android.providers.media.photopicker.v2.sqlite.SelectSQLiteQueryBuilder;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+@EnableFlags(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH)
+public class MediaSetsSyncWorkerTest {
+
+ private SQLiteDatabase mDatabase;
+ private PickerDbFacade mFacade;
+ private Context mContext;
+ @Mock
+ private PickerSyncController mMockSyncController;
+ @Mock
+ private SyncTracker mLocalMediaSetsSyncTracker;
+ @Mock
+ private SyncTracker mCloudMediaSetsSyncTracker;
+ private final String mCategoryId = "categoryId";
+ private final String[] mMimeTypes = new String[] { "image/*" };
+
+ @Before
+ public void setup() {
+ initMocks(this);
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ initializeTestWorkManager(mContext);
+ PickerSyncController.setInstance(mMockSyncController);
+ SyncTrackerRegistry.setCloudMediaSetsSyncTracker(mCloudMediaSetsSyncTracker);
+ SyncTrackerRegistry.setLocalMediaSetsSyncTracker(mLocalMediaSetsSyncTracker);
+ final File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ final PickerDatabaseHelper helper = new PickerDatabaseHelper(mContext);
+ mDatabase = helper.getWritableDatabase();
+ mFacade = new PickerDbFacade(
+ mContext, new PickerSyncLockManager(), LOCAL_PICKER_PROVIDER_AUTHORITY);
+ mFacade.setCloudProvider(CLOUD_PROVIDER);
+ doReturn(mFacade).when(mMockSyncController).getDbFacade();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController).getLocalProvider();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController).getCloudProvider();
+ }
+
+ @After
+ public void teardown() {
+ mDatabase.close();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ }
+
+ @Test
+ public void testMediaSetsSyncWithInvalidSyncSource()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaSetsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, 56,
+ SYNC_WORKER_INPUT_CATEGORY_ID, mCategoryId,
+ EXTRA_MIME_TYPES, mMimeTypes)))
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+ }
+
+ @Test
+ public void testMediaSetsSyncWithMissingSyncSource()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaSetsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(
+ SYNC_WORKER_INPUT_CATEGORY_ID, mCategoryId,
+ SYNC_WORKER_INPUT_AUTHORITY, SearchProvider.AUTHORITY,
+ EXTRA_MIME_TYPES, mMimeTypes)))
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+ }
+
+ @Test
+ public void testMediaSetsSyncWithInvalidCategoryId()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaSetsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(
+ SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_ONLY,
+ SYNC_WORKER_INPUT_CATEGORY_ID, "",
+ SYNC_WORKER_INPUT_AUTHORITY, SearchProvider.AUTHORITY,
+ EXTRA_MIME_TYPES, mMimeTypes)))
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+ }
+
+ @Test
+ public void testMediaSetsSyncWithMissingCategoryId()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaSetsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(
+ SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_ONLY,
+ SYNC_WORKER_INPUT_AUTHORITY, SearchProvider.AUTHORITY,
+ EXTRA_MIME_TYPES, mMimeTypes)))
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+ }
+
+ @Test
+ public void testMediaSetsSyncWithMissingCategoryAuthority()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaSetsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(
+ SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_CLOUD_ONLY,
+ SYNC_WORKER_INPUT_CATEGORY_ID, mCategoryId,
+ EXTRA_MIME_TYPES, mMimeTypes)))
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+ }
+
+ @Test
+ public void testMediaSetsSyncWithValidSyncSourceAndCategoryIdForCloudAuth() throws
+ ExecutionException, InterruptedException {
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaSetsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_CLOUD_ONLY,
+ SYNC_WORKER_INPUT_CATEGORY_ID, mCategoryId,
+ SYNC_WORKER_INPUT_AUTHORITY, SearchProvider.AUTHORITY,
+ EXTRA_MIME_TYPES, mMimeTypes)))
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ Cursor cursorFromSearchProvider = SearchProvider.sMediaSets;
+
+ try (Cursor cursorFromMediaSetTable = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.MEDIA_SETS.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursorFromMediaSetTable)
+ .isNotNull();
+ assertEquals(cursorFromMediaSetTable.getCount(), cursorFromSearchProvider.getCount());
+
+ compareMediaSetCursorsForMediaSetProperties(
+ cursorFromMediaSetTable, cursorFromSearchProvider);
+ }
+
+ verify(mLocalMediaSetsSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mLocalMediaSetsSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+
+ verify(mCloudMediaSetsSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mCloudMediaSetsSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testMediaSetsSyncWithValidSyncSourceAndCategoryIdForLocalAuth() throws
+ ExecutionException, InterruptedException {
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaSetsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(
+ SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_ONLY,
+ SYNC_WORKER_INPUT_CATEGORY_ID, mCategoryId,
+ SYNC_WORKER_INPUT_AUTHORITY, SearchProvider.AUTHORITY,
+ EXTRA_MIME_TYPES, mMimeTypes)))
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ Cursor cursorFromSearchProvider = SearchProvider.sMediaSets;
+
+ try (Cursor cursorFromMediaSetTable = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.MEDIA_SETS.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursorFromMediaSetTable)
+ .isNotNull();
+ assertEquals(cursorFromMediaSetTable.getCount(), cursorFromSearchProvider.getCount());
+
+ compareMediaSetCursorsForMediaSetProperties(
+ cursorFromMediaSetTable, cursorFromSearchProvider);
+ }
+ }
+
+ @Test
+ public void testMediaSetsSyncLoop() throws
+ ExecutionException, InterruptedException {
+ // Setup
+ final String repeatPageToken = "LOOP";
+ final Cursor cursorFromSearchProvider =
+ SearchProvider.getDefaultCursorForMediaSetSyncTest();
+ final Bundle bundle = new Bundle();
+ bundle.putString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN, repeatPageToken);
+ cursorFromSearchProvider.setExtras(bundle);
+ SearchProvider.setMediaSets(cursorFromSearchProvider);
+
+ // Run test
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(MediaSetsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_CLOUD_ONLY,
+ SYNC_WORKER_INPUT_CATEGORY_ID, mCategoryId,
+ SYNC_WORKER_INPUT_AUTHORITY, SearchProvider.AUTHORITY,
+ EXTRA_MIME_TYPES, mMimeTypes)))
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor cursorFromMediaSetTable = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.MEDIA_SETS.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursorFromMediaSetTable)
+ .isNotNull();
+ assertEquals(cursorFromMediaSetTable.getCount(), cursorFromSearchProvider.getCount());
+
+ compareMediaSetCursorsForMediaSetProperties(
+ cursorFromMediaSetTable, cursorFromSearchProvider);
+ }
+
+ verify(mLocalMediaSetsSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mLocalMediaSetsSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+
+ verify(mCloudMediaSetsSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mCloudMediaSetsSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+
+
+ private void compareMediaSetCursorsForMediaSetProperties(
+ Cursor cursorFromMediaSetTable, Cursor cursorFromSearchProvider) {
+
+ if (cursorFromMediaSetTable.moveToFirst() && cursorFromSearchProvider.moveToFirst()) {
+
+ assertEquals(/*expected*/cursorFromMediaSetTable.getString(
+ cursorFromMediaSetTable.getColumnIndex(
+ PickerSQLConstants.MediaSetsTableColumns.CATEGORY_ID
+ .getColumnName())),
+ /*actual*/mCategoryId);
+
+ assertEquals(cursorFromMediaSetTable.getString(
+ cursorFromMediaSetTable.getColumnIndex(
+ PickerSQLConstants.MediaSetsTableColumns.DISPLAY_NAME
+ .getColumnName())),
+ cursorFromSearchProvider.getString(
+ cursorFromSearchProvider.getColumnIndex(
+ CloudMediaProviderContract.MediaSetColumns.DISPLAY_NAME
+ ))
+ );
+
+ assertEquals(cursorFromMediaSetTable.getString(
+ cursorFromMediaSetTable.getColumnIndex(
+ PickerSQLConstants.MediaSetsTableColumns.MEDIA_SET_ID
+ .getColumnName())),
+ cursorFromSearchProvider.getString(
+ cursorFromSearchProvider.getColumnIndex(
+ CloudMediaProviderContract.MediaSetColumns.ID
+ ))
+ );
+
+ assertEquals(cursorFromMediaSetTable.getString(
+ cursorFromMediaSetTable.getColumnIndex(
+ PickerSQLConstants.MediaSetsTableColumns.COVER_ID
+ .getColumnName())),
+ cursorFromSearchProvider.getString(
+ cursorFromSearchProvider.getColumnIndex(
+ CloudMediaProviderContract.MediaSetColumns.MEDIA_COVER_ID
+ ))
+ );
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
index 857d14359..60e43e934 100644
--- a/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
@@ -16,10 +16,22 @@
package com.android.providers.media.photopicker.sync;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.EXPIRED_SUGGESTIONS_RESET;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.EXTRA_MIME_TYPES;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.IMMEDIATE_CLOUD_MEDIA_IN_MEDIA_SET_SYNC_WORK_NAME;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.IMMEDIATE_CLOUD_SEARCH_SYNC_WORK_NAME;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SEARCH_RESULTS_FULL_CACHE_RESET;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SEARCH_PARTIAL_CACHE_RESET;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SEARCH_RESULTS_RESET_DELAY;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SHOULD_SYNC_GRANTS;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_AND_CLOUD;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_AUTHORITY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_CATEGORY_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_RESET_TYPE;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SEARCH_REQUEST_ID;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
import static com.android.providers.media.util.BackgroundThreadUtils.waitForIdle;
@@ -27,9 +39,10 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;
@@ -37,6 +50,11 @@ import static org.mockito.MockitoAnnotations.initMocks;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
+import android.os.Bundle;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.ExistingWorkPolicy;
@@ -44,24 +62,40 @@ import androidx.work.OneTimeWorkRequest;
import androidx.work.Operation;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkContinuation;
+import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.cloudproviders.SearchProvider;
+import com.android.providers.media.flags.Flags;
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.PickerSyncRequestExtras;
+import com.android.providers.media.photopicker.v2.model.MediaInMediaSetSyncRequestParams;
+import com.android.providers.media.photopicker.v2.model.MediaSetsSyncRequestParams;
import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
public class PickerSyncManagerTest {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
private PickerSyncManager mPickerSyncManager;
private TestConfigStore mConfigStore;
@Mock
@@ -90,12 +124,15 @@ public class PickerSyncManagerTest {
mConfigStore = new TestConfigStore();
mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(
"com.hooli.super.awesome.cloudpicker");
+ final SettableFuture<List<WorkInfo>> listenableFuture = SettableFuture.create();
+ listenableFuture.set(List.of());
+ doReturn(listenableFuture).when(mMockWorkManager).getWorkInfosByTag(anyString());
}
@Test
public void testScheduleEndlessWorker() {
- setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+ setupPickerSyncManager(/* schedulePeriodicSyncs */ true);
// The third call here comes from the EndlessWorker
verify(mMockWorkManager, times(1))
@@ -110,10 +147,10 @@ public class PickerSyncManagerTest {
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
public void testSchedulePeriodicSyncs() {
setupPickerSyncManager(/* schedulePeriodicSyncs */ true);
- // The third call here comes from the EndlessWorker
verify(mMockWorkManager, times(2))
.enqueueUniquePeriodicWork(anyString(),
any(),
@@ -147,11 +184,63 @@ public class PickerSyncManagerTest {
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
+ public void testSchedulePeriodicSyncsWithSearchEnabled() {
+ setupPickerSyncManager(/* schedulePeriodicSyncs */ true);
+
+ verify(mMockWorkManager, times(3))
+ .enqueueUniquePeriodicWork(anyString(),
+ any(),
+ mPeriodicWorkRequestArgumentCaptor.capture());
+
+ final PeriodicWorkRequest periodicWorkRequest =
+ mPeriodicWorkRequestArgumentCaptor.getAllValues().get(0);
+ assertThat(periodicWorkRequest.getWorkSpec().workerClassName)
+ .isEqualTo(ProactiveSyncWorker.class.getName());
+ assertThat(periodicWorkRequest.getWorkSpec().expedited).isFalse();
+ assertThat(periodicWorkRequest.getWorkSpec().isPeriodic()).isTrue();
+ assertThat(periodicWorkRequest.getWorkSpec().id).isNotNull();
+ assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresCharging()).isTrue();
+ assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresDeviceIdle()).isTrue();
+ assertThat(periodicWorkRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_LOCAL_AND_CLOUD);
+
+ final PeriodicWorkRequest periodicResetRequest =
+ mPeriodicWorkRequestArgumentCaptor.getAllValues().get(1);
+ assertThat(periodicResetRequest.getWorkSpec().workerClassName)
+ .isEqualTo(MediaResetWorker.class.getName());
+ assertThat(periodicResetRequest.getWorkSpec().expedited).isFalse();
+ assertThat(periodicResetRequest.getWorkSpec().isPeriodic()).isTrue();
+ assertThat(periodicResetRequest.getWorkSpec().id).isNotNull();
+ assertThat(periodicResetRequest.getWorkSpec().constraints.requiresCharging()).isTrue();
+ assertThat(periodicResetRequest.getWorkSpec().constraints.requiresDeviceIdle()).isTrue();
+ assertThat(periodicResetRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_LOCAL_AND_CLOUD);
+
+ final PeriodicWorkRequest searchSuggestionsResetRequest =
+ mPeriodicWorkRequestArgumentCaptor.getAllValues().get(2);
+ assertThat(searchSuggestionsResetRequest.getWorkSpec().workerClassName)
+ .isEqualTo(SearchResetWorker.class.getName());
+ assertThat(searchSuggestionsResetRequest.getWorkSpec().expedited).isFalse();
+ assertThat(searchSuggestionsResetRequest.getWorkSpec().isPeriodic()).isTrue();
+ assertThat(searchSuggestionsResetRequest.getWorkSpec().id).isNotNull();
+ assertThat(searchSuggestionsResetRequest.getWorkSpec()
+ .constraints.requiresCharging()).isTrue();
+ assertThat(searchSuggestionsResetRequest.getWorkSpec()
+ .constraints.requiresDeviceIdle()).isTrue();
+ assertThat(searchSuggestionsResetRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_RESET_TYPE, -1))
+ .isEqualTo(EXPIRED_SUGGESTIONS_RESET);
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
public void testPeriodicWorkIsScheduledOnDeviceConfigChanges() {
mConfigStore.disableCloudMediaFeature();
-
setupPickerSyncManager(true);
// Ensure no syncs have been scheduled yet.
@@ -165,8 +254,8 @@ public class PickerSyncManagerTest {
waitForIdle();
- // Ensure the syncs are now scheduled.
- verify(mMockWorkManager, times(2))
+ // Ensure the media and album reset syncs are now scheduled.
+ verify(mMockWorkManager, atLeast(2))
.enqueueUniquePeriodicWork(anyString(),
any(),
mPeriodicWorkRequestArgumentCaptor.capture());
@@ -202,13 +291,76 @@ public class PickerSyncManagerTest {
mConfigStore.disableCloudMediaFeature();
waitForIdle();
+ // There should be 3 invocations, one for cancelling proactive media syncs,
+ // the other for albums reset and search reset syncs.
+ verify(mMockWorkManager, times(3)).cancelUniqueWork(anyString());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
+ public void testOnDeviceConfigChangesWithSearchEnabled() {
+
+ mConfigStore.disableCloudMediaFeature();
+
+ setupPickerSyncManager(true);
+
+ // Ensure only search sync is scheduled
+ verify(mMockWorkManager, times(1))
+ .enqueueUniquePeriodicWork(anyString(),
+ any(),
+ mPeriodicWorkRequestArgumentCaptor.capture());
+ clearInvocations(mMockWorkManager);
+
+ mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(
+ "com.hooli.some.cloud.provider");
+
+ waitForIdle();
+
+ // Ensure the media and album reset syncs are now scheduled.
+ verify(mMockWorkManager, times(3))
+ .enqueueUniquePeriodicWork(anyString(),
+ any(),
+ mPeriodicWorkRequestArgumentCaptor.capture());
+
+ final PeriodicWorkRequest periodicWorkRequest =
+ mPeriodicWorkRequestArgumentCaptor.getAllValues().get(1);
+ assertThat(periodicWorkRequest.getWorkSpec().workerClassName)
+ .isEqualTo(ProactiveSyncWorker.class.getName());
+ assertThat(periodicWorkRequest.getWorkSpec().expedited).isFalse();
+ assertThat(periodicWorkRequest.getWorkSpec().isPeriodic()).isTrue();
+ assertThat(periodicWorkRequest.getWorkSpec().id).isNotNull();
+ assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresCharging()).isTrue();
+ assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresDeviceIdle()).isTrue();
+ assertThat(periodicWorkRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_LOCAL_AND_CLOUD);
+
+ final PeriodicWorkRequest periodicResetRequest =
+ mPeriodicWorkRequestArgumentCaptor.getAllValues().get(2);
+ assertThat(periodicResetRequest.getWorkSpec().workerClassName)
+ .isEqualTo(MediaResetWorker.class.getName());
+ assertThat(periodicResetRequest.getWorkSpec().expedited).isFalse();
+ assertThat(periodicResetRequest.getWorkSpec().isPeriodic()).isTrue();
+ assertThat(periodicResetRequest.getWorkSpec().id).isNotNull();
+ assertThat(periodicResetRequest.getWorkSpec().constraints.requiresCharging()).isTrue();
+ assertThat(periodicResetRequest.getWorkSpec().constraints.requiresDeviceIdle()).isTrue();
+ assertThat(periodicResetRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_LOCAL_AND_CLOUD);
+
+ clearInvocations(mMockWorkManager);
+
+ mConfigStore.disableCloudMediaFeature();
+ waitForIdle();
+
+ // There should be 2 invocations, one for cancelling proactive media syncs,
+ // the other for albums reset.
verify(mMockWorkManager, times(2)).cancelUniqueWork(anyString());
}
@Test
public void testAdhocProactiveSyncLocalOnly() {
setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
- reset(mMockWorkManager);
mPickerSyncManager.syncMediaProactively(/* localOnly */ true);
verify(mMockWorkManager, times(1))
@@ -234,8 +386,6 @@ public class PickerSyncManagerTest {
public void testAdhocProactiveSync() {
setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
- reset(mMockWorkManager);
-
mPickerSyncManager.syncMediaProactively(/* localOnly */ false);
verify(mMockWorkManager, times(1))
.enqueueUniqueWork(anyString(),
@@ -261,10 +411,10 @@ public class PickerSyncManagerTest {
setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
mConfigStore.setIsModernPickerEnabled(true);
- reset(mMockWorkManager);
mPickerSyncManager.syncMediaImmediately(new PickerSyncRequestExtras(/* albumId */null,
/* albumAuthority */ null, /* initLocalDataOnly */ true,
- /* callingPackageUid */ 0, /* shouldSyncGrants */ true, null));
+ /* callingPackageUid */ 0, /* shouldSyncGrants */ true, null),
+ mConfigStore);
verify(mMockWorkManager, times(2))
.enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture());
@@ -293,10 +443,10 @@ public class PickerSyncManagerTest {
mConfigStore.setIsModernPickerEnabled(true);
setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
- reset(mMockWorkManager);
mPickerSyncManager.syncMediaImmediately(new PickerSyncRequestExtras(/* albumId */null,
/* albumAuthority */ null, /* initLocalDataOnly */ true,
- /* callingPackageUid */ 0, /* shouldSyncGrants */ false, null));
+ /* callingPackageUid */ 0, /* shouldSyncGrants */ false, null),
+ mConfigStore);
verify(mMockWorkManager, times(2))
.enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture());
@@ -322,11 +472,10 @@ public class PickerSyncManagerTest {
mConfigStore.setIsModernPickerEnabled(true);
setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
- reset(mMockWorkManager);
-
mPickerSyncManager.syncMediaImmediately(new PickerSyncRequestExtras(/* albumId */null,
/* albumAuthority */ null, /* initLocalDataOnly */ false,
- /* callingPackageUid */ 0, /* shouldSyncGrants */ false, null));
+ /* callingPackageUid */ 0, /* shouldSyncGrants */ false, null),
+ mConfigStore);
verify(mMockWorkManager, times(3))
.enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture());
@@ -436,6 +585,466 @@ public class PickerSyncManagerTest {
.isEqualTo(SYNC_CLOUD_ONLY);
}
+ @Test
+ public void testSearchResultsLocalSync() {
+ setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+ mPickerSyncManager.syncSearchResultsForProvider(
+ /* searchRequestId */ 10,
+ SYNC_LOCAL_ONLY,
+ PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY
+ );
+ verify(mMockWorkManager, times(1))
+ .enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture());
+
+ final List<OneTimeWorkRequest> workRequestList =
+ mOneTimeWorkRequestArgumentCaptor.getAllValues();
+ assertThat(workRequestList.size()).isEqualTo(1);
+
+ WorkRequest workRequest = workRequestList.get(0);
+ assertThat(workRequest.getWorkSpec().workerClassName)
+ .isEqualTo(SearchResultsSyncWorker.class.getName());
+ assertThat(workRequest.getWorkSpec().expedited).isTrue();
+ assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse();
+ assertThat(workRequest.getWorkSpec().id).isNotNull();
+ assertThat(workRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_LOCAL_ONLY);
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SEARCH_REQUEST_ID, -1))
+ .isEqualTo(10);
+ }
+
+ @Test
+ public void testExistingSearchResultsSync() {
+ setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+ final int searchRequestId = 10;
+ final String authority = PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+
+ final SettableFuture<List<WorkInfo>> listenableFuture = SettableFuture.create();
+ final WorkInfo workInfo = new WorkInfo(
+ UUID.randomUUID(), WorkInfo.State.RUNNING, new HashSet<>());
+ final String tag = String.format(Locale.ROOT, "%s-%s-%s",
+ IMMEDIATE_CLOUD_SEARCH_SYNC_WORK_NAME, authority, searchRequestId);
+ listenableFuture.set(List.of(workInfo));
+ doReturn(listenableFuture).when(mMockWorkManager).getWorkInfosByTag(eq(tag));
+
+ mPickerSyncManager.syncSearchResultsForProvider(
+ searchRequestId,
+ SYNC_CLOUD_ONLY,
+ authority
+ );
+ verify(mMockWorkManager, times(0))
+ .enqueueUniqueWork(anyString(), any(), any(OneTimeWorkRequest.class));
+ }
+
+ @Test
+ public void testExistingMediaSetContentsSync() {
+ setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+ final int pickerMediaSetId = 10;
+ final String authority = PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+
+ final SettableFuture<List<WorkInfo>> listenableFuture = SettableFuture.create();
+ final WorkInfo workInfo = new WorkInfo(
+ UUID.randomUUID(), WorkInfo.State.RUNNING, new HashSet<>());
+ final String tag = String.format(Locale.ROOT, "%s-%s-%s",
+ IMMEDIATE_CLOUD_MEDIA_IN_MEDIA_SET_SYNC_WORK_NAME, authority, pickerMediaSetId);
+ listenableFuture.set(List.of(workInfo));
+ doReturn(listenableFuture).when(mMockWorkManager).getWorkInfosByTag(eq(tag));
+
+ Bundle extras = new Bundle();
+ extras.putString(MediaInMediaSetSyncRequestParams.KEY_PARENT_MEDIA_SET_AUTHORITY,
+ authority);
+ extras.putLong(MediaInMediaSetSyncRequestParams.KEY_PARENT_MEDIA_SET_PICKER_ID,
+ pickerMediaSetId);
+ extras.putStringArrayList("providers", new ArrayList<>(List.of(authority)));
+
+ MediaInMediaSetSyncRequestParams requestParams = new MediaInMediaSetSyncRequestParams(
+ extras);
+ mPickerSyncManager.syncMediaInMediaSetForProvider(
+ requestParams,
+ SYNC_CLOUD_ONLY
+ );
+
+ verify(mMockWorkManager, times(0))
+ .enqueueUniqueWork(anyString(), any(), any(OneTimeWorkRequest.class));
+ }
+
+ @Test
+ public void testSearchResultsSyncIsScheduled() {
+ setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+ final int searchRequestId = 10;
+ final String authority = PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+
+ final SettableFuture<List<WorkInfo>> listenableFuture = SettableFuture.create();
+ final WorkInfo workInfo = new WorkInfo(
+ UUID.randomUUID(), WorkInfo.State.RUNNING, new HashSet<>());
+ final String tag = String.format(Locale.ROOT, "%s-%s-%s",
+ IMMEDIATE_CLOUD_SEARCH_SYNC_WORK_NAME, authority, searchRequestId + 1);
+ listenableFuture.set(List.of(workInfo));
+ doReturn(listenableFuture).when(mMockWorkManager).getWorkInfosByTag(eq(tag));
+
+ mPickerSyncManager.syncSearchResultsForProvider(
+ searchRequestId,
+ SYNC_CLOUD_ONLY,
+ authority
+ );
+ verify(mMockWorkManager, times(1))
+ .enqueueUniqueWork(anyString(), any(), any(OneTimeWorkRequest.class));
+ }
+
+ @Test
+ public void testMediaSetContentsSyncIsScheduled() {
+ setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+ final int pickerMediaSetId = 10;
+ final String authority = PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+
+ final SettableFuture<List<WorkInfo>> listenableFuture = SettableFuture.create();
+ final WorkInfo workInfo = new WorkInfo(
+ UUID.randomUUID(), WorkInfo.State.RUNNING, new HashSet<>());
+ final String tag = String.format(Locale.ROOT, "%s-%s-%s",
+ IMMEDIATE_CLOUD_MEDIA_IN_MEDIA_SET_SYNC_WORK_NAME,
+ authority, pickerMediaSetId + 1);
+ listenableFuture.set(List.of(workInfo));
+ doReturn(listenableFuture).when(mMockWorkManager).getWorkInfosByTag(eq(tag));
+
+ Bundle extras = new Bundle();
+ extras.putString(MediaInMediaSetSyncRequestParams.KEY_PARENT_MEDIA_SET_AUTHORITY,
+ authority);
+ extras.putLong(MediaInMediaSetSyncRequestParams.KEY_PARENT_MEDIA_SET_PICKER_ID,
+ pickerMediaSetId);
+ extras.putStringArrayList("providers", new ArrayList<>(List.of(authority)));
+
+ MediaInMediaSetSyncRequestParams requestParams = new MediaInMediaSetSyncRequestParams(
+ extras);
+ mPickerSyncManager.syncMediaInMediaSetForProvider(
+ requestParams,
+ SYNC_CLOUD_ONLY
+ );
+
+ verify(mMockWorkManager, times(1))
+ .enqueueUniqueWork(anyString(), any(), any(OneTimeWorkRequest.class));
+ }
+
+ @Test
+ public void testSearchResultsCloudSync() {
+ setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+ mPickerSyncManager.syncSearchResultsForProvider(
+ /* searchRequestId */ 10,
+ SYNC_CLOUD_ONLY,
+ PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY
+ );
+ verify(mMockWorkManager, times(1))
+ .enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture());
+
+ final List<OneTimeWorkRequest> workRequestList =
+ mOneTimeWorkRequestArgumentCaptor.getAllValues();
+ assertThat(workRequestList.size()).isEqualTo(1);
+
+ WorkRequest workRequest = workRequestList.get(0);
+ assertThat(workRequest.getWorkSpec().workerClassName)
+ .isEqualTo(SearchResultsSyncWorker.class.getName());
+ assertThat(workRequest.getWorkSpec().expedited).isTrue();
+ assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse();
+ assertThat(workRequest.getWorkSpec().id).isNotNull();
+ assertThat(workRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_CLOUD_ONLY);
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SEARCH_REQUEST_ID, -1))
+ .isEqualTo(10);
+ }
+
+ @Test
+ public void testMediaSetsSyncLocalProvider() {
+ setupPickerSyncManager(/*schedulePeriodicSyncs*/ false);
+
+ String categoryId = "id";
+ String[] mimeTypes = new String[] { "image/*" };
+ Bundle extras = new Bundle();
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY,
+ SearchProvider.AUTHORITY);
+ extras.putStringArrayList(MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ new ArrayList<String>(Arrays.asList(mimeTypes)));
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, categoryId);
+ extras.putStringArrayList("providers", new ArrayList<>(List.of(
+ PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)));
+
+ MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
+
+ mPickerSyncManager.syncMediaSetsForProvider(requestParams, SYNC_LOCAL_ONLY);
+ verify(mMockWorkManager, times(1))
+ .beginUniqueWork(
+ anyString(),
+ any(ExistingWorkPolicy.class),
+ mOneTimeWorkRequestListArgumentCaptor.capture());
+ verify(mMockWorkContinuation, times(1))
+ .then(mOneTimeWorkRequestListArgumentCaptor.capture());
+ verify(mMockWorkContinuation).enqueue();
+
+ final List<List<OneTimeWorkRequest>> workRequestList =
+ mOneTimeWorkRequestListArgumentCaptor.getAllValues();
+ assertThat(workRequestList.size()).isEqualTo(2);
+
+ WorkRequest resetRequest = workRequestList.get(0).get(0);
+ assertThat(resetRequest.getWorkSpec().workerClassName)
+ .isEqualTo(MediaSetsResetWorker.class.getName());
+ assertThat(resetRequest.getWorkSpec().expedited).isTrue();
+ assertThat(resetRequest.getWorkSpec().isPeriodic()).isFalse();
+ assertThat(resetRequest.getWorkSpec().id).isNotNull();
+ assertThat(resetRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+
+ WorkRequest syncRequest = workRequestList.get(1).get(0);
+ assertThat(syncRequest.getWorkSpec().workerClassName)
+ .isEqualTo(MediaSetsSyncWorker.class.getName());
+ assertThat(syncRequest.getWorkSpec().expedited).isTrue();
+ assertThat(syncRequest.getWorkSpec().isPeriodic()).isFalse();
+ assertThat(syncRequest.getWorkSpec().id).isNotNull();
+ assertThat(syncRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+ assertThat(syncRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_LOCAL_ONLY);
+ assertThat(syncRequest.getWorkSpec().input
+ .getString(SYNC_WORKER_INPUT_CATEGORY_ID))
+ .isEqualTo(categoryId);
+ assertThat(syncRequest.getWorkSpec().input
+ .getString(SYNC_WORKER_INPUT_AUTHORITY))
+ .isEqualTo(SearchProvider.AUTHORITY);
+ assertThat(syncRequest.getWorkSpec().input
+ .getStringArray(EXTRA_MIME_TYPES))
+ .isEqualTo(mimeTypes);
+ }
+
+ @Test
+ public void testMediaSetsSyncCloudProvider() {
+ setupPickerSyncManager(/*schedulePeriodicSyncs*/ false);
+
+ String categoryId = "id";
+ String[] mimeTypes = new String[] { "image/*" };
+ Bundle extras = new Bundle();
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY,
+ SearchProvider.AUTHORITY);
+ extras.putStringArrayList(MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ new ArrayList<String>(Arrays.asList(mimeTypes)));
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, categoryId);
+ extras.putStringArrayList("providers", new ArrayList<>(List.of(
+ SearchProvider.AUTHORITY)));
+
+ MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
+
+ mPickerSyncManager.syncMediaSetsForProvider(requestParams, SYNC_CLOUD_ONLY);
+ verify(mMockWorkManager, times(1))
+ .beginUniqueWork(
+ anyString(),
+ any(ExistingWorkPolicy.class),
+ mOneTimeWorkRequestListArgumentCaptor.capture());
+ verify(mMockWorkContinuation, times(1))
+ .then(mOneTimeWorkRequestListArgumentCaptor.capture());
+ verify(mMockWorkContinuation).enqueue();
+
+ final List<List<OneTimeWorkRequest>> workRequestList =
+ mOneTimeWorkRequestListArgumentCaptor.getAllValues();
+ assertThat(workRequestList.size()).isEqualTo(2);
+
+ WorkRequest resetRequest = workRequestList.get(0).get(0);
+ assertThat(resetRequest.getWorkSpec().workerClassName)
+ .isEqualTo(MediaSetsResetWorker.class.getName());
+ assertThat(resetRequest.getWorkSpec().expedited).isTrue();
+ assertThat(resetRequest.getWorkSpec().isPeriodic()).isFalse();
+ assertThat(resetRequest.getWorkSpec().id).isNotNull();
+ assertThat(resetRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+
+ WorkRequest syncRequest = workRequestList.get(1).get(0);
+ assertThat(syncRequest.getWorkSpec().workerClassName)
+ .isEqualTo(MediaSetsSyncWorker.class.getName());
+ assertThat(syncRequest.getWorkSpec().expedited).isTrue();
+ assertThat(syncRequest.getWorkSpec().isPeriodic()).isFalse();
+ assertThat(syncRequest.getWorkSpec().id).isNotNull();
+ assertThat(syncRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+ assertThat(syncRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_CLOUD_ONLY);
+ assertThat(syncRequest.getWorkSpec().input
+ .getString(SYNC_WORKER_INPUT_CATEGORY_ID))
+ .isEqualTo(categoryId);
+ assertThat(syncRequest.getWorkSpec().input
+ .getString(SYNC_WORKER_INPUT_AUTHORITY))
+ .isEqualTo(SearchProvider.AUTHORITY);
+ assertThat(syncRequest.getWorkSpec().input
+ .getStringArray(EXTRA_MIME_TYPES))
+ .isEqualTo(mimeTypes);
+ }
+
+ @Test
+ public void testMediaInMediaSetSyncLocalProvider() {
+ setupPickerSyncManager(/*schedulePeriodicSyncs*/ false);
+
+ Long mediaSetPickerId = 1L;
+ Bundle extras = new Bundle();
+ extras.putString(MediaInMediaSetSyncRequestParams.KEY_PARENT_MEDIA_SET_AUTHORITY,
+ SearchProvider.AUTHORITY);
+ extras.putLong(MediaInMediaSetSyncRequestParams.KEY_PARENT_MEDIA_SET_PICKER_ID,
+ mediaSetPickerId);
+ extras.putStringArrayList("providers", new ArrayList<>(List.of(
+ PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)));
+
+ MediaInMediaSetSyncRequestParams requestParams = new MediaInMediaSetSyncRequestParams(
+ extras);
+
+ mPickerSyncManager.syncMediaInMediaSetForProvider(requestParams, SYNC_LOCAL_ONLY);
+ verify(mMockWorkManager, times(1))
+ .enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture());
+
+ final List<OneTimeWorkRequest> workRequestList =
+ mOneTimeWorkRequestArgumentCaptor.getAllValues();
+ assertThat(workRequestList.size()).isEqualTo(1);
+
+ WorkRequest workRequest = workRequestList.get(0);
+ assertThat(workRequest.getWorkSpec().workerClassName)
+ .isEqualTo(MediaInMediaSetsSyncWorker.class.getName());
+ assertThat(workRequest.getWorkSpec().expedited).isTrue();
+ assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse();
+ assertThat(workRequest.getWorkSpec().id).isNotNull();
+ assertThat(workRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_LOCAL_ONLY);
+ assertThat(workRequest.getWorkSpec().input
+ .getLong(SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID, -1))
+ .isEqualTo(mediaSetPickerId);
+ assertThat(workRequest.getWorkSpec().input
+ .getString(SYNC_WORKER_INPUT_AUTHORITY))
+ .isEqualTo(SearchProvider.AUTHORITY);
+ }
+
+ @Test
+ public void testMediaInMediaSetSyncCloudProvider() {
+ setupPickerSyncManager(/*schedulePeriodicSyncs*/ false);
+
+ Long mediaSetPickerId = 1L;
+ Bundle extras = new Bundle();
+ extras.putString(
+ MediaInMediaSetSyncRequestParams.KEY_PARENT_MEDIA_SET_AUTHORITY,
+ SearchProvider.AUTHORITY);
+ extras.putLong(
+ MediaInMediaSetSyncRequestParams.KEY_PARENT_MEDIA_SET_PICKER_ID,
+ mediaSetPickerId);
+ extras.putStringArrayList("providers", new ArrayList<>(List.of(
+ PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)));
+
+ MediaInMediaSetSyncRequestParams requestParams = new MediaInMediaSetSyncRequestParams(
+ extras);
+
+ mPickerSyncManager.syncMediaInMediaSetForProvider(requestParams, SYNC_CLOUD_ONLY);
+ verify(mMockWorkManager, times(1))
+ .enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture());
+
+ final List<OneTimeWorkRequest> workRequestList =
+ mOneTimeWorkRequestArgumentCaptor.getAllValues();
+ assertThat(workRequestList.size()).isEqualTo(1);
+
+ WorkRequest workRequest = workRequestList.get(0);
+ assertThat(workRequest.getWorkSpec().workerClassName)
+ .isEqualTo(MediaInMediaSetsSyncWorker.class.getName());
+ assertThat(workRequest.getWorkSpec().expedited).isTrue();
+ assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse();
+ assertThat(workRequest.getWorkSpec().id).isNotNull();
+ assertThat(workRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_CLOUD_ONLY);
+ assertThat(workRequest.getWorkSpec().input
+ .getLong(SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID, -1))
+ .isEqualTo(mediaSetPickerId);
+ assertThat(workRequest.getWorkSpec().input
+ .getString(SYNC_WORKER_INPUT_AUTHORITY))
+ .isEqualTo(SearchProvider.AUTHORITY);
+ }
+
+ @Test
+ public void testResetCloudSearchResults() {
+ setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+ mPickerSyncManager.resetCloudSearchCache(null);
+ verify(mMockWorkManager, times(1))
+ .enqueueUniqueWork(anyString(),
+ any(),
+ mOneTimeWorkRequestArgumentCaptor.capture());
+
+ final OneTimeWorkRequest workRequest = mOneTimeWorkRequestArgumentCaptor.getValue();
+ assertThat(workRequest.getWorkSpec().workerClassName)
+ .isEqualTo(SearchResetWorker.class.getName());
+ assertThat(workRequest.getWorkSpec().expedited).isTrue();
+ assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse();
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_CLOUD_ONLY);
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_RESET_TYPE, -1))
+ .isEqualTo(SEARCH_PARTIAL_CACHE_RESET);
+ }
+
+ @Test
+ public void testDelayedResetSearchCache() {
+ setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+ mPickerSyncManager.delayedResetSearchCache();
+ verify(mMockWorkManager, times(1))
+ .enqueueUniqueWork(anyString(),
+ any(),
+ mOneTimeWorkRequestArgumentCaptor.capture());
+
+ final OneTimeWorkRequest workRequest = mOneTimeWorkRequestArgumentCaptor.getValue();
+ assertThat(workRequest.getWorkSpec().workerClassName)
+ .isEqualTo(SearchResetWorker.class.getName());
+ assertThat(workRequest.getWorkSpec().expedited).isFalse();
+ assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse();
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_LOCAL_AND_CLOUD);
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_RESET_TYPE, -1))
+ .isEqualTo(SEARCH_RESULTS_FULL_CACHE_RESET);
+ assertThat(workRequest.getWorkSpec().initialDelay)
+ .isEqualTo(TimeUnit.MINUTES.toMillis(SEARCH_RESULTS_RESET_DELAY));
+ assertThat(workRequest.getWorkSpec().constraints.requiresDeviceIdle())
+ .isTrue();
+
+ }
+
+ @Test
+ public void testScheduleClearExpiredSuggestions() {
+ setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+ mPickerSyncManager.schedulePeriodicSearchSuggestionsReset();
+ verify(mMockWorkManager, times(1))
+ .enqueueUniquePeriodicWork(anyString(),
+ any(),
+ mPeriodicWorkRequestArgumentCaptor.capture());
+
+ final PeriodicWorkRequest workRequest =
+ mPeriodicWorkRequestArgumentCaptor.getAllValues().get(0);
+ assertThat(workRequest.getWorkSpec().workerClassName)
+ .isEqualTo(SearchResetWorker.class.getName());
+ assertThat(workRequest.getWorkSpec().expedited).isFalse();
+ assertThat(workRequest.getWorkSpec().isPeriodic()).isTrue();
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_LOCAL_AND_CLOUD);
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_RESET_TYPE, -1))
+ .isEqualTo(EXPIRED_SUGGESTIONS_RESET);
+ assertThat(workRequest.getWorkSpec().constraints.requiresCharging()).isTrue();
+ assertThat(workRequest.getWorkSpec().constraints.requiresDeviceIdle()).isTrue();
+ }
+
private void setupPickerSyncManager(boolean schedulePeriodicSyncs) {
doReturn(mMockOperation).when(mMockWorkManager)
.enqueueUniquePeriodicWork(anyString(),
@@ -456,8 +1065,11 @@ public class PickerSyncManagerTest {
doReturn(mMockOperation).when(mMockWorkContinuation).enqueue();
doReturn(mMockFuture).when(mMockOperation).getResult();
- mPickerSyncManager =
- new PickerSyncManager(mMockWorkManager, mMockContext,
- mConfigStore, schedulePeriodicSyncs);
+ mPickerSyncManager = new PickerSyncManager(mMockWorkManager, mMockContext);
+ if (schedulePeriodicSyncs) {
+ mPickerSyncManager.schedulePeriodicSync(
+ mConfigStore, /* periodicSyncInitialDelay */ 0L);
+ }
+ waitForIdle();
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorkerTest.java b/tests/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorkerTest.java
new file mode 100644
index 000000000..ca8112d5f
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorkerTest.java
@@ -0,0 +1,1002 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker.sync;
+
+import static android.provider.CloudMediaProviderContract.SEARCH_SUGGESTION_ALBUM;
+import static android.provider.CloudMediaProviderContract.SEARCH_SUGGESTION_FACE;
+
+import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SEARCH_REQUEST_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
+import static com.android.providers.media.photopicker.sync.SearchResultsSyncWorker.SYNC_COMPLETE_RESUME_KEY;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getCloudSearchResultsSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getInvalidSearchResultsSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getLocalSearchResultsSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.initializeTestWorkManager;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.annotation.NonNull;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.CloudMediaProviderContract;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.work.Data;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkInfo;
+import androidx.work.WorkManager;
+
+import com.android.providers.media.cloudproviders.SearchProvider;
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.SearchState;
+import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.v2.model.SearchRequest;
+import com.android.providers.media.photopicker.v2.model.SearchSuggestionRequest;
+import com.android.providers.media.photopicker.v2.model.SearchTextRequest;
+import com.android.providers.media.photopicker.v2.sqlite.PickerSQLConstants;
+import com.android.providers.media.photopicker.v2.sqlite.SearchRequestDatabaseUtil;
+import com.android.providers.media.photopicker.v2.sqlite.SelectSQLiteQueryBuilder;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+public class SearchResultsSyncWorkerTest {
+ @Mock
+ private PickerSyncController mMockSyncController;
+ @Mock
+ private SyncTracker mMockLocalSearchSyncTracker;
+ @Mock
+ private SyncTracker mMockCloudSearchSyncTracker;
+ @Mock
+ private SearchState mSearchState;
+ private Context mContext;
+ private SQLiteDatabase mDatabase;
+ private PickerDbFacade mFacade;
+ private String mLocalAuthority;
+ private String mCloudAuthority;
+
+ @Before
+ public void setup() {
+ initMocks(this);
+
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ initializeTestWorkManager(mContext);
+
+ SyncTrackerRegistry.setLocalSearchSyncTracker(mMockLocalSearchSyncTracker);
+ SyncTrackerRegistry.setCloudSearchSyncTracker(mMockCloudSearchSyncTracker);
+ PickerSyncController.setInstance(mMockSyncController);
+
+ final File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ final PickerDatabaseHelper helper = new PickerDatabaseHelper(mContext);
+
+ mLocalAuthority = LOCAL_PICKER_PROVIDER_AUTHORITY;
+ mCloudAuthority = SearchProvider.AUTHORITY;
+
+ mDatabase = helper.getWritableDatabase();
+ mFacade = new PickerDbFacade(
+ mContext, new PickerSyncLockManager(), mLocalAuthority);
+ mFacade.setCloudProvider(mCloudAuthority);
+
+ doReturn(mLocalAuthority).when(mMockSyncController).getLocalProvider();
+ doReturn(mCloudAuthority).when(mMockSyncController).getCloudProvider();
+ doReturn(mCloudAuthority).when(mMockSyncController)
+ .getCloudProviderOrDefault(any());
+ doReturn(mFacade).when(mMockSyncController).getDbFacade();
+ doReturn(mSearchState).when(mMockSyncController).getSearchState();
+ doReturn(true).when(mSearchState).isCloudSearchEnabled(any());
+ doReturn(true).when(mSearchState).isCloudSearchEnabled(any(), any());
+ doReturn(new PickerSyncLockManager()).when(mMockSyncController).getPickerSyncLockManager();
+ }
+
+ @Test
+ public void testInvalidSyncSource()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ SearchTextRequest searchRequest = new SearchTextRequest(
+ null,
+ "search text"
+ );
+ final int searchRequestId = saveSearchRequest(searchRequest);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(
+ getInvalidSearchResultsSyncInputData(
+ searchRequestId, mCloudAuthority))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testMissingSearchRequestId()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_ONLY)))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testInvalidSearchRequestId()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(getLocalSearchResultsSyncInputData(
+ /* searchRequestId */ 10, mLocalAuthority))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testMissingInputAuthority()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ SearchTextRequest searchRequest = new SearchTextRequest(
+ null,
+ "search text"
+ );
+ final int searchRequestId = saveSearchRequest(searchRequest);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(
+ new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_ONLY,
+ SYNC_WORKER_INPUT_SEARCH_REQUEST_ID, searchRequestId)))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testIncorrectInputAuthority()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ SearchTextRequest searchRequest = new SearchTextRequest(
+ null,
+ "search text"
+ );
+ final int searchRequestId = saveSearchRequest(searchRequest);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(getCloudSearchResultsSyncInputData(
+ searchRequestId, "random.authority"))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testInvalidAlbumSuggestionsSearchRequestId()
+ throws ExecutionException, InterruptedException {
+ // Setup cloud search results sync for local album
+ SearchSuggestionRequest searchRequest = new SearchSuggestionRequest(
+ null,
+ "search text",
+ "media-set-id",
+ mCloudAuthority,
+ SEARCH_SUGGESTION_ALBUM
+ );
+ final int searchRequestId = saveSearchRequest(searchRequest);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(getLocalSearchResultsSyncInputData(
+ searchRequestId, mLocalAuthority))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+ }
+
+ @Test
+ public void testTextSearchSyncWithCloudProvider()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ SearchTextRequest searchRequest = new SearchTextRequest(
+ null,
+ "search text"
+ );
+ final int searchRequestId = saveSearchRequest(searchRequest);
+
+ final Cursor inputCursor = SearchProvider.getDefaultCloudSearchResults();
+ SearchProvider.setSearchResults(inputCursor);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(getCloudSearchResultsSyncInputData(
+ searchRequestId, mCloudAuthority))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor cursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.SEARCH_RESULT_MEDIA.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(inputCursor.getCount());
+
+ if (cursor.moveToFirst() && inputCursor.moveToFirst()) {
+ do {
+ final ContentValues dbValues = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(cursor, dbValues);
+
+ final ContentValues inputValues = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(inputCursor, inputValues);
+
+ assertWithMessage("Cloud id is not as expected")
+ .that(dbValues.getAsString(PickerSQLConstants
+ .SearchResultMediaTableColumns.CLOUD_ID.getColumnName()))
+ .isEqualTo(inputValues.get(CloudMediaProviderContract.MediaColumns.ID));
+
+ assertWithMessage("Search request id is not as expected")
+ .that(dbValues.getAsInteger(
+ PickerSQLConstants.SearchResultMediaTableColumns
+ .SEARCH_REQUEST_ID.getColumnName()))
+ .isEqualTo(searchRequestId);
+
+ final String inputMediaStoreUri = inputValues
+ .getAsString(CloudMediaProviderContract.MediaColumns.MEDIA_STORE_URI);
+ if (inputMediaStoreUri == null) {
+ assertWithMessage("Local id is not null")
+ .that(dbValues.getAsInteger(
+ PickerSQLConstants.SearchResultMediaTableColumns
+ .LOCAL_ID.getColumnName()))
+ .isNull();
+ } else {
+ assertWithMessage("Local id is not as expected")
+ .that(dbValues.getAsInteger(
+ PickerSQLConstants.SearchResultMediaTableColumns
+ .LOCAL_ID.getColumnName()))
+ .isEqualTo(ContentUris.parseId(Uri.parse(inputMediaStoreUri)));
+ }
+ } while (cursor.moveToNext() && inputCursor.moveToNext());
+ }
+ }
+
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testTextSearchSyncWithLocalProvider()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ SearchTextRequest searchRequest = new SearchTextRequest(
+ null,
+ "search text"
+ );
+ final int searchRequestId = saveSearchRequest(searchRequest);
+
+ mLocalAuthority = SearchProvider.AUTHORITY;
+ doReturn(mLocalAuthority).when(mMockSyncController).getLocalProvider();
+
+ final Cursor inputCursor = SearchProvider.getDefaultLocalSearchResults();
+ SearchProvider.setSearchResults(inputCursor);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(getLocalSearchResultsSyncInputData(
+ searchRequestId, mLocalAuthority))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor cursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.SEARCH_RESULT_MEDIA.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(inputCursor.getCount());
+
+ if (cursor.moveToFirst() && inputCursor.moveToFirst()) {
+ do {
+ final ContentValues dbValues = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(cursor, dbValues);
+
+ final ContentValues inputValues = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(inputCursor, inputValues);
+
+ assertWithMessage("Local id is not as expected")
+ .that(dbValues.getAsString(PickerSQLConstants
+ .SearchResultMediaTableColumns.LOCAL_ID.getColumnName()))
+ .isEqualTo(inputValues.get(CloudMediaProviderContract.MediaColumns.ID));
+
+ assertWithMessage("Cloud id is not null")
+ .that(dbValues.getAsString(PickerSQLConstants
+ .SearchResultMediaTableColumns.CLOUD_ID.getColumnName()))
+ .isNull();
+
+ assertWithMessage("Search request id is not as expected")
+ .that(dbValues.getAsInteger(
+ PickerSQLConstants.SearchResultMediaTableColumns
+ .SEARCH_REQUEST_ID.getColumnName()))
+ .isEqualTo(searchRequestId);
+ } while (cursor.moveToNext() && inputCursor.moveToNext());
+ }
+ }
+
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testSuggestionSearchSyncWithCloudProvider()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ SearchSuggestionRequest searchRequest = new SearchSuggestionRequest(
+ null,
+ "search text",
+ "media-set-id",
+ mLocalAuthority,
+ SEARCH_SUGGESTION_FACE
+ );
+ final int searchRequestId = saveSearchRequest(searchRequest);
+
+ final Cursor inputCursor = SearchProvider.getDefaultCloudSearchResults();
+ SearchProvider.setSearchResults(inputCursor);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(getCloudSearchResultsSyncInputData(
+ searchRequestId, mCloudAuthority))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor cursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.SEARCH_RESULT_MEDIA.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(inputCursor.getCount());
+
+ if (cursor.moveToFirst() && inputCursor.moveToFirst()) {
+ do {
+ final ContentValues dbValues = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(cursor, dbValues);
+
+ final ContentValues inputValues = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(inputCursor, inputValues);
+
+ assertWithMessage("Cloud id is not as expected")
+ .that(dbValues.getAsString(PickerSQLConstants
+ .SearchResultMediaTableColumns.CLOUD_ID.getColumnName()))
+ .isEqualTo(inputValues.get(CloudMediaProviderContract.MediaColumns.ID));
+
+ assertWithMessage("Search request id is not as expected")
+ .that(dbValues.getAsInteger(
+ PickerSQLConstants.SearchResultMediaTableColumns
+ .SEARCH_REQUEST_ID.getColumnName()))
+ .isEqualTo(searchRequestId);
+
+ final String inputMediaStoreUri = inputValues
+ .getAsString(CloudMediaProviderContract.MediaColumns.MEDIA_STORE_URI);
+ if (inputMediaStoreUri == null) {
+ assertWithMessage("Local id is not null")
+ .that(dbValues.getAsInteger(
+ PickerSQLConstants.SearchResultMediaTableColumns
+ .LOCAL_ID.getColumnName()))
+ .isNull();
+ } else {
+ assertWithMessage("Local id is not as expected")
+ .that(dbValues.getAsInteger(
+ PickerSQLConstants.SearchResultMediaTableColumns
+ .LOCAL_ID.getColumnName()))
+ .isEqualTo(ContentUris.parseId(Uri.parse(inputMediaStoreUri)));
+ }
+ } while (cursor.moveToNext() && inputCursor.moveToNext());
+ }
+ }
+
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testSuggestionSearchSyncWithLocalProvider()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ SearchSuggestionRequest searchRequest = new SearchSuggestionRequest(
+ null,
+ "search text",
+ "media-set-id",
+ mCloudAuthority,
+ SEARCH_SUGGESTION_FACE,
+ "Random-local-resume-key",
+ "local-authority",
+ "Random-cloud-resume-key",
+ mCloudAuthority
+ );
+ final int searchRequestId = saveSearchRequest(searchRequest);
+
+ mLocalAuthority = SearchProvider.AUTHORITY;
+ doReturn(mLocalAuthority).when(mMockSyncController).getLocalProvider();
+
+ final Cursor inputCursor = SearchProvider.getDefaultLocalSearchResults();
+ SearchProvider.setSearchResults(inputCursor);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(getLocalSearchResultsSyncInputData(
+ searchRequestId, mLocalAuthority))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor cursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.SEARCH_RESULT_MEDIA.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(inputCursor.getCount());
+
+ if (cursor.moveToFirst() && inputCursor.moveToFirst()) {
+ do {
+ final ContentValues dbValues = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(cursor, dbValues);
+
+ final ContentValues inputValues = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(inputCursor, inputValues);
+
+ assertWithMessage("Local id is not as expected")
+ .that(dbValues.getAsString(PickerSQLConstants
+ .SearchResultMediaTableColumns.LOCAL_ID.getColumnName()))
+ .isEqualTo(inputValues.get(CloudMediaProviderContract.MediaColumns.ID));
+
+ assertWithMessage("Cloud id is not null")
+ .that(dbValues.getAsString(PickerSQLConstants
+ .SearchResultMediaTableColumns.CLOUD_ID.getColumnName()))
+ .isNull();
+
+ assertWithMessage("Search request id is not as expected")
+ .that(dbValues.getAsInteger(
+ PickerSQLConstants.SearchResultMediaTableColumns
+ .SEARCH_REQUEST_ID.getColumnName()))
+ .isEqualTo(searchRequestId);
+ } while (cursor.moveToNext() && inputCursor.moveToNext());
+ }
+ }
+
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testSyncWasAlreadyComplete()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ SearchSuggestionRequest searchRequest = new SearchSuggestionRequest(
+ null,
+ "search text",
+ "media-set-id",
+ mLocalAuthority,
+ SEARCH_SUGGESTION_FACE,
+ "Random-local-resume-key",
+ "local-authority",
+ SYNC_COMPLETE_RESUME_KEY,
+ mCloudAuthority
+ );
+ final int searchRequestId = saveSearchRequest(searchRequest);
+
+ final Cursor inputCursor = SearchProvider.getDefaultCloudSearchResults();
+ SearchProvider.setSearchResults(inputCursor);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(getCloudSearchResultsSyncInputData(
+ searchRequestId, mCloudAuthority))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor cursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.SEARCH_RESULT_MEDIA.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(0);
+ }
+
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testCloudSyncResumeInfoIsCleared()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ SearchSuggestionRequest searchRequest = new SearchSuggestionRequest(
+ null,
+ "search text",
+ "media-set-id",
+ mLocalAuthority,
+ SEARCH_SUGGESTION_FACE
+ );
+ final int searchRequestId = saveSearchRequest(searchRequest);
+
+ final Cursor inputCursor = SearchProvider.getDefaultCloudSearchResults();
+ SearchProvider.setSearchResults(inputCursor);
+
+ // Run the search results worker to sync with SearchProvider.
+ final OneTimeWorkRequest request1 =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(getCloudSearchResultsSyncInputData(
+ searchRequestId, mCloudAuthority))
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request1).getResult().get();
+
+ // Verify that sync happened
+ final WorkInfo workInfo1 = workManager.getWorkInfoById(request1.getId()).get();
+ assertThat(workInfo1.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor cursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.SEARCH_RESULT_MEDIA.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(inputCursor.getCount());
+ }
+
+ final SearchRequest searchRequest1 =
+ SearchRequestDatabaseUtil.getSearchRequestDetails(mDatabase, searchRequestId);
+ assertWithMessage("Search details are null")
+ .that(searchRequest1)
+ .isNotNull();
+ assertWithMessage("Cloud sync authority is not as expected")
+ .that(searchRequest1.getCloudAuthority())
+ .isEqualTo(SearchProvider.AUTHORITY);
+ assertWithMessage("Cloud sync resume key is not as expected")
+ .that(searchRequest1.getCloudSyncResumeKey())
+ .isEqualTo(SYNC_COMPLETE_RESUME_KEY);
+
+ // Run the search results worker to sync with a random provider
+ final String newCloudAuthority = "random.authority";
+ doReturn(newCloudAuthority).when(mMockSyncController).getCloudProvider();
+ doReturn(newCloudAuthority).when(mMockSyncController).getCloudProviderOrDefault(any());
+ final OneTimeWorkRequest request2 =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(getCloudSearchResultsSyncInputData(
+ searchRequestId, newCloudAuthority))
+ .build();
+
+ workManager.enqueue(request2).getResult().get();
+
+ // Verify that the database was cleared.
+ final WorkInfo workInfo2 = workManager.getWorkInfoById(request2.getId()).get();
+ assertThat(workInfo2.getState()).isEqualTo(WorkInfo.State.FAILED);
+
+ try (Cursor cursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.SEARCH_RESULT_MEDIA.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(0);
+ }
+
+ final SearchRequest searchRequest2 =
+ SearchRequestDatabaseUtil.getSearchRequestDetails(mDatabase, searchRequestId);
+ assertWithMessage("Search details are null")
+ .that(searchRequest2)
+ .isNotNull();
+ assertWithMessage("Cloud sync authority is not as expected")
+ .that(searchRequest2.getCloudAuthority())
+ .isNull();
+ assertWithMessage("Cloud sync resume key is not as expected")
+ .that(searchRequest2.getCloudSyncResumeKey())
+ .isNull();
+ }
+
+ @Test
+ public void testLocalSyncResumeInfoIsCleared()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ SearchSuggestionRequest searchRequest = new SearchSuggestionRequest(
+ null,
+ "search text",
+ "media-set-id",
+ mLocalAuthority,
+ SEARCH_SUGGESTION_FACE
+ );
+ final int searchRequestId = saveSearchRequest(searchRequest);
+
+ mLocalAuthority = SearchProvider.AUTHORITY;
+ doReturn(mLocalAuthority).when(mMockSyncController).getLocalProvider();
+
+ final Cursor inputCursor = SearchProvider.getDefaultLocalSearchResults();
+ SearchProvider.setSearchResults(inputCursor);
+
+ // Run the search results worker to sync with SearchProvider.
+ final OneTimeWorkRequest request1 =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(getLocalSearchResultsSyncInputData(
+ searchRequestId, mLocalAuthority))
+ .build();
+
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request1).getResult().get();
+
+ // Verify that sync happened
+ final WorkInfo workInfo1 = workManager.getWorkInfoById(request1.getId()).get();
+ assertThat(workInfo1.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor cursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.SEARCH_RESULT_MEDIA.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(inputCursor.getCount());
+ }
+
+ final SearchRequest searchRequest1 =
+ SearchRequestDatabaseUtil.getSearchRequestDetails(mDatabase, searchRequestId);
+ assertWithMessage("Search details are null")
+ .that(searchRequest1)
+ .isNotNull();
+ assertWithMessage("Local sync authority is not as expected")
+ .that(searchRequest1.getLocalAuthority())
+ .isEqualTo(mLocalAuthority);
+ assertWithMessage("Local sync resume key is not as expected")
+ .that(searchRequest1.getLocalSyncResumeKey())
+ .isEqualTo(SYNC_COMPLETE_RESUME_KEY);
+
+ // Run the search results worker to sync with a random provider
+ final String newLocalAuthority = "random.authority";
+ doReturn(newLocalAuthority).when(mMockSyncController).getLocalProvider();
+ final OneTimeWorkRequest request2 =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(getLocalSearchResultsSyncInputData(
+ searchRequestId, newLocalAuthority))
+ .build();
+ workManager.enqueue(request2).getResult().get();
+
+ // Verify that the database was cleared.
+ final WorkInfo workInfo2 = workManager.getWorkInfoById(request2.getId()).get();
+ assertThat(workInfo2.getState()).isEqualTo(WorkInfo.State.FAILED);
+
+ try (Cursor cursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.SEARCH_RESULT_MEDIA.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(0);
+ }
+
+ final SearchRequest searchRequest2 =
+ SearchRequestDatabaseUtil.getSearchRequestDetails(mDatabase, searchRequestId);
+ assertWithMessage("Search details are null")
+ .that(searchRequest2)
+ .isNotNull();
+ assertWithMessage("Local sync authority is not as expected")
+ .that(searchRequest2.getLocalAuthority())
+ .isNull();
+ assertWithMessage("Local sync resume key is not as expected")
+ .that(searchRequest2.getLocalSyncResumeKey())
+ .isNull();
+ }
+
+ @Test
+ public void testSuggestionSearchSyncLoop()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ SearchSuggestionRequest searchRequest = new SearchSuggestionRequest(
+ null,
+ "search text",
+ "media-set-id",
+ mLocalAuthority,
+ SEARCH_SUGGESTION_FACE
+ );
+ final int searchRequestId = saveSearchRequest(searchRequest);
+
+ final Cursor inputCursor = SearchProvider.getDefaultCloudSearchResults();
+ final String repeatPageToken = "LOOP";
+ final Bundle bundle = new Bundle();
+ bundle.putString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN, repeatPageToken);
+ inputCursor.setExtras(bundle);
+ SearchProvider.setSearchResults(inputCursor);
+
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(SearchResultsSyncWorker.class)
+ .setInputData(getCloudSearchResultsSyncInputData(
+ searchRequestId, mCloudAuthority))
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ try (Cursor cursor = mDatabase.rawQuery(
+ new SelectSQLiteQueryBuilder(mDatabase).setTables(
+ PickerSQLConstants.Table.SEARCH_RESULT_MEDIA.name()
+ ).buildQuery(), null
+ )) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(inputCursor.getCount());
+ }
+
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockLocalSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .markSyncCompleted(any());
+
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockCloudSearchSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+
+ /**
+ * Saves the given search request in DB and asserts that it was saved.
+ * Returns the generated search request id.
+ */
+ private int saveSearchRequest(@NonNull SearchRequest searchRequest) {
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest);
+ final int searchRequestId =
+ SearchRequestDatabaseUtil.getSearchRequestID(mDatabase, searchRequest);
+
+ assertWithMessage("Could not find search request is the database " + searchRequest)
+ .that(searchRequestId)
+ .isNotEqualTo(-1);
+
+ return searchRequestId;
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java b/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java
index 1eac73a78..574580963 100644
--- a/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java
+++ b/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java
@@ -50,6 +50,19 @@ public class SyncTrackerTests {
}
@Test
+ public void testMarkAllSyncsComplete() {
+ SyncTracker syncTracker = new SyncTracker();
+ syncTracker.createSyncFuture(UUID.randomUUID());
+ syncTracker.createSyncFuture(UUID.randomUUID());
+ syncTracker.createSyncFuture(UUID.randomUUID());
+ Collection<CompletableFuture<Object>> futures = syncTracker.pendingSyncFutures();
+ assertThat(futures.size()).isEqualTo(3);
+
+ syncTracker.markAllSyncsCompleted();
+ assertThat(futures.size()).isEqualTo(0);
+ }
+
+ @Test
public void testCompleteOnTimeoutSyncFuture()
throws InterruptedException, ExecutionException, TimeoutException {
SyncTracker syncTracker = new SyncTracker();
diff --git a/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java b/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java
index 52702ac7f..30f139c76 100644
--- a/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java
+++ b/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java
@@ -24,6 +24,7 @@ import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYN
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_ALBUM_ID;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_AUTHORITY;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_RESET_TYPE;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SEARCH_REQUEST_ID;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
import android.content.Context;
@@ -31,6 +32,7 @@ import android.content.Intent;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.work.Configuration;
import androidx.work.Data;
import androidx.work.Worker;
@@ -108,6 +110,39 @@ public class SyncWorkerTestUtils {
SYNC_WORKER_INPUT_ALBUM_ID, albumId));
}
+ /**
+ * Returns input data for the SearchResultsSyncWorker to perform sync with the
+ * local provider.
+ */
+ public static Data getLocalSearchResultsSyncInputData(int searchRequestId,
+ @NonNull String authority) {
+ return new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_ONLY,
+ SYNC_WORKER_INPUT_SEARCH_REQUEST_ID, searchRequestId,
+ SYNC_WORKER_INPUT_AUTHORITY, authority));
+ }
+
+ /**
+ * Returns input data for the SearchResultsSyncWorker to perform sync with the
+ * cloud provider.
+ */
+ public static Data getCloudSearchResultsSyncInputData(int searchRequestId,
+ @NonNull String authority) {
+ return new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_CLOUD_ONLY,
+ SYNC_WORKER_INPUT_SEARCH_REQUEST_ID, searchRequestId,
+ SYNC_WORKER_INPUT_AUTHORITY, authority));
+ }
+
+ /**
+ * Returns input data for the SearchResultsSyncWorker to perform sync with the
+ * an invalid sync source
+ */
+ public static Data getInvalidSearchResultsSyncInputData(int searchRequestId,
+ @Nullable String authority) {
+ return new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_AND_CLOUD,
+ SYNC_WORKER_INPUT_SEARCH_REQUEST_ID, searchRequestId,
+ SYNC_WORKER_INPUT_AUTHORITY, authority));
+ }
+
static <W extends Worker> W buildTestWorker(@NonNull Context context,
@NonNull Class<W> workerClass) {
return TestWorkerBuilder.from(context, workerClass)
diff --git a/tests/src/com/android/providers/media/photopicker/util/CloudProviderUtilsTest.java b/tests/src/com/android/providers/media/photopicker/util/CloudProviderUtilsTest.java
index 8649d4d56..bebbb557a 100644
--- a/tests/src/com/android/providers/media/photopicker/util/CloudProviderUtilsTest.java
+++ b/tests/src/com/android/providers/media/photopicker/util/CloudProviderUtilsTest.java
@@ -36,7 +36,6 @@ import java.util.Set;
public class CloudProviderUtilsTest {
-
@Test
public void getAllAvailableCloudProvidersTest() {
final Context context = InstrumentationRegistry.getTargetContext();
diff --git a/tests/src/com/android/providers/media/photopicker/util/PickerDbTestUtils.java b/tests/src/com/android/providers/media/photopicker/util/PickerDbTestUtils.java
index df29f9d40..1f6c0d428 100644
--- a/tests/src/com/android/providers/media/photopicker/util/PickerDbTestUtils.java
+++ b/tests/src/com/android/providers/media/photopicker/util/PickerDbTestUtils.java
@@ -16,6 +16,8 @@
package com.android.providers.media.photopicker.util;
+import static android.provider.CloudMediaProviderContract.SEARCH_SUGGESTION_ALBUM;
+
import static com.android.providers.media.util.MimeUtils.getExtensionFromMimeType;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -62,6 +64,7 @@ public class PickerDbTestUtils {
public static final int STANDARD_MIME_TYPE_EXTENSION =
CloudMediaProviderContract.MediaColumns.STANDARD_MIME_TYPE_EXTENSION_GIF;
public static final String TEST_PACKAGE_NAME = "com.test.package";
+ public static final String TEST_DIFFERENT_PACKAGE_NAME = "com.test.different.package";
public static final String LOCAL_PROVIDER = "com.local.provider";
public static final String CLOUD_PROVIDER = "com.cloud.provider";
@@ -312,6 +315,51 @@ public class PickerDbTestUtils {
SIZE_BYTES, mimeType, STANDARD_MIME_TYPE_EXTENSION, isFavorite);
}
+ public static Cursor getSuggestionCursor(String mediaSetId) {
+ String[] projectionKey = new String[]{
+ CloudMediaProviderContract.SearchSuggestionColumns.MEDIA_SET_ID,
+ CloudMediaProviderContract.SearchSuggestionColumns.DISPLAY_TEXT,
+ CloudMediaProviderContract.SearchSuggestionColumns.TYPE,
+ CloudMediaProviderContract.SearchSuggestionColumns.MEDIA_COVER_ID,
+ };
+
+ String[] projectionValue = new String[]{
+ mediaSetId,
+ "display_text",
+ SEARCH_SUGGESTION_ALBUM,
+ CLOUD_ID_1,
+ };
+
+ MatrixCursor c = new MatrixCursor(projectionKey);
+ c.addRow(projectionValue);
+ return c;
+ }
+
+ public static Cursor getMediaCategoriesCursor(String categoryId) {
+ String[] projectionKey = new String[]{
+ CloudMediaProviderContract.MediaCategoryColumns.ID,
+ CloudMediaProviderContract.MediaCategoryColumns.DISPLAY_NAME,
+ CloudMediaProviderContract.MediaCategoryColumns.MEDIA_CATEGORY_TYPE,
+ CloudMediaProviderContract.MediaCategoryColumns.MEDIA_COVER_ID1,
+ CloudMediaProviderContract.MediaCategoryColumns.MEDIA_COVER_ID2,
+ CloudMediaProviderContract.MediaCategoryColumns.MEDIA_COVER_ID3,
+ CloudMediaProviderContract.MediaCategoryColumns.MEDIA_COVER_ID4,
+ };
+
+ String[] projectionValue = new String[]{
+ categoryId,
+ "display_text",
+ CloudMediaProviderContract.MEDIA_CATEGORY_TYPE_PEOPLE_AND_PETS,
+ CLOUD_ID_1,
+ CLOUD_ID_2,
+ /* MEDIA_COVER_ID3 */ null,
+ /* MEDIA_COVER_ID4 */ null
+ };
+
+ MatrixCursor c = new MatrixCursor(projectionKey);
+ c.addRow(projectionValue);
+ return c;
+ }
public static String toMediaStoreUri(String localId) {
if (localId == null) {
return null;
@@ -324,8 +372,8 @@ public class PickerDbTestUtils {
}
public static String getData(String authority, String displayName, String pickerSegmentType) {
- return "/sdcard/.transforms/synthetic/" + pickerSegmentType + "/0/" + authority + "/media/"
- + displayName;
+ return "/sdcard/.transforms/synthetic/" + pickerSegmentType + "/" + UserHandle.myUserId()
+ + "/" + authority + "/media/" + displayName;
}
public static void assertCloudAlbumCursor(Cursor cursor, String albumId, String displayName,
diff --git a/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java b/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java
index 852d5c7ae..83c5c1ada 100644
--- a/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java
+++ b/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java
@@ -16,6 +16,8 @@
package com.android.providers.media.photopicker.v2;
+import static android.provider.MediaStore.PER_USER_RANGE;
+
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.ALBUM_ID;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_1;
@@ -24,8 +26,10 @@ import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLO
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_4;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_PROVIDER;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.DATE_TAKEN_MS;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.DURATION_MS;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.GENERATION_MODIFIED;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.GIF_IMAGE_MIME_TYPE;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.HEIGHT;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.JPEG_IMAGE_MIME_TYPE;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_1;
@@ -34,9 +38,12 @@ import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOC
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_4;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_PROVIDER;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.MP4_VIDEO_MIME_TYPE;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.ORIENTATION;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.PNG_IMAGE_MIME_TYPE;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.STANDARD_MIME_TYPE_EXTENSION;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.TEST_DIFFERENT_PACKAGE_NAME;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.TEST_PACKAGE_NAME;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.WIDTH;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddAlbumMediaOperation;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddMediaOperation;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertInsertGrantsOperation;
@@ -49,53 +56,101 @@ import static com.android.providers.media.photopicker.util.PickerDbTestUtils.get
import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.COLUMN_GRANTS_COUNT;
import static com.android.providers.media.photopicker.v2.model.AlbumsCursorWrapper.EMPTY_MEDIA_ID;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;
+import android.Manifest;
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
+import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Process;
import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.CloudMediaProviderContract;
import android.provider.MediaStore;
import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;
import androidx.test.InstrumentationRegistry;
-
+import androidx.test.filters.SdkSuppress;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.work.ExistingWorkPolicy;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.Operation;
+import androidx.work.WorkContinuation;
+import androidx.work.WorkManager;
+
+import com.android.providers.media.MediaProvider;
import com.android.providers.media.PickerUriResolver;
+import com.android.providers.media.cloudproviders.SearchProvider;
+import com.android.providers.media.flags.Flags;
+import com.android.providers.media.photopicker.CategoriesState;
import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.SearchState;
import com.android.providers.media.photopicker.data.ItemsProvider;
import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
import com.android.providers.media.photopicker.data.PickerDbFacade;
import com.android.providers.media.photopicker.data.model.UserId;
import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
+import com.android.providers.media.photopicker.v2.model.MediaGroup;
+import com.android.providers.media.photopicker.v2.model.MediaInMediaSetSyncRequestParams;
+import com.android.providers.media.photopicker.v2.model.MediaSetsSyncRequestParams;
import com.android.providers.media.photopicker.v2.model.MediaSource;
+import com.android.providers.media.photopicker.v2.model.SearchSuggestion;
+import com.android.providers.media.photopicker.v2.model.SearchTextRequest;
+import com.android.providers.media.photopicker.v2.sqlite.MediaInMediaSetsDatabaseUtil;
+import com.android.providers.media.photopicker.v2.sqlite.MediaSetsDatabaseUtil;
+import com.android.providers.media.photopicker.v2.sqlite.PickerSQLConstants;
+import com.android.providers.media.photopicker.v2.sqlite.SearchSuggestionsDatabaseUtils;
+import com.android.providers.media.photopicker.v2.sqlite.SearchSuggestionsQuery;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
import org.mockito.Mock;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+@RunWith(AndroidJUnit4.class)
public class PickerDataLayerV2Test {
@Mock
private PickerSyncController mMockSyncController;
@@ -103,12 +158,30 @@ public class PickerDataLayerV2Test {
private Context mMockContext;
@Mock
private PackageManager mMockPackageManager;
+ @Mock
+ private SearchState mSearchState;
+ @Mock
+ private WorkManager mMockWorkManager;
+ @Mock
+ private Operation mMockOperation;
+ @Mock
+ private WorkContinuation mMockWorkContinuation;
+ @Mock
+ private ListenableFuture<Operation.State.SUCCESS> mMockFuture;
+ @Mock
+ CategoriesState mCategoriesState;
private PickerDbFacade mFacade;
private Context mContext;
private MockContentResolver mMockContentResolver;
private TestContentProvider mLocalProvider;
private TestContentProvider mCloudProvider;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ @Rule
+ public final TestRule mCompatChangeRule = new PlatformCompatChangeRule();
+
private static class TestContentProvider extends MockContentProvider {
private Cursor mQueryResult = null;
@@ -148,9 +221,20 @@ public class PickerDataLayerV2Test {
doReturn(LOCAL_PROVIDER).when(mMockSyncController).getLocalProvider();
doReturn(CLOUD_PROVIDER).when(mMockSyncController).getCloudProvider();
doReturn(CLOUD_PROVIDER).when(mMockSyncController).getCloudProviderOrDefault(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMediaSets(any(), any());
+ doReturn(true).when(mMockSyncController).shouldQueryLocalMediaSets(any());
doReturn(mFacade).when(mMockSyncController).getDbFacade();
+ doReturn(mSearchState).when(mMockSyncController).getSearchState();
+ doReturn(mCategoriesState).when(mMockSyncController).getCategoriesState();
doReturn(new PickerSyncLockManager()).when(mMockSyncController).getPickerSyncLockManager();
doReturn(mMockContentResolver).when(mMockContext).getContentResolver();
+
+ androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity(
+ Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ Manifest.permission.READ_DEVICE_CONFIG);
}
@After
@@ -435,7 +519,12 @@ public class PickerDataLayerV2Test {
/* writeCount */1);
assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorForMediaWithGrants,
/* writeCount */1);
- int testUid = 123;
+ // testUid should be selected such that the userId computed from this uid later in the code
+ // flow matches the current userId. UserId is computed using
+ // PickerSyncController#uidToUser() where the userId = uid / PER_USER_RANGE.
+ // So testUid is =
+ // (a random number smaller than PER_USER_RANGE) + (PER_USER_RANGE * UserHandle.myUserId())
+ int testUid = 11 + (PER_USER_RANGE * UserHandle.myUserId());
doReturn(mMockPackageManager)
.when(mMockContext).getPackageManager();
String[] packageNames = new String[]{TEST_PACKAGE_NAME};
@@ -470,6 +559,188 @@ public class PickerDataLayerV2Test {
}
@Test
+ @EnableFlags(Flags.FLAG_REVOKE_ACCESS_OWNED_PHOTOS)
+ @EnableCompatChanges({MediaProvider.ENABLE_OWNED_PHOTOS})
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA)
+ public void testPreGrantsForOwnedPhotos() {
+ assumeTrue(MediaProvider.isOwnedPhotosEnabled(Process.myUid()));
+
+ doReturn(mMockPackageManager)
+ .when(mMockContext).getPackageManager();
+ String[] packageNames = new String[]{TEST_PACKAGE_NAME};
+ doReturn(packageNames).when(mMockPackageManager).getPackagesForUid(Process.myUid());
+ Map<String, Integer> idVsExpectedPreGrantedValue = populateMediaAndMediaGrantsTable();
+ int totalCount = idVsExpectedPreGrantedValue.size();
+
+ try (Cursor cr = PickerDataLayerV2.queryMedia(
+ mMockContext, getMediaQueryExtras(Long.MAX_VALUE, Long.MAX_VALUE, /* pageSize */ 10,
+ new ArrayList<>(List.of(LOCAL_PROVIDER)),
+ new ArrayList<>(List.of("image/*")),
+ MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP,
+ /*callingUid*/ Process.myUid()))) {
+
+ assertWithMessage(
+ "Unexpected number of rows in media query result")
+ .that(cr.getCount()).isEqualTo(totalCount);
+
+ cr.moveToFirst();
+ for (int i = 0; i < totalCount; i++) {
+ int id = cr.getInt(cr.getColumnIndex("id"));
+ int isPreGranted = cr.getInt(cr.getColumnIndex("is_pre_granted"));
+ assertEquals(idVsExpectedPreGrantedValue.get(String.valueOf(id)).intValue(),
+ isPreGranted);
+ cr.moveToNext();
+ }
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_REVOKE_ACCESS_OWNED_PHOTOS)
+ @EnableCompatChanges({MediaProvider.ENABLE_OWNED_PHOTOS})
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA)
+ public void testPreGrantedCountForOwnedPhotos() {
+ assumeTrue(MediaProvider.isOwnedPhotosEnabled(Process.myUid()));
+
+ doReturn(mMockPackageManager)
+ .when(mMockContext).getPackageManager();
+ String[] packageNames = new String[]{TEST_PACKAGE_NAME};
+ doReturn(packageNames).when(mMockPackageManager).getPackagesForUid(0);
+ Map<String, Integer> idVsPreGranted = populateMediaAndMediaGrantsTable();
+ int totalPreGranted = (int) idVsPreGranted.values().stream()
+ .filter(preGranted -> Integer.valueOf(1).equals(preGranted))
+ .count();
+
+ Bundle queryArgs = new Bundle();
+ queryArgs.putInt(Intent.EXTRA_UID, 0);
+ try (Cursor cr = PickerDataLayerV2.fetchCountForPreGrantedItems(mMockContext, queryArgs)) {
+ cr.moveToFirst();
+ assertEquals(totalPreGranted, cr.getInt(cr.getColumnIndex(COLUMN_GRANTS_COUNT)));
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_REVOKE_ACCESS_OWNED_PHOTOS)
+ @EnableCompatChanges({MediaProvider.ENABLE_OWNED_PHOTOS})
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA)
+ public void testPreviewForOwnedPhotos() {
+ assumeTrue(MediaProvider.isOwnedPhotosEnabled(Process.myUid()));
+
+ doReturn(mMockPackageManager)
+ .when(mMockContext).getPackageManager();
+ String[] packageNames = new String[]{TEST_PACKAGE_NAME};
+ doReturn(packageNames).when(mMockPackageManager).getPackagesForUid(0);
+
+ Map<String, Integer> idVsPreGranted = populateMediaAndMediaGrantsTable();
+ int totalPreGranted = (int) idVsPreGranted.values().stream()
+ .filter(preGranted -> Integer.valueOf(1).equals(preGranted))
+ .count();
+
+ Bundle queryArgs = getMediaQueryExtras(Long.MAX_VALUE, Long.MAX_VALUE, /* pageSize */ 10,
+ new ArrayList<>(List.of(LOCAL_PROVIDER)),
+ new ArrayList<>(List.of("image/*")),
+ MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP,
+ 0);
+ queryArgs.putBoolean("is_preview_session", true);
+
+ try (Cursor cr = PickerDataLayerV2.queryPreviewMedia(mMockContext, queryArgs)) {
+ assertEquals(totalPreGranted, cr.getCount());
+ cr.moveToFirst();
+ for (int i = 0; i < totalPreGranted; i++) {
+ int id = cr.getInt(cr.getColumnIndex("id"));
+ int preGranted = cr.getInt(cr.getColumnIndex("is_pre_granted"));
+ assertEquals(1, (int) idVsPreGranted.get(String.valueOf(id)));
+ assertEquals(1, preGranted);
+ cr.moveToNext();
+ }
+ }
+ }
+
+ private Map<String, Integer> populateMediaAndMediaGrantsTable() {
+
+ Map<String, Integer> idVsExpectedPreGrantedValue = new HashMap<>();
+
+ // 1. ownerPackageName != TEST_PACKAGE_NAME and no media grants.
+ // preGranted should be false
+ Cursor cursorWithDifferentOwnerPackageName = getMediaCursorWithOwnerPackageNameAndUserId(
+ "101", TEST_DIFFERENT_PACKAGE_NAME);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorWithDifferentOwnerPackageName,
+ /*writeCount*/ 1);
+ idVsExpectedPreGrantedValue.put("101", 0);
+
+ // 2. ownerPackageName == TEST_PACKAGE_NAME and no media grants.
+ // preGranted should be true
+ Cursor cursorWithCorrectOwnerPackageName = getMediaCursorWithOwnerPackageNameAndUserId(
+ "102", TEST_PACKAGE_NAME);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorWithCorrectOwnerPackageName,
+ /*writeCount*/ 1);
+ idVsExpectedPreGrantedValue.put("102", 1);
+
+ // 3. ownerPackageName != TEST_PACKAGE_NAME
+ // and media_grants with packageName == TEST_PACKAGE_NAME.
+ // preGranted should be true
+ Cursor cursorWithCorrectMediaGrants = getMediaCursorWithOwnerPackageNameAndUserId(
+ "103", TEST_DIFFERENT_PACKAGE_NAME);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorWithCorrectMediaGrants,
+ /*writeCount*/ 1);
+ assertInsertGrantsOperation(mFacade, getMediaGrantsCursor("103", TEST_PACKAGE_NAME,
+ 0), /*writeCount*/ 1);
+ idVsExpectedPreGrantedValue.put("103", 1);
+
+ // 4. ownerPackageName != TEST_PACKAGE_NAME
+ // and media_grants with packageName != TEST_PACKAGE_NAME.
+ // preGranted should be false
+ Cursor cursorWithDifferentMediaGrants = getMediaCursorWithOwnerPackageNameAndUserId(
+ "104", TEST_DIFFERENT_PACKAGE_NAME);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorWithDifferentMediaGrants,
+ /*writeCount*/ 1);
+ assertInsertGrantsOperation(mFacade, getMediaGrantsCursor("104",
+ TEST_DIFFERENT_PACKAGE_NAME, 0), /*writeCount*/ 1);
+ idVsExpectedPreGrantedValue.put("104", 0);
+
+ return idVsExpectedPreGrantedValue;
+ }
+
+ private Cursor getMediaCursorWithOwnerPackageNameAndUserId(String id, String ownerPackageName) {
+ String[] projectionKey = new String[]{
+ CloudMediaProviderContract.MediaColumns.ID,
+ CloudMediaProviderContract.MediaColumns.MEDIA_STORE_URI,
+ CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS,
+ CloudMediaProviderContract.MediaColumns.SYNC_GENERATION,
+ CloudMediaProviderContract.MediaColumns.SIZE_BYTES,
+ CloudMediaProviderContract.MediaColumns.MIME_TYPE,
+ CloudMediaProviderContract.MediaColumns.STANDARD_MIME_TYPE_EXTENSION,
+ CloudMediaProviderContract.MediaColumns.DURATION_MILLIS,
+ CloudMediaProviderContract.MediaColumns.IS_FAVORITE,
+ CloudMediaProviderContract.MediaColumns.HEIGHT,
+ CloudMediaProviderContract.MediaColumns.WIDTH,
+ CloudMediaProviderContract.MediaColumns.ORIENTATION,
+ CloudMediaProviderContract.MediaColumns.OWNER_PACKAGE_NAME,
+ CloudMediaProviderContract.MediaColumns.USER_ID
+ };
+
+ String[] projectionValue = new String[]{
+ id,
+ null,
+ String.valueOf(DATE_TAKEN_MS),
+ String.valueOf(GENERATION_MODIFIED),
+ String.valueOf(1),
+ JPEG_IMAGE_MIME_TYPE,
+ String.valueOf(STANDARD_MIME_TYPE_EXTENSION),
+ String.valueOf(DURATION_MS),
+ String.valueOf(0),
+ String.valueOf(HEIGHT),
+ String.valueOf(WIDTH),
+ String.valueOf(ORIENTATION),
+ ownerPackageName,
+ String.valueOf(0)
+ };
+
+ MatrixCursor c = new MatrixCursor(projectionKey);
+ c.addRow(projectionValue);
+ return c;
+ }
+
+ @Test
public void testQueryLocalMediaForPreview() {
Cursor cursorForMediaWithoutGrants = getMediaCursor(LOCAL_ID_1, DATE_TAKEN_MS + 1,
GENERATION_MODIFIED, /* mediaStoreUri */ null, /* sizeBytes */ 1,
@@ -490,7 +761,12 @@ public class PickerDataLayerV2Test {
assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorForMediaWithGrantsButDeSelected,
/* writeCount */1);
- int testUid = 123;
+ // testUid should be selected such that the userId computed from this uid later in the code
+ // flow matches the current userId. UserId is computed using
+ // PickerSyncController#uidToUser() where the userId = uid / PER_USER_RANGE.
+ // So testUid is =
+ // (a random number smaller than PER_USER_RANGE) + (PER_USER_RANGE * UserHandle.myUserId())
+ int testUid = 11 + (PER_USER_RANGE * UserHandle.myUserId());
doReturn(mMockPackageManager)
.when(mMockContext).getPackageManager();
String[] packageNames = new String[]{TEST_PACKAGE_NAME};
@@ -539,6 +815,77 @@ public class PickerDataLayerV2Test {
}
@Test
+ public void testQueryMediaSets() {
+ List<String> mimeTypes = new ArrayList<>();
+ mimeTypes.add("image/*");
+ String mediaSetId1 = "mediaSetId1";
+ String mediaSetId2 = "mediaSetId2";
+ String displayName1 = "displayName1";
+ String displayName2 = "displayName2";
+ String coverId1 = "56";
+ String coverId2 = "76";
+ String categoryId = "id";
+
+ String[] columns = new String[]{
+ CloudMediaProviderContract.MediaSetColumns.ID,
+ CloudMediaProviderContract.MediaSetColumns.DISPLAY_NAME,
+ CloudMediaProviderContract.MediaSetColumns.MEDIA_COVER_ID
+ };
+
+ // Prep the media sets table
+ MatrixCursor cursor = new MatrixCursor(columns);
+ cursor.addRow(new Object[] { mediaSetId1, displayName1, coverId1 });
+ cursor.addRow(new Object[] { mediaSetId2, displayName2, coverId2 });
+
+ MediaSetsDatabaseUtil.cacheMediaSets(
+ mFacade.getDatabase(), cursor, categoryId,
+ SearchProvider.AUTHORITY, mimeTypes);
+
+ Bundle extras = new Bundle();
+ extras.putString(
+ MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY,
+ SearchProvider.AUTHORITY);
+ extras.putStringArrayList(
+ MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ new ArrayList<>(List.of("image/*")));
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, categoryId);
+ extras.putStringArrayList("providers", new ArrayList<>(List.of(SearchProvider.AUTHORITY)));
+
+ try (Cursor mediaSets = PickerDataLayerV2.queryMediaSets(extras)) {
+ assertNotNull(mediaSets);
+ assertEquals(2, mediaSets.getCount());
+
+ if (mediaSets.moveToFirst()) {
+ String retrievedMediaSetId1 = mediaSets.getString(mediaSets.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns.GROUP_ID.getColumnName()));
+ assertEquals(mediaSetId1, retrievedMediaSetId1);
+ String retrievedDisplayName1 = mediaSets.getString(mediaSets.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns.DISPLAY_NAME.getColumnName()));
+ assertEquals(retrievedDisplayName1, displayName1);
+ String retrievedUri1 = mediaSets.getString(mediaSets.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns.UNWRAPPED_COVER_URI
+ .getColumnName()
+ ));
+ assertTrue(retrievedUri1.contains(coverId1));
+
+ mediaSets.moveToNext();
+ String retrievedMediaSetId2 = mediaSets.getString(mediaSets.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns.GROUP_ID.getColumnName()));
+ assertEquals(mediaSetId2, retrievedMediaSetId2);
+ String retrievedDisplayName2 = mediaSets.getString(mediaSets.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns.DISPLAY_NAME.getColumnName()));
+ assertEquals(retrievedDisplayName2, displayName2);
+ String retrievedUri2 = mediaSets.getString(mediaSets.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns.UNWRAPPED_COVER_URI
+ .getColumnName()
+ ));
+ assertTrue(retrievedUri2.contains(coverId2));
+
+ }
+ }
+ }
+
+ @Test
public void queryMediaOnlyLocalWithPreSelection() {
Cursor cursorLocal1 = getMediaCursor(LOCAL_ID_1, DATE_TAKEN_MS, GENERATION_MODIFIED,
/* mediaStoreUri */ null, /* sizeBytes */ 1, MP4_VIDEO_MIME_TYPE,
@@ -683,6 +1030,76 @@ public class PickerDataLayerV2Test {
}
@Test
+ public void testQueryMediaInMediaSet() {
+ final Cursor cursor1 = getLocalMediaCursor(LOCAL_ID_1, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getLocalMediaCursor(LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ Long mediaSetPickerId = 1L;
+
+ int cloudRowsInserted = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mFacade.getDatabase(), List.of(
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, mediaSetPickerId)
+ ), CLOUD_PROVIDER
+ );
+ assertEquals(
+ "Number of rows inserted should be equal to the number of items in the cursor,",
+ /*expected*/cloudRowsInserted,
+ /*actual*/1);
+
+ int localRowsInserted = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mFacade.getDatabase(), List.of(
+ getContentValues(LOCAL_ID_1, null, mediaSetPickerId)
+ ), LOCAL_PROVIDER
+ );
+ assertEquals(
+ "Number of rows inserted is incorrect",
+ localRowsInserted,
+ 1);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+ extras.putLong(
+ MediaInMediaSetSyncRequestParams.KEY_PARENT_MEDIA_SET_PICKER_ID,
+ mediaSetPickerId);
+ extras.putString(
+ MediaInMediaSetSyncRequestParams.KEY_PARENT_MEDIA_SET_AUTHORITY,
+ LOCAL_PROVIDER);
+
+ try (Cursor cursor =
+ PickerDataLayerV2.queryMediaInMediaSet(extras)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(2);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_2);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_1);
+ }
+ }
+
+
+ @Test
+ @DisableFlags(Flags.FLAG_REVOKE_ACCESS_OWNED_PHOTOS)
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public void testFetchMediaGrantsCount() {
int testUid = 123;
int userId = PickerSyncController.uidToUserId(testUid);
@@ -718,7 +1135,7 @@ public class PickerDataLayerV2Test {
Bundle input = new Bundle();
input.putInt(Intent.EXTRA_UID, testUid);
- try (Cursor cr = PickerDataLayerV2.fetchMediaGrantsCount(
+ try (Cursor cr = PickerDataLayerV2.fetchCountForPreGrantedItems(
mMockContext, input)) {
// cursor should only contain 1 row that represents the count.
@@ -2077,6 +2494,379 @@ public class PickerDataLayerV2Test {
}
}
+ @Test
+ public void testQuerySearchSuggestionsZeroState() {
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController).getCloudProvider();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController)
+ .getCloudProviderOrDefault(any());
+ doReturn(mSearchState).when(mMockSyncController).getSearchState();
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+ doReturn(true).when(mSearchState).isCloudSearchEnabled(any());
+
+ final Bundle bundle = new Bundle();
+ bundle.putString("prefix", "");
+ bundle.putStringArrayList("providers", new ArrayList<>(List.of(SearchProvider.AUTHORITY)));
+ final SearchSuggestionsQuery query = new SearchSuggestionsQuery(bundle);
+
+ // Async tasks are run synchronously during tests to make tests deterministic and prevent
+ // flaky test results.
+ final Executor currentThreadExecutor = Runnable::run;
+
+ try (Cursor cursor = PickerDataLayerV2.querySearchSuggestions(
+ mContext, bundle, currentThreadExecutor, null)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(SearchProvider.DEFAULT_SUGGESTION_RESULTS.getCount());
+
+ final String projection = PickerSQLConstants.SearchSuggestionsResponseColumns
+ .MEDIA_SET_ID.getProjection();
+ if (cursor.moveToFirst() && SearchProvider.DEFAULT_SUGGESTION_RESULTS.moveToFirst()) {
+ do {
+ assertWithMessage("Media ID is not as expected")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(projection)))
+ .isEqualTo(SearchProvider.DEFAULT_SUGGESTION_RESULTS.getString(
+ SearchProvider.DEFAULT_SUGGESTION_RESULTS
+ .getColumnIndexOrThrow(projection)));
+ } while (cursor.moveToNext()
+ && SearchProvider.DEFAULT_SUGGESTION_RESULTS.moveToNext());
+ }
+ }
+
+ final List<SearchSuggestion> searchSuggestions = SearchSuggestionsDatabaseUtils
+ .getCachedSuggestions(mFacade.getDatabase(), query);
+
+ assertWithMessage("Suggestions should not be null")
+ .that(searchSuggestions)
+ .isNotNull();
+
+ assertWithMessage("Suggestions size is not as expected")
+ .that(searchSuggestions.size())
+ .isEqualTo(SearchProvider.DEFAULT_SUGGESTION_RESULTS.getCount());
+ }
+
+ @Test
+ public void testQuerySearchSuggestionsNonZeroState() {
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController).getCloudProvider();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController)
+ .getCloudProviderOrDefault(any());
+ doReturn(mSearchState).when(mMockSyncController).getSearchState();
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+ doReturn(true).when(mSearchState).isCloudSearchEnabled(any());
+
+ final Bundle bundle = new Bundle();
+ bundle.putString("prefix", "x");
+ bundle.putStringArrayList("providers", new ArrayList<>(List.of(SearchProvider.AUTHORITY)));
+ final SearchSuggestionsQuery query = new SearchSuggestionsQuery(bundle);
+
+ // Async tasks are run synchronously during tests to make tests deterministic and prevent
+ // flaky test results.
+ final Executor currentThreadExecutor = Runnable::run;
+
+ try (Cursor cursor = PickerDataLayerV2.querySearchSuggestions(
+ mContext, bundle, currentThreadExecutor, null)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(SearchProvider.DEFAULT_SUGGESTION_RESULTS.getCount());
+
+ final String projection = PickerSQLConstants.SearchSuggestionsResponseColumns
+ .MEDIA_SET_ID.getProjection();
+ if (cursor.moveToFirst() && SearchProvider.DEFAULT_SUGGESTION_RESULTS.moveToFirst()) {
+ do {
+ assertWithMessage("Media ID is not as expected")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(projection)))
+ .isEqualTo(SearchProvider.DEFAULT_SUGGESTION_RESULTS.getString(
+ SearchProvider.DEFAULT_SUGGESTION_RESULTS
+ .getColumnIndexOrThrow(projection)));
+ } while (cursor.moveToNext()
+ && SearchProvider.DEFAULT_SUGGESTION_RESULTS.moveToNext());
+ }
+ }
+
+ final List<SearchSuggestion> searchSuggestions = SearchSuggestionsDatabaseUtils
+ .getCachedSuggestions(mFacade.getDatabase(), query);
+
+ assertWithMessage("Suggestions should not be null")
+ .that(searchSuggestions)
+ .isNotNull();
+
+ assertWithMessage("Suggestions size is not as expected")
+ .that(searchSuggestions.size())
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void testQuerySearchSuggestionsWithHistory() {
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController).getCloudProvider();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController)
+ .getCloudProviderOrDefault(any());
+ doReturn(mSearchState).when(mMockSyncController).getSearchState();
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+ doReturn(true).when(mSearchState).isCloudSearchEnabled(any());
+
+ final Bundle bundle = new Bundle();
+ bundle.putString("prefix", "");
+ bundle.putStringArrayList("providers", new ArrayList<>(List.of(SearchProvider.AUTHORITY)));
+ final SearchSuggestionsQuery query = new SearchSuggestionsQuery(bundle);
+
+ // Async tasks are run synchronously during tests to make tests deterministic and prevent
+ // flaky test results.
+ final Executor currentThreadExecutor = Runnable::run;
+
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(
+ mFacade.getDatabase(),
+ new SearchTextRequest(null, "mountains"));
+
+ try (Cursor cursor = PickerDataLayerV2.querySearchSuggestions(
+ mContext, bundle, currentThreadExecutor, null)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(SearchProvider.DEFAULT_SUGGESTION_RESULTS.getCount() + 1);
+
+ final String projection = PickerSQLConstants.SearchSuggestionsResponseColumns
+ .MEDIA_SET_ID.getProjection();
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(projection)))
+ .isNull();
+
+ if (cursor.moveToNext() && SearchProvider.DEFAULT_SUGGESTION_RESULTS.moveToFirst()) {
+ do {
+ assertWithMessage("Media ID is not as expected")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(projection)))
+ .isEqualTo(SearchProvider.DEFAULT_SUGGESTION_RESULTS.getString(
+ SearchProvider.DEFAULT_SUGGESTION_RESULTS
+ .getColumnIndexOrThrow(projection)));
+ } while (cursor.moveToNext()
+ && SearchProvider.DEFAULT_SUGGESTION_RESULTS.moveToNext());
+ }
+ }
+ }
+
+ @Test
+ public void testHandleNewSearchRequest() {
+ doReturn(true).when(mMockSyncController).shouldQueryLocalMediaForSearch(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMediaForSearch(any(), any());
+ doReturn(mMockOperation).when(mMockWorkManager)
+ .enqueueUniqueWork(anyString(), any(ExistingWorkPolicy.class),
+ any(OneTimeWorkRequest.class));
+ doReturn(mMockFuture).when(mMockOperation).getResult();
+
+ final String searchText = "volcano";
+ final Bundle extras = getCreateSearchRequestExtras(new SearchTextRequest(null, searchText));
+ final Executor currentThreadExecutor = Runnable::run;
+
+ final Bundle result = PickerDataLayerV2.handleNewSearchRequest(
+ mMockContext, extras, currentThreadExecutor, mMockWorkManager);
+
+ // Assert that a new search request was created
+ assertThat(result).isNotNull();
+ assertThat(result.getInt("search_request_id")).isEqualTo(1);
+
+ // Assert that local sync, cloud sync and cache clearing work was scheduled
+ verify(mMockWorkManager, times(3))
+ .enqueueUniqueWork(anyString(), any(ExistingWorkPolicy.class),
+ any(OneTimeWorkRequest.class));
+
+ // Assert that search request was saved as search history in database
+ final List<SearchSuggestion> suggestions =
+ SearchSuggestionsDatabaseUtils.getHistorySuggestions(
+ mFacade.getDatabase(),
+ new SearchSuggestionsQuery("", new ArrayList<>()));
+ assertThat(suggestions.size()).isEqualTo(1);
+ assertThat(suggestions.get(0).getSearchText()).isEqualTo(searchText);
+ }
+
+ @Test
+ public void testTriggerMediaSetsSyncRequest() {
+ doReturn(true).when(mMockSyncController).shouldQueryLocalMediaSets(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMediaSets(any(), any());
+ doReturn(mMockWorkContinuation)
+ .when(mMockWorkManager)
+ .beginUniqueWork(
+ anyString(), any(ExistingWorkPolicy.class), any(List.class));
+ // Handle .then chaining
+ doReturn(mMockWorkContinuation)
+ .when(mMockWorkContinuation)
+ .then(any(List.class));
+ doReturn(mMockOperation).when(mMockWorkContinuation).enqueue();
+ doReturn(mMockFuture).when(mMockOperation).getResult();
+
+ Bundle extras = new Bundle();
+ extras.putString(
+ MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY,
+ SearchProvider.AUTHORITY);
+ extras.putStringArray(
+ MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ new String[] { "image/*" });
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, "id");
+ extras.putStringArrayList("providers", new ArrayList<>(List.of(SearchProvider.AUTHORITY)));
+
+ PickerDataLayerV2.triggerMediaSetsSync(extras, mContext, mMockWorkManager);
+
+ // Assert that both local and cloud syncs were scheduled
+ verify(mMockWorkManager, times(1)).beginUniqueWork(
+ anyString(), any(ExistingWorkPolicy.class), any(List.class));
+ verify(mMockWorkContinuation, times(1)).then(any(List.class));
+ verify(mMockWorkContinuation, times(1)).enqueue();
+ }
+ @Test
+ public void testTriggerMediaInMediaSetSyncRequest() {
+ doReturn(true).when(mMockSyncController).shouldQueryLocalMediaSets(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMediaSets(any(), any());
+ doReturn(mMockOperation).when(mMockWorkManager)
+ .enqueueUniqueWork(anyString(), any(ExistingWorkPolicy.class),
+ any(OneTimeWorkRequest.class));
+ doReturn(mMockFuture).when(mMockOperation).getResult();
+
+ Bundle extras = new Bundle();
+ extras.putString(
+ MediaInMediaSetSyncRequestParams.KEY_PARENT_MEDIA_SET_AUTHORITY,
+ SearchProvider.AUTHORITY);
+ extras.putLong(MediaInMediaSetSyncRequestParams.KEY_PARENT_MEDIA_SET_PICKER_ID, 1);
+ extras.putStringArrayList("providers", new ArrayList<>(List.of(SearchProvider.AUTHORITY)));
+
+ PickerDataLayerV2.triggerMediaSyncForMediaSet(extras, mContext, mMockWorkManager);
+
+ // Assert that both local and cloud syncs were scheduled
+ verify(mMockWorkManager, times(1))
+ .enqueueUniqueWork(anyString(), any(ExistingWorkPolicy.class),
+ any(OneTimeWorkRequest.class));
+ }
+
+ @Test
+ public void testQueryCategoriesAndAlbums() {
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController).getCloudProvider();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController)
+ .getCloudProviderOrDefault(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+ doReturn(true).when(mCategoriesState).areCategoriesEnabled(any(), any());
+
+ final Cursor cursor1 = getLocalMediaCursor(LOCAL_ID_1, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_1, LOCAL_ID_1, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+
+ try (Cursor cursor = PickerDataLayerV2.queryCategoriesAndAlbums(
+ mContext,
+ getMediaQueryExtras(Long.MAX_VALUE, Long.MAX_VALUE, 100,
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, SearchProvider.AUTHORITY))),
+ /* cancellationSignal */ null)) {
+ assertWithMessage("Count of albums and categories")
+ .that(cursor.getCount())
+ .isEqualTo(5);
+
+ cursor.moveToFirst();
+ assertWithMessage("Unexpected media group")
+ .that(MediaGroup.valueOf(
+ cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns
+ .MEDIA_GROUP.getColumnName()))))
+ .isEqualTo(MediaGroup.ALBUM);
+ assertWithMessage("Unexpected album id")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns.GROUP_ID.getColumnName())))
+ .isEqualTo(CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES);
+ assertWithMessage("Unexpected picker id")
+ .that(cursor.getLong(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns
+ .PICKER_ID.getColumnName())))
+ .isEqualTo(0L);
+
+ cursor.moveToNext();
+ assertWithMessage("Unexpected media group")
+ .that(MediaGroup.valueOf(
+ cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns
+ .MEDIA_GROUP.getColumnName()))))
+ .isEqualTo(MediaGroup.ALBUM);
+ assertWithMessage("Unexpected album id")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns.GROUP_ID.getColumnName())))
+ .isEqualTo(CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA);
+ assertWithMessage("Unexpected picker id")
+ .that(cursor.getLong(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns
+ .PICKER_ID.getColumnName())))
+ .isEqualTo(1L);
+
+ cursor.moveToNext();
+ // Assert that the next media group is people and pets category
+ assertWithMessage("Unexpected media group")
+ .that(MediaGroup.valueOf(
+ cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns
+ .MEDIA_GROUP.getColumnName()))))
+ .isEqualTo(MediaGroup.CATEGORY);
+ assertWithMessage("Unexpected picker id")
+ .that(cursor.getLong(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns
+ .PICKER_ID.getColumnName())))
+ .isEqualTo(2L);
+
+ cursor.moveToNext();
+ assertWithMessage("Unexpected media group")
+ .that(MediaGroup.valueOf(
+ cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns
+ .MEDIA_GROUP.getColumnName()))))
+ .isEqualTo(MediaGroup.ALBUM);
+ assertWithMessage("Unexpected album id")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns.GROUP_ID.getColumnName())))
+ .isEqualTo(CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS);
+ assertWithMessage("Unexpected picker id")
+ .that(cursor.getLong(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns
+ .PICKER_ID.getColumnName())))
+ .isEqualTo(3L);
+
+ cursor.moveToNext();
+ // Assert that the next media group is a cloud album
+ assertWithMessage("Unexpected media group")
+ .that(MediaGroup.valueOf(
+ cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns
+ .MEDIA_GROUP.getColumnName()))))
+ .isEqualTo(MediaGroup.ALBUM);
+ final Uri coverUri = Uri.parse(
+ cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns
+ .UNWRAPPED_COVER_URI.getColumnName())));
+ assertWithMessage("Unexpected media group")
+ .that(coverUri.getLastPathSegment())
+ .isEqualTo(LOCAL_ID_1);
+ assertWithMessage("Unexpected picker id")
+ .that(cursor.getLong(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaGroupResponseColumns
+ .PICKER_ID.getColumnName())))
+ .isEqualTo(4L);
+ }
+ }
+
+ private static Bundle getCreateSearchRequestExtras(SearchTextRequest searchTextRequest) {
+ final Bundle bundle = new Bundle();
+ bundle.putString("search_text", searchTextRequest.getSearchText());
+ bundle.putStringArrayList("providers", new ArrayList<>(List.of(SearchProvider.AUTHORITY)));
+ return bundle;
+ }
+
private static void assertMediaCursor(Cursor cursor, String id, String authority,
Long dateTaken, String mimeType) {
assertMediaCursor(cursor, id, authority, dateTaken, mimeType,
@@ -2227,4 +3017,18 @@ public class PickerDataLayerV2Test {
extras.putString("album_authority", albumAuthority);
return extras;
}
+
+ private ContentValues getContentValues(
+ String localId, String cloudId, Long mediaSetPickerId) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(
+ PickerSQLConstants.MediaInMediaSetsTableColumns.CLOUD_ID.getColumnName(), cloudId);
+ contentValues.put(
+ PickerSQLConstants.MediaInMediaSetsTableColumns.LOCAL_ID.getColumnName(), localId);
+ contentValues.put(
+ PickerSQLConstants.MediaInMediaSetsTableColumns.MEDIA_SETS_PICKER_ID
+ .getColumnName(),
+ mediaSetPickerId);
+ return contentValues;
+ }
}
diff --git a/tests/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2Test.java b/tests/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2Test.java
index 8b89795de..4c03b59e7 100644
--- a/tests/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2Test.java
+++ b/tests/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2Test.java
@@ -25,33 +25,46 @@ import org.junit.Test;
public class PickerUriResolverV2Test {
@Test
public void testMediaQuery() {
- assertEquals(PickerUriResolverV2.sUriMatcher.match(
- Uri.parse("content://media/picker_internal/v2/media")),
- PickerUriResolverV2.PICKER_INTERNAL_MEDIA
+ assertEquals(
+ PickerUriResolverV2.PICKER_INTERNAL_MEDIA,
+ PickerUriResolverV2.sUriMatcher.match(
+ Uri.parse("content://media/picker_internal/v2/media"))
);
}
@Test
public void testAlbumQuery() {
- assertEquals(PickerUriResolverV2.sUriMatcher.match(
- Uri.parse("content://media/picker_internal/v2/album")),
- PickerUriResolverV2.PICKER_INTERNAL_ALBUM
+ assertEquals(
+ PickerUriResolverV2.PICKER_INTERNAL_ALBUM,
+ PickerUriResolverV2.sUriMatcher.match(
+ Uri.parse("content://media/picker_internal/v2/album"))
);
}
@Test
public void testAlbumContentQuery() {
- assertEquals(PickerUriResolverV2.sUriMatcher.match(
- Uri.parse("content://media/picker_internal/v2/album/album_id")),
- PickerUriResolverV2.PICKER_INTERNAL_ALBUM_CONTENT
+ assertEquals(
+ PickerUriResolverV2.PICKER_INTERNAL_ALBUM_CONTENT,
+ PickerUriResolverV2.sUriMatcher.match(
+ Uri.parse("content://media/picker_internal/v2/album/album_id"))
);
}
@Test
public void testAvailableProvidersQuery() {
- assertEquals(PickerUriResolverV2.sUriMatcher.match(
- Uri.parse("content://media/picker_internal/v2/available_providers")),
- PickerUriResolverV2.PICKER_INTERNAL_AVAILABLE_PROVIDERS
+ assertEquals(
+ PickerUriResolverV2.PICKER_INTERNAL_AVAILABLE_PROVIDERS,
+ PickerUriResolverV2.sUriMatcher.match(
+ Uri.parse("content://media/picker_internal/v2/available_providers"))
+ );
+ }
+
+ @Test
+ public void testSearchResultsQuery() {
+ assertEquals(
+ PickerUriResolverV2.PICKER_INTERNAL_SEARCH_MEDIA,
+ PickerUriResolverV2.sUriMatcher.match(
+ Uri.parse("content://media/picker_internal/v2/search_media/132"))
);
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/v2/SearchSuggestionsProviderTest.java b/tests/src/com/android/providers/media/photopicker/v2/SearchSuggestionsProviderTest.java
new file mode 100644
index 000000000..adc000bd1
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/v2/SearchSuggestionsProviderTest.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker.v2;
+
+import static android.provider.CloudMediaProviderContract.SEARCH_SUGGESTION_ALBUM;
+import static android.provider.CloudMediaProviderContract.SEARCH_SUGGESTION_HISTORY;
+import static android.provider.CloudMediaProviderContract.SEARCH_SUGGESTION_FACE;
+
+import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.platform.test.annotations.EnableFlags;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.providers.media.cloudproviders.SearchProvider;
+import com.android.providers.media.flags.Flags;
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.SearchState;
+import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
+import com.android.providers.media.photopicker.v2.model.SearchSuggestion;
+import com.android.providers.media.photopicker.v2.sqlite.PickerSQLConstants;
+import com.android.providers.media.photopicker.v2.sqlite.SearchSuggestionsDatabaseUtils;
+import com.android.providers.media.photopicker.v2.sqlite.SearchSuggestionsQuery;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+@EnableFlags(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH)
+public class SearchSuggestionsProviderTest {
+ @Mock
+ private PickerSyncController mMockSyncController;
+ @Mock
+ private SearchState mSearchState;
+ private Context mContext;
+ private SQLiteDatabase mDatabase;
+ private PickerDbFacade mFacade;
+
+ @Before
+ public void setup() {
+ initMocks(this);
+
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ PickerSyncController.setInstance(mMockSyncController);
+
+ final File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ final PickerDatabaseHelper helper = new PickerDatabaseHelper(mContext);
+ mDatabase = helper.getWritableDatabase();
+ mFacade = new PickerDbFacade(
+ mContext, new PickerSyncLockManager(), LOCAL_PICKER_PROVIDER_AUTHORITY);
+ mFacade.setCloudProvider(SearchProvider.AUTHORITY);
+
+ doReturn(LOCAL_PICKER_PROVIDER_AUTHORITY).when(mMockSyncController).getLocalProvider();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController).getCloudProvider();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController)
+ .getCloudProviderOrDefault(any());
+ doReturn(mFacade).when(mMockSyncController).getDbFacade();
+ doReturn(mSearchState).when(mMockSyncController).getSearchState();
+ doReturn(new PickerSyncLockManager()).when(mMockSyncController).getPickerSyncLockManager();
+ }
+
+ @Test
+ public void testCacheSearchSuggestionsNonZeroState() {
+ final SearchSuggestionsQuery query = new SearchSuggestionsQuery(getQueryArgs("x"));
+ final SearchSuggestion suggestion = new SearchSuggestion(
+ "search-text",
+ "media-set-id",
+ "authority",
+ SEARCH_SUGGESTION_ALBUM,
+ null);
+
+ final boolean result = SearchSuggestionsProvider
+ .maybeCacheSearchSuggestions(query, List.of(suggestion));
+
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void testCacheEmptySearchSuggestions() {
+ final SearchSuggestionsQuery query = new SearchSuggestionsQuery(getQueryArgs(""));
+
+ final boolean result = SearchSuggestionsProvider
+ .maybeCacheSearchSuggestions(query, List.of());
+
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void testCacheSearchSuggestions() {
+ final SearchSuggestionsQuery query = new SearchSuggestionsQuery(getQueryArgs(""));
+ final String mediaSetId = "media-set-id";
+ final SearchSuggestion suggestion = new SearchSuggestion(
+ "search-text",
+ mediaSetId,
+ SearchProvider.AUTHORITY,
+ SEARCH_SUGGESTION_ALBUM,
+ null);
+
+ final boolean result = SearchSuggestionsProvider
+ .maybeCacheSearchSuggestions(query, List.of(suggestion));
+
+ assertThat(result).isTrue();
+
+ final List<SearchSuggestion> searchSuggestionsResult =
+ SearchSuggestionsDatabaseUtils.getCachedSuggestions(mDatabase, query);
+
+ assertWithMessage("Search suggestions should not be null")
+ .that(searchSuggestionsResult)
+ .isNotNull();
+
+ assertWithMessage("Search suggestions size is not as expected")
+ .that(searchSuggestionsResult.size())
+ .isEqualTo(1);
+
+ assertWithMessage("Search suggestion media set id is not as expected")
+ .that(searchSuggestionsResult.get(0).getMediaSetId())
+ .isEqualTo(mediaSetId);
+ }
+
+ @Test
+ public void testSuggestionsToCursor() {
+ final String authority = "authority";
+ final SearchSuggestion albumSuggestion = new SearchSuggestion(
+ "album",
+ "album-set-id",
+ authority,
+ SEARCH_SUGGESTION_ALBUM,
+ null);
+ final SearchSuggestion faceSuggestion = new SearchSuggestion(
+ null,
+ "face-set-id",
+ authority,
+ SEARCH_SUGGESTION_FACE,
+ "id");
+ final SearchSuggestion historySuggestion = new SearchSuggestion(
+ "history",
+ null,
+ null,
+ SEARCH_SUGGESTION_HISTORY,
+ null);
+
+ try (Cursor cursor = SearchSuggestionsProvider
+ .suggestionsToCursor(List.of(albumSuggestion, faceSuggestion, historySuggestion))) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(3);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.SearchSuggestionsResponseColumns
+ .MEDIA_SET_ID.getProjection())))
+ .isEqualTo("album-set-id");
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.SearchSuggestionsResponseColumns
+ .MEDIA_SET_ID.getProjection())))
+ .isEqualTo("face-set-id");
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.SearchSuggestionsResponseColumns
+ .MEDIA_SET_ID.getProjection())))
+ .isNull();
+ }
+ }
+
+ @Test
+ public void testSuggestionsFromCloudProvider() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+ doReturn(true).when(mSearchState).isCloudSearchEnabled(any());
+
+ final List<SearchSuggestion> searchSuggestions =
+ SearchSuggestionsProvider.getSuggestionsFromCloudProvider(
+ mContext, new SearchSuggestionsQuery(getQueryArgs("")), null);
+
+ assertWithMessage("Suggestions should not be null")
+ .that(searchSuggestions)
+ .isNotNull();
+
+ assertWithMessage("Suggestions size is not as expected")
+ .that(searchSuggestions.size())
+ .isEqualTo(SearchProvider.DEFAULT_SUGGESTION_RESULTS.getCount());
+
+ SearchProvider.DEFAULT_SUGGESTION_RESULTS.moveToFirst();
+ for (int iterator = 0; iterator < searchSuggestions.size(); iterator++) {
+ assertWithMessage("Media ID is not as expected")
+ .that(searchSuggestions.get(iterator).getMediaSetId())
+ .isEqualTo(SearchProvider.DEFAULT_SUGGESTION_RESULTS.getString(
+ SearchProvider.DEFAULT_SUGGESTION_RESULTS.getColumnIndexOrThrow(
+ PickerSQLConstants.SearchSuggestionsResponseColumns
+ .MEDIA_SET_ID.getProjection())));
+
+ SearchProvider.DEFAULT_SUGGESTION_RESULTS.moveToNext();
+ }
+ }
+
+ @Test
+ public void testSuggestionsFromInactiveCloudProvider() {
+ doReturn(false).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(false).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+ doReturn(true).when(mSearchState).isCloudSearchEnabled(any());
+
+ final List<SearchSuggestion> searchSuggestions =
+ SearchSuggestionsProvider.getSuggestionsFromCloudProvider(
+ mContext, new SearchSuggestionsQuery(getQueryArgs("")), null);
+
+ assertWithMessage("Suggestions should not be null")
+ .that(searchSuggestions)
+ .isNotNull();
+
+ assertWithMessage("Suggestions size is not as expected")
+ .that(searchSuggestions.size())
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void testSuggestionsFromCloudProviderWithSearchDisabled() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+ doReturn(false).when(mSearchState).isCloudSearchEnabled(any());
+
+ final List<SearchSuggestion> searchSuggestions =
+ SearchSuggestionsProvider.getSuggestionsFromCloudProvider(
+ mContext, new SearchSuggestionsQuery(getQueryArgs("")), null);
+
+ assertWithMessage("Suggestions should not be null")
+ .that(searchSuggestions)
+ .isNotNull();
+
+ assertWithMessage("Suggestions size is not as expected")
+ .that(searchSuggestions.size())
+ .isEqualTo(0);
+ }
+
+ private Bundle getQueryArgs(@Nullable String prefix) {
+ return getQueryArgs(prefix, new ArrayList<>(List.of(SearchProvider.AUTHORITY)));
+ }
+
+ private Bundle getQueryArgs(@Nullable String prefix, @NonNull ArrayList<String> providers) {
+ final Bundle bundle = new Bundle();
+ bundle.putString("prefix", prefix);
+ bundle.putStringArrayList("providers", providers);
+ return bundle;
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaGroupCursorUtilsTest.java b/tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaGroupCursorUtilsTest.java
new file mode 100644
index 000000000..5a8920674
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaGroupCursorUtilsTest.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker.v2;
+
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_1;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_1;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_2;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_PROVIDER;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddMediaOperation;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getCloudMediaCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getLocalMediaCursor;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.UserHandle;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.providers.media.cloudproviders.SearchProvider;
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
+import com.android.providers.media.photopicker.v2.sqlite.MediaGroupCursorUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+public class MediaGroupCursorUtilsTest {
+ @Mock
+ private PickerSyncController mMockSyncController;
+ private PickerDbFacade mFacade;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ PickerSyncController.setInstance(mMockSyncController);
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ mFacade = new PickerDbFacade(mContext, new PickerSyncLockManager(), LOCAL_PROVIDER);
+ mFacade.setCloudProvider(SearchProvider.AUTHORITY);
+
+ doReturn(mFacade).when(mMockSyncController).getDbFacade();
+ doReturn(LOCAL_PROVIDER).when(mMockSyncController).getLocalProvider();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController).getCloudProvider();
+ doReturn(SearchProvider.AUTHORITY).when(mMockSyncController)
+ .getCloudProviderOrDefault(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+ }
+
+ @After
+ public void tearDown() {
+ if (mFacade != null) {
+ mFacade.setCloudProvider(null);
+ }
+ }
+
+ @Test
+ public void testGetLocalIdForCloudUri() {
+ final Cursor cursor1 = getLocalMediaCursor(LOCAL_ID_1, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_1, LOCAL_ID_1, 0);
+ assertAddMediaOperation(mFacade, SearchProvider.AUTHORITY, cursor2, 1);
+ final Cursor cursor3 = getLocalMediaCursor(LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor3, 1);
+
+ final List<String> mediaUris = List.of(
+ "content://" + SearchProvider.AUTHORITY + "/" + CLOUD_ID_1,
+ "content://" + LOCAL_PROVIDER + "/" + LOCAL_ID_1
+ );
+
+ final Map<String, String> result = MediaGroupCursorUtils.getLocalIds(mediaUris);
+
+ assertWithMessage("Result map should not be null")
+ .that(result)
+ .isNotNull();
+ assertWithMessage("Result map size is not as expected")
+ .that(result.size())
+ .isEqualTo(1);
+ assertWithMessage("Result map should contain cloud id as key")
+ .that(result.containsKey(CLOUD_ID_1))
+ .isTrue();
+ assertWithMessage("Mapped local id is incorrect")
+ .that(result.get(CLOUD_ID_1))
+ .isEqualTo(LOCAL_ID_1);
+ }
+
+ @Test
+ public void testGetLocalIdForCloudUriNoMatch() {
+ final Cursor cursor1 = getLocalMediaCursor(LOCAL_ID_1, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_1, /* localId */ null, 0);
+ assertAddMediaOperation(mFacade, SearchProvider.AUTHORITY, cursor2, 1);
+ final Cursor cursor3 = getLocalMediaCursor(LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor3, 1);
+
+ final List<String> mediaUris = List.of(
+ "content://" + SearchProvider.AUTHORITY + "/" + CLOUD_ID_1
+ );
+
+ final Map<String, String> result = MediaGroupCursorUtils.getLocalIds(mediaUris);
+
+ assertWithMessage("Result map should not be null")
+ .that(result)
+ .isNotNull();
+ assertWithMessage("Result map size is not as expected")
+ .that(result.size())
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void testGetValidLocalIdForCloudUri() {
+ final Cursor cursor1 = getLocalMediaCursor(LOCAL_ID_1, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_1, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, SearchProvider.AUTHORITY, cursor2, 1);
+
+ final List<String> mediaUris = List.of(
+ "content://" + SearchProvider.AUTHORITY + "/" + CLOUD_ID_1
+ );
+
+ final Map<String, String> result = MediaGroupCursorUtils.getLocalIds(mediaUris);
+
+ assertWithMessage("Result map should not be null")
+ .that(result)
+ .isNotNull();
+ assertWithMessage("Result map size is not as expected")
+ .that(result.size())
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void testGetValidLocalIdForEmptyUriList() {
+ final Cursor cursor1 = getLocalMediaCursor(LOCAL_ID_1, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_1, /* localId */ null, 0);
+ assertAddMediaOperation(mFacade, SearchProvider.AUTHORITY, cursor2, 1);
+ final Cursor cursor3 = getLocalMediaCursor(LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor3, 1);
+
+ final List<String> mediaUris = List.of();
+
+ final Map<String, String> result = MediaGroupCursorUtils.getLocalIds(mediaUris);
+
+ assertWithMessage("Result map should not be null")
+ .that(result)
+ .isNotNull();
+ assertWithMessage("Result map size is not as expected")
+ .that(result.size())
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void testGetLocalUri() {
+ final String cloudAuthority = "cloud.authority";
+ final String cloudMediaId = "cloud-id";
+ final String localMediaId = "local-id";
+ final Uri cloudUri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .encodedAuthority(cloudAuthority)
+ .appendPath("media")
+ .appendPath(cloudMediaId)
+ .build();
+
+ final String localAuthority = PickerSyncController.getInstanceOrThrow().getLocalProvider();
+ final Uri expectedLocalUri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .encodedAuthority(UserHandle.myUserId() + "@" + localAuthority)
+ .appendPath("media")
+ .appendPath(localMediaId)
+ .build();
+
+ final String actualLocalUri = MediaGroupCursorUtils.maybeGetLocalUri(
+ cloudUri.toString(), Map.of(cloudMediaId, localMediaId));
+
+ assertWithMessage("Mapped local uri is not as expected.")
+ .that(actualLocalUri)
+ .isEqualTo(expectedLocalUri.toString());
+ }
+
+ @Test
+ public void testGetLocalUriWithNoMapping() {
+ final String cloudAuthority = "cloud.authority";
+ final String cloudMediaId = "cloud-id";
+ final Uri cloudUri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .encodedAuthority(cloudAuthority)
+ .appendPath("media")
+ .appendPath(cloudMediaId)
+ .build();
+
+ final String actualUri = MediaGroupCursorUtils.maybeGetLocalUri(
+ cloudUri.toString(), Map.of());
+
+ assertWithMessage("Returned uri is not as expected.")
+ .that(actualUri)
+ .isEqualTo(cloudUri.toString());
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsDatabaseUtilTest.java b/tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsDatabaseUtilTest.java
new file mode 100644
index 000000000..f132eb626
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsDatabaseUtilTest.java
@@ -0,0 +1,664 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker.v2.sqlite;
+
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_1;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_2;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_3;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_PROVIDER;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.DATE_TAKEN_MS;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.GENERATION_MODIFIED;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.GIF_IMAGE_MIME_TYPE;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.JPEG_IMAGE_MIME_TYPE;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_1;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_2;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_3;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_4;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_PROVIDER;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.MP4_VIDEO_MIME_TYPE;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.PNG_IMAGE_MIME_TYPE;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.STANDARD_MIME_TYPE_EXTENSION;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddMediaOperation;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getCloudMediaCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getLocalMediaCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getMediaCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.toMediaStoreUri;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.provider.MediaStore;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class MediaInMediaSetsDatabaseUtilTest {
+
+ @Mock
+ private PickerSyncController mMockSyncController;
+ private SQLiteDatabase mDatabase;
+ private Context mContext;
+ private PickerDbFacade mFacade;
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ PickerSyncController.setInstance(mMockSyncController);
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ PickerDatabaseHelper helper = new PickerDatabaseHelper(mContext);
+ mDatabase = helper.getWritableDatabase();
+ mFacade = new PickerDbFacade(mContext, new PickerSyncLockManager(), LOCAL_PROVIDER);
+ mFacade.setCloudProvider(CLOUD_PROVIDER);
+ doReturn(mFacade).when(mMockSyncController).getDbFacade();
+ doReturn(LOCAL_PROVIDER).when(mMockSyncController).getLocalProvider();
+ }
+
+ @After
+ public void teardown() {
+ mDatabase.close();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ }
+
+ @Test
+ public void testQueryLocalMediaInMediaSet() {
+ final Cursor cursor1 = getLocalMediaCursor(LOCAL_ID_1, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getLocalMediaCursor(LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ Long mediaSetPickerId = 1L;
+
+ int cloudRowsInserted = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, mediaSetPickerId)
+ ), CLOUD_PROVIDER
+ );
+ assertEquals(
+ "Number of rows inserted should be equal to the number of items in the cursor,",
+ /*expected*/cloudRowsInserted,
+ /*actual*/1);
+
+ int localRowsInserted = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(LOCAL_ID_1, null, mediaSetPickerId)
+ ), LOCAL_PROVIDER
+ );
+ assertEquals(
+ "Number of rows inserted is incorrect",
+ localRowsInserted,
+ 1);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+ MediaInMediaSetsQuery mediaInMediaSetQuery = new MediaInMediaSetsQuery(
+ extras, mediaSetPickerId);
+ Cursor mediaCursor = MediaInMediaSetsDatabaseUtil.queryMediaInMediaSet(
+ mMockSyncController, mediaInMediaSetQuery, LOCAL_PROVIDER, CLOUD_PROVIDER);
+ assertNotNull(mediaCursor);
+ assertEquals(mediaCursor.getCount(), 2);
+
+ mediaCursor.moveToFirst();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_2);
+ mediaCursor.moveToNext();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_1);
+ mediaCursor.moveToNext();
+ }
+
+ @Test
+ public void testQueryCloudMediaInMediaSet() {
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ Long mediaSetPickerId = 1L;
+
+ final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(null, CLOUD_ID_3, mediaSetPickerId),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, mediaSetPickerId),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, mediaSetPickerId)
+ ), CLOUD_PROVIDER);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+ MediaInMediaSetsQuery mediaInMediaSetQuery = new MediaInMediaSetsQuery(
+ extras, mediaSetPickerId);
+ Cursor mediaCursor = MediaInMediaSetsDatabaseUtil.queryMediaInMediaSet(
+ mMockSyncController, mediaInMediaSetQuery, LOCAL_PROVIDER, CLOUD_PROVIDER);
+ assertNotNull(mediaCursor);
+ assertEquals(mediaCursor.getCount(), 3);
+
+ mediaCursor.moveToFirst();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_3);
+ mediaCursor.moveToNext();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_2);
+ mediaCursor.moveToNext();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_1);
+ }
+
+ @Test
+ public void testQueryMediaInMediaSetForSpecificMediaSetPickerId() {
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ Long mediaSetPickerId1 = 1L;
+ Long mediaSetPickerId2 = 2L;
+
+ final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(null, CLOUD_ID_3, mediaSetPickerId1),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, mediaSetPickerId2),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, mediaSetPickerId2)
+ ), CLOUD_PROVIDER);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+ MediaInMediaSetsQuery mediaInMediaSetQuery = new MediaInMediaSetsQuery(
+ extras, mediaSetPickerId2);
+ Cursor mediaCursor = MediaInMediaSetsDatabaseUtil.queryMediaInMediaSet(
+ mMockSyncController, mediaInMediaSetQuery, LOCAL_PROVIDER, CLOUD_PROVIDER);
+ assertNotNull(mediaCursor);
+ assertEquals(mediaCursor.getCount(), 2);
+
+ mediaCursor.moveToFirst();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_2);
+ mediaCursor.moveToNext();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_1);
+ }
+
+ @Test
+ public void testQueryMediaInMediaSetsSortOrder() {
+ final long dateTaken = 0L;
+
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, dateTaken + 1);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, dateTaken);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, dateTaken - 1);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+ final Cursor cursor4 = getLocalMediaCursor(LOCAL_ID_4, dateTaken);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor4, 1);
+
+ Long mediaSetPickerId = 1L;
+
+ final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(null, CLOUD_ID_3, mediaSetPickerId),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, mediaSetPickerId),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, mediaSetPickerId)
+ ), CLOUD_PROVIDER);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ int localRowsInserted = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(LOCAL_ID_4, null, mediaSetPickerId)
+ ), LOCAL_PROVIDER
+ );
+ assertEquals(
+ "Number of rows inserted is incorrect",
+ localRowsInserted,
+ 1);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+ MediaInMediaSetsQuery mediaInMediaSetQuery = new MediaInMediaSetsQuery(
+ extras, mediaSetPickerId);
+ Cursor mediaCursor = MediaInMediaSetsDatabaseUtil.queryMediaInMediaSet(
+ mMockSyncController, mediaInMediaSetQuery, LOCAL_PROVIDER, CLOUD_PROVIDER);
+ assertNotNull(mediaCursor);
+ assertEquals(mediaCursor.getCount(), 4);
+
+ mediaCursor.moveToFirst();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_1);
+ mediaCursor.moveToNext();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_4);
+ mediaCursor.moveToNext();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_2);
+ mediaCursor.moveToNext();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_3);
+ }
+
+ @Test
+ public void testQueryMediaInMediaSetsPagination() {
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ Long mediaSetPickerId = 1L;
+
+ final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(null, CLOUD_ID_3, mediaSetPickerId),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, mediaSetPickerId),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, mediaSetPickerId)
+ ), CLOUD_PROVIDER);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 2);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+ MediaInMediaSetsQuery mediaInMediaSetQuery = new MediaInMediaSetsQuery(
+ extras, mediaSetPickerId);
+ Cursor mediaCursor = MediaInMediaSetsDatabaseUtil.queryMediaInMediaSet(
+ mMockSyncController, mediaInMediaSetQuery, LOCAL_PROVIDER, CLOUD_PROVIDER);
+ assertNotNull(mediaCursor);
+ assertEquals(mediaCursor.getCount(), 2);
+
+ mediaCursor.moveToFirst();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_3);
+ mediaCursor.moveToNext();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_2);
+ mediaCursor.moveToNext();
+ }
+
+ @Test
+ public void testQueryMediaInMediaSetsMimeTypeFilter() {
+ final Cursor cursor1 = getMediaCursor(CLOUD_ID_1, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 1, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getMediaCursor(CLOUD_ID_2, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ toMediaStoreUri(LOCAL_ID_2), /* sizeBytes */ 1,
+ PNG_IMAGE_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getMediaCursor(CLOUD_ID_3, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ toMediaStoreUri(LOCAL_ID_3), /* sizeBytes */ 1,
+ GIF_IMAGE_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+ final Cursor cursor4 = getMediaCursor(LOCAL_ID_4, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ toMediaStoreUri(LOCAL_ID_4), /* sizeBytes */ 1,
+ JPEG_IMAGE_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor4, 1);
+
+ Long mediaSetPickerId = 1L;
+
+ final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(null, CLOUD_ID_3, mediaSetPickerId),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, mediaSetPickerId),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, mediaSetPickerId)
+ ), CLOUD_PROVIDER);
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ int localRowsInserted = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(LOCAL_ID_4, null, mediaSetPickerId)
+ ), LOCAL_PROVIDER
+ );
+ assertEquals(
+ "Number of rows inserted is incorrect",
+ localRowsInserted,
+ 1);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+ extras.putStringArrayList("mime_types", new ArrayList<>(List.of("video/mp4", "image/gif")));
+ MediaInMediaSetsQuery mediaInMediaSetQuery = new MediaInMediaSetsQuery(
+ extras, mediaSetPickerId);
+ Cursor mediaCursor = MediaInMediaSetsDatabaseUtil.queryMediaInMediaSet(
+ mMockSyncController, mediaInMediaSetQuery, LOCAL_PROVIDER, CLOUD_PROVIDER);
+ assertNotNull(mediaCursor);
+ assertEquals(mediaCursor.getCount(), 2);
+
+ mediaCursor.moveToFirst();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_3);
+ mediaCursor.moveToNext();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_1);
+ mediaCursor.moveToNext();
+ }
+
+ @Test
+ public void testQueryMediaInMediaSetsLocalProviderFilter() {
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+ final Cursor cursor4 = getLocalMediaCursor(LOCAL_ID_4, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor4, 1);
+
+ Long mediaSetPickerId = 1L;
+
+ final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(null, CLOUD_ID_3, mediaSetPickerId),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, mediaSetPickerId),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, mediaSetPickerId)
+ ), CLOUD_PROVIDER);
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ int localRowsInserted = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(LOCAL_ID_4, null, mediaSetPickerId)
+ ), LOCAL_PROVIDER
+ );
+ assertEquals(
+ "Number of rows inserted is incorrect",
+ localRowsInserted,
+ 1);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ MediaInMediaSetsQuery mediaInMediaSetQuery = new MediaInMediaSetsQuery(
+ extras, mediaSetPickerId);
+ Cursor mediaCursor = MediaInMediaSetsDatabaseUtil.queryMediaInMediaSet(
+ mMockSyncController, mediaInMediaSetQuery, LOCAL_PROVIDER, null);
+ assertNotNull(mediaCursor);
+ assertEquals(mediaCursor.getCount(), 1);
+
+ mediaCursor.moveToFirst();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_4);
+ }
+
+ @Test
+ public void testQueryMediaInMediaSetsCloudProviderFilter() {
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+ final Cursor cursor4 = getLocalMediaCursor(LOCAL_ID_4, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor4, 1);
+
+ Long mediaSetPickerId = 1L;
+
+ final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(null, CLOUD_ID_3, mediaSetPickerId),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, mediaSetPickerId),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, mediaSetPickerId)
+ ), CLOUD_PROVIDER);
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ int localRowsInserted = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(LOCAL_ID_4, null, mediaSetPickerId)
+ ), LOCAL_PROVIDER
+ );
+ assertEquals(
+ "Number of rows inserted is incorrect",
+ localRowsInserted,
+ 1);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ MediaInMediaSetsQuery mediaInMediaSetQuery = new MediaInMediaSetsQuery(
+ extras, mediaSetPickerId);
+ Cursor mediaCursor = MediaInMediaSetsDatabaseUtil.queryMediaInMediaSet(
+ mMockSyncController, mediaInMediaSetQuery, null, CLOUD_PROVIDER);
+ assertNotNull(mediaCursor);
+ assertEquals(mediaCursor.getCount(), 3);
+
+ mediaCursor.moveToFirst();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_3);
+ mediaCursor.moveToNext();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_2);
+ mediaCursor.moveToNext();
+
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(mediaCursor.getString(mediaCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_1);
+ mediaCursor.moveToNext();
+ }
+
+ @Test
+ public void testCacheMediaInMediaSet() {
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getLocalMediaCursor(LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ Long mediaSetPickerId = 1L;
+
+ int cloudRowsInserted = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(null, CLOUD_ID_1, mediaSetPickerId),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, mediaSetPickerId)
+ ), CLOUD_PROVIDER
+ );
+ assertEquals(
+ "Number of rows inserted is incorrect",
+ cloudRowsInserted,
+ 2);
+
+ // Try to insert the item with same LOCAL_ID as before
+ int localRowsInserted = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(LOCAL_ID_2, null, mediaSetPickerId)
+ ), LOCAL_PROVIDER
+ );
+ assertEquals(
+ "Number of rows inserted is incorrect",
+ localRowsInserted,
+ 1);
+ }
+
+ @Test
+ public void testClearMediaInMediaSetCache() {
+ // Insert data
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ Long mediaSetPickerId = 1L;
+
+ final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(null, CLOUD_ID_3, mediaSetPickerId),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, mediaSetPickerId),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, mediaSetPickerId)
+ ), CLOUD_PROVIDER);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ // Clear the data
+ MediaInMediaSetsDatabaseUtil.clearMediaInMediaSetsCache(mDatabase);
+
+ // Retrieved cursor should be empty
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+ MediaInMediaSetsQuery mediaInMediaSetQuery = new MediaInMediaSetsQuery(
+ extras, mediaSetPickerId);
+ Cursor mediaCursor = MediaInMediaSetsDatabaseUtil.queryMediaInMediaSet(
+ mMockSyncController, mediaInMediaSetQuery, LOCAL_PROVIDER, CLOUD_PROVIDER);
+ assertNotNull(mediaCursor);
+ assertEquals(/*expected*/0, /*actual*/ mediaCursor.getCount());
+
+ }
+
+ private ContentValues getContentValues(
+ String localId, String cloudId, Long mediaSetPickerId) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(
+ PickerSQLConstants.MediaInMediaSetsTableColumns.CLOUD_ID.getColumnName(), cloudId);
+ contentValues.put(
+ PickerSQLConstants.MediaInMediaSetsTableColumns.LOCAL_ID.getColumnName(), localId);
+ contentValues.put(
+ PickerSQLConstants.MediaInMediaSetsTableColumns.MEDIA_SETS_PICKER_ID
+ .getColumnName(),
+ mediaSetPickerId);
+ return contentValues;
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaSetsDatabaseUtilsTest.java b/tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaSetsDatabaseUtilsTest.java
new file mode 100644
index 000000000..72ccb7795
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaSetsDatabaseUtilsTest.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker.v2.sqlite;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.provider.CloudMediaProviderContract;
+import android.util.Pair;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.v2.model.MediaSetsSyncRequestParams;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class MediaSetsDatabaseUtilsTest {
+ private SQLiteDatabase mDatabase;
+ private Context mContext;
+ private final String mMediaSetId = "mediaSetId";
+ private final String mCategoryId = "categoryId";
+ private final String mAuthority = "auth";
+ private final String mMimeType = "img";
+ private final String mDisplayName = "name";
+ private final String mCoverId = "id";
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ PickerDatabaseHelper helper = new PickerDatabaseHelper(mContext);
+ mDatabase = helper.getWritableDatabase();
+ }
+
+ @After
+ public void teardown() {
+ mDatabase.close();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ }
+
+ @Test
+ public void testInsertMediaSetMetadataIntoMediaSetsTable() {
+ Cursor c = getCursorForMediaSetInsertionTest();
+ List<String> mimeTypes = new ArrayList<>();
+ mimeTypes.add(mMimeType);
+
+ int mediaSetsInserted = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, mCategoryId, mAuthority, mimeTypes);
+ assertEquals("Count of inserted media sets should be equal to the cursor size",
+ /*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
+ }
+
+ @Test
+ public void testInsertMediaSetMetadataIntoMediaTableMimeTypeFilter() {
+ Cursor c = getCursorForMediaSetInsertionTest();
+ List<String> firstMimeTypeFilter = new ArrayList<>();
+ firstMimeTypeFilter.add("image/*");
+ firstMimeTypeFilter.add("video/*");
+
+ int firstInsertionCount = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, mCategoryId, mAuthority, firstMimeTypeFilter);
+ assertEquals("Count of inserted media sets should be equal to the cursor size",
+ /*expected*/ c.getCount(), /*actual*/ firstInsertionCount);
+
+ // Reversing the order of the mimeTypeFilter.
+ // It should still be treated the same and should not be reinserted
+ List<String> secondMimeTypeFilter = new ArrayList<>();
+ secondMimeTypeFilter.add("video/*");
+ secondMimeTypeFilter.add("image/*");
+
+ int secondInsertionCount = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, mCategoryId, mAuthority, secondMimeTypeFilter);
+ assertEquals("MediaSet metadata with same mimetype filters should not be inserted "
+ + "again",
+ /*expected*/ 0, /*actual*/ secondInsertionCount);
+
+ }
+
+ @Test
+ public void testInsertMediaSetMetadataWhenMediaSetIdIsNull() {
+ List<String> mimeTypes = new ArrayList<>();
+ mimeTypes.add(mMimeType);
+
+ String[] columns = new String[]{
+ CloudMediaProviderContract.MediaSetColumns.ID,
+ CloudMediaProviderContract.MediaSetColumns.DISPLAY_NAME,
+ CloudMediaProviderContract.MediaSetColumns.MEDIA_COVER_ID
+ };
+
+ MatrixCursor cursor = new MatrixCursor(columns);
+ cursor.addRow(new Object[] { null, mDisplayName, mCoverId });
+
+ int mediaSetsInserted = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, cursor, mCategoryId, mAuthority, mimeTypes);
+ assertEquals("Count of inserted media sets should be 0 when the mediaSetId is null",
+ /*expected*/0, /*actual*/ mediaSetsInserted);
+ }
+
+ @Test
+ public void testGetMediaSetMetadataForCategory() {
+ Cursor c = getCursorForMediaSetInsertionTest();
+ List<String> mimeTypes = new ArrayList<>();
+ mimeTypes.add(mMimeType);
+
+ long insertResult = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, mCategoryId, mAuthority, mimeTypes);
+ // Assert successful insertion
+ assertWithMessage("MediaSet metadata insertion failed")
+ .that(insertResult)
+ .isAtLeast(/* expected min row id */ 0);
+ Bundle extras = new Bundle();
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, mAuthority);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, mCategoryId);
+ extras.putStringArrayList(
+ MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ new ArrayList<String>(mimeTypes));
+ MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
+
+ Cursor mediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
+ mDatabase, requestParams);
+ assertNotNull(mediaSetCursor);
+ assertWithMessage("Cursor size should be greater than 0. Expected size: 1")
+ .that(mediaSetCursor.getCount())
+ .isEqualTo(1);
+ if (mediaSetCursor.moveToFirst()) {
+ int mediaSetIdIndex = mediaSetCursor.getColumnIndex(PickerSQLConstants
+ .MediaSetsTableColumns.MEDIA_SET_ID.getColumnName());
+ String retrievedMediaSetId = mediaSetCursor.getString(mediaSetIdIndex);
+ assertEquals(mMediaSetId, retrievedMediaSetId);
+ }
+ }
+
+ @Test
+ public void testUpdateAndGetMediaInMediaSetResumeKey() {
+ Cursor c = getCursorForMediaSetInsertionTest();
+ List<String> mimeTypes = new ArrayList<>();
+ mimeTypes.add(mMimeType);
+
+ long mediaSetsInserted = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, mCategoryId, mAuthority, mimeTypes);
+ // Assert successful insertion
+ assertEquals("Count of inserted media sets should be equal to the cursor size",
+ /*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
+ Bundle extras = new Bundle();
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, mAuthority);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, mCategoryId);
+ extras.putStringArrayList(
+ MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ new ArrayList<String>(mimeTypes));
+ MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
+ Cursor fetchMediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
+ mDatabase, requestParams);
+ Long mediaSetPickerId = 1L;
+ if (fetchMediaSetCursor.moveToFirst()) {
+ mediaSetPickerId = fetchMediaSetCursor.getLong(
+ fetchMediaSetCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaSetsTableColumns.PICKER_ID.getColumnName()));
+ }
+
+ String resumeKey = "resume";
+ MediaSetsDatabaseUtil.updateMediaInMediaSetSyncResumeKey(
+ mDatabase, mediaSetPickerId, resumeKey);
+ String retrievedMediaSetResumeKey = MediaSetsDatabaseUtil.getMediaResumeKey(
+ mDatabase, mediaSetPickerId);
+ assertNotNull(retrievedMediaSetResumeKey);
+ assertWithMessage("Retrieved mediaSetResumeKey did not match")
+ .that(retrievedMediaSetResumeKey)
+ .isEqualTo(resumeKey);
+ }
+
+ @Test
+ public void testGetMediaSetIdAndMimeTypesUsingMediaSetPickerId() {
+ Cursor c = getCursorForMediaSetInsertionTest();
+ List<String> mimeTypes = new ArrayList<>();
+ mimeTypes.add(mMimeType);
+
+ long mediaSetsInserted = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, mCategoryId, mAuthority, mimeTypes);
+ // Assert successful insertion
+ assertEquals("Count of inserted media sets should be equal to the cursor size",
+ /*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
+ Bundle extras = new Bundle();
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, mAuthority);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, mCategoryId);
+ extras.putStringArrayList(
+ MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ new ArrayList<String>(mimeTypes));
+ MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
+ Cursor fetchMediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
+ mDatabase, requestParams);
+ Long mediaSetPickerId = 1L;
+ if (fetchMediaSetCursor.moveToFirst()) {
+ mediaSetPickerId = fetchMediaSetCursor.getLong(
+ fetchMediaSetCursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaSetsTableColumns.PICKER_ID.getColumnName()));
+ }
+
+ Pair<String, String[]> retrievedData = MediaSetsDatabaseUtil
+ .getMediaSetIdAndMimeType(mDatabase, mediaSetPickerId);
+ assertEquals(/*expected*/retrievedData.first, /*actual*/mMediaSetId);
+ assertTrue(Arrays.toString(retrievedData.second).contains(mMimeType));
+ }
+
+ @Test
+ public void testClearMediaSetsCache() {
+ // Insert metadata into the table
+ Cursor c = getCursorForMediaSetInsertionTest();
+ List<String> mimeTypes = new ArrayList<>();
+ mimeTypes.add(mMimeType);
+
+ int mediaSetsInserted = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, mCategoryId, mAuthority, mimeTypes);
+ assertEquals("Count of inserted media sets should be equal to the cursor size",
+ /*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
+
+ // Delete the inserted items
+ MediaSetsDatabaseUtil.clearMediaSetsCache(mDatabase);
+
+ // Retrieved cursor should be empty
+ Bundle extras = new Bundle();
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, mAuthority);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, mCategoryId);
+ extras.putStringArrayList(
+ MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ new ArrayList<String>(mimeTypes));
+ MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
+
+ Cursor mediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
+ mDatabase, requestParams);
+ assertNotNull(mediaSetCursor);
+ assertEquals(/*expected*/ 0, /*actual*/ mediaSetCursor.getCount());
+ }
+
+ private Cursor getCursorForMediaSetInsertionTest() {
+ String[] columns = new String[]{
+ CloudMediaProviderContract.MediaSetColumns.ID,
+ CloudMediaProviderContract.MediaSetColumns.DISPLAY_NAME,
+ CloudMediaProviderContract.MediaSetColumns.MEDIA_COVER_ID
+ };
+
+ MatrixCursor cursor = new MatrixCursor(columns);
+ cursor.addRow(new Object[] { mMediaSetId, mDisplayName, mCoverId });
+
+ return cursor;
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/v2/sqlite/SearchRequestDatabaseUtilTest.java b/tests/src/com/android/providers/media/photopicker/v2/sqlite/SearchRequestDatabaseUtilTest.java
new file mode 100644
index 000000000..2ddbd7b99
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/v2/sqlite/SearchRequestDatabaseUtilTest.java
@@ -0,0 +1,572 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker.v2.sqlite;
+
+import static android.provider.CloudMediaProviderContract.SEARCH_SUGGESTION_LOCATION;
+import static android.provider.CloudMediaProviderContract.SEARCH_SUGGESTION_TEXT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.v2.model.SearchRequest;
+import com.android.providers.media.photopicker.v2.model.SearchSuggestionRequest;
+import com.android.providers.media.photopicker.v2.model.SearchTextRequest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.List;
+
+public class SearchRequestDatabaseUtilTest {
+ private SQLiteDatabase mDatabase;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ PickerDatabaseHelper helper = new PickerDatabaseHelper(mContext);
+ mDatabase = helper.getWritableDatabase();
+ }
+
+ @After
+ public void teardown() {
+ mDatabase.close();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ }
+
+ @Test
+ public void testInsertSearchTextRequest() {
+ final SearchTextRequest searchRequest = new SearchTextRequest(
+ /* mimeTypes */ null,
+ "mountains"
+ );
+
+ final long firstInsertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest);
+ assertWithMessage("Insert search request failed")
+ .that(firstInsertResult)
+ .isAtLeast(/* minimum row id */ 0);
+
+ final long secondInsertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest);
+ assertWithMessage("Second insert for same search request should fail silently")
+ .that(secondInsertResult)
+ .isEqualTo(/* failed to insert row on constraint conflict */ -1);
+ }
+
+ @Test
+ public void testInsertSearchSuggestionRequest() {
+ final SearchSuggestionRequest suggestionRequest = new SearchSuggestionRequest(
+ /* mimeTypes */ null,
+ "mountains",
+ "media-set-id",
+ "authority",
+ SEARCH_SUGGESTION_TEXT
+ );
+
+ final long firstInsertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, suggestionRequest);
+ assertWithMessage("Insert search request failed")
+ .that(firstInsertResult)
+ .isAtLeast(/* minimum row id */ 0);
+
+ final long secondInsertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, suggestionRequest);
+ assertWithMessage("Second insert for same search request should fail silently")
+ .that(secondInsertResult)
+ .isEqualTo(/* failed to insert row on constraint conflict */ -1);
+ }
+
+ @Test
+ public void testInsertSearchRequestsWithSameQuery() {
+ // Insert a search text request with "mountains" search text. This insert should be
+ // successful.
+ final SearchTextRequest searchRequest1 = new SearchTextRequest(
+ /* mimeTypes */ null,
+ "mountains"
+ );
+
+ final long firstInsertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest1);
+ assertWithMessage("Insert search request failed")
+ .that(firstInsertResult)
+ .isAtLeast(/* minimum row id */ 0);
+
+ // Insert search suggestion request with "mountains" search text. This insert should be
+ // successful.
+ final SearchSuggestionRequest searchRequest2 = new SearchSuggestionRequest(
+ /* mimeTypes */ null,
+ "mountains",
+ "media-set-id",
+ "authority",
+ SEARCH_SUGGESTION_TEXT
+ );
+
+ final long secondInsertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest2);
+ assertWithMessage("Insert search request failed")
+ .that(secondInsertResult)
+ .isAtLeast(/* minimum row id */ 0);
+
+ // Insert search text request with "Mountains" search text. This insert should be
+ // successful since search text is text sensitive.
+ final SearchTextRequest searchRequest3 = new SearchTextRequest(
+ /* mimeTypes */ null,
+ "Mountains"
+ );
+
+ final long thirdInsertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest3);
+ assertWithMessage("Insert search request failed")
+ .that(thirdInsertResult)
+ .isAtLeast(/* minimum row id */ 0);
+
+ // Insert search text request with "mountains" search text but a different media set id
+ // than before. This insert should be successful since search text is text sensitive.
+ final SearchSuggestionRequest searchRequest4 = new SearchSuggestionRequest(
+ /* mimeTypes */ null,
+ "mountains",
+ "different-media-set-id",
+ "authority",
+ SEARCH_SUGGESTION_TEXT
+ );
+
+ final long fourthInsertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest4);
+ assertWithMessage("Insert search request failed")
+ .that(fourthInsertResult)
+ .isAtLeast(/* minimum row id */ 0);
+ }
+
+ @Test
+ public void testMimeTypeUniqueConstraintSearchRequest() {
+ SearchTextRequest request = new SearchTextRequest(
+ /* mimeTypes */ List.of("image/*", "video/*", "image/gif"),
+ /* searchText */ "volcano"
+ );
+
+ final long firstInsertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, request);
+ assertWithMessage("Insert search request failed")
+ .that(firstInsertResult)
+ .isAtLeast(/* minimum row id */ 0);
+
+ request = new SearchTextRequest(
+ /* mimeTypes */ List.of("image/gif", "video/*", "image/*"),
+ /* searchText */ "volcano"
+ );
+ final long secondInsertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, request);
+ assertWithMessage("Second insert for same search request should fail silently")
+ .that(secondInsertResult)
+ .isEqualTo(/* failed to insert row on constraint conflict */ -1);
+
+ request = new SearchTextRequest(
+ /* mimeTypes */ List.of("image/GIF", "Video/*", "IMAGE/*"),
+ /* searchText */ "volcano"
+ );
+ final long thirdInsertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, request);
+ assertWithMessage("Third insert for same search request should fail silently")
+ .that(thirdInsertResult)
+ .isEqualTo(/* failed to insert row on constraint conflict */ -1);
+ }
+
+ @Test
+ public void testGetSearchRequestID() {
+ SearchTextRequest searchRequest = new SearchTextRequest(
+ /* mimeTypes */ null,
+ "mountains"
+ );
+ assertWithMessage("Search request should not exist in database yet")
+ .that(SearchRequestDatabaseUtil.getSearchRequestID(mDatabase, searchRequest))
+ .isEqualTo(/* expectedRequestID */ -1);
+
+
+ final long firstInsertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest);
+ assertWithMessage("Insert search request failed")
+ .that(firstInsertResult)
+ .isAtLeast(/* minimum row id */ 0);
+
+ assertWithMessage("Search request ID should exist in DB")
+ .that(SearchRequestDatabaseUtil.getSearchRequestID(mDatabase, searchRequest))
+ .isAtLeast(0);
+
+ searchRequest = new SearchTextRequest(
+ /* mimeTypes */ List.of("image/*"),
+ "mountains"
+ );
+ assertWithMessage("Search request should not exist in database for the given mime types")
+ .that(SearchRequestDatabaseUtil.getSearchRequestID(mDatabase, searchRequest))
+ .isEqualTo(/* expectedRequestID */ -1);
+ }
+
+ @Test
+ public void testGetSearchTextRequestDetails() {
+ final List<String> mimeTypes = List.of("video/mp4", "image/*", "image/gif");
+ final String searchText = "mountains";
+ final String cloudResumeKey = "RANDOM_RESUME_KEY_CLOUD";
+ final String localResumeKey = "RANDOM_RESUME_KEY_LOCAL";
+ final String cloudAuthority = "com.random.cloud.authority";
+ final String localAuthority = "com.random.local.authority";
+ SearchTextRequest searchRequest = new SearchTextRequest(
+ mimeTypes,
+ searchText,
+ localResumeKey,
+ localAuthority,
+ cloudResumeKey,
+ cloudAuthority
+ );
+
+ // Insert a search request
+ final long insertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest);
+ assertWithMessage("Insert search request failed")
+ .that(insertResult)
+ .isAtLeast(/* minimum row id */ 0);
+
+ // Get search request ID
+ final int searchRequestID =
+ SearchRequestDatabaseUtil.getSearchRequestID(mDatabase, searchRequest);
+ assertWithMessage("Search request ID should exist in DB")
+ .that(searchRequestID)
+ .isAtLeast(0);
+
+ // Fetch search details from search request ID
+ final SearchRequest resultSearchRequest =
+ SearchRequestDatabaseUtil.getSearchRequestDetails(mDatabase, searchRequestID);
+ assertWithMessage("Unable to fetch search details from the database")
+ .that(resultSearchRequest)
+ .isNotNull();
+ assertWithMessage("Search request should be an instance of SearchTextRequest")
+ .that(resultSearchRequest)
+ .isInstanceOf(SearchTextRequest.class);
+ assertWithMessage("Search request mime types are not as expected")
+ .that(resultSearchRequest.getMimeTypes())
+ .containsExactlyElementsIn(mimeTypes);
+ assertWithMessage("Search request cloud resume key is not as expected")
+ .that(resultSearchRequest.getCloudSyncResumeKey())
+ .isEqualTo(cloudResumeKey);
+ assertWithMessage("Search request cloud authority is not as expected")
+ .that(resultSearchRequest.getCloudAuthority())
+ .isEqualTo(cloudAuthority);
+ assertWithMessage("Search request local resume key is not as expected")
+ .that(resultSearchRequest.getLocalSyncResumeKey())
+ .isEqualTo(localResumeKey);
+ assertWithMessage("Search request local authority is not as expected")
+ .that(resultSearchRequest.getLocalAuthority())
+ .isEqualTo(localAuthority);
+
+ final SearchTextRequest resultSearchTextRequest = (SearchTextRequest) resultSearchRequest;
+ assertWithMessage("Search request search text is not as expected")
+ .that(resultSearchTextRequest.getSearchText())
+ .isEqualTo(searchText);
+ }
+
+ @Test
+ public void testGetSearchSuggestionRequestDetails() {
+ final List<String> mimeTypes = List.of("video/mp4", "image/*", "image/gif");
+ final String cloudResumeKey = "RANDOM_RESUME_KEY_CLOUD";
+ final String localResumeKey = "RANDOM_RESUME_KEY_LOCAL";
+ final String mediaSetID = "MEDIA-SET-ID";
+ final String cloudAuthority = "com.random.cloud.authority";
+ final String localAuthority = "com.random.local.authority";
+ final String suggestionType = SEARCH_SUGGESTION_LOCATION;
+ SearchSuggestionRequest searchRequest = new SearchSuggestionRequest(
+ mimeTypes,
+ null,
+ mediaSetID,
+ cloudAuthority,
+ suggestionType,
+ localResumeKey,
+ localAuthority,
+ cloudResumeKey,
+ cloudAuthority
+ );
+
+ // Insert a search request
+ final long insertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest);
+ assertWithMessage("Insert search request failed")
+ .that(insertResult)
+ .isAtLeast(/* minimum row id */ 0);
+
+ // Get search request ID
+ final int searchRequestID =
+ SearchRequestDatabaseUtil.getSearchRequestID(mDatabase, searchRequest);
+ assertWithMessage("Search request ID should exist in DB")
+ .that(searchRequestID)
+ .isAtLeast(0);
+
+ // Fetch search details from search request ID
+ final SearchRequest resultSearchRequest =
+ SearchRequestDatabaseUtil.getSearchRequestDetails(mDatabase, searchRequestID);
+ assertWithMessage("Unable to fetch search details from the database")
+ .that(resultSearchRequest)
+ .isNotNull();
+ assertWithMessage("Search request should be an instance of SearchSuggestionRequest")
+ .that(resultSearchRequest)
+ .isInstanceOf(SearchSuggestionRequest.class);
+ assertWithMessage("Search request mime types are not as expected")
+ .that(resultSearchRequest.getMimeTypes())
+ .containsExactlyElementsIn(mimeTypes);
+ assertWithMessage("Search request cloud resume key is not as expected")
+ .that(resultSearchRequest.getCloudSyncResumeKey())
+ .isEqualTo(cloudResumeKey);
+ assertWithMessage("Search request cloud authority is not as expected")
+ .that(resultSearchRequest.getCloudAuthority())
+ .isEqualTo(cloudAuthority);
+ assertWithMessage("Search request local resume key is not as expected")
+ .that(resultSearchRequest.getLocalSyncResumeKey())
+ .isEqualTo(localResumeKey);
+ assertWithMessage("Search request local authority is not as expected")
+ .that(resultSearchRequest.getLocalAuthority())
+ .isEqualTo(localAuthority);
+
+ final SearchSuggestionRequest resultSearchSuggestionRequest =
+ (SearchSuggestionRequest) resultSearchRequest;
+ assertWithMessage("Search request search text is not as expected")
+ .that(resultSearchSuggestionRequest.getSearchSuggestion().getSearchText())
+ .isNull();
+ assertWithMessage("Search request search text is not as expected")
+ .that(resultSearchSuggestionRequest.getSearchSuggestion().getMediaSetId())
+ .isEqualTo(mediaSetID);
+ assertWithMessage("Search request search text is not as expected")
+ .that(resultSearchSuggestionRequest.getSearchSuggestion().getAuthority())
+ .isEqualTo(cloudAuthority);
+ assertWithMessage("Search request search text is not as expected")
+ .that(resultSearchSuggestionRequest.getSearchSuggestion().getSearchSuggestionType())
+ .isEqualTo(suggestionType);
+ }
+
+ @Test
+ public void testResumeKeyUpdate() {
+ final List<String> mimeTypes = List.of("video/mp4", "image/*", "image/gif");
+ final String mediaSetID = "MEDIA-SET-ID";
+ final String authority = "com.random.authority";
+ final String suggestionType = SEARCH_SUGGESTION_LOCATION;
+ SearchSuggestionRequest searchRequest = new SearchSuggestionRequest(
+ mimeTypes,
+ null,
+ mediaSetID,
+ authority,
+ suggestionType
+ );
+
+ // Insert a search request
+ final long insertResult =
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest);
+ assertWithMessage("Insert search request failed")
+ .that(insertResult)
+ .isAtLeast(/* minimum row id */ 0);
+
+ // Get search request ID
+ final int searchRequestID =
+ SearchRequestDatabaseUtil.getSearchRequestID(mDatabase, searchRequest);
+ assertWithMessage("Search request ID should exist in DB")
+ .that(searchRequestID)
+ .isAtLeast(0);
+
+ // Fetch search details from search request ID
+ final SearchRequest savedSearchRequest =
+ SearchRequestDatabaseUtil.getSearchRequestDetails(mDatabase, searchRequestID);
+ assertWithMessage("Search request is null")
+ .that(savedSearchRequest)
+ .isNotNull();
+ assertWithMessage("Initial search request cloud resume key is not null")
+ .that(savedSearchRequest.getCloudSyncResumeKey())
+ .isNull();
+ assertWithMessage("Initial search request cloud authority is not null")
+ .that(savedSearchRequest.getCloudAuthority())
+ .isNull();
+ assertWithMessage("Initial search request local sync resume key is not null")
+ .that(savedSearchRequest.getLocalSyncResumeKey())
+ .isNull();
+ assertWithMessage("Initial search request local authority is not null")
+ .that(savedSearchRequest.getLocalAuthority())
+ .isNull();
+
+ // Update cloud resume key and save
+ final String cloudResumeKey = "CLOUD_RESUME_KEY";
+ final String cloudAuthority = "CLOUD_AUTHORITY";
+ SearchRequestDatabaseUtil.updateResumeKey(mDatabase, searchRequestID, cloudResumeKey,
+ cloudAuthority, /* isLocal */ false);
+
+ // Fetch updated search details from search request ID
+ final SearchRequest updatedSearchRequest1 =
+ SearchRequestDatabaseUtil.getSearchRequestDetails(mDatabase, searchRequestID);
+ assertWithMessage("Search request is null")
+ .that(updatedSearchRequest1)
+ .isNotNull();
+ assertWithMessage("Search request cloud resume key is not as expected")
+ .that(updatedSearchRequest1.getCloudSyncResumeKey())
+ .isEqualTo(cloudResumeKey);
+ assertWithMessage("Search request cloud authority is not as expected")
+ .that(updatedSearchRequest1.getCloudAuthority())
+ .isEqualTo(cloudAuthority);
+ assertWithMessage("Search request local sync resume key is not null")
+ .that(updatedSearchRequest1.getLocalSyncResumeKey())
+ .isNull();
+ assertWithMessage("Initial search request local authority is not null")
+ .that(updatedSearchRequest1.getLocalAuthority())
+ .isNull();
+
+ // Update local resume key and save
+ final String localResumeKey = "LOCAL_RESUME_KEY";
+ final String localAuthority = "LOCAL_AUTHORITY";
+ SearchRequestDatabaseUtil.updateResumeKey(mDatabase, searchRequestID, localResumeKey,
+ localAuthority, /* isLocal */ true);
+
+ // Clear cloud resume key
+ SearchRequestDatabaseUtil.clearSyncResumeInfo(mDatabase, List.of(searchRequestID),
+ /* isLocal */ false);
+
+ // Fetch updated search details from search request ID
+ final SearchRequest updatedSearchRequest2 =
+ SearchRequestDatabaseUtil.getSearchRequestDetails(mDatabase, searchRequestID);
+ assertWithMessage("Search request is null")
+ .that(updatedSearchRequest2)
+ .isNotNull();
+ assertWithMessage("Search request local resume key is not as expected")
+ .that(updatedSearchRequest2.getLocalSyncResumeKey())
+ .isEqualTo(localResumeKey);
+ assertWithMessage("Search request local authority is not as expected")
+ .that(updatedSearchRequest2.getLocalAuthority())
+ .isEqualTo(localAuthority);
+ assertWithMessage("Search request cloud sync resume key is not null")
+ .that(updatedSearchRequest2.getCloudSyncResumeKey())
+ .isNull();
+ assertWithMessage("Initial search request cloud authority is not null")
+ .that(updatedSearchRequest2.getCloudAuthority())
+ .isNull();
+ }
+
+ @Test
+ public void testGetSyncedRequestIds() {
+ // Insert a search request in the database.
+ final SearchTextRequest searchRequest1 = new SearchTextRequest(
+ /* mimeTypes */ null,
+ "mountains"
+ );
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest1);
+
+ // Get search request ID
+ final int searchRequestID1 =
+ SearchRequestDatabaseUtil.getSearchRequestID(mDatabase, searchRequest1);
+ assertWithMessage("Search request ID should exist in DB")
+ .that(searchRequestID1)
+ .isAtLeast(0);
+
+ // Update local resume key and save
+ final String localResumeKey = "LOCAL_RESUME_KEY";
+ final String localAuthority = "LOCAL_AUTHORITY";
+ SearchRequestDatabaseUtil.updateResumeKey(mDatabase, searchRequestID1, localResumeKey,
+ localAuthority, /* isLocal */ true);
+
+ // Insert another search request in the database.
+ final SearchTextRequest searchRequest2 = new SearchTextRequest(
+ /* mimeTypes */ null,
+ "volcano"
+ );
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest2);
+
+ // Get search request ID
+ final int searchRequestID2 =
+ SearchRequestDatabaseUtil.getSearchRequestID(mDatabase, searchRequest2);
+ assertWithMessage("Search request ID should exist in DB")
+ .that(searchRequestID2)
+ .isAtLeast(0);
+
+ // Update cloud resume key and save
+ final String cloudResumeKey = "CLOUD_RESUME_KEY";
+ final String cloudAuthority = "CLOUD_AUTHORITY";
+ SearchRequestDatabaseUtil.updateResumeKey(mDatabase, searchRequestID2, cloudResumeKey,
+ cloudAuthority, /* isLocal */ false);
+
+ final List<Integer> localSyncedSearchRequests =
+ SearchRequestDatabaseUtil.getSyncedRequestIds(
+ mDatabase,
+ /* isLocal */ true
+ );
+ assertWithMessage("Unexpected count of search requests received.")
+ .that(localSyncedSearchRequests.size())
+ .isEqualTo(1);
+ assertWithMessage("Unexpected search request id.")
+ .that(localSyncedSearchRequests.get(0))
+ .isEqualTo(searchRequestID1);
+
+ final List<Integer> cloudSyncedSearchRequests =
+ SearchRequestDatabaseUtil.getSyncedRequestIds(
+ mDatabase,
+ /* isLocal */ false
+ );
+ assertWithMessage("Unexpected count of search requests received.")
+ .that(cloudSyncedSearchRequests.size())
+ .isEqualTo(1);
+ assertWithMessage("Unexpected search request id.")
+ .that(cloudSyncedSearchRequests.get(0))
+ .isEqualTo(searchRequestID2);
+ }
+
+ @Test
+ public void testClearAllSearchRequests() {
+ // Insert a search request in the database.
+ final SearchTextRequest searchRequest1 = new SearchTextRequest(
+ /* mimeTypes */ null,
+ "mountains"
+ );
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest1);
+
+ // Insert another search request in the database.
+ final SearchTextRequest searchRequest2 = new SearchTextRequest(
+ /* mimeTypes */ null,
+ "volcano"
+ );
+ SearchRequestDatabaseUtil.saveSearchRequest(mDatabase, searchRequest2);
+
+ assertWithMessage("Search request ID should exist in DB")
+ .that(SearchRequestDatabaseUtil.getSearchRequestID(mDatabase, searchRequest1))
+ .isAtLeast(0);
+ assertWithMessage("Search request ID should exist in DB")
+ .that(SearchRequestDatabaseUtil.getSearchRequestID(mDatabase, searchRequest2))
+ .isAtLeast(0);
+
+ final int deletedSearchRequestsCount =
+ SearchRequestDatabaseUtil.clearAllSearchRequests(mDatabase);
+
+ assertWithMessage("Incorrect search requests were deleted.")
+ .that(deletedSearchRequestsCount)
+ .isEqualTo(2);
+ assertWithMessage("Search request ID should not exist in DB")
+ .that(SearchRequestDatabaseUtil.getSearchRequestID(mDatabase, searchRequest1))
+ .isEqualTo(-1);
+ assertWithMessage("Search request ID should not exist in DB")
+ .that(SearchRequestDatabaseUtil.getSearchRequestID(mDatabase, searchRequest2))
+ .isEqualTo(-1);
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/v2/sqlite/SearchResultsDatabaseUtilTest.java b/tests/src/com/android/providers/media/photopicker/v2/sqlite/SearchResultsDatabaseUtilTest.java
new file mode 100644
index 000000000..0cc04f7ca
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/v2/sqlite/SearchResultsDatabaseUtilTest.java
@@ -0,0 +1,1004 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker.v2.sqlite;
+
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_1;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_2;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_3;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_PROVIDER;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.DATE_TAKEN_MS;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.GENERATION_MODIFIED;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.GIF_IMAGE_MIME_TYPE;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.JPEG_IMAGE_MIME_TYPE;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_1;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_2;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_3;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_4;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_PROVIDER;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.MP4_VIDEO_MIME_TYPE;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.PNG_IMAGE_MIME_TYPE;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.STANDARD_MIME_TYPE_EXTENSION;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddMediaOperation;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getCloudMediaCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getLocalMediaCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getMediaCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.toMediaStoreUri;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.provider.MediaStore;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.SearchState;
+import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
+import com.android.providers.media.photopicker.v2.PickerDataLayerV2;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class SearchResultsDatabaseUtilTest {
+ @Mock
+ private PickerSyncController mMockSyncController;
+ @Mock
+ private SearchState mSearchState;
+ private SQLiteDatabase mDatabase;
+ private Context mContext;
+ private PickerDbFacade mFacade;
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ PickerSyncController.setInstance(mMockSyncController);
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ PickerDatabaseHelper helper = new PickerDatabaseHelper(mContext);
+ mDatabase = helper.getWritableDatabase();
+ mFacade = new PickerDbFacade(mContext, new PickerSyncLockManager(), LOCAL_PROVIDER);
+ mFacade.setCloudProvider(CLOUD_PROVIDER);
+
+ doReturn(LOCAL_PROVIDER).when(mMockSyncController).getLocalProvider();
+ doReturn(CLOUD_PROVIDER).when(mMockSyncController).getCloudProvider();
+ doReturn(CLOUD_PROVIDER).when(mMockSyncController).getCloudProviderOrDefault(any());
+ doReturn(mFacade).when(mMockSyncController).getDbFacade();
+ doReturn(mSearchState).when(mMockSyncController).getSearchState();
+ doReturn(true).when(mSearchState).isCloudSearchEnabled(any());
+ doReturn(true).when(mSearchState).isCloudSearchEnabled(any(), any());
+ doReturn(new PickerSyncLockManager()).when(mMockSyncController).getPickerSyncLockManager();
+ }
+
+ @After
+ public void teardown() {
+ mDatabase.close();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ }
+
+ @Test
+ public void testQuerySearchResultsLocalItems() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+ final Cursor cursor1 = getLocalMediaCursor(LOCAL_ID_1, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getLocalMediaCursor(LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ final int searchRequestId1 = 1;
+
+ final long cloudRowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, CLOUD_PROVIDER, List.of(
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, searchRequestId1)),
+ /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(1);
+
+ final long localRowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, LOCAL_PROVIDER, List.of(
+ getContentValues(LOCAL_ID_1, null, searchRequestId1)),
+ /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(localRowsInsertedCount)
+ .isEqualTo(1);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(2);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_2);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_1);
+ }
+ }
+
+ @Test
+ public void testQuerySearchResultsCloudItems() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ final int searchRequestId1 = 1;
+
+ // Batch insert items in the search results table.
+ final long cloudRowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, CLOUD_PROVIDER, List.of(
+ getContentValues(null, CLOUD_ID_3, searchRequestId1),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, searchRequestId1),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(3);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_3);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_2);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_1);
+ }
+ }
+
+ @Test
+ public void testQuerySearchResultsIdFilter() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ final int searchRequestId1 = 1;
+ final int searchRequestId2 = 2;
+ final int searchRequestId3 = 3;
+
+ // Batch insert items in the search results table.
+ final long cloudRowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, CLOUD_PROVIDER, List.of(
+ getContentValues(null, CLOUD_ID_3, searchRequestId3),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, searchRequestId2),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId2)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(1);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_2);
+ }
+ }
+
+ @Test
+ public void testQuerySearchResultsSortOrder() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+ final long dateTaken = 0L;
+
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, dateTaken + 1);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, dateTaken);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, dateTaken - 1);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+ final Cursor cursor4 = getLocalMediaCursor(LOCAL_ID_4, dateTaken);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor4, 1);
+
+ final int searchRequestId1 = 1;
+
+ // Batch insert items in the search results table.
+ final long cloudRowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, CLOUD_PROVIDER, List.of(
+ getContentValues(null, CLOUD_ID_3, searchRequestId1),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, searchRequestId1),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ final long rowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, LOCAL_PROVIDER, List.of(
+ getContentValues(LOCAL_ID_4, null, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(rowsInsertedCount)
+ .isEqualTo(1);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(4);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_1);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_4);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_2);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_3);
+ }
+ }
+
+ @Test
+ public void testQuerySearchResultsPagination() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+ final long dateTaken = 0L;
+
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, dateTaken + 1);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, dateTaken);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, dateTaken - 1);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+ final Cursor cursor4 = getLocalMediaCursor(LOCAL_ID_4, dateTaken);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor4, 1);
+
+ final int searchRequestId1 = 1;
+
+ // Batch insert items in the search results table.
+ final long cloudRowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, CLOUD_PROVIDER, List.of(
+ getContentValues(null, CLOUD_ID_3, searchRequestId1),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, searchRequestId1),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ final long rowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, LOCAL_PROVIDER, List.of(
+ getContentValues(LOCAL_ID_4, null, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(rowsInsertedCount)
+ .isEqualTo(1);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 2);
+ extras.putLong("picker_id", 2);
+ extras.putLong("date_taken_millis", dateTaken);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(2);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_2);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_3);
+ }
+ }
+
+ @Test
+ public void testQuerySearchResultsMimeTypeFilter() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+ final Cursor cursor1 = getMediaCursor(CLOUD_ID_1, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 1, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getMediaCursor(CLOUD_ID_2, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ toMediaStoreUri(LOCAL_ID_2), /* sizeBytes */ 1,
+ PNG_IMAGE_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getMediaCursor(CLOUD_ID_3, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ toMediaStoreUri(LOCAL_ID_3), /* sizeBytes */ 1,
+ GIF_IMAGE_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+ final Cursor cursor4 = getMediaCursor(LOCAL_ID_4, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ toMediaStoreUri(LOCAL_ID_4), /* sizeBytes */ 1,
+ JPEG_IMAGE_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor4, 1);
+
+ final int searchRequestId1 = 1;
+
+ // Batch insert items in the search results table.
+ final long cloudRowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, CLOUD_PROVIDER, List.of(
+ getContentValues(null, CLOUD_ID_3, searchRequestId1),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, searchRequestId1),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ final long rowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, LOCAL_PROVIDER, List.of(
+ getContentValues(LOCAL_ID_4, null, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(rowsInsertedCount)
+ .isEqualTo(1);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+ extras.putStringArrayList("mime_types", new ArrayList<>(List.of("video/mp4", "image/gif")));
+
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(2);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_3);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_1);
+ }
+ }
+
+ @Test
+ public void testQuerySearchResultsLocalProvidersFilter() {
+ doReturn(false).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(false).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+ final Cursor cursor4 = getLocalMediaCursor(LOCAL_ID_4, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor4, 1);
+
+ final int searchRequestId1 = 1;
+
+ // Batch insert items in the search results table.
+ final long cloudRowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, CLOUD_PROVIDER, List.of(
+ getContentValues(null, CLOUD_ID_3, searchRequestId1),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, searchRequestId1),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(cloudRowsInsertedCount)
+ .isEqualTo(3);
+
+ final long rowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, LOCAL_PROVIDER, List.of(
+ getContentValues(LOCAL_ID_4, null, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(rowsInsertedCount)
+ .isEqualTo(1);
+
+ Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(List.of(LOCAL_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(1);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_4);
+ }
+ }
+
+
+ @Test
+ public void testQuerySearchResultsCloudProvidersFilter() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+ final Cursor cursor4 = getLocalMediaCursor(LOCAL_ID_4, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor4, 1);
+
+ final int searchRequestId1 = 1;
+
+ // Batch insert items in the search results table.
+ final long rowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, CLOUD_PROVIDER, List.of(
+ getContentValues(null, CLOUD_ID_3, searchRequestId1),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, searchRequestId1),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, searchRequestId1),
+ getContentValues(LOCAL_ID_4, null, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(rowsInsertedCount)
+ .isEqualTo(4);
+
+ final Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers", new ArrayList<>(List.of(CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(3);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_3);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_2);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_1);
+ }
+ }
+
+
+ @Test
+ public void testInsertLocalItems() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+ final Cursor cursor1 = getLocalMediaCursor(LOCAL_ID_1, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getLocalMediaCursor(LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor2, 1);
+
+ final int searchRequestId1 = 1;
+ final int searchRequestId2 = 2;
+
+ // Batch insert items in the search results table.
+ final long rowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, LOCAL_PROVIDER, List.of(
+ getContentValues(LOCAL_ID_1, null, searchRequestId1),
+ getContentValues(LOCAL_ID_1, null, searchRequestId2),
+ getContentValues(LOCAL_ID_2, null, searchRequestId2),
+ getContentValues(LOCAL_ID_2, null, searchRequestId2)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(rowsInsertedCount)
+ .isEqualTo(4);
+
+ final Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(List.of(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ // Query items for searchRequestId1
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(1);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_1);
+ }
+
+ // Query items for searchRequestId2
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId2)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(2);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_2);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_1);
+ }
+ }
+
+ @Test
+ public void testInsertCloudItems() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getLocalMediaCursor(LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ final int searchRequestId1 = 1;
+ final int searchRequestId2 = 2;
+
+ // Batch insert items in the search results table.
+ final long rowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, CLOUD_PROVIDER, List.of(
+ getContentValues(null, CLOUD_ID_1, searchRequestId1),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, searchRequestId2),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, searchRequestId1),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(rowsInsertedCount)
+ .isEqualTo(3);
+
+ final Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(List.of(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ // Query items for searchRequestId1
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(2);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_2);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_1);
+ }
+
+ // Query items for searchRequestId2
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId2)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(1);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_2);
+ }
+ }
+
+ @Test
+ public void testInsertLocalAndCloudItems() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getLocalMediaCursor(LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ final int searchRequestId1 = 1;
+
+ // Batch insert items in the search results table.
+ long rowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, LOCAL_PROVIDER, List.of(
+ getContentValues(LOCAL_ID_2, null, searchRequestId1)
+ ), /* cancellationSignal */ null);
+ rowsInsertedCount += SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, CLOUD_PROVIDER, List.of(
+ getContentValues(null, CLOUD_ID_1, searchRequestId1),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(rowsInsertedCount)
+ .isEqualTo(2);
+
+ final Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(List.of(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ // Query items for searchRequestId
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(2);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_2);
+
+ cursor.moveToNext();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(CLOUD_ID_1);
+ }
+ }
+
+ @Test
+ public void testClearObsoleteSearchResults() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getLocalMediaCursor(LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor2, 1);
+ final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
+
+ final int searchRequestId1 = 1;
+ // Batch insert items in the search results table.
+ long rowsInsertedCount = SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, LOCAL_PROVIDER, List.of(
+ getContentValues(LOCAL_ID_2, null, searchRequestId1)
+ ), /* cancellationSignal */ null);
+ rowsInsertedCount += SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, CLOUD_PROVIDER, List.of(
+ getContentValues(null, CLOUD_ID_1, searchRequestId1),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(rowsInsertedCount)
+ .isEqualTo(2);
+
+ final Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(List.of(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ // Query items for searchRequestId
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(2);
+ }
+
+ // Clear search results received from the cloud provider.
+ SearchResultsDatabaseUtil.clearObsoleteSearchResults(mDatabase, List.of(searchRequestId1),
+ /* isLocal */ false);
+
+ // Verify cloud results have been cleared
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(1);
+
+ cursor.moveToFirst();
+ assertWithMessage("Media ID is not as expected in the search results")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
+ .isEqualTo(LOCAL_ID_2);
+ }
+
+ // Clear search results received from the local provider.
+ SearchResultsDatabaseUtil.clearObsoleteSearchResults(mDatabase, List.of(searchRequestId1),
+ /* isLocal */ true);
+
+ // Verify cloud results have been cleared
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(0);
+ }
+ }
+
+ @Test
+ public void testClearAllSearchResults() {
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+ final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
+ final Cursor cursor2 = getLocalMediaCursor(LOCAL_ID_1, 0);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor2, 1);
+
+ final int searchRequestId1 = 1;
+ SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, LOCAL_PROVIDER, List.of(
+ getContentValues(LOCAL_ID_1, null, searchRequestId1)
+ ), /* cancellationSignal */ null);
+ SearchResultsDatabaseUtil.cacheSearchResults(
+ mDatabase, CLOUD_PROVIDER, List.of(
+ getContentValues(null, CLOUD_ID_1, searchRequestId1)
+ ), /* cancellationSignal */ null);
+
+ final Bundle extras = new Bundle();
+ extras.putInt("page_size", 100);
+ extras.putStringArrayList("providers",
+ new ArrayList<>(List.of(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+
+ // Query items for searchRequestId
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(2);
+ }
+
+ // Clear all search results
+ SearchResultsDatabaseUtil.clearAllSearchResults(mDatabase);
+
+ // Query items for searchRequestId
+ try (Cursor cursor =
+ PickerDataLayerV2.querySearchMedia(mContext, extras, searchRequestId1)) {
+ assertWithMessage("Cursor should not be null")
+ .that(cursor)
+ .isNotNull();
+
+ assertWithMessage("Cursor count is not as expected")
+ .that(cursor.getCount())
+ .isEqualTo(0);
+ }
+ }
+
+ private ContentValues getContentValues(String localId, String cloudId, int searchRequestId) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(
+ PickerSQLConstants.SearchResultMediaTableColumns.CLOUD_ID.getColumnName(), cloudId);
+ contentValues.put(
+ PickerSQLConstants.SearchResultMediaTableColumns.LOCAL_ID.getColumnName(), localId);
+ contentValues.put(
+ PickerSQLConstants.SearchResultMediaTableColumns.SEARCH_REQUEST_ID.getColumnName(),
+ searchRequestId);
+ return contentValues;
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/v2/sqlite/SearchSuggestionsDatabaseUtilTest.java b/tests/src/com/android/providers/media/photopicker/v2/sqlite/SearchSuggestionsDatabaseUtilTest.java
new file mode 100644
index 000000000..d38801741
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/v2/sqlite/SearchSuggestionsDatabaseUtilTest.java
@@ -0,0 +1,908 @@
+/*
+ * Copyright (C) 2024 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.media.photopicker.v2.sqlite;
+
+import static android.provider.CloudMediaProviderContract.SEARCH_SUGGESTION_ALBUM;
+import static android.provider.CloudMediaProviderContract.SEARCH_SUGGESTION_HISTORY;
+import static android.provider.CloudMediaProviderContract.SEARCH_SUGGESTION_FACE;
+import static android.provider.CloudMediaProviderContract.SEARCH_SUGGESTION_LOCATION;
+
+import static com.android.providers.media.photopicker.v2.sqlite.SearchSuggestionsDatabaseUtils.TTL_CACHED_SUGGESTIONS_IN_DAYS;
+import static com.android.providers.media.photopicker.v2.sqlite.SearchSuggestionsDatabaseUtils.TTL_HISTORY_SUGGESTIONS_IN_DAYS;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.provider.CloudMediaProviderContract;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.v2.model.SearchRequest;
+import com.android.providers.media.photopicker.v2.model.SearchSuggestion;
+import com.android.providers.media.photopicker.v2.model.SearchSuggestionRequest;
+import com.android.providers.media.photopicker.v2.model.SearchTextRequest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class SearchSuggestionsDatabaseUtilTest {
+ private SQLiteDatabase mDatabase;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ PickerDatabaseHelper helper = new PickerDatabaseHelper(mContext);
+ mDatabase = helper.getWritableDatabase();
+ }
+
+ @After
+ public void teardown() {
+ mDatabase.close();
+ File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+ dbPath.delete();
+ }
+
+ @Test
+ public void testSaveTextSearchRequestHistory() {
+ final String searchText = "mountains";
+ final SearchTextRequest searchRequest = new SearchTextRequest(
+ /* mimeTypes */ null,
+ searchText
+ );
+
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest);
+
+ final List<SearchSuggestion> searchSuggestions =
+ SearchSuggestionsDatabaseUtils.getHistorySuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of(),
+ /* limit */ 10));
+
+ assertWithMessage("Search history suggestions cannot be null")
+ .that(searchSuggestions)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search history suggestions.")
+ .that(searchSuggestions.size())
+ .isEqualTo(1);
+
+ final SearchSuggestion result = searchSuggestions.get(0);
+ assertWithMessage("Search history search text is not as expected")
+ .that(result.getSearchText())
+ .isEqualTo(searchText);
+ assertWithMessage("Search history media set id is not as expected")
+ .that(result.getMediaSetId())
+ .isNull();
+ assertWithMessage("Search history authority is not as expected")
+ .that(result.getAuthority())
+ .isNull();
+ assertWithMessage("Search history suggestion type is not as expected")
+ .that(result.getSearchSuggestionType())
+ .isEqualTo(SEARCH_SUGGESTION_HISTORY);
+ }
+
+ @Test
+ public void testSaveSuggestionSearchRequestHistory() {
+ final String mediaSetID = "MEDIA-SET-ID";
+ final String authority = "com.random.authority";
+ SearchSuggestionRequest searchRequest = new SearchSuggestionRequest(
+ List.of("video/mp4", "image/*", "image/gif"),
+ null,
+ mediaSetID,
+ authority,
+ SEARCH_SUGGESTION_LOCATION
+ );
+
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest);
+
+ final List<SearchSuggestion> searchSuggestions =
+ SearchSuggestionsDatabaseUtils.getHistorySuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of("random.authority", authority),
+ /* limit */ 10));
+
+ assertWithMessage("Search history suggestions cannot be null")
+ .that(searchSuggestions)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search history suggestions.")
+ .that(searchSuggestions.size())
+ .isEqualTo(1);
+
+ final SearchSuggestion result = searchSuggestions.get(0);
+ assertWithMessage("Search history search text is not as expected")
+ .that(result.getSearchText())
+ .isNull();
+ assertWithMessage("Search history media set id is not as expected")
+ .that(result.getMediaSetId())
+ .isEqualTo(mediaSetID);
+ assertWithMessage("Search history authority is not as expected")
+ .that(result.getAuthority())
+ .isEqualTo(authority);
+ assertWithMessage("Search history suggestion type is not as expected")
+ .that(result.getSearchSuggestionType())
+ .isEqualTo(SEARCH_SUGGESTION_HISTORY);
+ }
+
+ @Test
+ public void testQueryHistoryForProviders() {
+ final String mediaSetID = "MEDIA-SET-ID";
+ final String authority = "com.random.authority";
+ SearchSuggestionRequest searchRequest = new SearchSuggestionRequest(
+ List.of("video/mp4", "image/*", "image/gif"),
+ null,
+ mediaSetID,
+ authority,
+ SEARCH_SUGGESTION_LOCATION
+ );
+
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest);
+
+ final List<SearchSuggestion> searchSuggestions =
+ SearchSuggestionsDatabaseUtils.getHistorySuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of("random.authority",
+ "another.random.authority"),
+ /* limit */ 10));
+
+ assertWithMessage("Search history suggestions cannot be null")
+ .that(searchSuggestions)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search history suggestions.")
+ .that(searchSuggestions.size())
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void testQueryHistorySortOrder() {
+ final String searchText1 = "mountains";
+ final SearchTextRequest searchRequest1 = new SearchTextRequest(
+ /* mimeTypes */ null,
+ searchText1
+ );
+
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest1);
+
+ final String mediaSetId2 = "MEDIA-SET-ID";
+ final String authority2 = "com.random.authority";
+ SearchSuggestionRequest searchRequest2 = new SearchSuggestionRequest(
+ List.of("video/mp4", "image/*", "image/gif"),
+ null,
+ mediaSetId2,
+ authority2,
+ SEARCH_SUGGESTION_LOCATION
+ );
+
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest2);
+
+ final List<SearchSuggestion> searchSuggestions =
+ SearchSuggestionsDatabaseUtils.getHistorySuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of(authority2),
+ /* limit */ 10));
+
+ assertWithMessage("Search history suggestions cannot be null")
+ .that(searchSuggestions)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search history suggestions.")
+ .that(searchSuggestions.size())
+ .isEqualTo(2);
+
+ final SearchSuggestion firstSuggestion = searchSuggestions.get(0);
+ assertWithMessage("Search history search text is not as expected")
+ .that(firstSuggestion.getSearchText())
+ .isNull();
+ assertWithMessage("Search history media set id is not as expected")
+ .that(firstSuggestion.getMediaSetId())
+ .isEqualTo(mediaSetId2);
+ assertWithMessage("Search history authority is not as expected")
+ .that(firstSuggestion.getAuthority())
+ .isEqualTo(authority2);
+ assertWithMessage("Search history suggestion type is not as expected")
+ .that(firstSuggestion.getSearchSuggestionType())
+ .isEqualTo(SEARCH_SUGGESTION_HISTORY);
+
+ final SearchSuggestion secondSuggestion = searchSuggestions.get(1);
+ assertWithMessage("Search history search text is not as expected")
+ .that(secondSuggestion.getSearchText())
+ .isEqualTo(searchText1);
+ assertWithMessage("Search history media set id is not as expected")
+ .that(secondSuggestion.getMediaSetId())
+ .isNull();
+ assertWithMessage("Search history authority is not as expected")
+ .that(secondSuggestion.getAuthority())
+ .isNull();
+ assertWithMessage("Search history suggestion type is not as expected")
+ .that(secondSuggestion.getSearchSuggestionType())
+ .isEqualTo(SEARCH_SUGGESTION_HISTORY);
+ }
+
+ @Test
+ public void testQueryHistoryLimit() {
+ final String searchText1 = "mountains";
+ final SearchTextRequest searchRequest1 = new SearchTextRequest(
+ /* mimeTypes */ null,
+ searchText1
+ );
+
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest1);
+
+ final String mediaSetId2 = "MEDIA-SET-ID";
+ final String authority2 = "com.random.authority";
+ SearchSuggestionRequest searchRequest2 = new SearchSuggestionRequest(
+ List.of("video/mp4", "image/*", "image/gif"),
+ null,
+ mediaSetId2,
+ authority2,
+ SEARCH_SUGGESTION_LOCATION
+ );
+
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest2);
+
+ final String searchText3 = "Mormot";
+ final SearchTextRequest searchRequest3 = new SearchTextRequest(
+ /* mimeTypes */ null,
+ searchText3
+ );
+
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest3);
+
+ final List<SearchSuggestion> searchSuggestions =
+ SearchSuggestionsDatabaseUtils.getHistorySuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of(authority2),
+ /* limit */ 10,
+ /* historyLimit */ 2,
+ /* prefix */ "mo"));
+
+ assertWithMessage("Search history suggestions cannot be null")
+ .that(searchSuggestions)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search history suggestions.")
+ .that(searchSuggestions.size())
+ .isEqualTo(2);
+
+ final SearchSuggestion suggestion1 = searchSuggestions.get(0);
+ assertWithMessage("Search history search text is not as expected")
+ .that(suggestion1.getSearchText())
+ .isEqualTo(searchText3);
+
+ final SearchSuggestion suggestion2 = searchSuggestions.get(1);
+ assertWithMessage("Search history search text is not as expected")
+ .that(suggestion2.getSearchText())
+ .isEqualTo(searchText1);
+ }
+
+ @Test
+ public void testHistoryPrefixFilter() {
+ final String searchText1 = "mountains";
+ final SearchTextRequest searchRequest1 = new SearchTextRequest(
+ /* mimeTypes */ null,
+ searchText1
+ );
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest1);
+
+ final String searchText2 = "beaches";
+ SearchSuggestionRequest searchRequest2 = new SearchSuggestionRequest(
+ List.of("video/mp4", "image/*", "image/gif"),
+ searchText2,
+ "mediaSetId",
+ "authority",
+ SEARCH_SUGGESTION_LOCATION
+ );
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest2);
+
+ final List<SearchSuggestion> searchSuggestions =
+ SearchSuggestionsDatabaseUtils.getHistorySuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of("authority"),
+ /* limit */ 10,
+ /* historyLimit */ 3,
+ /* prefix */ "Beach"));
+
+ assertWithMessage("Search history suggestions cannot be null")
+ .that(searchSuggestions)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search history suggestions.")
+ .that(searchSuggestions.size())
+ .isEqualTo(1);
+
+ final SearchSuggestion result = searchSuggestions.get(0);
+ assertWithMessage("Search history search text is not as expected")
+ .that(result.getSearchText())
+ .isEqualTo(searchText2);
+ }
+
+ @Test
+ public void testExtractSearchSuggestions() {
+ final String authority = "authority";
+ final SearchSuggestion expectedSearchSuggestion = new SearchSuggestion(
+ /* searchText */ "mountains",
+ /* mediaSetId */ "media-set-id",
+ authority,
+ SEARCH_SUGGESTION_ALBUM,
+ /* coverMediaId */ "media-id"
+ );
+
+ try (Cursor cursor = getCursor(List.of(expectedSearchSuggestion))) {
+ final List<SearchSuggestion> result =
+ SearchSuggestionsDatabaseUtils.extractSearchSuggestions(cursor, authority);
+
+ assertWithMessage("Search suggestions cannot be null")
+ .that(result)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search suggestions.")
+ .that(result.size())
+ .isEqualTo(1);
+ assertWithMessage("Search text is not as expected")
+ .that(result.get(0).getSearchText())
+ .isEqualTo(expectedSearchSuggestion.getSearchText());
+ assertWithMessage("Media set id is not as expected")
+ .that(result.get(0).getMediaSetId())
+ .isEqualTo(expectedSearchSuggestion.getMediaSetId());
+ assertWithMessage("Authority is not as expected")
+ .that(result.get(0).getAuthority())
+ .isEqualTo(expectedSearchSuggestion.getAuthority());
+ assertWithMessage("Suggestion type is not as expected")
+ .that(result.get(0).getSearchSuggestionType())
+ .isEqualTo(expectedSearchSuggestion.getSearchSuggestionType());
+ }
+ }
+
+ @Test
+ public void testSaveSuggestionSearchCache() {
+ final String mediaSetId1 = "MEDIA-SET-ID-1";
+ final String authority1 = "com.random.authority";
+ SearchSuggestion searchSuggestion1 = new SearchSuggestion(
+ /* searchText */ null,
+ mediaSetId1,
+ authority1,
+ SEARCH_SUGGESTION_LOCATION,
+ /* coverMediaId */ null
+ );
+
+ final String mediaSetId2 = "MEDIA-SET-ID-2";
+ final String authority2 = "com.another.random.authority";
+ SearchSuggestion searchSuggestion2 = new SearchSuggestion(
+ /* searchText */ null,
+ mediaSetId2,
+ authority2,
+ SEARCH_SUGGESTION_ALBUM,
+ /* coverMediaId */ null
+ );
+
+ final String mediaSetId3 = "MEDIA-SET-ID-3";
+ SearchSuggestion searchSuggestion3 = new SearchSuggestion(
+ /* searchText */ null,
+ mediaSetId3,
+ authority2,
+ SEARCH_SUGGESTION_FACE,
+ /* coverMediaId */ null
+ );
+
+ SearchSuggestionsDatabaseUtils.cacheSearchSuggestions(
+ mDatabase, authority1, List.of(searchSuggestion1));
+ SearchSuggestionsDatabaseUtils.cacheSearchSuggestions(
+ mDatabase, authority2, List.of(searchSuggestion2, searchSuggestion3));
+
+ final List<SearchSuggestion> resultSearchSuggestions1 =
+ SearchSuggestionsDatabaseUtils.getCachedSuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of("test", authority1),
+ /* limit */ 10));
+
+ assertWithMessage("Search suggestions cannot be null")
+ .that(resultSearchSuggestions1)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search suggestions.")
+ .that(resultSearchSuggestions1.size())
+ .isEqualTo(1);
+
+ final SearchSuggestion result1 = resultSearchSuggestions1.get(0);
+ assertWithMessage("Search search text is not as expected")
+ .that(result1.getSearchText())
+ .isNull();
+ assertWithMessage("Search media set id is not as expected")
+ .that(result1.getMediaSetId())
+ .isEqualTo(searchSuggestion1.getMediaSetId());
+ assertWithMessage("Search authority is not as expected")
+ .that(result1.getAuthority())
+ .isEqualTo(searchSuggestion1.getAuthority());
+ assertWithMessage("Search suggestion type is not as expected")
+ .that(result1.getSearchSuggestionType())
+ .isEqualTo(searchSuggestion1.getSearchSuggestionType());
+
+ final List<SearchSuggestion> resultSearchSuggestions2 =
+ SearchSuggestionsDatabaseUtils.getCachedSuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of("test", authority2),
+ /* limit */ 10));
+
+ assertWithMessage("Search suggestions cannot be null")
+ .that(resultSearchSuggestions2)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search suggestions.")
+ .that(resultSearchSuggestions2.size())
+ .isEqualTo(2);
+
+ final SearchSuggestion result2 = resultSearchSuggestions2.get(0);
+ assertWithMessage("Search search text is not as expected")
+ .that(result2.getSearchText())
+ .isNull();
+ assertWithMessage("Search media set id is not as expected")
+ .that(result2.getMediaSetId())
+ .isEqualTo(searchSuggestion2.getMediaSetId());
+ assertWithMessage("Search authority is not as expected")
+ .that(result2.getAuthority())
+ .isEqualTo(searchSuggestion2.getAuthority());
+ assertWithMessage("Search suggestion type is not as expected")
+ .that(result2.getSearchSuggestionType())
+ .isEqualTo(searchSuggestion2.getSearchSuggestionType());
+ }
+
+ @Test
+ public void testSuggestionsCachePrefixFilter() {
+ final String searchText1 = "BUTTER";
+ final String mediaSetId1 = "MEDIA-SET-ID-1";
+ final String authority = "com.random.authority";
+ SearchSuggestion searchSuggestion1 = new SearchSuggestion(
+ searchText1,
+ mediaSetId1,
+ authority,
+ SEARCH_SUGGESTION_LOCATION,
+ /* coverMediaId */ null
+ );
+
+ final String searchText2 = "Butterfly";
+ final String mediaSetId2 = "MEDIA-SET-ID-2";
+ SearchSuggestion searchSuggestion2 = new SearchSuggestion(
+ searchText2,
+ mediaSetId2,
+ authority,
+ SEARCH_SUGGESTION_ALBUM,
+ /* coverMediaId */ null
+ );
+
+ final String searchText3 = "dragon";
+ final String mediaSetId3 = "MEDIA-SET-ID-3";
+ SearchSuggestion searchSuggestion3 = new SearchSuggestion(
+ searchText3,
+ mediaSetId3,
+ authority,
+ SEARCH_SUGGESTION_FACE,
+ /* coverMediaId */ null
+ );
+
+ SearchSuggestionsDatabaseUtils.cacheSearchSuggestions(mDatabase, authority,
+ List.of(searchSuggestion1, searchSuggestion2, searchSuggestion3));
+
+ final List<SearchSuggestion> resultSearchSuggestions =
+ SearchSuggestionsDatabaseUtils.getCachedSuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of("test", authority),
+ /* limit */ 10,
+ /* historyLimit */ 3,
+ /* prefix */ "but"));
+
+ assertWithMessage("Search suggestions cannot be null")
+ .that(resultSearchSuggestions)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search suggestions.")
+ .that(resultSearchSuggestions.size())
+ .isEqualTo(2);
+
+ assertWithMessage("Search search text is not as expected")
+ .that(resultSearchSuggestions.get(0).getSearchText())
+ .isEqualTo(searchSuggestion1.getSearchText());
+ assertWithMessage("Search search text is not as expected")
+ .that(resultSearchSuggestions.get(1).getSearchText())
+ .isEqualTo(searchSuggestion2.getSearchText());
+ }
+
+ @Test
+ public void testSuggestionsCacheQueryLimit() {
+ final String searchText1 = "BUTTER";
+ final String mediaSetId1 = "MEDIA-SET-ID-1";
+ final String authority = "com.random.authority";
+ SearchSuggestion searchSuggestion1 = new SearchSuggestion(
+ searchText1,
+ mediaSetId1,
+ authority,
+ SEARCH_SUGGESTION_LOCATION,
+ /* coverMediaId */ null
+ );
+
+ final String searchText2 = "dragon";
+ final String mediaSetId2 = "MEDIA-SET-ID-2";
+ SearchSuggestion searchSuggestion2 = new SearchSuggestion(
+ searchText2,
+ mediaSetId2,
+ authority,
+ SEARCH_SUGGESTION_ALBUM,
+ /* coverMediaId */ null
+ );
+
+ final String searchText3 = "Butterfly";
+ final String mediaSetId3 = "MEDIA-SET-ID-3";
+ SearchSuggestion searchSuggestion3 = new SearchSuggestion(
+ searchText3,
+ mediaSetId3,
+ authority,
+ SEARCH_SUGGESTION_FACE,
+ /* coverMediaId */ null
+ );
+
+ SearchSuggestionsDatabaseUtils.cacheSearchSuggestions(mDatabase, authority,
+ List.of(searchSuggestion1, searchSuggestion2, searchSuggestion3));
+
+ final List<SearchSuggestion> resultSearchSuggestions =
+ SearchSuggestionsDatabaseUtils.getCachedSuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of("test", authority),
+ /* limit */ 2,
+ /* historyLimit */ 3,
+ /* prefix */ "but"));
+
+ assertWithMessage("Search suggestions cannot be null")
+ .that(resultSearchSuggestions)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search suggestions.")
+ .that(resultSearchSuggestions.size())
+ .isEqualTo(2);
+
+ assertWithMessage("Search search text is not as expected")
+ .that(resultSearchSuggestions.get(1).getSearchText())
+ .isEqualTo(searchSuggestion3.getSearchText());
+ assertWithMessage("Search search text is not as expected")
+ .that(resultSearchSuggestions.get(0).getSearchText())
+ .isEqualTo(searchSuggestion1.getSearchText());
+ }
+
+ @Test
+ public void testSaveSuggestionWithNullMediaSetId() {
+ final String authority = "com.random.authority";
+ SearchSuggestion searchSuggestion = new SearchSuggestion(
+ /* searchText */ null,
+ /* mediaSetId */ null,
+ authority,
+ SEARCH_SUGGESTION_LOCATION,
+ /* coverMediaId */ null
+ );
+
+ SearchSuggestionsDatabaseUtils.cacheSearchSuggestions(
+ mDatabase, authority, List.of(searchSuggestion));
+
+ final List<SearchSuggestion> resultSearchSuggestions =
+ SearchSuggestionsDatabaseUtils.getCachedSuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of("random.authority", authority),
+ /* limit */ 10));
+
+ assertWithMessage("Search suggestions cannot be null")
+ .that(resultSearchSuggestions)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search suggestions.")
+ .that(resultSearchSuggestions.size())
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void testCleanUpSearchSuggestionsBeforeCaching() {
+ final String authority = "com.random.authority";
+ SearchSuggestion searchSuggestion1 = new SearchSuggestion(
+ /* searchText */ null,
+ "media-set-id",
+ authority,
+ SEARCH_SUGGESTION_ALBUM,
+ /* coverMediaId */ null
+ );
+
+ SearchSuggestionsDatabaseUtils.cacheSearchSuggestions(
+ mDatabase, authority, List.of(searchSuggestion1));
+
+ final List<SearchSuggestion> resultSearchSuggestions1 =
+ SearchSuggestionsDatabaseUtils.getCachedSuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of("random.authority", authority),
+ /* limit */ 10));
+
+ assertWithMessage("Search suggestions cannot be null")
+ .that(resultSearchSuggestions1)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search suggestions.")
+ .that(resultSearchSuggestions1.size())
+ .isEqualTo(1);
+
+ SearchSuggestion searchSuggestion2 = new SearchSuggestion(
+ /* searchText */ null,
+ "media-set-id",
+ authority,
+ SEARCH_SUGGESTION_FACE,
+ /* coverMediaId */ null
+ );
+
+ SearchSuggestionsDatabaseUtils.cacheSearchSuggestions(
+ mDatabase, authority, List.of(searchSuggestion2));
+
+ final List<SearchSuggestion> resultSearchSuggestions2 =
+ SearchSuggestionsDatabaseUtils.getCachedSuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of("random.authority", authority),
+ /* limit */ 10));
+
+ assertWithMessage("Search suggestions cannot be null")
+ .that(resultSearchSuggestions2)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search suggestions.")
+ .that(resultSearchSuggestions2.size())
+ .isEqualTo(1);
+ }
+
+ @Test
+ public void testClearExpiredCachedSuggestions() {
+ final String authority = "com.random.authority";
+ SearchSuggestion searchSuggestion1 = new SearchSuggestion(
+ /* searchText */ null,
+ "media-set-id1",
+ authority,
+ SEARCH_SUGGESTION_ALBUM,
+ /* coverMediaId */ null
+ );
+ SearchSuggestion searchSuggestion2 = new SearchSuggestion(
+ /* searchText */ "test",
+ "media-set-id2",
+ authority,
+ SEARCH_SUGGESTION_ALBUM,
+ /* coverMediaId */ null
+ );
+
+ SearchSuggestionsDatabaseUtils.cacheSearchSuggestions(
+ mDatabase, authority, List.of(searchSuggestion1, searchSuggestion2));
+
+ final ContentValues contentValues = new ContentValues();
+ contentValues.put(
+ PickerSQLConstants.SearchSuggestionsTableColumns.CREATION_TIME_MS.getColumnName(),
+ System.currentTimeMillis()
+ - TimeUnit.DAYS.toMillis(TTL_CACHED_SUGGESTIONS_IN_DAYS + 1));
+ mDatabase.update(
+ PickerSQLConstants.Table.SEARCH_SUGGESTION.name(),
+ contentValues,
+ PickerSQLConstants.SearchSuggestionsTableColumns.MEDIA_SET_ID.getColumnName()
+ + " = 'media-set-id2'", null);
+
+ final int rowsDeletedCount =
+ SearchSuggestionsDatabaseUtils.clearExpiredCachedSearchSuggestions(mDatabase);
+ assertWithMessage("Unexpected number of expired cached suggestions deleted.")
+ .that(rowsDeletedCount)
+ .isEqualTo(1);
+ }
+
+ @Test
+ public void testClearExpiredHistorySuggestions() {
+ SearchRequest searchRequest1 = new SearchTextRequest(
+ /* mimeTypes */ null,
+ "summer"
+ );
+ SearchRequest searchRequest2 = new SearchTextRequest(
+ /* mimeTypes */ null,
+ "coffee"
+ );
+
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest1);
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest2);
+
+ final ContentValues contentValues = new ContentValues();
+ contentValues.put(
+ PickerSQLConstants.SearchHistoryTableColumns.CREATION_TIME_MS.getColumnName(),
+ System.currentTimeMillis()
+ - TimeUnit.DAYS.toMillis(TTL_HISTORY_SUGGESTIONS_IN_DAYS + 1));
+ mDatabase.update(
+ PickerSQLConstants.Table.SEARCH_HISTORY.name(),
+ contentValues,
+ PickerSQLConstants.SearchHistoryTableColumns.SEARCH_TEXT.getColumnName()
+ + " = 'summer'", null);
+
+ final int rowsDeletedCount =
+ SearchSuggestionsDatabaseUtils.clearExpiredHistorySearchSuggestions(mDatabase);
+ assertWithMessage("Unexpected number of expired history suggestions deleted.")
+ .that(rowsDeletedCount)
+ .isEqualTo(1);
+ }
+
+ @Test
+ public void testClearCachedSuggestionsForAuthority() {
+ final String authority1 = "com.random.authority1";
+ final String authority2 = "com.random.authority2";
+ SearchSuggestion searchSuggestion1 = new SearchSuggestion(
+ /* searchText */ null,
+ "media-set-id1",
+ authority1,
+ SEARCH_SUGGESTION_ALBUM,
+ /* coverMediaId */ null
+ );
+ SearchSuggestion searchSuggestion2 = new SearchSuggestion(
+ /* searchText */ "test",
+ "media-set-id2",
+ authority2,
+ SEARCH_SUGGESTION_ALBUM,
+ /* coverMediaId */ null
+ );
+
+ SearchSuggestionsDatabaseUtils.cacheSearchSuggestions(
+ mDatabase, authority1, List.of(searchSuggestion1));
+ SearchSuggestionsDatabaseUtils.cacheSearchSuggestions(
+ mDatabase, authority2, List.of(searchSuggestion2));
+
+ int rowsDeletedCount =
+ SearchSuggestionsDatabaseUtils.clearCachedSearchSuggestionsForAuthority(
+ mDatabase, authority1);
+ assertWithMessage("Unexpected number of cached suggestions deleted.")
+ .that(rowsDeletedCount)
+ .isEqualTo(1);
+
+ rowsDeletedCount =
+ SearchSuggestionsDatabaseUtils.clearCachedSearchSuggestionsForAuthority(
+ mDatabase, null);
+ assertWithMessage("Unexpected number of cached suggestions deleted.")
+ .that(rowsDeletedCount)
+ .isEqualTo(1);
+ }
+
+ @Test
+ public void testClearHistorySuggestionsForAuthority() {
+ final String authority1 = "com.random.authority1";
+ final String authority2 = "com.random.authority2";
+
+ SearchRequest searchRequest1 = new SearchTextRequest(
+ /* mimeTypes */ null,
+ "summer"
+ );
+ SearchRequest searchRequest2 = new SearchSuggestionRequest(
+ /* mimeTypes */ null,
+ /* searchText */ null,
+ "media-set-id1",
+ authority1,
+ SEARCH_SUGGESTION_ALBUM
+ );
+ SearchRequest searchRequest3 = new SearchSuggestionRequest(
+ /* mimeTypes */ null,
+ /* searchText */ "test",
+ "media-set-id2",
+ authority2,
+ SEARCH_SUGGESTION_ALBUM
+ );
+
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest1);
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest2);
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest3);
+
+ int rowsDeletedCount =
+ SearchSuggestionsDatabaseUtils.clearHistorySearchSuggestionsForAuthority(
+ mDatabase, authority1);
+ assertWithMessage("Unexpected number of history suggestions deleted.")
+ .that(rowsDeletedCount)
+ .isEqualTo(1);
+
+ rowsDeletedCount =
+ SearchSuggestionsDatabaseUtils.clearHistorySearchSuggestionsForAuthority(
+ mDatabase, null);
+ assertWithMessage("Unexpected number of history suggestions deleted.")
+ .that(rowsDeletedCount)
+ .isEqualTo(1);
+ }
+
+ @Test
+ public void testSaveDuplicateSearchHistorySuggestion() {
+ final String searchText = "mountains";
+ final SearchTextRequest searchRequest = new SearchTextRequest(
+ /* mimeTypes */ null,
+ searchText
+ );
+
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest);
+ SearchSuggestionsDatabaseUtils.saveSearchHistory(mDatabase, searchRequest);
+
+ final List<SearchSuggestion> searchSuggestions =
+ SearchSuggestionsDatabaseUtils.getHistorySuggestions(
+ mDatabase,
+ getSearchSuggestionQuery(
+ /* providers */ List.of(),
+ /* limit */ 10));
+
+ assertWithMessage("Search history suggestions cannot be null")
+ .that(searchSuggestions)
+ .isNotNull();
+ assertWithMessage("Unexpected number of search history suggestions.")
+ .that(searchSuggestions.size())
+ .isEqualTo(1);
+
+ final SearchSuggestion result = searchSuggestions.get(0);
+ assertWithMessage("Search history search text is not as expected")
+ .that(result.getSearchText())
+ .isEqualTo(searchText);
+ assertWithMessage("Search history media set id is not as expected")
+ .that(result.getMediaSetId())
+ .isNull();
+ assertWithMessage("Search history authority is not as expected")
+ .that(result.getAuthority())
+ .isNull();
+ assertWithMessage("Search history suggestion type is not as expected")
+ .that(result.getSearchSuggestionType())
+ .isEqualTo(SEARCH_SUGGESTION_HISTORY);
+ }
+
+ private Cursor getCursor(@NonNull List<SearchSuggestion> searchSuggestions) {
+ final MatrixCursor cursor = new MatrixCursor(
+ CloudMediaProviderContract.SearchSuggestionColumns.ALL_PROJECTION);
+
+ for (SearchSuggestion searchSuggestion : searchSuggestions) {
+ cursor.addRow(List.of(
+ searchSuggestion.getMediaSetId(),
+ searchSuggestion.getSearchText(),
+ searchSuggestion.getSearchSuggestionType(),
+ searchSuggestion.getCoverMediaId()
+ ).toArray(new Object[4]));
+ }
+
+ return cursor;
+ }
+
+ private SearchSuggestionsQuery getSearchSuggestionQuery(@Nullable List<String> providers,
+ int limit) {
+ return getSearchSuggestionQuery(providers, limit, 10, "");
+ }
+
+ private SearchSuggestionsQuery getSearchSuggestionQuery(@Nullable List<String> providers,
+ int limit,
+ int historyLimit,
+ @Nullable String prefix) {
+ final Bundle bundle = new Bundle();
+ bundle.putInt("limit", limit);
+ bundle.putStringArrayList("providers", new ArrayList<>(providers));
+ bundle.putInt("history_limit", historyLimit);
+ bundle.putString("prefix", prefix);
+ return new SearchSuggestionsQuery(bundle);
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelPaginationTest.java b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelPaginationTest.java
index 8bb2500c6..432508d54 100644
--- a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelPaginationTest.java
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelPaginationTest.java
@@ -129,6 +129,10 @@ public class PickerViewModelPaginationTest {
final Context isolatedContext = new IsolatedContext(sTargetContext, /* tag */ "databases",
/* asFuseThread */ false, sTargetContext.getUser(), testConfigStore);
when(mApplication.getApplicationContext()).thenReturn(isolatedContext);
+
+ final UserIdManager userIdManager = mock(UserIdManager.class);
+ when(userIdManager.getCurrentUserProfileId()).thenReturn(UserId.CURRENT_USER);
+
sInstrumentation.runOnMainSync(() -> {
mPickerViewModel = new PickerViewModel(mApplication) {
@Override
@@ -136,14 +140,13 @@ public class PickerViewModelPaginationTest {
setConfigStore(testConfigStore);
}
};
+ mPickerViewModel.initUserManagers(userIdManager);
});
if (testConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) {
final UserManagerState userManagerState = mock(UserManagerState.class);
when(userManagerState.getCurrentUserProfileId()).thenReturn(UserId.CURRENT_USER);
mPickerViewModel.setUserManagerState(userManagerState);
} else {
- final UserIdManager userIdManager = mock(UserIdManager.class);
- when(userIdManager.getCurrentUserProfileId()).thenReturn(UserId.CURRENT_USER);
mPickerViewModel.setUserIdManager(userIdManager);
}
diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
index 607dca406..31dc50652 100644
--- a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
@@ -157,6 +157,9 @@ public class PickerViewModelTest {
mConfigStore.disablePrivateSpaceInPhotoPicker();
}
+ final UserIdManager userIdManager = mock(UserIdManager.class);
+ when(userIdManager.getCurrentUserProfileId()).thenReturn(UserId.CURRENT_USER);
+
getInstrumentation().runOnMainSync(() -> {
mPickerViewModel = new PickerViewModel(mApplication) {
@Override
@@ -164,6 +167,7 @@ public class PickerViewModelTest {
setConfigStore(mConfigStore);
}
};
+ mPickerViewModel.initUserManagers(userIdManager);
});
mItemsProvider = new TestItemsProvider(sTargetContext);
mPickerViewModel.setItemsProvider(mItemsProvider);
@@ -176,8 +180,6 @@ public class PickerViewModelTest {
mBannerManager = BannerTestUtils.getTestCloudBannerManager(
sTargetContext, userManagerState, mConfigStore);
} else {
- final UserIdManager userIdManager = mock(UserIdManager.class);
- when(userIdManager.getCurrentUserProfileId()).thenReturn(UserId.CURRENT_USER);
mPickerViewModel.setUserIdManager(userIdManager);
mBannerManager = BannerTestUtils.getTestCloudBannerManager(
sTargetContext, userIdManager, mConfigStore);
diff --git a/tests/src/com/android/providers/media/photopickersearch/CloudMediaProviderSearch.java b/tests/src/com/android/providers/media/photopickersearch/CloudMediaProviderSearch.java
new file mode 100644
index 000000000..38f6f8f71
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopickersearch/CloudMediaProviderSearch.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 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.media.photopickersearch;
+
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.graphics.Point;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.provider.CloudMediaProvider;
+import android.provider.CloudMediaProviderContract;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.FileNotFoundException;
+
+public class CloudMediaProviderSearch extends CloudMediaProvider {
+ public static final String SEARCH_PROVIDER_FOR_PICKER_CLIENT_AUTHORITY =
+ "com.android.providers.media.photopicker.tests.cloud_provider_for_search_client";
+ public static final String[] MEDIA_PROJECTIONS = new String[] {
+ CloudMediaProviderContract.MediaColumns.ID
+ };
+
+ public static final String TEST_MEDIA_ID_FROM_SUGGESTED_SEARCH = "1";
+ public static final String TEST_MEDIA_ID_IN_MEDIA_SET = "2";
+ public static final String TEST_MEDIA_ID_FROM_TEXT_SEARCH = "3";
+
+ public static final String TEST_MEDIA_SET_ID = "11";
+ public static final String TEST_MEDIA_CATEGORY_ID = "111";
+ public static final String TEST_SEARCH_SUGGESTION_MEDIA_SET_ID = "2";
+
+ @Override
+ public Cursor onSearchMedia(String mediaSetId, String fallbackSearchText,
+ Bundle extras, CancellationSignal cancellationSignal) {
+ MatrixCursor mockCursor = new MatrixCursor(MEDIA_PROJECTIONS);
+ mockCursor.addRow(new Object[]{TEST_MEDIA_ID_FROM_SUGGESTED_SEARCH});
+ return mockCursor;
+ }
+
+ @Override
+ public Cursor onSearchMedia(String searchText,
+ Bundle extras, CancellationSignal cancellationSignal) {
+ MatrixCursor mockCursor = new MatrixCursor(MEDIA_PROJECTIONS);
+ mockCursor.addRow(new Object[]{TEST_MEDIA_ID_FROM_TEXT_SEARCH});
+ return mockCursor;
+ }
+
+ @Override
+ public Cursor onQueryMediaInMediaSet(String mediaSetId, Bundle extras,
+ CancellationSignal cancellationSignal) {
+ MatrixCursor mockCursor = new MatrixCursor(MEDIA_PROJECTIONS);
+ mockCursor.addRow(new Object[]{TEST_MEDIA_ID_IN_MEDIA_SET});
+ return mockCursor;
+ }
+
+ @Override
+ public Cursor onQueryMediaSets(String mediaCategoryId, Bundle extras,
+ CancellationSignal cancellationSignal) {
+ MatrixCursor mockCursor = new MatrixCursor(
+ CloudMediaProviderContract.MediaSetColumns.ALL_PROJECTION);
+ mockCursor.addRow(new Object[]{TEST_MEDIA_SET_ID, "Media Set 1", 1, 25});
+ return mockCursor;
+ }
+
+ @Override
+ public Cursor onQuerySearchSuggestions(String prefixText, Bundle extras,
+ CancellationSignal cancellationSignal) {
+ MatrixCursor mockCursor = new MatrixCursor(
+ CloudMediaProviderContract.SearchSuggestionColumns.ALL_PROJECTION);
+ mockCursor.addRow(new Object[]{TEST_SEARCH_SUGGESTION_MEDIA_SET_ID,
+ 1, "song", "Suggestion 1 for " + prefixText});
+ return mockCursor;
+ }
+
+ @Override
+ public Cursor onQueryMediaCategories(String parentCategoryId,
+ Bundle extras, CancellationSignal cancellationSignal) {
+ MatrixCursor mockCursor =
+ new MatrixCursor(CloudMediaProviderContract.MediaCategoryColumns.ALL_PROJECTION);
+ mockCursor.addRow(new Object[]{TEST_MEDIA_CATEGORY_ID, 1, null, 1, 2, 3, 4});
+ return mockCursor;
+ }
+
+ @Override
+ public Bundle onGetMediaCollectionInfo(@NonNull Bundle extras) {
+ return new Bundle();
+ }
+
+ @Override
+ public Cursor onQueryMedia(@NonNull Bundle extras) {
+ return new MatrixCursor(new String[0]);
+ }
+
+ @Override
+ public Cursor onQueryDeletedMedia(@NonNull Bundle extras) {
+ return new MatrixCursor(new String[0]);
+ }
+
+ @Override
+ public AssetFileDescriptor onOpenPreview(@NonNull String mediaId, @NonNull Point size,
+ @Nullable Bundle extras, @Nullable CancellationSignal signal)
+ throws FileNotFoundException {
+ throw new UnsupportedOperationException("onOpenPreview not supported");
+ }
+
+ @Override
+ public ParcelFileDescriptor onOpenMedia(@NonNull String mediaId, @Nullable Bundle extras,
+ @Nullable CancellationSignal signal) throws FileNotFoundException {
+ throw new UnsupportedOperationException("onOpenMedia not supported");
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+}
diff --git a/tests/src/com/android/providers/media/photopickersearch/PickerSearchProviderClientTest.java b/tests/src/com/android/providers/media/photopickersearch/PickerSearchProviderClientTest.java
new file mode 100644
index 000000000..60da5d032
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopickersearch/PickerSearchProviderClientTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 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.media.photopickersearch;
+
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.MEDIA_PROJECTIONS;
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.SEARCH_PROVIDER_FOR_PICKER_CLIENT_AUTHORITY;
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.TEST_MEDIA_CATEGORY_ID;
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.TEST_MEDIA_ID_FROM_SUGGESTED_SEARCH;
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.TEST_MEDIA_ID_FROM_TEXT_SEARCH;
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.TEST_MEDIA_ID_IN_MEDIA_SET;
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.TEST_MEDIA_SET_ID;
+import static com.android.providers.media.photopickersearch.CloudMediaProviderSearch.TEST_SEARCH_SUGGESTION_MEDIA_SET_ID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.provider.CloudMediaProviderContract;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.flags.Flags;
+import com.android.providers.media.photopicker.sync.PickerSearchProviderClient;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RequiresFlagsEnabled(Flags.FLAG_CLOUD_MEDIA_PROVIDER_SEARCH)
+@RunWith(AndroidJUnit4.class)
+public class PickerSearchProviderClientTest {
+
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ private PickerSearchProviderClient mPickerSearchProviderClient;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getTargetContext();
+ mPickerSearchProviderClient =
+ PickerSearchProviderClient.create(
+ mContext, SEARCH_PROVIDER_FOR_PICKER_CLIENT_AUTHORITY);
+ }
+
+ @Test
+ public void testFetchSearchSuggestionsFromCmp() {
+ Cursor cursor = mPickerSearchProviderClient.fetchSearchSuggestionsFromCmp("test",
+ 10, null);
+ cursor.moveToFirst();
+ assertEquals(TEST_SEARCH_SUGGESTION_MEDIA_SET_ID, cursor.getString(cursor.getColumnIndex(
+ CloudMediaProviderContract.SearchSuggestionColumns.MEDIA_SET_ID)));
+ assertCursorColumns(cursor,
+ CloudMediaProviderContract.SearchSuggestionColumns.ALL_PROJECTION);
+ }
+
+ @Test
+ public void testFetchSuggestedSearchResultsFromCmp() {
+ Cursor cursor = mPickerSearchProviderClient.fetchSearchResultsFromCmp(
+ TEST_SEARCH_SUGGESTION_MEDIA_SET_ID, null, 1, null, 100,
+ null, null);
+ cursor.moveToFirst();
+ assertEquals(TEST_MEDIA_ID_FROM_SUGGESTED_SEARCH, cursor.getString(cursor.getColumnIndex(
+ CloudMediaProviderContract.MediaColumns.ID)));
+ assertCursorColumns(cursor, MEDIA_PROJECTIONS);
+ }
+
+ @Test
+ public void testFetchTextSearchResultsFromCmp() {
+ Cursor cursor = mPickerSearchProviderClient.fetchSearchResultsFromCmp(
+ null, "test", 1, null, 100,
+ null, null);
+ cursor.moveToFirst();
+ assertEquals(TEST_MEDIA_ID_FROM_TEXT_SEARCH, cursor.getString(cursor.getColumnIndex(
+ CloudMediaProviderContract.MediaColumns.ID)));
+ assertCursorColumns(cursor, MEDIA_PROJECTIONS);
+ }
+
+ @Test
+ public void testFetchMediasInMediaSetFromCmp() {
+ Cursor cursor = mPickerSearchProviderClient.fetchMediasInMediaSetFromCmp(TEST_MEDIA_SET_ID,
+ null, 100, CloudMediaProviderContract.SORT_ORDER_DESC_DATE_TAKEN,
+ null, null);
+ cursor.moveToFirst();
+ assertEquals(TEST_MEDIA_ID_IN_MEDIA_SET, cursor.getString(cursor.getColumnIndex(
+ CloudMediaProviderContract.MediaColumns.ID)));
+ assertCursorColumns(cursor, MEDIA_PROJECTIONS);
+ }
+
+
+ @Test
+ public void testFetchMediaCategoriesFromCmp() {
+ Cursor cursor = mPickerSearchProviderClient.fetchMediaCategoriesFromCmp(null, null, null);
+ cursor.moveToFirst();
+ assertEquals(TEST_MEDIA_CATEGORY_ID, cursor.getString(
+ cursor.getColumnIndex(CloudMediaProviderContract.MediaCategoryColumns.ID)));
+ assertCursorColumns(cursor, CloudMediaProviderContract.MediaCategoryColumns.ALL_PROJECTION);
+ }
+
+ @Test
+ public void testFetchMediaSetsFromCmp() {
+ Cursor cursor = mPickerSearchProviderClient.fetchMediaSetsFromCmp(TEST_MEDIA_CATEGORY_ID,
+ null, 10, null, null);
+ cursor.moveToFirst();
+ assertEquals(TEST_MEDIA_SET_ID, cursor.getString(
+ cursor.getColumnIndex(CloudMediaProviderContract.MediaSetColumns.ID)));
+ assertCursorColumns(cursor, CloudMediaProviderContract.MediaSetColumns.ALL_PROJECTION);
+ }
+
+ private static void assertCursorColumns(Cursor cursor, String[] projections) {
+ for (String columnName : projections) {
+ assertTrue(cursor.getColumnIndex(columnName) >= 0);
+ }
+ }
+
+}
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index b3698d849..a87af175b 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -30,6 +30,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -49,6 +50,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
+import android.os.UserManager;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.MediaStore;
@@ -468,6 +470,11 @@ public class ModernMediaScannerTest {
*/
@Test
public void testVisibleDefaultFolders() throws Exception {
+ // Skip test if running in Headless System User Mode
+ // We cannot create nomedia file in public directories for secondary users except
+ // in [Documents, Downloads].
+ assumeFalse(UserManager.isHeadlessSystemUserMode());
+
final File root = new File("storage/emulated/0");
assertVisibleFolder(root);
@@ -494,6 +501,10 @@ public class ModernMediaScannerTest {
*/
@Test
public void testVisibleRootWithNoMediaDirectory() throws Exception {
+ // Skip test if running in Headless System User Mode
+ // We cannot create test files in root directory for secondary users.
+ assumeFalse(UserManager.isHeadlessSystemUserMode());
+
final File root = new File("storage/emulated/0");
final File nomediaDir = new File(root, ".nomedia");
final File file = new File(nomediaDir, "test.jpg");
@@ -949,8 +960,8 @@ public class ModernMediaScannerTest {
assertThat(cursor.getCount()).isEqualTo(1);
}
- // Delete the pending file to make the row is stale
- executeShellCommand("rm " + audio.getAbsolutePath());
+ // Delete the pending file to make the row is stale.
+ audio.delete();
assertThat(audio.exists()).isFalse();
// the row still exists
@@ -992,7 +1003,7 @@ public class ModernMediaScannerTest {
}
// Delete the pending file to make the row is stale
- executeShellCommand("rm " + audio.getAbsolutePath());
+ audio.delete();
assertThat(audio.exists()).isFalse();
// the row still exists
@@ -1034,7 +1045,7 @@ public class ModernMediaScannerTest {
}
// Delete the trashed file to make the row is stale
- executeShellCommand("rm " + audio.getAbsolutePath());
+ audio.delete();
assertThat(audio.exists()).isFalse();
// the row still exists
@@ -1076,7 +1087,7 @@ public class ModernMediaScannerTest {
}
// Delete the trashed file to make the row is stale
- executeShellCommand("rm " + audio.getAbsolutePath());
+ audio.delete();
assertThat(audio.exists()).isFalse();
// the row still exists
@@ -1113,7 +1124,7 @@ public class ModernMediaScannerTest {
}
// Delete the file to make the row is stale
- executeShellCommand("rm " + audio.getAbsolutePath());
+ audio.delete();
assertThat(audio.exists()).isFalse();
// the row still exists
diff --git a/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java b/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java
index 4a6cd851a..9784141ff 100644
--- a/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java
+++ b/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java
@@ -282,6 +282,7 @@ public class StableUriIdleMaintenanceServiceTest {
.adoptShellPermissionIdentity(
Manifest.permission.READ_DEVICE_CONFIG,
Manifest.permission.WRITE_DEVICE_CONFIG,
+ Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG,
Manifest.permission.WRITE_MEDIA_STORAGE,
android.Manifest.permission.LOG_COMPAT_CHANGE,
android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index 7c63807e5..637d4be80 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -73,6 +73,7 @@ import static org.junit.Assert.fail;
import android.content.ContentValues;
import android.os.Environment;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.MediaColumns;
@@ -935,8 +936,9 @@ public class FileUtilsTest {
// Visibility of default dirs is tested in ModernMediaScannerTest#testVisibleDefaultFolders.
@Test
public void testIsDirectoryHidden() throws Exception {
+ int userId = UserHandle.myUserId();
for (String prefix : new String[] {
- "/storage/emulated/0",
+ String.format(Locale.ROOT, "/storage/emulated/%d", userId),
"/storage/0000-0000",
}) {
assertDirectoryNotHidden(new File(prefix));
@@ -945,7 +947,9 @@ public class FileUtilsTest {
assertDirectoryHidden(new File(prefix + "/.meow"));
}
- final File nomediaFile = new File("storage/emulated/0/Download/meow", ".nomedia");
+ final File nomediaFile = new File(
+ String.format(Locale.ROOT, "storage/emulated/%d/Download/meow", userId),
+ ".nomedia");
try {
assertTrue(nomediaFile.getParentFile().mkdirs());
assertTrue(nomediaFile.createNewFile());
@@ -1219,13 +1223,18 @@ public class FileUtilsTest {
@Test
public void testToAndFromFuseFile() throws Exception {
- final File fuseFilePrimary = new File("/mnt/user/0/emulated/0/foo");
- final File fuseFileSecondary = new File("/mnt/user/0/0000-0000/foo");
-
- final File lowerFsFilePrimary = new File("/storage/emulated/0/foo");
+ int userId = UserHandle.myUserId();
+ final File fuseFilePrimary = new File(
+ String.format(Locale.ROOT, "/mnt/user/%d/emulated/%d/foo", userId, userId));
+ final File fuseFileSecondary = new File(
+ String.format(Locale.ROOT, "/mnt/user/%d/0000-0000/foo", userId));
+
+ final File lowerFsFilePrimary = new File(
+ String.format(Locale.ROOT, "/storage/emulated/%d/foo", userId));
final File lowerFsFileSecondary = new File("/storage/0000-0000/foo");
- final File unexpectedFile = new File("/mnt/pass_through/0/emulated/0/foo");
+ final File unexpectedFile = new File(
+ String.format(Locale.ROOT, "/mnt/pass_through/%d/emulated/%d/foo", userId, userId));
assertThat(fromFuseFile(fuseFilePrimary)).isEqualTo(lowerFsFilePrimary);
assertThat(fromFuseFile(fuseFileSecondary)).isEqualTo(lowerFsFileSecondary);
diff --git a/tests/src/com/android/providers/media/util/MimeTypeFixHandlerTest.java b/tests/src/com/android/providers/media/util/MimeTypeFixHandlerTest.java
new file mode 100644
index 000000000..de86f97d7
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/MimeTypeFixHandlerTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2025 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.media.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.ClipDescription;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Build;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Files.FileColumns;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.flags.Flags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Objects;
+import java.util.Optional;
+
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class MimeTypeFixHandlerTest {
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private SQLiteDatabase mDatabase;
+ private static final String FILES_TABLE_NAME = MediaStore.Files.TABLE;
+ private static final String DATABASE_FILE = "mime_type_fix.db";
+
+ @Before
+ public void setUp() throws Exception {
+ assumeTrue(Build.VERSION.SDK_INT == Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ final Context context = InstrumentationRegistry.getTargetContext();
+
+ context.deleteDatabase(DATABASE_FILE);
+ mDatabase = Objects.requireNonNull(
+ context.openOrCreateDatabase(DATABASE_FILE, Context.MODE_PRIVATE, null));
+ MimeTypeFixHandler.loadMimeTypes(context);
+ createFilesTable();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ final Context context = InstrumentationRegistry.getTargetContext();
+
+ if (mDatabase != null) {
+ mDatabase.close();
+ }
+ context.deleteDatabase(DATABASE_FILE);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MIME_TYPE_FIX_FOR_ANDROID_15)
+ public void testGetMimeType() {
+ Optional<String> dwgMimeType = MimeTypeFixHandler.getMimeType("dwg");
+ assertTrue(dwgMimeType.isPresent());
+ assertEquals(ClipDescription.MIMETYPE_UNKNOWN, dwgMimeType.get());
+
+
+ Optional<String> avifMimeType = MimeTypeFixHandler.getMimeType("avif");
+ assertTrue(avifMimeType.isPresent());
+ assertEquals("image/avif", avifMimeType.get());
+
+
+ Optional<String> ecmascriptMimeType = MimeTypeFixHandler.getMimeType("es");
+ assertTrue(ecmascriptMimeType.isPresent());
+ assertEquals("application/ecmascript", ecmascriptMimeType.get());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MIME_TYPE_FIX_FOR_ANDROID_15)
+ public void testUpdateUnsupportedMimeTypesForWrongEntries() {
+ createEntriesInFilesTable();
+
+ // Assert incorrect MIME types before the fix
+ try (Cursor cursor = getCursorFilesTable()) {
+ assertNotNull(cursor);
+
+ while (cursor.moveToNext()) {
+ int id = cursor.getInt(cursor.getColumnIndexOrThrow(FileColumns._ID));
+ String mimeType = cursor.getString(
+ cursor.getColumnIndexOrThrow(FileColumns.MIME_TYPE));
+ int mediaType = cursor.getInt(cursor.getColumnIndexOrThrow(FileColumns.MEDIA_TYPE));
+
+ switch (id) {
+ case 1: // dwg
+ assertEquals("image/vnd.dwg", mimeType);
+ assertEquals(FileColumns.MEDIA_TYPE_IMAGE, mediaType);
+ break;
+ case 2: // avif
+ assertEquals("image/avif", mimeType);
+ assertEquals(FileColumns.MEDIA_TYPE_IMAGE, mediaType);
+ break;
+ case 3: // ecmascript
+ assertEquals("text/javascript", mimeType);
+ assertEquals(FileColumns.MEDIA_TYPE_DOCUMENT, mediaType);
+ break;
+ default:
+ fail("Unexpected _ID: " + id);
+ }
+ }
+ }
+
+ // fix the data
+ MimeTypeFixHandler.updateUnsupportedMimeTypes(mDatabase);
+
+ // Assert correct MIME types after the fix
+ try (Cursor cursor = getCursorFilesTable()) {
+ assertNotNull(cursor);
+
+ while (cursor.moveToNext()) {
+ int id = cursor.getInt(cursor.getColumnIndexOrThrow(FileColumns._ID));
+ String mimeType = cursor.getString(
+ cursor.getColumnIndexOrThrow(FileColumns.MIME_TYPE));
+ int mediaType = cursor.getInt(cursor.getColumnIndexOrThrow(FileColumns.MEDIA_TYPE));
+
+ switch (id) {
+ case 1: // dwg
+ assertEquals(ClipDescription.MIMETYPE_UNKNOWN, mimeType);
+ assertEquals(FileColumns.MEDIA_TYPE_NONE, mediaType);
+ break;
+ case 2: // avif
+ assertEquals("image/avif", mimeType);
+ assertEquals(FileColumns.MEDIA_TYPE_IMAGE, mediaType);
+ break;
+ case 3: // ecmascript
+ assertEquals("application/ecmascript", mimeType);
+ assertEquals(FileColumns.MEDIA_TYPE_NONE, mediaType);
+ break;
+ default:
+ fail("Unexpected _ID: " + id);
+ }
+ }
+ }
+ }
+
+ private void createFilesTable() {
+ mDatabase.execSQL("DROP TABLE IF EXISTS " + FILES_TABLE_NAME + ";");
+ mDatabase.execSQL("CREATE TABLE " + FILES_TABLE_NAME + " ("
+ + FileColumns._ID + " INTEGER PRIMARY KEY, "
+ + FileColumns.DATA + " TEXT, "
+ + FileColumns.DISPLAY_NAME + " TEXT, "
+ + FileColumns.MIME_TYPE + " TEXT, "
+ + FileColumns.MEDIA_TYPE + " INTEGER);");
+ }
+
+ private Cursor getCursorFilesTable() {
+ String[] projections = new String[]{
+ FileColumns._ID,
+ FileColumns.DATA,
+ FileColumns.DISPLAY_NAME,
+ FileColumns.MIME_TYPE,
+ FileColumns.MEDIA_TYPE,
+ };
+
+ return mDatabase.query(
+ FILES_TABLE_NAME,
+ projections,
+ null,
+ null,
+ null,
+ null,
+ FileColumns._ID
+ );
+ }
+
+ private void createEntriesInFilesTable() {
+ // dwg in corrupted mime types
+ String dwgFileName = "image1.dwg";
+ insertFileRecord("/path/" + dwgFileName, "image/vnd.dwg",
+ FileColumns.MEDIA_TYPE_IMAGE, dwgFileName);
+
+ // avif in corrupted mime types but also in android_mime_types
+ String avifFileName = "image2.avif";
+ insertFileRecord("/path/" + avifFileName, "image/avif",
+ FileColumns.MEDIA_TYPE_IMAGE, avifFileName);
+
+ String ecamascriptFileName = "file1.es";
+ insertFileRecord("/path/" + ecamascriptFileName, "text/javascript",
+ FileColumns.MEDIA_TYPE_DOCUMENT, ecamascriptFileName);
+ }
+
+ private void insertFileRecord(String data, String mimeType, int mediaType, String displayName) {
+ String sql = "INSERT INTO " + FILES_TABLE_NAME + " ("
+ + FileColumns.DATA + ", "
+ + FileColumns.MIME_TYPE + ", "
+ + FileColumns.MEDIA_TYPE + ", "
+ + FileColumns.DISPLAY_NAME + ") "
+ + "VALUES (?, ?, ?, ?);";
+
+ mDatabase.execSQL(sql, new Object[]{data, mimeType, mediaType, displayName});
+ }
+}
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
index 93def1738..eafc383a4 100644
--- a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -139,12 +139,18 @@ public class PermissionUtilsTest {
assertThat(checkPermissionReadStorage(context, pid, uid, packageName, null)).isTrue();
assertThat(checkPermissionWriteStorage(context, pid, uid, packageName, null)).isTrue();
- assertThat(checkPermissionReadAudio(context, pid, uid, packageName, null, false)).isTrue();
- assertThat(checkPermissionWriteAudio(context, pid, uid, packageName, null)).isFalse();
- assertThat(checkPermissionReadVideo(context, pid, uid, packageName, null, false)).isTrue();
- assertThat(checkPermissionWriteVideo(context, pid, uid, packageName, null)).isFalse();
- assertThat(checkPermissionReadImages(context, pid, uid, packageName, null, false)).isTrue();
- assertThat(checkPermissionWriteImages(context, pid, uid, packageName, null)).isFalse();
+ assertThat(checkPermissionReadAudio(context, pid, uid, packageName, null, false,
+ /* forDataDelivery */ true)).isTrue();
+ assertThat(checkPermissionWriteAudio(context, pid, uid, packageName, null,
+ /* forDataDelivery */ true)).isFalse();
+ assertThat(checkPermissionReadVideo(context, pid, uid, packageName, null, false,
+ /* forDataDelivery */ true)).isTrue();
+ assertThat(checkPermissionWriteVideo(context, pid, uid, packageName, null,
+ /* forDataDelivery */ true)).isFalse();
+ assertThat(checkPermissionReadImages(context, pid, uid, packageName, null, false,
+ /* forDataDelivery */ true)).isTrue();
+ assertThat(checkPermissionWriteImages(context, pid, uid, packageName, null,
+ /* forDataDelivery */ true)).isFalse();
assertThat(checkPermissionInstallPackages(context, pid, uid, packageName, null)).isFalse();
}
@@ -291,9 +297,9 @@ public class PermissionUtilsTest {
assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
true /* targetSdkIsAtLeastT */, false /* expected */);
assertThat(checkPermissionReadVisualUserSelected(getContext(), TEST_APP_PID, testAppUid,
- packageName, null, false)).isFalse();
+ packageName, null, false, /* forDataDelivery */ true)).isFalse();
assertThat(checkPermissionReadVisualUserSelected(getContext(), TEST_APP_PID, testAppUid,
- packageName, null, true)).isFalse();
+ packageName, null, true, /* forDataDelivery */ true)).isFalse();
} finally {
dropShellPermission();
}
@@ -462,19 +468,19 @@ public class PermissionUtilsTest {
try {
assertThat(checkPermissionReadVisualUserSelected(getApplicationContext(), TEST_APP_PID,
testAppUid,
- packageName, null, targetSdkIsAtLeastT)).isTrue();
+ packageName, null, targetSdkIsAtLeastT, /* forDataDelivery */ true)).isTrue();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_VISUAL_USER_SELECTED,
AppOpsManager.MODE_ERRORED);
assertThat(checkPermissionReadVisualUserSelected(getApplicationContext(), TEST_APP_PID,
testAppUid,
- packageName, null, targetSdkIsAtLeastT)).isFalse();
+ packageName, null, targetSdkIsAtLeastT, /* forDataDelivery */ true)).isFalse();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_VISUAL_USER_SELECTED,
AppOpsManager.MODE_ALLOWED);
assertThat(checkPermissionReadVisualUserSelected(getApplicationContext(), TEST_APP_PID,
testAppUid,
- packageName, null, targetSdkIsAtLeastT)).isTrue();
+ packageName, null, targetSdkIsAtLeastT, /* forDataDelivery */ true)).isTrue();
} finally {
dropShellPermission();
}
@@ -487,15 +493,15 @@ public class PermissionUtilsTest {
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
assertThat(checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid,
- packageName, null, isAtLeastT)).isTrue();
+ packageName, null, isAtLeastT, /* forDataDelivery */ true)).isTrue();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_VIDEO, AppOpsManager.MODE_ERRORED);
assertThat(checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid,
- packageName, null, isAtLeastT)).isFalse();
+ packageName, null, isAtLeastT, /* forDataDelivery */ true)).isFalse();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_VIDEO, AppOpsManager.MODE_ALLOWED);
// Adding sleep before appops check to allow appops change to propagate
SystemClock.sleep(200);
assertThat(checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid,
- packageName, null, isAtLeastT)).isTrue();
+ packageName, null, isAtLeastT, /* forDataDelivery */ true)).isTrue();
} finally {
dropShellPermission();
}
@@ -511,17 +517,17 @@ public class PermissionUtilsTest {
try {
assertThat(
checkPermissionWriteAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
- null)).isFalse();
+ null, /* forDataDelivery */ true)).isFalse();
modifyAppOp(testAppUid, OPSTR_WRITE_MEDIA_AUDIO, AppOpsManager.MODE_ALLOWED);
assertThat(
checkPermissionWriteAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
- null)).isTrue();
+ null, /* forDataDelivery */ true)).isTrue();
modifyAppOp(testAppUid, OPSTR_WRITE_MEDIA_AUDIO, AppOpsManager.MODE_ERRORED);
assertThat(
checkPermissionWriteAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
- null)).isFalse();
+ null, /* forDataDelivery */ true)).isFalse();
} finally {
dropShellPermission();
}
@@ -545,17 +551,17 @@ public class PermissionUtilsTest {
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
assertThat(checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid,
- packageName, null, isAtLeastT)).isTrue();
+ packageName, null, isAtLeastT, /* forDataDelivery */ true)).isTrue();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_AUDIO, AppOpsManager.MODE_ERRORED);
assertThat(checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid,
- packageName, null, isAtLeastT)).isFalse();
+ packageName, null, isAtLeastT, /* forDataDelivery */ true)).isFalse();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_AUDIO, AppOpsManager.MODE_ALLOWED);
// Adding sleep before appops check to allow appops change to propagate
SystemClock.sleep(200);
assertThat(checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid,
- packageName, null, isAtLeastT)).isTrue();
+ packageName, null, isAtLeastT, /* forDataDelivery */ true)).isTrue();
} finally {
dropShellPermission();
}
@@ -579,17 +585,17 @@ public class PermissionUtilsTest {
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
assertThat(checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid,
- packageName, null, isAtLeastT)).isTrue();
+ packageName, null, isAtLeastT, /* forDataDelivery */ true)).isTrue();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_IMAGES, AppOpsManager.MODE_ERRORED);
assertThat(checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid,
- packageName, null, isAtLeastT)).isFalse();
+ packageName, null, isAtLeastT, /* forDataDelivery */ true)).isFalse();
modifyAppOp(testAppUid, OPSTR_READ_MEDIA_IMAGES, AppOpsManager.MODE_ALLOWED);
// Adding sleep before appops check to allow appops change to propagate
SystemClock.sleep(200);
assertThat(checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid,
- packageName, null, isAtLeastT)).isTrue();
+ packageName, null, isAtLeastT, /* forDataDelivery */ true)).isTrue();
} finally {
dropShellPermission();
}
@@ -685,11 +691,14 @@ public class PermissionUtilsTest {
assertEquals(expected,
checkWriteImagesOrVideoAppOps(getContext(), uid, packageName, null));
assertEquals(expected,
- checkPermissionWriteImages(getContext(), pid, uid, packageName, null));
+ checkPermissionWriteImages(getContext(), pid, uid, packageName, null,
+ /* forDataDelivery */ true));
assertEquals(expected,
- checkPermissionWriteVideo(getContext(), pid, uid, packageName, null));
+ checkPermissionWriteVideo(getContext(), pid, uid, packageName, null,
+ /* forDataDelivery */ true));
assertThat(
- checkPermissionWriteAudio(getContext(), pid, uid, packageName, null))
+ checkPermissionWriteAudio(getContext(), pid, uid, packageName, null,
+ /* forDataDelivery */ true))
.isFalse();
}
@@ -698,14 +707,17 @@ public class PermissionUtilsTest {
assertEquals(
expected,
checkPermissionReadAudio(
- getContext(), pid, uid, packageName, null, targetSdkIsAtLeastT));
+ getContext(), pid, uid, packageName, null, targetSdkIsAtLeastT,
+ /* forDataDelivery */ true));
assertEquals(
expected,
checkPermissionReadImages(
- getContext(), pid, uid, packageName, null, targetSdkIsAtLeastT));
+ getContext(), pid, uid, packageName, null, targetSdkIsAtLeastT,
+ /* forDataDelivery */ true));
assertEquals(
expected,
checkPermissionReadVideo(
- getContext(), pid, uid, packageName, null, targetSdkIsAtLeastT));
+ getContext(), pid, uid, packageName, null, targetSdkIsAtLeastT,
+ /* forDataDelivery */ true));
}
}
diff --git a/tests/src/com/android/providers/media/util/SyntheticPathUtilsTest.java b/tests/src/com/android/providers/media/util/SyntheticPathUtilsTest.java
index 0c8c5df10..dbfafe75b 100644
--- a/tests/src/com/android/providers/media/util/SyntheticPathUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/SyntheticPathUtilsTest.java
@@ -54,6 +54,8 @@ public class SyntheticPathUtilsTest {
".transforms/synthetic/picker");
assertThat(getPickerRelativePath(PickerUriResolver.PICKER_GET_CONTENT_SEGMENT)).isEqualTo(
".transforms/synthetic/picker_get_content");
+ assertThat(getPickerRelativePath(PickerUriResolver.PICKER_TRANSCODED_SEGMENT)).isEqualTo(
+ ".transforms/synthetic/picker_transcoded");
}
@Test
@@ -100,6 +102,10 @@ public class SyntheticPathUtilsTest {
/* userId */ 0)).isTrue();
assertThat(isPickerPath("/storage/emulated/10/.transforms/synthetic/picker/foo",
/* userId */ 10)).isTrue();
+ assertThat(isPickerPath("/storage/emulated/0/.transforms/synthetic/picker_transcoded/foo",
+ /* userId */ 0)).isTrue();
+ assertThat(isPickerPath("/storage/emulated/10/.transforms/synthetic/picker_transcoded/foo",
+ /* userId */ 10)).isTrue();
assertThat(isPickerPath("/storage/emulated/0/.transforms/synthetic/PICKER/bar/baz",
/* userId */ 0)).isTrue();
@@ -107,6 +113,10 @@ public class SyntheticPathUtilsTest {
/* userId */ 10)).isFalse();
assertThat(isPickerPath("/storage/emulated/10/.transforms/synthetic/picker/foo",
/* userId */ 0)).isFalse();
+ assertThat(isPickerPath("/storage/emulated/0/.transforms/synthetic/picker_transcoded/foo",
+ /* userId */ 10)).isFalse();
+ assertThat(isPickerPath("/storage/emulated/10/.transforms/synthetic/picker_transcoded/foo",
+ /* userId */ 0)).isFalse();
assertThat(isPickerPath("/storage/emulated/0/.transforms/synthetic/redacted/foo",
/* userId */ 0)).isFalse();
assertThat(isPickerPath("/storage/emulated/0/.transforms/picker/foo", /* userId */ 0))