summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/framework/java/android/provider/MediaStore.java3
-rw-r--r--photopicker/src/com/android/photopicker/data/MediaProviderClient.kt43
-rw-r--r--photopicker/src/com/android/photopicker/data/PrefetchDataService.kt8
-rw-r--r--photopicker/src/com/android/photopicker/data/PrefetchDataServiceImpl.kt73
-rw-r--r--photopicker/src/com/android/photopicker/features/search/Search.kt6
-rw-r--r--photopicker/src/com/android/photopicker/features/search/SearchFeature.kt8
-rw-r--r--photopicker/src/com/android/photopicker/features/search/SearchViewModel.kt4
-rw-r--r--photopicker/src/com/android/photopicker/features/search/data/FakeSearchDataServiceImpl.kt96
-rw-r--r--photopicker/src/com/android/photopicker/features/search/data/SearchDataService.kt10
-rw-r--r--photopicker/src/com/android/photopicker/features/search/data/SearchDataServiceImpl.kt30
-rw-r--r--photopicker/src/com/android/photopicker/features/search/inject/SearchEmbeddedServiceModule.kt3
-rw-r--r--photopicker/src/com/android/photopicker/features/search/model/GlobalSearchState.kt (renamed from photopicker/src/com/android/photopicker/features/search/model/SearchEnabledState.kt)13
-rw-r--r--photopicker/src/com/android/photopicker/features/search/model/GlobalSearchStateInfo.kt46
-rw-r--r--photopicker/src/com/android/photopicker/features/search/model/UserSearchState.kt31
-rw-r--r--photopicker/src/com/android/photopicker/features/search/model/UserSearchStateInfo.kt27
-rw-r--r--photopicker/src/com/android/photopicker/inject/ActivityModule.kt18
-rw-r--r--photopicker/src/com/android/photopicker/inject/ApplicationModule.kt9
-rw-r--r--photopicker/src/com/android/photopicker/inject/EmbeddedServiceModule.kt18
-rw-r--r--photopicker/src/com/android/photopicker/util/MapOfDeferredWithTimeout.kt2
-rw-r--r--photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt14
-rw-r--r--photopicker/tests/src/com/android/photopicker/data/PrefetchDataServiceImplTest.kt361
-rw-r--r--photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt14
-rw-r--r--photopicker/tests/src/com/android/photopicker/data/TestPrefetchDataService.kt8
-rw-r--r--photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt42
-rw-r--r--photopicker/tests/src/com/android/photopicker/features/search/SearchFeatureTest.kt4
-rw-r--r--photopicker/tests/src/com/android/photopicker/features/search/TestSearchDataServiceImpl.kt6
-rw-r--r--photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt7
-rw-r--r--src/com/android/providers/media/MediaProvider.java15
-rw-r--r--src/com/android/providers/media/photopicker/SearchState.java2
-rw-r--r--src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java32
-rw-r--r--src/com/android/providers/media/photopicker/v2/sqlite/PickerSQLConstants.java2
31 files changed, 800 insertions, 155 deletions
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 19076f617..3c25ef22b 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -343,6 +343,9 @@ public final class MediaStore {
public static final String PICKER_MEDIA_IN_MEDIA_SET_INIT_CALL =
"picker_media_in_media_set_init";
/** {@hide} */
+ public static final String PICKER_GET_SEARCH_PROVIDERS_CALL =
+ "picker_internal_get_search_providers";
+ /** {@hide} */
public static final String PICKER_TRANSCODE_CALL = "picker_transcode";
/** {@hide} */
public static final String PICKER_TRANSCODE_RESULT = "picker_transcode_result";
diff --git a/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt b/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt
index 4733523dc..014630182 100644
--- a/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt
+++ b/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt
@@ -46,8 +46,9 @@ import com.android.photopicker.features.search.model.SearchSuggestionType
open class MediaProviderClient {
companion object {
private const val TAG = "MediaProviderClient"
- private const val MEDIA_INIT_CALL_METHOD: String = "picker_media_init"
- private const val SEARCH_REQUEST_INIT_CALL_METHOD = "picker_internal_search_media_init"
+ const val MEDIA_INIT_CALL_METHOD: String = "picker_media_init"
+ const val SEARCH_REQUEST_INIT_CALL_METHOD = "picker_internal_search_media_init"
+ const val GET_SEARCH_PROVIDERS_CALL_METHOD = "picker_internal_get_search_providers"
private const val EXTRA_MIME_TYPES = "mime_types"
private const val EXTRA_INTENT_ACTION = "intent_action"
private const val EXTRA_PROVIDERS = "providers"
@@ -56,6 +57,7 @@ open class MediaProviderClient {
private const val EXTRA_ALBUM_AUTHORITY = "album_authority"
private const val COLUMN_GRANTS_COUNT = "grants_count"
private const val PRE_SELECTION_URIS = "pre_selection_uris"
+ const val SEARCH_PROVIDER_AUTHORITIES = "search_provider_authorities"
const val SEARCH_REQUEST_ID = "search_request_id"
}
@@ -678,6 +680,43 @@ open class MediaProviderClient {
}
}
+ /**
+ * Get available search providers from the Media Provider client using the available
+ * [ContentResolver].
+ *
+ * If the available providers are known at the time of the query, this method will filter the
+ * results of the call so that search providers are a subset of the available providers.
+ *
+ * @param resolver The [ContentResolver] that resolves to the desired instance of MediaProvider.
+ * (This may resolve in a cross profile instance of MediaProvider).
+ * @param availableProviders
+ */
+ suspend fun fetchSearchProviderAuthorities(
+ resolver: ContentResolver,
+ availableProviders: List<Provider>? = null,
+ ): List<String>? {
+ try {
+ val availableProviderAuthorities: Set<String>? =
+ availableProviders?.map { it.authority }?.toSet()
+ val result: Bundle? =
+ resolver.call(
+ MEDIA_PROVIDER_AUTHORITY,
+ GET_SEARCH_PROVIDERS_CALL_METHOD,
+ /* arg */ null,
+ /* extras */ null,
+ )
+ return result?.getStringArrayList(SEARCH_PROVIDER_AUTHORITIES)?.filter {
+ availableProviderAuthorities?.contains(it) ?: true
+ }
+ } catch (e: RuntimeException) {
+ // If we can't fetch the available providers, basic functionality of photopicker does
+ // not work. In order to catch this earlier in testing, throw an error instead of
+ // silencing it.
+ Log.e(TAG, "Could not fetch providers with search enabled", e)
+ return null
+ }
+ }
+
/** Creates a list of [Provider] from the given [Cursor]. */
private fun getListOfProviders(cursor: Cursor): List<Provider> {
val result: MutableList<Provider> = mutableListOf<Provider>()
diff --git a/photopicker/src/com/android/photopicker/data/PrefetchDataService.kt b/photopicker/src/com/android/photopicker/data/PrefetchDataService.kt
index a23b139a2..a151b70b4 100644
--- a/photopicker/src/com/android/photopicker/data/PrefetchDataService.kt
+++ b/photopicker/src/com/android/photopicker/data/PrefetchDataService.kt
@@ -16,7 +16,7 @@
package com.android.photopicker.data
-import com.android.photopicker.features.search.model.SearchEnabledState
+import com.android.photopicker.features.search.model.GlobalSearchState
/** Class responsible to fetch all the required data before feature initialization */
interface PrefetchDataService {
@@ -24,5 +24,9 @@ interface PrefetchDataService {
val TAG: String = "PrefetchDataService"
}
- suspend fun getSearchState(): SearchEnabledState
+ /**
+ * Get the global search state from the Data Source. The global search state refers to the
+ * search state of all providers in all user profiles.
+ */
+ suspend fun getGlobalSearchState(): GlobalSearchState
}
diff --git a/photopicker/src/com/android/photopicker/data/PrefetchDataServiceImpl.kt b/photopicker/src/com/android/photopicker/data/PrefetchDataServiceImpl.kt
index 2d2044ba5..fada902ce 100644
--- a/photopicker/src/com/android/photopicker/data/PrefetchDataServiceImpl.kt
+++ b/photopicker/src/com/android/photopicker/data/PrefetchDataServiceImpl.kt
@@ -16,11 +16,76 @@
package com.android.photopicker.data
-import com.android.photopicker.features.search.model.SearchEnabledState
+import android.content.Context
+import android.util.Log
+import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.core.user.UserProfile
+import com.android.photopicker.features.search.model.GlobalSearchState
+import com.android.photopicker.features.search.model.GlobalSearchStateInfo
+import com.android.photopicker.util.mapOfDeferredWithTimeout
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Deferred
-class PrefetchDataServiceImpl() : PrefetchDataService {
+/** Implementation of [PrefetchDataService] that typically fetches data from MediaProvider. */
+class PrefetchDataServiceImpl(
+ val mediaProviderClient: MediaProviderClient,
+ val userMonitor: UserMonitor,
+ val context: Context,
+ val dispatcher: CoroutineDispatcher,
+) : PrefetchDataService {
- override suspend fun getSearchState(): SearchEnabledState {
- return SearchEnabledState.DISABLED
+ override suspend fun getGlobalSearchState(): GlobalSearchState {
+ // Create a map of user id to lambda that fetches search provider authorities for that
+ // user.
+ val inputMap: Map<Int, suspend (MediaProviderClient) -> Any?> =
+ userMonitor.userStatus.value.allProfiles
+ .map { profile: UserProfile ->
+ val lambda: suspend (MediaProviderClient) -> Any? =
+ { mediaProviderClient: MediaProviderClient ->
+ mediaProviderClient.fetchSearchProviderAuthorities(
+ context
+ .createPackageContextAsUser(
+ context.packageName, /* flags */
+ 0,
+ profile.handle,
+ )
+ .contentResolver
+ )
+ }
+ profile.identifier to lambda
+ }
+ .toMap()
+
+ // Get a map of user id to Deferred task that fetches search provider authorities for
+ // that user in parallel with a timeout.
+ val deferredMap: Map<Int, Deferred<Any?>> =
+ mapOfDeferredWithTimeout(
+ inputMap = inputMap,
+ input = mediaProviderClient,
+ timeoutMillis = 100L,
+ )
+
+ // Await all the deferred tasks and create a map of user id to the search provider
+ // authorities.
+ @Suppress("UNCHECKED_CAST")
+ val globalSearchProviders: Map<Int, List<String>?> =
+ deferredMap
+ .map {
+ val searchProviders: Any? = it.value.await()
+ it.key to if (searchProviders is List<*>?) searchProviders else null
+ }
+ .toMap() as Map<Int, List<String>?>
+
+ val globalSearchStateInfo =
+ GlobalSearchStateInfo(
+ globalSearchProviders,
+ userMonitor.userStatus.value.activeUserProfile.identifier,
+ )
+ Log.d(
+ PrefetchDataService.TAG,
+ "Global search providers available are $globalSearchProviders. " +
+ "Search state is $globalSearchStateInfo.state",
+ )
+ return globalSearchStateInfo.state
}
}
diff --git a/photopicker/src/com/android/photopicker/features/search/Search.kt b/photopicker/src/com/android/photopicker/features/search/Search.kt
index 400d13c83..cb6097c3f 100644
--- a/photopicker/src/com/android/photopicker/features/search/Search.kt
+++ b/photopicker/src/com/android/photopicker/features/search/Search.kt
@@ -104,9 +104,9 @@ import com.android.photopicker.core.selection.LocalSelection
import com.android.photopicker.core.theme.LocalWindowSizeClass
import com.android.photopicker.extensions.navigateToPreviewMedia
import com.android.photopicker.features.preview.PreviewFeature
-import com.android.photopicker.features.search.model.SearchEnabledState
import com.android.photopicker.features.search.model.SearchSuggestion
import com.android.photopicker.features.search.model.SearchSuggestionType
+import com.android.photopicker.features.search.model.UserSearchState
import com.android.photopicker.util.rememberBitmapFromUri
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@@ -161,9 +161,9 @@ fun Search(
params: LocationParams,
viewModel: SearchViewModel = obtainViewModel(),
) {
- val searchEnabled by viewModel.searchEnabled.collectAsStateWithLifecycle()
+ val userSearchStateInfo by viewModel.userSearchStateInfo.collectAsStateWithLifecycle()
when {
- searchEnabled == SearchEnabledState.ENABLED -> {
+ userSearchStateInfo.state == UserSearchState.ENABLED -> {
SearchBarEnabled(params, viewModel, modifier)
}
else -> {
diff --git a/photopicker/src/com/android/photopicker/features/search/SearchFeature.kt b/photopicker/src/com/android/photopicker/features/search/SearchFeature.kt
index 26ed22801..343f5776e 100644
--- a/photopicker/src/com/android/photopicker/features/search/SearchFeature.kt
+++ b/photopicker/src/com/android/photopicker/features/search/SearchFeature.kt
@@ -31,7 +31,7 @@ import com.android.photopicker.core.features.PhotopickerUiFeature
import com.android.photopicker.core.features.PrefetchResultKey
import com.android.photopicker.core.features.Priority
import com.android.photopicker.data.PrefetchDataService
-import com.android.photopicker.features.search.model.SearchEnabledState
+import com.android.photopicker.features.search.model.GlobalSearchState
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.runBlocking
@@ -51,7 +51,7 @@ class SearchFeature : PhotopickerUiFeature {
mapOf(
PrefetchResultKey.SEARCH_STATE to
{ prefetchDataService ->
- prefetchDataService.getSearchState()
+ prefetchDataService.getGlobalSearchState()
}
)
} else {
@@ -72,7 +72,9 @@ class SearchFeature : PhotopickerUiFeature {
val searchStatus: Any? =
deferredPrefetchResultsMap[PrefetchResultKey.SEARCH_STATE]?.await()
when (searchStatus) {
- is SearchEnabledState -> searchStatus == SearchEnabledState.ENABLED
+ is GlobalSearchState ->
+ searchStatus == GlobalSearchState.ENABLED ||
+ searchStatus == GlobalSearchState.ENABLED_IN_OTHER_PROFILES_ONLY
else -> false // prefetch may have timed out
}
}
diff --git a/photopicker/src/com/android/photopicker/features/search/SearchViewModel.kt b/photopicker/src/com/android/photopicker/features/search/SearchViewModel.kt
index e55bd7cd0..47f001991 100644
--- a/photopicker/src/com/android/photopicker/features/search/SearchViewModel.kt
+++ b/photopicker/src/com/android/photopicker/features/search/SearchViewModel.kt
@@ -35,9 +35,9 @@ import com.android.photopicker.data.model.Media
import com.android.photopicker.extensions.insertMonthSeparators
import com.android.photopicker.extensions.toMediaGridItemFromMedia
import com.android.photopicker.features.search.data.SearchDataService
-import com.android.photopicker.features.search.model.SearchEnabledState
import com.android.photopicker.features.search.model.SearchSuggestion
import com.android.photopicker.features.search.model.SearchSuggestionType
+import com.android.photopicker.features.search.model.UserSearchStateInfo
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -109,7 +109,7 @@ constructor(
*
* This `StateFlow` emits updates whenever the search enabled state of a profile changes.
*/
- val searchEnabled: StateFlow<SearchEnabledState> = searchDataService.isSearchEnabled
+ val userSearchStateInfo: StateFlow<UserSearchStateInfo> = searchDataService.userSearchStateInfo
private val suggestionCache = SearchSuggestionCache()
diff --git a/photopicker/src/com/android/photopicker/features/search/data/FakeSearchDataServiceImpl.kt b/photopicker/src/com/android/photopicker/features/search/data/FakeSearchDataServiceImpl.kt
deleted file mode 100644
index 3e6adbeea..000000000
--- a/photopicker/src/com/android/photopicker/features/search/data/FakeSearchDataServiceImpl.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright 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.photopicker.features.search.data
-
-import android.net.Uri
-import android.os.CancellationSignal
-import androidx.paging.PagingSource
-import com.android.photopicker.data.DataService
-import com.android.photopicker.data.model.Media
-import com.android.photopicker.data.model.MediaPageKey
-import com.android.photopicker.features.search.model.SearchEnabledState
-import com.android.photopicker.features.search.model.SearchSuggestion
-import com.android.photopicker.features.search.model.SearchSuggestionType
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-
-/**
- * Placeholder for the actual [SearchDataService] implementation class. This class can be used to
- * unblock and test UI development till we have the actual implementation in ready.
- */
-// TODO(b/361043596) Clean up once we have the implementation for [SearchDataService] class.
-class FakeSearchDataServiceImpl(private val dataService: DataService) : SearchDataService {
- // Use the internal flow of type StateFlow<Map<UserProfile, Boolean>> which would cache
- // the result for all profiles, to populate this flow for the current profile.
- override val isSearchEnabled: StateFlow<SearchEnabledState> =
- MutableStateFlow(SearchEnabledState.ENABLED)
-
- /** Returns a few static suggestions to unblock UI development. */
- override suspend fun getSearchSuggestions(
- prefix: String,
- limit: Int,
- cancellationSignal: CancellationSignal?,
- ): List<SearchSuggestion> {
- if (prefix == "testempty") {
- return emptyList()
- }
- return listOf(
- SearchSuggestion("1", "authority", "France", SearchSuggestionType.LOCATION, null),
- SearchSuggestion(
- "2",
- "authority",
- "Favorites",
- SearchSuggestionType.FAVORITES_ALBUM,
- Uri.parse("xyz"),
- ),
- SearchSuggestion(
- "8",
- "authority",
- "Album",
- SearchSuggestionType.ALBUM,
- Uri.parse("xyz"),
- ),
- SearchSuggestion("2", "authority", "Videos", SearchSuggestionType.VIDEOS_ALBUM, null),
- SearchSuggestion(null, "authority", "france", SearchSuggestionType.HISTORY, null),
- SearchSuggestion(null, "authority", "paris", SearchSuggestionType.HISTORY, null),
- SearchSuggestion("3", "authority", "March", SearchSuggestionType.DATE, null),
- SearchSuggestion(
- "3",
- "authority",
- "Screenshot",
- SearchSuggestionType.SCREENSHOTS_ALBUM,
- null,
- ),
- SearchSuggestion("4", "authority", "Emma", SearchSuggestionType.FACE, Uri.parse("xyz")),
- SearchSuggestion("5", "authority", "Bob", SearchSuggestionType.FACE, Uri.parse("xyz")),
- SearchSuggestion("6", "authority", "April", SearchSuggestionType.DATE, null),
- SearchSuggestion("7", "authority", null, SearchSuggestionType.FACE, Uri.parse("xyz")),
- )
- }
-
- /** Returns all media to unblock UI development. */
- override fun getSearchResults(
- suggestion: SearchSuggestion,
- cancellationSignal: CancellationSignal?,
- ): PagingSource<MediaPageKey, Media> = dataService.mediaPagingSource()
-
- /** Returns all media to unblock UI development. */
- override fun getSearchResults(
- searchText: String,
- cancellationSignal: CancellationSignal?,
- ): PagingSource<MediaPageKey, Media> = dataService.mediaPagingSource()
-}
diff --git a/photopicker/src/com/android/photopicker/features/search/data/SearchDataService.kt b/photopicker/src/com/android/photopicker/features/search/data/SearchDataService.kt
index d04aac74b..9bb5691e5 100644
--- a/photopicker/src/com/android/photopicker/features/search/data/SearchDataService.kt
+++ b/photopicker/src/com/android/photopicker/features/search/data/SearchDataService.kt
@@ -20,8 +20,8 @@ import android.os.CancellationSignal
import androidx.paging.PagingSource
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaPageKey
-import com.android.photopicker.features.search.model.SearchEnabledState
import com.android.photopicker.features.search.model.SearchSuggestion
+import com.android.photopicker.features.search.model.UserSearchStateInfo
import kotlinx.coroutines.flow.StateFlow
/**
@@ -37,11 +37,11 @@ interface SearchDataService {
}
/**
- * A [StateFlow] that emits a value when current profile changes or search config in the data
- * source changes. It hold that value of the current profile's search enabled state
- * [SearchEnabledState].
+ * A [StateFlow] that emits a value when current profile changes or the current profile's
+ * available provider changes. It hold that value of the current profile's search enabled state
+ * [UserSearchStateInfo].
*/
- val isSearchEnabled: StateFlow<SearchEnabledState>
+ val userSearchStateInfo: StateFlow<UserSearchStateInfo>
/**
* Get search suggestions for the user in zero state and as the user is typing.
diff --git a/photopicker/src/com/android/photopicker/features/search/data/SearchDataServiceImpl.kt b/photopicker/src/com/android/photopicker/features/search/data/SearchDataServiceImpl.kt
index f2ad67545..c95db2064 100644
--- a/photopicker/src/com/android/photopicker/features/search/data/SearchDataServiceImpl.kt
+++ b/photopicker/src/com/android/photopicker/features/search/data/SearchDataServiceImpl.kt
@@ -29,14 +29,15 @@ import com.android.photopicker.data.NotificationService
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaPageKey
import com.android.photopicker.data.model.Provider
-import com.android.photopicker.features.search.model.SearchEnabledState
import com.android.photopicker.features.search.model.SearchRequest
import com.android.photopicker.features.search.model.SearchSuggestion
+import com.android.photopicker.features.search.model.UserSearchStateInfo
import java.util.concurrent.TimeoutException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
@@ -106,13 +107,17 @@ class SearchDataServiceImpl(
searchResultsPagingSources.clear()
searchRequestIdMap.clear()
}
+
+ _userSearchStateInfo.update { fetchSearchStateInfo() }
}
}
}
- // TODO(b/381819838)
- override val isSearchEnabled: StateFlow<SearchEnabledState> =
- MutableStateFlow(SearchEnabledState.ENABLED)
+ // Internal mutable flow of the current user's search state info.
+ private val _userSearchStateInfo: MutableStateFlow<UserSearchStateInfo> =
+ MutableStateFlow(UserSearchStateInfo(null))
+
+ override val userSearchStateInfo: StateFlow<UserSearchStateInfo> = _userSearchStateInfo
/**
* Try to get a list fo search suggestions from Media Provider in the background thread with a
@@ -287,4 +292,21 @@ class SearchDataServiceImpl(
}
}
}
+
+ /** Get search state info for the current user. */
+ private suspend fun fetchSearchStateInfo(): UserSearchStateInfo {
+ val contentResolver: ContentResolver = dataService.activeContentResolver.value
+ val searchProviderAuthorities: List<String>? =
+ mediaProviderClient.fetchSearchProviderAuthorities(
+ contentResolver,
+ dataService.availableProviders.value,
+ )
+ val userSearchStateInfo = UserSearchStateInfo(searchProviderAuthorities)
+ Log.d(
+ SearchDataService.TAG,
+ "Available search providers for current user $searchProviderAuthorities. " +
+ "Search state is ${userSearchStateInfo.state}",
+ )
+ return userSearchStateInfo
+ }
}
diff --git a/photopicker/src/com/android/photopicker/features/search/inject/SearchEmbeddedServiceModule.kt b/photopicker/src/com/android/photopicker/features/search/inject/SearchEmbeddedServiceModule.kt
index 409a40c23..b50d72f19 100644
--- a/photopicker/src/com/android/photopicker/features/search/inject/SearchEmbeddedServiceModule.kt
+++ b/photopicker/src/com/android/photopicker/features/search/inject/SearchEmbeddedServiceModule.kt
@@ -63,6 +63,7 @@ class SearchEmbeddedServiceModule {
configurationManager: ConfigurationManager,
@Background scope: CoroutineScope,
@Background dispatcher: CoroutineDispatcher,
+ mediaProviderClient: MediaProviderClient,
notificationService: NotificationService,
events: Events,
): SearchDataService {
@@ -83,7 +84,7 @@ class SearchEmbeddedServiceModule {
scope,
dispatcher,
notificationService,
- MediaProviderClient(),
+ mediaProviderClient,
events,
)
return searchDataService
diff --git a/photopicker/src/com/android/photopicker/features/search/model/SearchEnabledState.kt b/photopicker/src/com/android/photopicker/features/search/model/GlobalSearchState.kt
index f969e62d3..1718a21cb 100644
--- a/photopicker/src/com/android/photopicker/features/search/model/SearchEnabledState.kt
+++ b/photopicker/src/com/android/photopicker/features/search/model/GlobalSearchState.kt
@@ -16,13 +16,18 @@
package com.android.photopicker.features.search.model
-/** This represents the search enabled states the current profile could have. */
-enum class SearchEnabledState {
+/**
+ * This represents valid global search states.
+ *
+ * Global search state refers to the search state of all user profiles available on the device.
+ */
+enum class GlobalSearchState() {
/* Search is enabled for the current profile */
ENABLED,
- /* Search is disabled in the current profile but enabled in other profiles */
+ /* Search is disabled in the current profile but enabled in at least one of the
+ * other profiles */
ENABLED_IN_OTHER_PROFILES_ONLY,
- /* Search is disabled in all profiles */
+ /* Search is disabled in current profile and other profiles */
DISABLED,
/* Either the state of the current profile is unknown, or the current profile has search
* disabled and the state of other profile(s) is unknown. */
diff --git a/photopicker/src/com/android/photopicker/features/search/model/GlobalSearchStateInfo.kt b/photopicker/src/com/android/photopicker/features/search/model/GlobalSearchStateInfo.kt
new file mode 100644
index 000000000..eb3326384
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/features/search/model/GlobalSearchStateInfo.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 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.photopicker.features.search.model
+
+/** Holds search state info for all user profiles on the device. */
+data class GlobalSearchStateInfo(
+ // Map of all available profiles to the provider authorities that have search feature
+ // enabled. If no providers have search enabled, tha value in map should be an empty string.
+ // If the information is unknown for a given profile, the value in map should be null.
+ val providersWithSearchEnabled: Map<Int, List<String>?>,
+ val currentUserId: Int,
+) {
+ val state: GlobalSearchState =
+ when {
+ // Check if search is enabled in current profile
+ providersWithSearchEnabled[currentUserId]?.isNotEmpty() ?: false ->
+ GlobalSearchState.ENABLED
+
+ // Check if search is enabled in any other profile
+ providersWithSearchEnabled.values.any { providers ->
+ providers?.isNotEmpty() ?: false
+ } -> GlobalSearchState.ENABLED_IN_OTHER_PROFILES_ONLY
+
+ // Check if there is missing information
+ providersWithSearchEnabled.values.any { providers -> providers == null } ->
+ GlobalSearchState.UNKNOWN
+
+ // If we have all information and search is not enabled in any profile,
+ // search is disabled.
+ else -> GlobalSearchState.DISABLED
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/features/search/model/UserSearchState.kt b/photopicker/src/com/android/photopicker/features/search/model/UserSearchState.kt
new file mode 100644
index 000000000..c9bfaf3d2
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/features/search/model/UserSearchState.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 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.photopicker.features.search.model
+
+/**
+ * This represents valid user search states.
+ *
+ * User search state refers to the search state of the current selected profile in a Picker session.
+ */
+enum class UserSearchState() {
+ /* Search is enabled in the current profile */
+ ENABLED,
+ /* Search is disabled in the current profile */
+ DISABLED,
+ /* Search state for the current profile is unknown */
+ UNKNOWN,
+}
diff --git a/photopicker/src/com/android/photopicker/features/search/model/UserSearchStateInfo.kt b/photopicker/src/com/android/photopicker/features/search/model/UserSearchStateInfo.kt
new file mode 100644
index 000000000..885cbf218
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/features/search/model/UserSearchStateInfo.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 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.photopicker.features.search.model
+
+/** Holds search state info for the current selected profile in a Picker session. */
+data class UserSearchStateInfo(val searchProviderAuthorities: List<String>?) {
+ val state: UserSearchState =
+ when {
+ searchProviderAuthorities == null -> UserSearchState.UNKNOWN
+ searchProviderAuthorities.isEmpty() -> UserSearchState.DISABLED
+ else -> UserSearchState.ENABLED
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/inject/ActivityModule.kt b/photopicker/src/com/android/photopicker/inject/ActivityModule.kt
index dc69be86a..d95d30733 100644
--- a/photopicker/src/com/android/photopicker/inject/ActivityModule.kt
+++ b/photopicker/src/com/android/photopicker/inject/ActivityModule.kt
@@ -194,6 +194,7 @@ class ActivityModule {
@Background scope: CoroutineScope,
@Background dispatcher: CoroutineDispatcher,
userMonitor: UserMonitor,
+ mediaProviderClient: MediaProviderClient,
notificationService: NotificationService,
configurationManager: ConfigurationManager,
featureManager: FeatureManager,
@@ -212,7 +213,7 @@ class ActivityModule {
scope,
dispatcher,
notificationService,
- MediaProviderClient(),
+ mediaProviderClient,
configurationManager.configuration,
featureManager,
appContext,
@@ -311,7 +312,12 @@ class ActivityModule {
@Provides
@ActivityRetainedScoped
- fun providePrefetchDataService(): PrefetchDataService {
+ fun providePrefetchDataService(
+ userMonitor: UserMonitor,
+ @ApplicationContext context: Context,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ mediaProviderClient: MediaProviderClient,
+ ): PrefetchDataService {
if (!::prefetchDataService.isInitialized) {
Log.d(
@@ -319,7 +325,13 @@ class ActivityModule {
"PrefetchDataService requested but not yet initialized. " +
"Initializing PrefetchDataService.",
)
- prefetchDataService = PrefetchDataServiceImpl()
+ prefetchDataService =
+ PrefetchDataServiceImpl(
+ mediaProviderClient,
+ userMonitor,
+ context,
+ backgroundDispatcher,
+ )
}
return prefetchDataService
}
diff --git a/photopicker/src/com/android/photopicker/inject/ApplicationModule.kt b/photopicker/src/com/android/photopicker/inject/ApplicationModule.kt
index cdadb5295..f9f3a45d8 100644
--- a/photopicker/src/com/android/photopicker/inject/ApplicationModule.kt
+++ b/photopicker/src/com/android/photopicker/inject/ApplicationModule.kt
@@ -22,6 +22,7 @@ import android.util.Log
import com.android.photopicker.core.configuration.DeviceConfigProxy
import com.android.photopicker.core.configuration.DeviceConfigProxyImpl
import com.android.photopicker.core.network.NetworkMonitor
+import com.android.photopicker.data.MediaProviderClient
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -78,6 +79,12 @@ class ApplicationModule {
return DeviceConfigProxyImpl()
}
+ /** Provider for [MediaProviderClient]. */
+ @Provides
+ fun providerMediaProviderClient(): MediaProviderClient {
+ return MediaProviderClient()
+ }
+
/**
* Provider for the [NetworkMonitor]. This is lazily initialized only when requested to save on
* initialization costs of this module.
@@ -93,7 +100,7 @@ class ApplicationModule {
} else {
Log.d(
NetworkMonitor.TAG,
- "NetworkMonitor requested, but not yet initialized. Initializing NetworkMonitor."
+ "NetworkMonitor requested, but not yet initialized. Initializing NetworkMonitor.",
)
networkMonitor = NetworkMonitor(context, scope)
return networkMonitor
diff --git a/photopicker/src/com/android/photopicker/inject/EmbeddedServiceModule.kt b/photopicker/src/com/android/photopicker/inject/EmbeddedServiceModule.kt
index fd87f4a86..8821d56bc 100644
--- a/photopicker/src/com/android/photopicker/inject/EmbeddedServiceModule.kt
+++ b/photopicker/src/com/android/photopicker/inject/EmbeddedServiceModule.kt
@@ -254,6 +254,7 @@ class EmbeddedServiceModule {
@ApplicationContext appContext: Context,
events: Events,
processOwnerHandle: UserHandle,
+ mediaProviderClient: MediaProviderClient,
): DataService {
if (!::dataService.isInitialized) {
@@ -267,7 +268,7 @@ class EmbeddedServiceModule {
scope,
dispatcher,
notificationService,
- MediaProviderClient(),
+ mediaProviderClient,
configurationManager.configuration,
featureManager,
appContext,
@@ -389,7 +390,12 @@ class EmbeddedServiceModule {
@Provides
@SessionScoped
- fun providePrefetchDataService(): PrefetchDataService {
+ fun providePrefetchDataService(
+ userMonitor: UserMonitor,
+ @ApplicationContext context: Context,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ mediaProviderClient: MediaProviderClient,
+ ): PrefetchDataService {
if (!::prefetchDataService.isInitialized) {
Log.d(
@@ -397,7 +403,13 @@ class EmbeddedServiceModule {
"PrefetchDataService requested but not yet initialized. " +
"Initializing PrefetchDataService.",
)
- prefetchDataService = PrefetchDataServiceImpl()
+ prefetchDataService =
+ PrefetchDataServiceImpl(
+ mediaProviderClient,
+ userMonitor,
+ context,
+ backgroundDispatcher,
+ )
}
return prefetchDataService
}
diff --git a/photopicker/src/com/android/photopicker/util/MapOfDeferredWithTimeout.kt b/photopicker/src/com/android/photopicker/util/MapOfDeferredWithTimeout.kt
index 776e59f60..11e992272 100644
--- a/photopicker/src/com/android/photopicker/util/MapOfDeferredWithTimeout.kt
+++ b/photopicker/src/com/android/photopicker/util/MapOfDeferredWithTimeout.kt
@@ -52,7 +52,7 @@ suspend fun <A, B> mapOfDeferredWithTimeout(
result
}
} catch (e: RuntimeException) {
- Log.e(TAG, "An error occurred in fetching result for key: $key")
+ Log.e(TAG, "An error occurred in fetching result for key: $key", e)
null
}
}
diff --git a/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt b/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt
index d74cc8f7e..49540e5e4 100644
--- a/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt
@@ -76,8 +76,6 @@ import org.mockito.Mockito.verify
@OptIn(ExperimentalCoroutinesApi::class)
class DataServiceImplTest {
- val testSessionId = generatePickerSessionId()
-
companion object {
private fun createUserHandle(userId: Int = 0): UserHandle {
val parcel = Parcel.obtain()
@@ -191,7 +189,7 @@ class DataServiceImplTest {
defaultConfiguration =
PhotopickerConfiguration(
action = "TEST_ACTION",
- sessionId = testSessionId,
+ sessionId = sessionId,
flags =
PhotopickerFlags(
CLOUD_MEDIA_ENABLED = true,
@@ -219,7 +217,7 @@ class DataServiceImplTest {
defaultConfiguration =
PhotopickerConfiguration(
action = "TEST_ACTION",
- sessionId = testSessionId,
+ sessionId = sessionId,
flags =
PhotopickerFlags(
CLOUD_MEDIA_ENABLED = true,
@@ -358,7 +356,7 @@ class DataServiceImplTest {
defaultConfiguration =
PhotopickerConfiguration(
action = "TEST_ACTION",
- sessionId = testSessionId,
+ sessionId = sessionId,
flags =
PhotopickerFlags(
CLOUD_MEDIA_ENABLED = true,
@@ -385,7 +383,7 @@ class DataServiceImplTest {
this.backgroundScope,
PhotopickerConfiguration(
action = "TEST_ACTION",
- sessionId = testSessionId,
+ sessionId = sessionId,
flags =
PhotopickerFlags(
CLOUD_MEDIA_ENABLED = true,
@@ -1052,7 +1050,7 @@ class DataServiceImplTest {
defaultConfiguration =
PhotopickerConfiguration(
action = "TEST_ACTION",
- sessionId = testSessionId,
+ sessionId = sessionId,
flags =
PhotopickerFlags(
CLOUD_MEDIA_ENABLED = true,
@@ -1080,7 +1078,7 @@ class DataServiceImplTest {
defaultConfiguration =
PhotopickerConfiguration(
action = "TEST_ACTION",
- sessionId = testSessionId,
+ sessionId = sessionId,
flags =
PhotopickerFlags(
CLOUD_MEDIA_ENABLED = true,
diff --git a/photopicker/tests/src/com/android/photopicker/data/PrefetchDataServiceImplTest.kt b/photopicker/tests/src/com/android/photopicker/data/PrefetchDataServiceImplTest.kt
new file mode 100644
index 000000000..02d1ceddf
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/data/PrefetchDataServiceImplTest.kt
@@ -0,0 +1,361 @@
+/*
+ * 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 src.com.android.photopicker.data
+
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.pm.UserProperties
+import android.os.Parcel
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.MediaStore
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
+import com.android.photopicker.R
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
+import com.android.photopicker.core.configuration.provideTestConfigurationFlow
+import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.core.user.UserProfile
+import com.android.photopicker.data.MediaProviderClient
+import com.android.photopicker.data.PrefetchDataServiceImpl
+import com.android.photopicker.data.TestMediaProvider
+import com.android.photopicker.features.search.model.GlobalSearchState
+import com.android.photopicker.util.test.mockSystemService
+import com.android.photopicker.util.test.nonNullableEq
+import com.android.photopicker.util.test.whenever
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+public class PrefetchDataServiceImplTest {
+ @Mock private lateinit var mockPrimaryUserContext: Context
+ @Mock private lateinit var mockManagedUserContext: Context
+ @Mock private lateinit var mockUserManager: UserManager
+ @Mock private lateinit var mockPackageManager: PackageManager
+ @Mock private lateinit var mockResolveInfo: ResolveInfo
+
+ private lateinit var testPrimaryUserContentProvider: TestMediaProvider
+ private lateinit var testManagedUserContentProvider: TestMediaProvider
+ private lateinit var testPrimaryUserContentResolver: ContentResolver
+ private lateinit var testManagedUserContentResolver: ContentResolver
+
+ private val PLATFORM_PROVIDED_PROFILE_LABEL = "Platform Label"
+
+ private val USER_HANDLE_PRIMARY: UserHandle
+ private val USER_ID_PRIMARY: Int = 0
+ private val PRIMARY_PROFILE_BASE: UserProfile
+
+ private val USER_HANDLE_MANAGED: UserHandle
+ private val USER_ID_MANAGED: Int = 10
+ private val MANAGED_PROFILE_BASE: UserProfile
+
+ init {
+ val parcel1 = Parcel.obtain()
+ parcel1.writeInt(USER_ID_PRIMARY)
+ parcel1.setDataPosition(0)
+ USER_HANDLE_PRIMARY = UserHandle(parcel1)
+ parcel1.recycle()
+
+ PRIMARY_PROFILE_BASE =
+ UserProfile(
+ handle = USER_HANDLE_PRIMARY,
+ profileType = UserProfile.ProfileType.PRIMARY,
+ label = PLATFORM_PROVIDED_PROFILE_LABEL,
+ )
+
+ val parcel2 = Parcel.obtain()
+ parcel2.writeInt(USER_ID_MANAGED)
+ parcel2.setDataPosition(0)
+ USER_HANDLE_MANAGED = UserHandle(parcel2)
+ parcel2.recycle()
+
+ MANAGED_PROFILE_BASE =
+ UserProfile(
+ handle = USER_HANDLE_MANAGED,
+ profileType = UserProfile.ProfileType.MANAGED,
+ label = PLATFORM_PROVIDED_PROFILE_LABEL,
+ )
+ }
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources()
+
+ testPrimaryUserContentProvider = TestMediaProvider()
+ testPrimaryUserContentResolver = ContentResolver.wrap(testPrimaryUserContentProvider)
+ testManagedUserContentProvider = TestMediaProvider()
+ testManagedUserContentResolver = ContentResolver.wrap(testManagedUserContentProvider)
+
+ mockSystemService(mockPrimaryUserContext, UserManager::class.java) { mockUserManager }
+ mockSystemService(mockManagedUserContext, UserManager::class.java) { mockUserManager }
+
+ whenever(mockPrimaryUserContext.packageManager) { mockPackageManager }
+ whenever(mockPrimaryUserContext.contentResolver) { testPrimaryUserContentResolver }
+ whenever(mockManagedUserContext.contentResolver) { testManagedUserContentResolver }
+ whenever(
+ mockPrimaryUserContext.createPackageContextAsUser(
+ any(),
+ anyInt(),
+ nonNullableEq(USER_HANDLE_PRIMARY),
+ )
+ ) {
+ mockPrimaryUserContext
+ }
+ whenever(
+ mockPrimaryUserContext.createPackageContextAsUser(
+ any(),
+ anyInt(),
+ nonNullableEq(USER_HANDLE_MANAGED),
+ )
+ ) {
+ mockManagedUserContext
+ }
+ whenever(
+ mockManagedUserContext.createPackageContextAsUser(
+ any(),
+ anyInt(),
+ nonNullableEq(USER_HANDLE_PRIMARY),
+ )
+ ) {
+ mockPrimaryUserContext
+ }
+ whenever(
+ mockManagedUserContext.createPackageContextAsUser(
+ any(),
+ anyInt(),
+ nonNullableEq(USER_HANDLE_MANAGED),
+ )
+ ) {
+ mockManagedUserContext
+ }
+ whenever(
+ mockPrimaryUserContext.createContextAsUser(nonNullableEq(USER_HANDLE_PRIMARY), anyInt())
+ ) {
+ mockPrimaryUserContext
+ }
+ whenever(
+ mockPrimaryUserContext.createContextAsUser(nonNullableEq(USER_HANDLE_MANAGED), anyInt())
+ ) {
+ mockManagedUserContext
+ }
+ whenever(
+ mockManagedUserContext.createContextAsUser(nonNullableEq(USER_HANDLE_PRIMARY), anyInt())
+ ) {
+ mockPrimaryUserContext
+ }
+ whenever(
+ mockManagedUserContext.createContextAsUser(nonNullableEq(USER_HANDLE_MANAGED), anyInt())
+ ) {
+ mockManagedUserContext
+ }
+
+ // Initial setup state: Two profiles (Personal/Work), both enabled
+ whenever(mockUserManager.userProfiles) { listOf(USER_HANDLE_PRIMARY, USER_HANDLE_MANAGED) }
+
+ // Default responses for relevant UserManager apis
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_PRIMARY)) { false }
+ whenever(mockUserManager.isManagedProfile(USER_ID_PRIMARY)) { false }
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { false }
+ whenever(mockUserManager.isManagedProfile(USER_ID_MANAGED)) { true }
+ whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { USER_HANDLE_PRIMARY }
+
+ whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true }
+ whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
+ listOf(mockResolveInfo)
+ }
+
+ if (SdkLevel.isAtLeastV()) {
+ whenever(mockUserManager.getUserBadge()) {
+ resources.getDrawable(R.drawable.android, /* theme= */ null)
+ }
+ whenever(mockUserManager.getProfileLabel()) { PLATFORM_PROVIDED_PROFILE_LABEL }
+ whenever(mockUserManager.getUserProperties(USER_HANDLE_PRIMARY)) {
+ UserProperties.Builder().build()
+ }
+ // By default, allow managed profile to be available
+ whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED)) {
+ UserProperties.Builder()
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT
+ )
+ .build()
+ }
+ }
+ }
+
+ @Test
+ fun testGetGlobalSearchStateEnabled() = runTest {
+ testManagedUserContentProvider.searchProviders = listOf()
+
+ val userMonitor =
+ UserMonitor(
+ mockPrimaryUserContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val prefetchDataService =
+ PrefetchDataServiceImpl(
+ MediaProviderClient(),
+ userMonitor,
+ mockPrimaryUserContext,
+ StandardTestDispatcher(this.testScheduler),
+ )
+
+ val globalSearchState = prefetchDataService.getGlobalSearchState()
+
+ assertWithMessage("Global search state is not enabled")
+ .that(globalSearchState)
+ .isEqualTo(GlobalSearchState.ENABLED)
+ }
+
+ @Test
+ fun testGetGlobalSearchStateEnabledInOtherProfiles() = runTest {
+ val userMonitor =
+ UserMonitor(
+ mockPrimaryUserContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val prefetchDataService =
+ PrefetchDataServiceImpl(
+ MediaProviderClient(),
+ userMonitor,
+ mockPrimaryUserContext,
+ StandardTestDispatcher(this.testScheduler),
+ )
+
+ testPrimaryUserContentProvider.searchProviders = listOf()
+ val globalSearchState1 = prefetchDataService.getGlobalSearchState()
+ assertWithMessage("Global search state is not enabled in other profiles")
+ .that(globalSearchState1)
+ .isEqualTo(GlobalSearchState.ENABLED_IN_OTHER_PROFILES_ONLY)
+
+ testPrimaryUserContentProvider.searchProviders = null
+ val globalSearchState2 = prefetchDataService.getGlobalSearchState()
+ assertWithMessage("Global search state is not enabled in other profiles")
+ .that(globalSearchState2)
+ .isEqualTo(GlobalSearchState.ENABLED_IN_OTHER_PROFILES_ONLY)
+ }
+
+ @Test
+ fun testGetGlobalSearchStateUnknown() = runTest {
+ val userMonitor =
+ UserMonitor(
+ mockPrimaryUserContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val prefetchDataService =
+ PrefetchDataServiceImpl(
+ MediaProviderClient(),
+ userMonitor,
+ mockPrimaryUserContext,
+ StandardTestDispatcher(this.testScheduler),
+ )
+
+ testPrimaryUserContentProvider.searchProviders = listOf()
+ testManagedUserContentProvider.searchProviders = null
+ val globalSearchState = prefetchDataService.getGlobalSearchState()
+
+ assertWithMessage("Global search state is not enabled in other profiles")
+ .that(globalSearchState)
+ .isEqualTo(GlobalSearchState.UNKNOWN)
+ }
+
+ @Test
+ fun testGetGlobalSearchStateDisabled() = runTest {
+ testPrimaryUserContentProvider.searchProviders = listOf()
+ testManagedUserContentProvider.searchProviders = listOf()
+
+ val userMonitor =
+ UserMonitor(
+ mockPrimaryUserContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val prefetchDataService =
+ PrefetchDataServiceImpl(
+ MediaProviderClient(),
+ userMonitor,
+ mockPrimaryUserContext,
+ StandardTestDispatcher(this.testScheduler),
+ )
+
+ val globalSearchState = prefetchDataService.getGlobalSearchState()
+
+ assertWithMessage("Global search state is not enabled in other profiles")
+ .that(globalSearchState)
+ .isEqualTo(GlobalSearchState.DISABLED)
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt b/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt
index cab01bcc8..f5bef5275 100644
--- a/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt
@@ -138,6 +138,7 @@ class TestMediaProvider(
var albumMedia: Map<String, List<Media>> = DEFAULT_ALBUM_MEDIA,
var searchRequestId: Int = DEFAULT_SEARCH_REQUEST_ID,
var searchSuggestions: List<SearchSuggestion> = DEFAULT_SEARCH_SUGGESTIONS,
+ var searchProviders: List<Provider>? = DEFAULT_PROVIDERS,
) : MockContentProvider() {
var lastRefreshMediaRequest: Bundle? = null
var TEST_GRANTS_COUNT = 2
@@ -173,13 +174,22 @@ class TestMediaProvider(
override fun call(authority: String, method: String, arg: String?, extras: Bundle?): Bundle? {
return when (method) {
- "picker_media_init" -> {
+ MediaProviderClient.MEDIA_INIT_CALL_METHOD -> {
initMedia(extras)
null
}
- "picker_internal_search_media_init" -> {
+ MediaProviderClient.SEARCH_REQUEST_INIT_CALL_METHOD -> {
bundleOf(MediaProviderClient.SEARCH_REQUEST_ID to searchRequestId)
}
+ MediaProviderClient.GET_SEARCH_PROVIDERS_CALL_METHOD ->
+ bundleOf(
+ MediaProviderClient.SEARCH_PROVIDER_AUTHORITIES to
+ if (searchProviders == null) null
+ else
+ arrayListOf<String>().apply {
+ searchProviders?.map { it.authority }?.toCollection(this)
+ }
+ )
else -> throw UnsupportedOperationException("Could not recognize method $method")
}
}
diff --git a/photopicker/tests/src/com/android/photopicker/data/TestPrefetchDataService.kt b/photopicker/tests/src/com/android/photopicker/data/TestPrefetchDataService.kt
index c66d25635..a9c377cc5 100644
--- a/photopicker/tests/src/com/android/photopicker/data/TestPrefetchDataService.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/TestPrefetchDataService.kt
@@ -16,12 +16,12 @@
package com.android.photopicker.data
-import com.android.photopicker.features.search.model.SearchEnabledState
+import com.android.photopicker.features.search.model.GlobalSearchState
class TestPrefetchDataService() : PrefetchDataService {
- var searchEnabledState = SearchEnabledState.ENABLED
+ var globalSearchState = GlobalSearchState.ENABLED
- override suspend fun getSearchState(): SearchEnabledState {
- return searchEnabledState
+ override suspend fun getGlobalSearchState(): GlobalSearchState {
+ return globalSearchState
}
}
diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt b/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt
index cfc7b50e5..fcd3db580 100644
--- a/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt
@@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest
import com.android.photopicker.core.configuration.PhotopickerConfiguration
import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.events.generatePickerSessionId
+import com.android.photopicker.data.DEFAULT_PROVIDERS
import com.android.photopicker.data.DEFAULT_SEARCH_REQUEST_ID
import com.android.photopicker.data.DEFAULT_SEARCH_SUGGESTIONS
import com.android.photopicker.data.MediaProviderClient
@@ -487,4 +488,45 @@ class MediaProviderClientTest {
assertThat(searchSuggestions[index]).isEqualTo(DEFAULT_SEARCH_SUGGESTIONS[index])
}
}
+
+ @Test
+ fun testFetchSearchProvidersWithAvailableProvidersKnown() = runTest {
+ val mediaProviderClient = MediaProviderClient()
+ val localProvider =
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ )
+ val cloudProvider =
+ Provider(
+ authority = "cloud_authority",
+ mediaSource = MediaSource.REMOTE,
+ uid = 0,
+ displayName = "",
+ )
+ val testContentProvider: TestMediaProvider =
+ TestMediaProvider(searchProviders = listOf(localProvider, cloudProvider))
+ val testContentResolver: ContentResolver = ContentResolver.wrap(testContentProvider)
+
+ val searchProviderAuthorities =
+ mediaProviderClient.fetchSearchProviderAuthorities(
+ resolver = testContentResolver,
+ availableProviders = listOf(localProvider),
+ )
+
+ assertThat(searchProviderAuthorities).isEqualTo(listOf(localProvider.authority))
+ }
+
+ @Test
+ fun testFetchSearchProviders() = runTest {
+ val mediaProviderClient = MediaProviderClient()
+ val testContentResolver: ContentResolver = ContentResolver.wrap(testContentProvider)
+ val searchProviderAuthorities =
+ mediaProviderClient.fetchSearchProviderAuthorities(resolver = testContentResolver)
+
+ assertThat(searchProviderAuthorities)
+ .isEqualTo(DEFAULT_PROVIDERS.map { it.authority }.toList())
+ }
}
diff --git a/photopicker/tests/src/com/android/photopicker/features/search/SearchFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/search/SearchFeatureTest.kt
index a0aa0fdba..4454646aa 100644
--- a/photopicker/tests/src/com/android/photopicker/features/search/SearchFeatureTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/search/SearchFeatureTest.kt
@@ -59,7 +59,7 @@ import com.android.photopicker.core.features.PrefetchResultKey
import com.android.photopicker.core.selection.Selection
import com.android.photopicker.data.model.Media
import com.android.photopicker.features.PhotopickerFeatureBaseTest
-import com.android.photopicker.features.search.model.SearchEnabledState
+import com.android.photopicker.features.search.model.GlobalSearchState
import com.android.photopicker.inject.PhotopickerTestModule
import com.android.photopicker.tests.HiltTestActivity
import com.android.providers.media.flags.Flags
@@ -139,7 +139,7 @@ class SearchFeatureTest : PhotopickerFeatureBaseTest() {
PrefetchResultKey.SEARCH_STATE to
runBlocking {
async {
- return@async SearchEnabledState.ENABLED
+ return@async GlobalSearchState.ENABLED
}
}
)
diff --git a/photopicker/tests/src/com/android/photopicker/features/search/TestSearchDataServiceImpl.kt b/photopicker/tests/src/com/android/photopicker/features/search/TestSearchDataServiceImpl.kt
index f89933d7f..7254541ed 100644
--- a/photopicker/tests/src/com/android/photopicker/features/search/TestSearchDataServiceImpl.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/search/TestSearchDataServiceImpl.kt
@@ -23,9 +23,9 @@ import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaPageKey
import com.android.photopicker.data.paging.FakeInMemoryMediaPagingSource
import com.android.photopicker.features.search.data.SearchDataService
-import com.android.photopicker.features.search.model.SearchEnabledState
import com.android.photopicker.features.search.model.SearchSuggestion
import com.android.photopicker.features.search.model.SearchSuggestionType
+import com.android.photopicker.features.search.model.UserSearchStateInfo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -37,8 +37,8 @@ class TestSearchDataServiceImpl() : SearchDataService {
var mediaSetSize: Int = FakeInMemoryMediaPagingSource.DEFAULT_SIZE
var mediaList: List<Media>? = null
- override val isSearchEnabled: StateFlow<SearchEnabledState> =
- MutableStateFlow(SearchEnabledState.ENABLED)
+ override val userSearchStateInfo: StateFlow<UserSearchStateInfo> =
+ MutableStateFlow(UserSearchStateInfo(listOf("test_provider")))
override suspend fun getSearchSuggestions(
prefix: String,
diff --git a/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt b/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt
index 264f09021..411934ffb 100644
--- a/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt
+++ b/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt
@@ -39,6 +39,7 @@ import com.android.photopicker.core.selection.SelectionStrategy
import com.android.photopicker.core.selection.SelectionStrategy.Companion.determineSelectionStrategy
import com.android.photopicker.core.user.UserMonitor
import com.android.photopicker.data.DataService
+import com.android.photopicker.data.MediaProviderClient
import com.android.photopicker.data.PrefetchDataService
import com.android.photopicker.data.TestDataServiceImpl
import com.android.photopicker.data.TestPrefetchDataService
@@ -233,6 +234,12 @@ abstract class PhotopickerTestModule(val options: TestOptions = TestOptions.Buil
@Singleton
@Provides
+ fun createMediaProviderClient(): MediaProviderClient {
+ return MediaProviderClient()
+ }
+
+ @Singleton
+ @Provides
fun createPrefetchDataService(): PrefetchDataService {
return TestPrefetchDataService()
}
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 3e60f7c9b..6867319f4 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -7108,6 +7108,9 @@ public class MediaProvider extends ContentProvider {
initMediaSets(extras);
return new Bundle();
}
+ case MediaStore.PICKER_GET_SEARCH_PROVIDERS_CALL: {
+ return getPickerSearchProviders();
+ }
case MediaStore.PICKER_TRANSCODE_CALL: {
return getResultForPickerTranscode(extras);
}
@@ -7654,6 +7657,18 @@ public class MediaProvider extends ContentProvider {
PickerDataLayerV2.triggerMediaSyncForMediaSet(extras, getContext());
}
+ @Nullable
+ private Bundle getPickerSearchProviders() {
+ Log.i(TAG, "Received picker internal call to get available search providers.");
+ if (!checkPermissionShell(Binder.getCallingUid())
+ && !checkPermissionSelf(Binder.getCallingUid())
+ && !isCallerPhotoPicker()) {
+ throw new SecurityException(
+ getSecurityExceptionMessage("Picker get search providers"));
+ }
+ return PickerDataLayerV2.getSearchProviders(getContext());
+ }
+
/**
* Checks if the caller has the permission to handle picker search media init. If not,
* this method throws a security exception.
diff --git a/src/com/android/providers/media/photopicker/SearchState.java b/src/com/android/providers/media/photopicker/SearchState.java
index e6cbeabf3..44bf433ad 100644
--- a/src/com/android/providers/media/photopicker/SearchState.java
+++ b/src/com/android/providers/media/photopicker/SearchState.java
@@ -111,7 +111,7 @@ public class SearchState {
}
if (!Flags.enablePhotopickerSearch()) {
- Log.d(TAG, "Search feature is disabled.");
+ Log.d(TAG, "Search feature flag is disabled.");
return false;
}
diff --git a/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java b/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java
index 2712b5759..ca919e280 100644
--- a/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java
+++ b/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java
@@ -1439,6 +1439,36 @@ public class PickerDataLayerV2 {
}
/**
+ * @param context the application context.
+ * @return a bundle with the list of available provider authorities that support the
+ * search feature. If no providers are available, return an empty list in the bundle.
+ */
+ @NonNull
+ public static Bundle getSearchProviders(@NonNull Context context) {
+ Log.d(TAG, "Calculating available search providers.");
+
+ requireNonNull(context);
+
+ // Check the state of cloud and local search.
+ final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow();
+ final String cloudProvider = syncController.getCloudProviderOrDefault(null);
+ final boolean isCloudSearchEnabled =
+ syncController.getSearchState().isCloudSearchEnabled(context, cloudProvider);
+ final boolean isLocalSearchEnabled = syncController.getSearchState().isLocalSearchEnabled();
+
+ // Prepare a bundle response with the result.
+ final ArrayList<String> searchProviderAuthorities = new ArrayList<>();
+ if (isCloudSearchEnabled) searchProviderAuthorities.add(cloudProvider);
+ if (isLocalSearchEnabled) searchProviderAuthorities.add(syncController.getLocalProvider());
+
+ final Bundle result = new Bundle();
+ result.putStringArrayList(
+ PickerSQLConstants.EXTRA_SEARCH_PROVIDER_AUTHORITIES, searchProviderAuthorities);
+ Log.d(TAG, "Available search providers are: " + result);
+ return result;
+ }
+
+ /**
* Schedules MediaSets sync for both local and cloud provider if the corresponding
* providers implement Categories.
* @param appContext The application context
@@ -1598,7 +1628,7 @@ public class PickerDataLayerV2 {
@NonNull
private static Bundle getSearchRequestInitResponse(int searchRequestId) {
final Bundle response = new Bundle();
- response.putInt("search_request_id", searchRequestId);
+ response.putInt(PickerSQLConstants.EXTRA_SEARCH_REQUEST_ID, searchRequestId);
return response;
}
}
diff --git a/src/com/android/providers/media/photopicker/v2/sqlite/PickerSQLConstants.java b/src/com/android/providers/media/photopicker/v2/sqlite/PickerSQLConstants.java
index 750d3851a..8e5f2bffc 100644
--- a/src/com/android/providers/media/photopicker/v2/sqlite/PickerSQLConstants.java
+++ b/src/com/android/providers/media/photopicker/v2/sqlite/PickerSQLConstants.java
@@ -42,6 +42,8 @@ import java.util.Objects;
public class PickerSQLConstants {
public static final int DEFAULT_SEARCH_SUGGESTIONS_LIMIT = 50;
public static final int DEFAULT_SEARCH_HISTORY_SUGGESTIONS_LIMIT = 3;
+ public static String EXTRA_SEARCH_REQUEST_ID = "search_request_id";
+ public static String EXTRA_SEARCH_PROVIDER_AUTHORITIES = "search_provider_authorities";
static final String COUNT_COLUMN = "Count";
/**