summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/framework/java/android/provider/CloudMediaProviderContract.java10
-rw-r--r--apex/framework/java/android/provider/MediaStore.java3
-rw-r--r--pdf/framework-v/api/current.txt4
-rw-r--r--pdf/framework-v/java/android/graphics/pdf/PdfRenderer.java22
-rw-r--r--pdf/framework/api/current.txt21
-rw-r--r--pdf/framework/java/android/graphics/pdf/PdfDocumentProxy.java89
-rw-r--r--pdf/framework/java/android/graphics/pdf/PdfPageComponentsIdManager.java68
-rw-r--r--pdf/framework/java/android/graphics/pdf/PdfProcessor.java105
-rw-r--r--pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java23
-rw-r--r--pdf/framework/java/android/graphics/pdf/component/PdfPageObject.java30
-rw-r--r--pdf/framework/java/android/graphics/pdf/component/PdfPagePathObject.java110
-rw-r--r--pdf/framework/java/android/graphics/pdf/component/PdfPageTextObject.java3
-rw-r--r--pdf/framework/libs/pdfClient/annotation.cc163
-rw-r--r--pdf/framework/libs/pdfClient/annotation.h85
-rw-r--r--pdf/framework/libs/pdfClient/jni_conversion.cc416
-rw-r--r--pdf/framework/libs/pdfClient/jni_conversion.h23
-rw-r--r--pdf/framework/libs/pdfClient/page.cc202
-rw-r--r--pdf/framework/libs/pdfClient/page.h20
-rw-r--r--pdf/framework/libs/pdfClient/page_object.cc245
-rw-r--r--pdf/framework/libs/pdfClient/page_object.h91
-rw-r--r--pdf/framework/libs/pdfClient/page_test.cc237
-rw-r--r--pdf/framework/libs/pdfClient/pdf_document_jni.cc64
-rw-r--r--pdf/framework/libs/pdfClient/pdf_document_jni.h12
-rw-r--r--pdf/framework/libs/pdfClient/rect.h26
-rw-r--r--pdf/framework/libs/pdfClient/testdata/annotation.pdfbin0 -> 2197 bytes
-rwxr-xr-xpdf/framework/libs/pdfClient/testdata/image_object.pdfbin47896 -> 0 bytes
-rw-r--r--pdf/framework/libs/pdfClient/testdata/page_object.pdfbin0 -> 2442 bytes
-rw-r--r--photopicker/res/values-af/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-am/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-ar/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-as/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-az/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-b+sr+Latn/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-be/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-bg/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-bn/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-bs/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-ca/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-cs/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-da/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-de/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-el/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-en-rAU/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-en-rCA/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-en-rGB/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-en-rIN/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-es-rUS/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-es/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-et/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-eu/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-fa/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-fi/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-fr-rCA/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-fr/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-gl/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-gu/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-hi/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-hr/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-hu/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-hy/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-in/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-is/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-it/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-iw/feature_category_grid_strings.xml21
-rw-r--r--photopicker/res/values-iw/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-ja/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-ka/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-kk/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-km/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-kn/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-ko/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-ky/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-lo/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-lt/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-lv/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-mk/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-ml/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-mn/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-mr/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-ms/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-my/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-nb/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-ne/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-nl/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-or/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-pa/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-pl/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-pt-rBR/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-pt-rPT/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-pt/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-ro/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-ru/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-si/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-sk/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-sl/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-sq/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-sr/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-sv/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-sw/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-ta/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-te/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-th/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-tl/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-tr/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-uk/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-ur/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-uz/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-vi/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-zh-rCN/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-zh-rHK/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-zh-rTW/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/res/values-zu/feature_privacy_explainer_strings.xml3
-rw-r--r--photopicker/src/com/android/photopicker/data/DataServiceImpl.kt6
-rw-r--r--photopicker/src/com/android/photopicker/data/MediaProviderClient.kt556
-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/data/UriHelper.kt44
-rw-r--r--photopicker/src/com/android/photopicker/features/categorygrid/data/CategoryDataService.kt21
-rw-r--r--photopicker/src/com/android/photopicker/features/categorygrid/data/CategoryDataServiceImpl.kt334
-rw-r--r--photopicker/src/com/android/photopicker/features/categorygrid/data/FakeCategoryDataServiceImpl.kt59
-rw-r--r--photopicker/src/com/android/photopicker/features/categorygrid/inject/CategoryActivityRetainedModule.kt31
-rw-r--r--photopicker/src/com/android/photopicker/features/categorygrid/inject/CategoryEmbeddedServiceModule.kt31
-rw-r--r--photopicker/src/com/android/photopicker/features/categorygrid/paging/CategoryAndAlbumPagingSource.kt82
-rw-r--r--photopicker/src/com/android/photopicker/features/categorygrid/paging/MediaSetContentsPagingSource.kt83
-rw-r--r--photopicker/src/com/android/photopicker/features/categorygrid/paging/MediaSetsPagingSource.kt85
-rw-r--r--photopicker/src/com/android/photopicker/features/search/Search.kt20
-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.kt108
-rw-r--r--photopicker/src/com/android/photopicker/features/search/data/SearchResultsPagingSource.kt28
-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.kt166
-rw-r--r--photopicker/tests/src/com/android/photopicker/data/TestPrefetchDataService.kt8
-rw-r--r--photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt133
-rw-r--r--photopicker/tests/src/com/android/photopicker/features/categorygrid/data/CategoryDataServiceImplTest.kt354
-rw-r--r--photopicker/tests/src/com/android/photopicker/features/categorygrid/data/TestCategoryDataServiceImpl.kt8
-rw-r--r--photopicker/tests/src/com/android/photopicker/features/search/SearchDataServiceImplTest.kt67
-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/FilesOwnershipUtils.java2
-rw-r--r--src/com/android/providers/media/MediaProvider.java34
-rw-r--r--src/com/android/providers/media/backupandrestore/BackupExecutor.java9
-rw-r--r--src/com/android/providers/media/photopicker/SearchState.java11
-rw-r--r--src/com/android/providers/media/photopicker/sync/PickerSearchProviderClient.java79
-rw-r--r--src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java10
-rw-r--r--src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java59
-rw-r--r--src/com/android/providers/media/photopicker/v2/PickerNotificationSender.java33
-rw-r--r--src/com/android/providers/media/photopicker/v2/PickerUriResolverV2.java28
-rw-r--r--src/com/android/providers/media/photopicker/v2/model/MediaInMediaSetSyncRequestParams.java11
-rw-r--r--src/com/android/providers/media/photopicker/v2/model/MediaSetsSyncRequestParams.java9
-rw-r--r--src/com/android/providers/media/photopicker/v2/sqlite/MediaGroupCursorUtils.java17
-rw-r--r--src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsCloudSubQuery.java2
-rw-r--r--src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsLocalSubQuery.java2
-rw-r--r--src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsQuery.java5
-rw-r--r--src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsSubQuery.java4
-rw-r--r--src/com/android/providers/media/photopicker/v2/sqlite/PickerSQLConstants.java10
-rw-r--r--src/com/android/providers/media/scan/ModernMediaScanner.java42
-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/src/com/android/providers/media/MediaProviderForFuseTest.java17
-rw-r--r--tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java3
-rw-r--r--tests/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorkerTest.java22
-rw-r--r--tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java39
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java72
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsDatabaseUtilTest.java22
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaSetsDatabaseUtilsTest.java24
180 files changed, 5057 insertions, 892 deletions
diff --git a/apex/framework/java/android/provider/CloudMediaProviderContract.java b/apex/framework/java/android/provider/CloudMediaProviderContract.java
index b762957a2..e477af289 100644
--- a/apex/framework/java/android/provider/CloudMediaProviderContract.java
+++ b/apex/framework/java/android/provider/CloudMediaProviderContract.java
@@ -151,6 +151,16 @@ public final class CloudMediaProviderContract {
}
/**
+ * @hide
+ */
+ @Override
+ public String toString() {
+ return " isSearchEnabled=" + this.mSearchEnabled
+ + " isMediaCategoriesEnabled=" + this.mMediaCategoriesEnabled
+ + " isAlbumsAsCategoryEnabled=" + this.mAlbumsAsCategory;
+ }
+
+ /**
* Implemented for {@link Parcelable}
*/
@Override
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/pdf/framework-v/api/current.txt b/pdf/framework-v/api/current.txt
index ef07ae159..90cf2e276 100644
--- a/pdf/framework-v/api/current.txt
+++ b/pdf/framework-v/api/current.txt
@@ -37,8 +37,8 @@ package android.graphics.pdf {
method @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_page_objects") @NonNull public java.util.List<android.util.Pair<java.lang.Integer,android.graphics.pdf.component.PdfPageObject>> getPageObjects();
method @FlaggedApi("android.graphics.pdf.flags.enable_pdf_viewer") @NonNull public java.util.List<android.graphics.pdf.content.PdfPageTextContent> getTextContents();
method @IntRange(from=0) public int getWidth();
- method @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_annotations") @NonNull public android.graphics.pdf.component.PdfAnnotation removePageAnnotation(@IntRange(from=0) int);
- method @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_page_objects") @NonNull public android.graphics.pdf.component.PdfPageObject removePageObject(@IntRange(from=0) int);
+ method @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_annotations") public void removePageAnnotation(@IntRange(from=0) int);
+ method @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_page_objects") public void removePageObject(@IntRange(from=0) int);
method public void render(@NonNull android.graphics.Bitmap, @Nullable android.graphics.Rect, @Nullable android.graphics.Matrix, int);
method @FlaggedApi("android.graphics.pdf.flags.enable_pdf_viewer") public void render(@NonNull android.graphics.Bitmap, @Nullable android.graphics.Rect, @Nullable android.graphics.Matrix, @NonNull android.graphics.pdf.RenderParams);
method @FlaggedApi("android.graphics.pdf.flags.enable_pdf_viewer") @NonNull public java.util.List<android.graphics.pdf.models.PageMatchBounds> searchText(@NonNull String);
diff --git a/pdf/framework-v/java/android/graphics/pdf/PdfRenderer.java b/pdf/framework-v/java/android/graphics/pdf/PdfRenderer.java
index 2a621279d..6c8a59066 100644
--- a/pdf/framework-v/java/android/graphics/pdf/PdfRenderer.java
+++ b/pdf/framework-v/java/android/graphics/pdf/PdfRenderer.java
@@ -842,25 +842,17 @@ public final class PdfRenderer implements AutoCloseable {
* {@link PdfRenderer#write}.
*
* @param annotationId id of the annotation to remove from the page
- * @return the removed annotation
* @throws IllegalArgumentException if annotationId ie negative
* @throws IllegalStateException if {@link PdfRenderer} or {@link PdfRenderer.Page} is
* closed before invocation or if annotation is failed to
* get removed from the page.
*/
@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_ANNOTATIONS)
- @NonNull
- public PdfAnnotation removePageAnnotation(@IntRange(from = 0) int annotationId) {
+ public void removePageAnnotation(@IntRange(from = 0) int annotationId) {
throwIfDocumentOrPageClosed();
Preconditions.checkArgument(annotationId >= 0,
"Annotation id should be non-negative");
- PdfAnnotation removedAnnotation = mPdfProcessor.removePageAnnotation(mIndex,
- annotationId);
- if (removedAnnotation == null) {
- throw new IllegalStateException(
- "Failed to remove annotation with id " + annotationId);
- }
- return removedAnnotation;
+ mPdfProcessor.removePageAnnotation(mIndex, annotationId);
}
/**
@@ -976,21 +968,15 @@ public final class PdfRenderer implements AutoCloseable {
* {@link PdfRenderer#write}.
*
* @param objectId the id of the page object to remove from the page.
- * @return {@link PdfPageObject} that is removed.
* @throws IllegalArgumentException if the provided objectId doesn't exist.
* @throws IllegalStateException if the page object cannot be removed.
*/
@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_PAGE_OBJECTS)
- @NonNull
- public PdfPageObject removePageObject(@IntRange(from = 0) int objectId) {
+ public void removePageObject(@IntRange(from = 0) int objectId) {
throwIfDocumentOrPageClosed();
Preconditions.checkArgument(objectId >= 0,
"Page object id should be greater than equal to 0");
- PdfPageObject pageObject = mPdfProcessor.removePageObject(mIndex, objectId);
- if (pageObject == null) {
- throw new IllegalStateException("Page object cannot be removed.");
- }
- return pageObject;
+ mPdfProcessor.removePageObject(mIndex, objectId);
}
/**
diff --git a/pdf/framework/api/current.txt b/pdf/framework/api/current.txt
index c5b0d0d3d..3715cbdef 100644
--- a/pdf/framework/api/current.txt
+++ b/pdf/framework/api/current.txt
@@ -46,8 +46,8 @@ package android.graphics.pdf {
method @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_page_objects") @NonNull public java.util.List<android.util.Pair<java.lang.Integer,android.graphics.pdf.component.PdfPageObject>> getPageObjects();
method @NonNull public java.util.List<android.graphics.pdf.content.PdfPageTextContent> getTextContents();
method @IntRange(from=0) public int getWidth();
- method @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_annotations") @NonNull public android.graphics.pdf.component.PdfAnnotation removePageAnnotation(@IntRange(from=0) int);
- method @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_page_objects") @NonNull public android.graphics.pdf.component.PdfPageObject removePageObject(@IntRange(from=0) int);
+ method @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_annotations") public void removePageAnnotation(@IntRange(from=0) int);
+ method @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_page_objects") public void removePageObject(@IntRange(from=0) int);
method public void render(@NonNull android.graphics.Bitmap, @Nullable android.graphics.Rect, @Nullable android.graphics.Matrix, @NonNull android.graphics.pdf.RenderParams);
method @NonNull public java.util.List<android.graphics.pdf.models.PageMatchBounds> searchText(@NonNull String);
method @Nullable public android.graphics.pdf.models.selection.PageSelection selectContent(@NonNull android.graphics.pdf.models.selection.SelectionBoundary, @NonNull android.graphics.pdf.models.selection.SelectionBoundary);
@@ -113,10 +113,8 @@ package android.graphics.pdf.component {
}
@FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_page_objects") public abstract class PdfPageObject {
- method @NonNull public android.graphics.RectF getBounds();
method @NonNull public float[] getMatrix();
method public int getPdfObjectType();
- method public void setBounds(@NonNull android.graphics.RectF);
method public void setMatrix(@NonNull android.graphics.Matrix);
method public void transform(float, float, float, float, float, float);
}
@@ -130,21 +128,18 @@ package android.graphics.pdf.component {
}
@FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_page_objects") public final class PdfPagePathObject extends android.graphics.pdf.component.PdfPageObject {
- ctor public PdfPagePathObject();
+ ctor public PdfPagePathObject(@NonNull android.graphics.Path);
method @Nullable public android.graphics.Color getFillColor();
- method @Nullable public android.graphics.PathEffect getLineStyle();
- method @NonNull public android.graphics.Path getPath();
- method @NonNull public android.graphics.Color getStrokeColor();
+ method @Nullable public android.graphics.Color getStrokeColor();
method public float getStrokeWidth();
method public void setFillColor(@Nullable android.graphics.Color);
- method public void setLineStyle(int);
- method public void setPath(@NonNull android.graphics.Path);
- method public void setStrokeColor(@NonNull android.graphics.Color);
+ method public void setStrokeColor(@Nullable android.graphics.Color);
method public void setStrokeWidth(float);
+ method @NonNull public android.graphics.Path toPath();
}
- @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_page_objects") public final class PdfPageTextObject extends android.graphics.pdf.component.PdfPageObject {
- ctor @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_text_objects") public PdfPageTextObject(@NonNull String, @NonNull android.graphics.Typeface, float);
+ @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_text_objects") public final class PdfPageTextObject extends android.graphics.pdf.component.PdfPageObject {
+ ctor public PdfPageTextObject(@NonNull String, @NonNull android.graphics.Typeface, float);
method @Nullable public android.graphics.Color getFillColor();
method public float getFontSize();
method @NonNull public android.graphics.Color getStrokeColor();
diff --git a/pdf/framework/java/android/graphics/pdf/PdfDocumentProxy.java b/pdf/framework/java/android/graphics/pdf/PdfDocumentProxy.java
index a66b64b8d..3a1b065a7 100644
--- a/pdf/framework/java/android/graphics/pdf/PdfDocumentProxy.java
+++ b/pdf/framework/java/android/graphics/pdf/PdfDocumentProxy.java
@@ -31,7 +31,6 @@ import android.graphics.pdf.models.jni.PageSelection;
import android.graphics.pdf.models.jni.SelectionBoundary;
import android.graphics.pdf.utils.StrictModeUtils;
import android.os.ParcelFileDescriptor;
-import android.util.Pair;
import java.util.List;
@@ -260,94 +259,88 @@ public class PdfDocumentProxy {
int pageNum, int annotIndex, int[] selectedIndices);
/**
- * Gets the list of pair of annotations of supported types (freetext, image, stamp) and their
- * ids present on the page
+ * Returns the list of {@link PdfAnnotation} present on the page.
+ * The list item is non-null for supported types (freetext, image, stamp) and
+ * null for unsupported types.
*
* @param pageNum - page number of the page whose annotations list is to be returned
+ * @return A {@link List} of {@link PdfAnnotation}
*/
- public native @NonNull List<Pair<Integer, PdfAnnotation>> getPageAnnotations(
+ public native @NonNull List<PdfAnnotation> getPageAnnotations(
@IntRange(from = 0) int pageNum);
/**
- * Adds an annotation to the given page
+ * Adds the given {@link PdfAnnotation} to the given page
*
* @param pageNum - page number of the page to which annotation is to be added
- * @param annotation - annotation to be added to the given page
- * @return index of the annotation added and -1 in case of failure
+ * @param annotation - {@link PdfAnnotation} to be added to the given page
+ * @return index of the annotation added, -1 in case of failure
*/
public native int addPageAnnotation(@IntRange(from = 0) int pageNum,
@NonNull PdfAnnotation annotation);
/**
- * Removes an annotation from the given page
+ * Removes the {@link PdfAnnotation} with the specified index from the given page.
*
- * @param pageNum - page number of the page from which annotation is to be removed
- * @param annotationId - id of the annotation to be removed
+ * @param pageNum - page number from which {@link PdfAnnotation} is to be removed
+ * @param annotationIndex - index of the {@link PdfAnnotation} to be removed
+ *
+ * @return true if remove was successful, false otherwise
*/
- public native PdfAnnotation removePageAnnotation(@IntRange(from = 0) int pageNum,
- @IntRange(from = 0) int annotationId);
+ public native boolean removePageAnnotation(@IntRange(from = 0) int pageNum,
+ @IntRange(from = 0) int annotationIndex);
/**
- * Updates an annotation on the given page
+ * Update the given {@link PdfAnnotation} on the given page
*
- * @param pageNum page number of the page on which annotation is to be updated
- * @param annotationId id corresponding to which the annotation is to be updated
+ * @param pageNum page number on which annotation is to be updated
+ * @param annotationIndex index of the annotation
* @param annotation annotation to be updated
+ *
+ * @return true if page object is updated, false otherwise
*/
public native boolean updatePageAnnotation(@IntRange(from = 0) int pageNum,
- int annotationId, PdfAnnotation annotation);
+ int annotationIndex, PdfAnnotation annotation);
/**
- * Return list of supported {@link PdfPageObject} present on
- * the page.
- * The list will be empty if there are no supported page
- * objects present on the page, even if the page contains
- * other page object types.
+ * Returns the list of {@link PdfPageObject} present on the page.
+ * The list item is non-null for supported types and
+ * null for unsupported types.
*
- * @param pageNum - page number of the page whose annotations list is returned
- * @return A {@link List} of {@link Pair} objects, where each pair contains:
- * - An {@link Integer} representing the object ID.
- * - A {@link PdfPageObject} representing the page object.
- * @throws IllegalStateException if the document/page is
- * closed before invocation
+ * @param pageNum - page number of the page whose annotations list is to be returned
+ * @return A {@link List} of {@link PdfPageObject}
*/
- public native List<Pair<Integer, PdfPageObject>> getPageObjects(int pageNum);
+ public native List<PdfPageObject> getPageObjects(int pageNum);
/**
* Adds the given page object to the page.
*
* @param pageNum - page number of the page to which pageObject is to be added
- * @param pageObject the {@link PdfPageObject} object to
- * add
- * @return object id of added page object, -1 otherwise
- * @throws IllegalArgumentException if the provided {@link PdfPageObject} is unknown or null.
- * @throws IllegalStateException if the {@link PdfRenderer.Page} is closed before invocation.
+ * @param pageObject - {@link PdfPageObject} to be added to the given page
+ * @return index of added page object, -1 in the case of failure
*/
public native int addPageObject(int pageNum, @NonNull PdfPageObject pageObject);
/**
- * Update the given {@link PdfPageObject} to the page.
+ * Update the given {@link PdfPageObject} on the given page
+ *
+ * @param pageNum page number on which the {@link PdfPageObject} is to be updated
+ * @param objectIndex index of the pageObject
+ * @param pageObject pageObject to be updated
*
- * @param objectId The unique identifier of the page object to update.
- * @param pageObject the {@code PdfPageObject} object to
- * add
* @return true if page object is updated, false otherwise
- * @throws IllegalArgumentException if the provided {@link PdfPageObject} is unknown or null.
- * @throws IllegalStateException if the {@link PdfRenderer.Page} is closed before invocation.
*/
- public native boolean updatePageObject(int pageNum, int objectId,
+ public native boolean updatePageObject(int pageNum, int objectIndex,
@NonNull PdfPageObject pageObject);
/**
- * Removes the {@link PdfPageObject} with the specified ID.
+ * Removes the {@link PdfPageObject} with the specified Index from the given page.
+ *
+ * @param pageNum - page number from which {@link PdfPageObject} is to be removed
+ * @param objectIndex the index of the {@link PdfPageObject} to be removed
*
- * @param pageNum - page number of the page from which annotation is to be removed
- * @param objectId the ID of the page object to remove
- * from the page
- * @return {@link PdfPageObject} that is removed.
- * @throws IllegalStateException if the provided
- * objectId doesn't exist.
+ * @return true if remove was successful, false otherwise
*/
- public native PdfPageObject removePageObject(int pageNum, int objectId);
+ public native boolean removePageObject(int pageNum, int objectIndex);
}
diff --git a/pdf/framework/java/android/graphics/pdf/PdfPageComponentsIdManager.java b/pdf/framework/java/android/graphics/pdf/PdfPageComponentsIdManager.java
new file mode 100644
index 000000000..8ecbf7447
--- /dev/null
+++ b/pdf/framework/java/android/graphics/pdf/PdfPageComponentsIdManager.java
@@ -0,0 +1,68 @@
+/*
+ * 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 android.graphics.pdf;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @hide
+ * This class manages Id-Index Mapping for PdfAnnotations and PdfPageObjects.
+ */
+public class PdfPageComponentsIdManager {
+ private final HashMap<Integer, Integer> mIdIndexMap;
+ private HashMap<Integer, Integer> mIndexIdMap;
+ private int mComponentCurrentMaxId;
+
+ public PdfPageComponentsIdManager() {
+ mIdIndexMap = new HashMap<>();
+ mIndexIdMap = new HashMap<>();
+ mComponentCurrentMaxId = -1;
+ }
+
+ void deleteId(int id) {
+ int deletedComponentIndex = mIdIndexMap.get(id);
+ mIdIndexMap.remove(id);
+
+ HashMap<Integer, Integer> updatedIndexIdMap = new HashMap<>();
+ // Iterate and modify values greater than deletedComponentIndex
+ for (Map.Entry<Integer, Integer> entry : mIdIndexMap.entrySet()) {
+ if (entry.getValue() > deletedComponentIndex) {
+ entry.setValue(entry.getValue() - 1);
+ }
+ updatedIndexIdMap.put(entry.getValue(), entry.getKey());
+ }
+ mIndexIdMap = updatedIndexIdMap;
+ }
+
+ int getIndexForId(int id) {
+ if (!mIdIndexMap.containsKey(id)) {
+ return -1;
+ }
+ return mIdIndexMap.get(id);
+ }
+
+ int getIdForIndex(int index) {
+ if (!mIndexIdMap.containsKey(index)) {
+ mComponentCurrentMaxId += 1;
+ mIdIndexMap.put(mComponentCurrentMaxId, index);
+ mIndexIdMap.put(index, mComponentCurrentMaxId);
+ return mComponentCurrentMaxId;
+ }
+ return mIndexIdMap.get(index);
+ }
+}
diff --git a/pdf/framework/java/android/graphics/pdf/PdfProcessor.java b/pdf/framework/java/android/graphics/pdf/PdfProcessor.java
index e24465a5b..960d1cffe 100644
--- a/pdf/framework/java/android/graphics/pdf/PdfProcessor.java
+++ b/pdf/framework/java/android/graphics/pdf/PdfProcessor.java
@@ -53,7 +53,9 @@ import android.util.Pair;
import java.io.IOException;
import java.security.SecureRandom;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
@@ -79,6 +81,8 @@ public class PdfProcessor {
private static final Object sPdfiumLock = new Object();
private final PdfEventLogger mPdfEventLogger;
private PdfDocumentProxy mPdfDocument;
+ private final HashMap<Integer, PdfPageComponentsIdManager> mPageObjectIdManagerMap;
+ private final HashMap<Integer, PdfPageComponentsIdManager> mPageAnnotationsIdManagerMap;
public PdfProcessor() {
PdfDocumentProxy.loadLibPdf();
@@ -86,6 +90,8 @@ public class PdfProcessor {
mPdfEventLogger = new PdfEventLogger(
/* processId = */ Binder.getCallingUid(),
/* docId = */ new SecureRandom().nextLong());
+ mPageObjectIdManagerMap = new HashMap<>();
+ mPageAnnotationsIdManagerMap = new HashMap<>();
}
/**
@@ -371,6 +377,12 @@ public class PdfProcessor {
public void retainPage(int pageNum) {
synchronized (sPdfiumLock) {
assertPdfDocumentNotNull();
+ if (!mPageObjectIdManagerMap.containsKey(pageNum)) {
+ mPageObjectIdManagerMap.put(pageNum, new PdfPageComponentsIdManager());
+ }
+ if (!mPageAnnotationsIdManagerMap.containsKey(pageNum)) {
+ mPageAnnotationsIdManagerMap.put(pageNum, new PdfPageComponentsIdManager());
+ }
mPdfDocument.retainPage(pageNum);
}
}
@@ -379,6 +391,8 @@ public class PdfProcessor {
public void releasePage(int pageNum) {
synchronized (sPdfiumLock) {
assertPdfDocumentNotNull();
+ mPageObjectIdManagerMap.remove(pageNum);
+ mPageAnnotationsIdManagerMap.remove(pageNum);
mPdfDocument.releasePage(pageNum);
}
}
@@ -583,7 +597,18 @@ public class PdfProcessor {
public List<Pair<Integer, PdfAnnotation>> getPageAnnotations(@IntRange(from = 0) int pageNum) {
synchronized (sPdfiumLock) {
assertPdfDocumentNotNull();
- return mPdfDocument.getPageAnnotations(pageNum);
+ PdfPageComponentsIdManager pageAnnotationIdManager =
+ mPageAnnotationsIdManagerMap.get(pageNum);
+ List<PdfAnnotation> pdfAnnotations = mPdfDocument.getPageAnnotations(pageNum);
+ List<Pair<Integer, PdfAnnotation>> pdfAnnotationIdPairs = new ArrayList<>();
+ for (int i = 0; i < pdfAnnotations.size(); i++) {
+ if (pdfAnnotations.get(i) != null) {
+ pdfAnnotationIdPairs.add(
+ new Pair<>(pageAnnotationIdManager.getIdForIndex(i),
+ pdfAnnotations.get(i)));
+ }
+ }
+ return pdfAnnotationIdPairs;
}
}
@@ -603,23 +628,36 @@ public class PdfProcessor {
PdfAnnotation annotation) {
synchronized (sPdfiumLock) {
assertPdfDocumentNotNull();
- return mPdfDocument.addPageAnnotation(pageNum, annotation);
+ int addedAnnotationIndex = mPdfDocument.addPageAnnotation(pageNum, annotation);
+ if (addedAnnotationIndex == -1) {
+ throw new IllegalArgumentException("Failed to add annotation");
+ }
+ return mPageAnnotationsIdManagerMap.get(pageNum).getIdForIndex(addedAnnotationIndex);
}
}
/**
* Removes the annotation with the specified index.
*
- * @param annotationIndex the index of the annotation to remove
+ * @param annotationId the Id of the annotation to remove
* from the page
* @param pageNum page number from which annotation is to be removed
- * @return the removed annotation
*/
- public PdfAnnotation removePageAnnotation(@IntRange(from = 0) int pageNum,
- int annotationIndex) {
+ public void removePageAnnotation(@IntRange(from = 0) int pageNum,
+ int annotationId) {
synchronized (sPdfiumLock) {
assertPdfDocumentNotNull();
- return mPdfDocument.removePageAnnotation(pageNum, annotationIndex);
+ PdfPageComponentsIdManager pdfAnnotationsIdManager =
+ mPageAnnotationsIdManagerMap.get(pageNum);
+ int annotationIndex = pdfAnnotationsIdManager.getIndexForId(annotationId);
+ if (annotationIndex == -1) {
+ throw new IllegalArgumentException("Unknown annotationId. getPageAnnotations() "
+ + "call never made?");
+ }
+ if (!mPdfDocument.removePageAnnotation(pageNum, annotationIndex)) {
+ throw new IllegalArgumentException("Annotation cannot be removed.");
+ }
+ pdfAnnotationsIdManager.deleteId(annotationId);
}
}
@@ -638,7 +676,16 @@ public class PdfProcessor {
@NonNull PdfAnnotation annotation) {
synchronized (sPdfiumLock) {
assertPdfDocumentNotNull();
- return mPdfDocument.updatePageAnnotation(pageNum, annotationId, annotation);
+ int annotationIndex = mPageAnnotationsIdManagerMap.get(pageNum)
+ .getIndexForId(annotationId);
+ if (annotationIndex == -1) {
+ throw new IllegalArgumentException("Unknown annotation Id. getPageAnnotations()"
+ + " call never made?");
+ }
+ if (!mPdfDocument.updatePageAnnotation(pageNum, annotationIndex, annotation)) {
+ throw new IllegalArgumentException("Update Failed");
+ }
+ return true;
}
}
@@ -659,7 +706,16 @@ public class PdfProcessor {
public List<Pair<Integer, PdfPageObject>> getPageObjects(int pageNum) {
synchronized (sPdfiumLock) {
assertPdfDocumentNotNull();
- return mPdfDocument.getPageObjects(pageNum);
+ PdfPageComponentsIdManager pageObjectIdManager = mPageObjectIdManagerMap.get(pageNum);
+ List<PdfPageObject> pageObjects = mPdfDocument.getPageObjects(pageNum);
+ List<Pair<Integer, PdfPageObject>> pageObjectIdPairs = new ArrayList<>();
+ for (int i = 0; i < pageObjects.size(); i++) {
+ if (pageObjects.get(i) != null) {
+ pageObjectIdPairs.add(
+ new Pair<>(pageObjectIdManager.getIdForIndex(i), pageObjects.get(i)));
+ }
+ }
+ return pageObjectIdPairs;
}
}
@@ -676,7 +732,11 @@ public class PdfProcessor {
public int addPageObject(int pageNum, @NonNull PdfPageObject pageObject) {
synchronized (sPdfiumLock) {
assertPdfDocumentNotNull();
- return mPdfDocument.addPageObject(pageNum, pageObject);
+ int addedObjectIndex = mPdfDocument.addPageObject(pageNum, pageObject);
+ if (addedObjectIndex == -1) {
+ throw new IllegalArgumentException("Failed to add PageObject");
+ }
+ return mPageObjectIdManagerMap.get(pageNum).getIdForIndex(addedObjectIndex);
}
}
@@ -694,7 +754,15 @@ public class PdfProcessor {
@NonNull PdfPageObject pageObject) {
synchronized (sPdfiumLock) {
assertPdfDocumentNotNull();
- return mPdfDocument.updatePageObject(pageNum, objectId, pageObject);
+ int objectIndex = mPageObjectIdManagerMap.get(pageNum).getIndexForId(objectId);
+ if (objectIndex == -1) {
+ throw new IllegalArgumentException("Unknown objectId. "
+ + "getPageObjects() call never made?");
+ }
+ if (!mPdfDocument.updatePageObject(pageNum, objectIndex, pageObject)) {
+ throw new IllegalArgumentException("Update Failed");
+ }
+ return true;
}
}
@@ -703,15 +771,24 @@ public class PdfProcessor {
*
* @param objectId the id of the page object to remove
* from the page
- * @return {@link PdfPageObject} that is removed.
* @throws IllegalStateException if the provided
* objectId doesn't exist.
*/
@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_PAGE_OBJECTS)
- public PdfPageObject removePageObject(int pageNum, int objectId) {
+ public void removePageObject(int pageNum, int objectId) {
synchronized (sPdfiumLock) {
assertPdfDocumentNotNull();
- return mPdfDocument.removePageObject(pageNum, objectId);
+ PdfPageComponentsIdManager pageObjectIdManager =
+ mPageObjectIdManagerMap.get(pageNum);
+ int objectIndex = pageObjectIdManager.getIndexForId(objectId);
+ if (objectIndex == -1) {
+ throw new IllegalArgumentException("Unknown objectId. getPageObjects() "
+ + "call never made ?");
+ }
+ if (!mPdfDocument.removePageObject(pageNum, objectIndex)) {
+ throw new IllegalArgumentException("Page object cannot be removed.");
+ }
+ pageObjectIdManager.deleteId(objectId);
}
}
diff --git a/pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java b/pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java
index 05df63909..d5081ae98 100644
--- a/pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java
+++ b/pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java
@@ -674,28 +674,19 @@ public final class PdfRendererPreV implements AutoCloseable {
* {@link PdfRendererPreV#write}.
*
* @param annotationId id of the annotation to remove from the page
- * @return the removed annotation
* @throws IllegalArgumentException if annotationId ie negative
* @throws IllegalStateException if {@link PdfRendererPreV} or
* {@link PdfRendererPreV.Page} is closed before invocation
* or if annotation is failed to get removed from the page.
*/
@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_ANNOTATIONS)
- @NonNull
- public PdfAnnotation removePageAnnotation(@IntRange(from = 0) int annotationId) {
+ public void removePageAnnotation(@IntRange(from = 0) int annotationId) {
throwIfDocumentOrPageClosed();
Preconditions.checkArgument(annotationId >= 0,
"Annotation id should be non-negative");
- PdfAnnotation removedAnnotation = mPdfProcessor.removePageAnnotation(mIndex,
- annotationId);
- if (removedAnnotation == null) {
- throw new IllegalStateException(
- "Failed to remove annotation with id " + annotationId);
- }
- return removedAnnotation;
+ mPdfProcessor.removePageAnnotation(mIndex, annotationId);
}
-
/**
* Update the given {@link PdfAnnotation} to the page.
*
@@ -810,21 +801,15 @@ public final class PdfRendererPreV implements AutoCloseable {
* {@link PdfRenderer#write}.
*
* @param objectId the id of the page object to remove from the page.
- * @return {@link PdfPageObject} that is removed.
* @throws IllegalArgumentException if the provided objectId doesn't exist.
* @throws IllegalStateException if the page object cannot be removed.
*/
@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_PAGE_OBJECTS)
- @NonNull
- public PdfPageObject removePageObject(@IntRange(from = 0) int objectId) {
+ public void removePageObject(@IntRange(from = 0) int objectId) {
throwIfDocumentOrPageClosed();
Preconditions.checkArgument(objectId >= 0,
"Page object id should be greater than equal to 0.");
- PdfPageObject pageObject = mPdfProcessor.removePageObject(mIndex, objectId);
- if (pageObject == null) {
- throw new IllegalStateException("Page object cannot be removed.");
- }
- return pageObject;
+ mPdfProcessor.removePageObject(mIndex, objectId);
}
/**
diff --git a/pdf/framework/java/android/graphics/pdf/component/PdfPageObject.java b/pdf/framework/java/android/graphics/pdf/component/PdfPageObject.java
index 9eadf701e..890402e50 100644
--- a/pdf/framework/java/android/graphics/pdf/component/PdfPageObject.java
+++ b/pdf/framework/java/android/graphics/pdf/component/PdfPageObject.java
@@ -19,7 +19,6 @@ package android.graphics.pdf.component;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.graphics.Matrix;
-import android.graphics.RectF;
import android.graphics.pdf.flags.Flags;
/**
@@ -32,9 +31,6 @@ public abstract class PdfPageObject {
// Possible Values are {@link PdfPageObjectType}
private final int mType;
- // Bound of page object
- private RectF mBounds;
-
// Transformation matrix of page object
private Matrix mTransform;
@@ -58,25 +54,6 @@ public abstract class PdfPageObject {
}
/**
- * Returns the bounding rectangle of the object.
- *
- * @return The bounding rectangle of the object.
- */
- @NonNull
- public RectF getBounds() {
- return mBounds;
- }
-
- /**
- * Sets the bounding rectangle of the object.
- *
- * @param bounds The bounding rectangle of the object.
- */
- public void setBounds(@NonNull RectF bounds) {
- this.mBounds = bounds;
- }
-
- /**
* Transform the page object
* The matrix is composed as:
* |a c e|
@@ -87,13 +64,6 @@ public abstract class PdfPageObject {
Matrix matrix = new Matrix();
matrix.setValues(new float[]{a, e, d, c, b, f, 0, 0, 1}); // Set the matrix values
this.mTransform.postConcat(matrix); // Apply the transformation
-
- // Update the objectRect based on the new transformation
- if (this.mBounds != null) {
- RectF newRect = new RectF(this.mBounds);
- matrix.mapRect(newRect);
- this.mBounds.set(newRect);
- }
}
/**
diff --git a/pdf/framework/java/android/graphics/pdf/component/PdfPagePathObject.java b/pdf/framework/java/android/graphics/pdf/component/PdfPagePathObject.java
index e3d5d8020..08ed9ee3a 100644
--- a/pdf/framework/java/android/graphics/pdf/component/PdfPagePathObject.java
+++ b/pdf/framework/java/android/graphics/pdf/component/PdfPagePathObject.java
@@ -17,30 +17,20 @@
package android.graphics.pdf.component;
import android.annotation.FlaggedApi;
-import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Color;
-import android.graphics.DashPathEffect;
-import android.graphics.Matrix;
import android.graphics.Path;
-import android.graphics.PathEffect;
-import android.graphics.RectF;
import android.graphics.pdf.flags.Flags;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
/**
* Represents a path object on a PDF page. This class extends
* {@link PdfPageObject} and provides methods to access and modify the
- * path's content, such as its shape, fill color, stroke color, line width,
- * and line style.
+ * path's content, such as its shape, fill color, stroke color and line width.
*/
@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_PAGE_OBJECTS)
public final class PdfPagePathObject extends PdfPageObject {
- private Path mPath;
- private PathEffect mLineStyle;
+ private final Path mPath;
private Color mStrokeColor;
private float mStrokeWidth;
private Color mFillColor;
@@ -49,41 +39,24 @@ public final class PdfPagePathObject extends PdfPageObject {
* Constructor for the PdfPagePathObject. Sets the object type
* to {@link PdfPageObjectType#PATH}.
*/
- public PdfPagePathObject() {
+ public PdfPagePathObject(@NonNull Path path) {
super(PdfPageObjectType.PATH);
- this.mPath = new Path();
- this.mStrokeColor = new Color(); // Default is opaque black in the sRGB color space.
- this.mStrokeWidth = 1.0f;
+ this.mPath = path;
}
/**
* Returns the path of the object.
+ * The returned path object might be an approximation of the one used to
+ * create the original one if the original object has elements with curvature.
+ * <p>
+ * Note: The path is immutable because the underlying library does
+ * not allow modifying the path once it is created.
*
* @return The path.
*/
@NonNull
- public Path getPath() {
- return mPath;
- }
-
- /**
- * Sets the path of the object.
- *
- * @param path The path to set.
- */
- public void setPath(@NonNull Path path) {
- this.mPath = path;
- }
-
- /**
- * Returns the line style of the object's stroke.
- *
- * @return The {@link PathEffect} representing the line style, or null if no
- * style is set.
- */
- @Nullable
- public PathEffect getLineStyle() {
- return mLineStyle;
+ public Path toPath() {
+ return new Path(mPath);
}
/**
@@ -91,7 +64,7 @@ public final class PdfPagePathObject extends PdfPageObject {
*
* @return The stroke color of the object.
*/
- @NonNull
+ @Nullable
public Color getStrokeColor() {
return mStrokeColor;
}
@@ -101,7 +74,7 @@ public final class PdfPagePathObject extends PdfPageObject {
*
* @param strokeColor The stroke color of the object.
*/
- public void setStrokeColor(@NonNull Color strokeColor) {
+ public void setStrokeColor(@Nullable Color strokeColor) {
this.mStrokeColor = strokeColor;
}
@@ -124,25 +97,6 @@ public final class PdfPagePathObject extends PdfPageObject {
}
/**
- * Sets the line style of the object's stroke.
- *
- * @param lineStyle An integer representing the line style to set.
- */
- public void setLineStyle(int lineStyle) {
- switch (lineStyle) {
- case LineStyle.DASHED: // Example: Dashed line
- this.mLineStyle = new DashPathEffect(new float[]{10, 5}, 0);
- break;
- case LineStyle.DOTTED: // Example: Dotted line
- this.mLineStyle = new DashPathEffect(new float[]{2, 2}, 0);
- break;
- default: // Solid line (no effect)
- this.mLineStyle = null;
- break;
- }
- }
-
- /**
* Returns the fill color of the object.
*
* @return The fill color of the object.
@@ -161,42 +115,4 @@ public final class PdfPagePathObject extends PdfPageObject {
this.mFillColor = fillColor;
}
- /**
- * Overrides the
- * {@link PdfPageObject#transform(float, float, float, float, float, float)}
- * method to correctly transform the Path object.
- *
- * This method applies the given affine transformation matrix to the path and
- * also updates the object's bounding rectangle.
- *
- * @param a The a value of the transformation matrix.
- * @param b The b value of the transformation matrix.
- * @param c The c value of the transformation matrix.
- * @param d The d value of the transformation matrix.
- * @param e The e value of the transformation matrix.
- * @param f The f value of the transformation matrix.
- */
- @Override
- public void transform(float a, float b, float c, float d, float e, float f) {
- Matrix matrix = new Matrix();
- matrix.setValues(new float[]{a, c, e, b, d, f, 0, 0, 1});
- this.mPath.transform(matrix);
-
- // Also transform the objectRect
- RectF newRect = new RectF(this.getBounds());
- matrix.mapRect(newRect);
- this.getBounds().set(newRect);
- }
-
- /** @hide */
- @IntDef({LineStyle.SOLID, LineStyle.DASHED, LineStyle.DOTTED})
- @Retention(RetentionPolicy.SOURCE)
- public @interface LineStyle {
- /** Solid line (no effect). */
- int SOLID = 0;
- /** Dashed line. */
- int DASHED = 1;
- /** Dotted line. */
- int DOTTED = 2;
- }
}
diff --git a/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObject.java b/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObject.java
index c004af2a6..aa311fd5d 100644
--- a/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObject.java
+++ b/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObject.java
@@ -27,7 +27,7 @@ import android.graphics.pdf.flags.Flags;
* Represents a text object on a PDF page.
* This class extends PageObject and provides methods to access and modify the text content.
*/
-@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_PAGE_OBJECTS)
+@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_TEXT_OBJECTS)
public final class PdfPageTextObject extends PdfPageObject {
private String mText;
private Typeface mTypeface;
@@ -43,7 +43,6 @@ public final class PdfPageTextObject extends PdfPageObject {
* @param typeface The font of the text.
* @param fontSize The font size of the text.
*/
- @FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_TEXT_OBJECTS)
public PdfPageTextObject(@NonNull String text, @NonNull Typeface typeface, float fontSize) {
super(PdfPageObjectType.TEXT);
this.mText = text;
diff --git a/pdf/framework/libs/pdfClient/annotation.cc b/pdf/framework/libs/pdfClient/annotation.cc
new file mode 100644
index 000000000..63fce4038
--- /dev/null
+++ b/pdf/framework/libs/pdfClient/annotation.cc
@@ -0,0 +1,163 @@
+/*
+ * 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.
+ */
+
+#include "annotation.h"
+
+#include "logging.h"
+
+#define LOG_TAG "annotation"
+
+namespace pdfClient {
+
+std::vector<PageObject*> StampAnnotation::GetObjects() const {
+ std::vector<PageObject*> page_objects;
+ for (const auto& page_object : pageObjects_) {
+ page_objects.push_back(page_object.get());
+ }
+
+ return page_objects;
+}
+
+bool StampAnnotation::PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot) {
+ int num_of_objects = FPDFAnnot_GetObjectCount(fpdf_annot);
+
+ for (int object_index = 0; object_index < num_of_objects; object_index++) {
+ FPDF_PAGEOBJECT page_object = FPDFAnnot_GetObject(fpdf_annot, object_index);
+ int objectType = FPDFPageObj_GetType(page_object);
+
+ std::unique_ptr<PageObject> page_object_;
+
+ switch (objectType) {
+ case FPDF_PAGEOBJ_PATH: {
+ page_object_ = std::make_unique<PathObject>();
+ break;
+ }
+ case FPDF_PAGEOBJ_IMAGE: {
+ page_object_ = std::make_unique<ImageObject>();
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+
+ if (page_object_ && !page_object_->PopulateFromFPDFInstance(page_object)) {
+ LOGE("Failed to get all the data corresponding to object with index "
+ "%d ",
+ object_index);
+ page_object_ = nullptr;
+ }
+
+ // Add the page_object_ to the stamp annotation even if page_object_ is null
+ // as we are storing empty unique ptr for the unsupported page objects
+ AddObject(std::move(page_object_));
+ }
+ return true;
+}
+
+ScopedFPDFAnnotation StampAnnotation::CreatePdfiumInstance(FPDF_DOCUMENT document, FPDF_PAGE page) {
+ // Create a ScopedFPDFAnnotation, If it will fail to populate this pdfium annot with desired
+ // params, we will return null that will lead to scoped annot getting out of scope and thus
+ // getting destroyed
+ ScopedFPDFAnnotation scoped_annot =
+ ScopedFPDFAnnotation(FPDFPage_CreateAnnot(page, FPDF_ANNOT_STAMP));
+
+ if (!scoped_annot) {
+ LOGE("Failed to create stamp Annotation.");
+ return nullptr;
+ }
+
+ Rectangle_f annotation_bounds = GetBounds();
+ FS_RECTF rect;
+ rect.left = annotation_bounds.left;
+ rect.bottom = annotation_bounds.bottom;
+ rect.right = annotation_bounds.right;
+ rect.top = annotation_bounds.top;
+
+ if (!FPDFAnnot_SetRect(scoped_annot.get(), &rect)) {
+ LOGE("Stamp Annotation bounds can't be set");
+ return nullptr;
+ }
+
+ std::vector<PageObject*> pageObjects = GetObjects();
+ for (auto pageObject : pageObjects) {
+ ScopedFPDFPageObject scoped_page_object = pageObject->CreateFPDFInstance(document);
+
+ if (!scoped_page_object) {
+ LOGE("Failed to create page object to add in the stamp annotation");
+ return nullptr;
+ }
+
+ if (!FPDFAnnot_AppendObject(scoped_annot.get(), scoped_page_object.release())) {
+ LOGE("Page object can't be inserted in the stamp annotation");
+ return nullptr;
+ }
+ }
+
+ return scoped_annot;
+}
+
+bool StampAnnotation::UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document) {
+ if (FPDFAnnot_GetSubtype(fpdf_annot) != FPDF_ANNOT_STAMP) {
+ LOGE("Unsupported operation - can't update a stamp annotation with some other type of "
+ "annotation");
+ return false;
+ }
+
+ Rectangle_f new_bounds = GetBounds();
+ FS_RECTF rect;
+ rect.left = new_bounds.left;
+ rect.bottom = new_bounds.bottom;
+ rect.right = new_bounds.right;
+ rect.top = new_bounds.top;
+ if (!FPDFAnnot_SetRect(fpdf_annot, &rect)) {
+ LOGE("Failed to update the bounds of the stamp annotation at given index");
+ return false;
+ }
+
+ // First Remove all the known existing objects from the stamp annotation, and then rewrite
+ int num_objects = FPDFAnnot_GetObjectCount(fpdf_annot);
+ for (int object_index = 0; object_index < num_objects; object_index++) {
+ FPDF_PAGEOBJECT pageObject = FPDFAnnot_GetObject(fpdf_annot, object_index);
+ int object_type = FPDFPageObj_GetType(pageObject);
+ if (pageObject != nullptr &&
+ (object_type == FPDF_PAGEOBJ_IMAGE || object_type == FPDF_PAGEOBJ_PATH)) {
+ if (!FPDFAnnot_RemoveObject(fpdf_annot, object_index)) {
+ LOGE("Failed to remove existing object from stamp annotation");
+ return false;
+ }
+ }
+ }
+
+ // Rewrite
+ std::vector<PageObject*> newPageObjects = GetObjects();
+ for (auto pageObject : newPageObjects) {
+ ScopedFPDFPageObject scoped_page_object = pageObject->CreateFPDFInstance(document);
+
+ if (!scoped_page_object) {
+ LOGE("Failed to create new page object to add in the stamp annotation");
+ return false;
+ }
+
+ if (!FPDFAnnot_AppendObject(fpdf_annot, scoped_page_object.release())) {
+ LOGE("Page object can't be inserted in the stamp annotation");
+ return false;
+ }
+ }
+ return true;
+}
+
+} // namespace pdfClient \ No newline at end of file
diff --git a/pdf/framework/libs/pdfClient/annotation.h b/pdf/framework/libs/pdfClient/annotation.h
new file mode 100644
index 000000000..482ff8a37
--- /dev/null
+++ b/pdf/framework/libs/pdfClient/annotation.h
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+#ifndef MEDIAPROVIDER_PDF_JNI_PDFCLIENT_ANNOTATION_H_
+#define MEDIAPROVIDER_PDF_JNI_PDFCLIENT_ANNOTATION_H_
+
+#include <map>
+
+#include "fpdf_annot.h"
+#include "fpdfview.h"
+#include "page_object.h"
+#include "rect.h"
+
+using pdfClient::PageObject;
+using pdfClient::Rectangle_f;
+
+namespace pdfClient {
+// Base class for different type of annotations
+class Annotation {
+ public:
+ enum class Type { UNKNOWN = 0, Stamp = 1 };
+
+ Annotation(Type type, const Rectangle_f& bounds) : type_(type), bounds_(bounds) {}
+ virtual ~Annotation() = default;
+
+ Type GetType() const { return type_; }
+
+ Rectangle_f GetBounds() const { return bounds_; }
+ void SetBounds(Rectangle_f bounds) { bounds_ = bounds; }
+
+ virtual bool PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot) = 0;
+ virtual ScopedFPDFAnnotation CreatePdfiumInstance(FPDF_DOCUMENT document, FPDF_PAGE page) = 0;
+ virtual bool UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document) = 0;
+
+ private:
+ Type type_;
+ Rectangle_f bounds_;
+};
+
+// This class represents a stamp annotation on a page of the pdf document. It doesn't take the
+// ownership of the pdfium annotation. It takes the ownership of PdfPageObject inside it but not of
+// underlying pdfium page objects
+class StampAnnotation : public Annotation {
+ public:
+ StampAnnotation(const Rectangle_f& bounds) : Annotation(Type::Stamp, bounds) {}
+
+ // Return a const reference to the list
+ // Stamp annotation will have the ownership of the page objects inside it
+ std::vector<PageObject*> GetObjects() const;
+
+ void AddObject(std::unique_ptr<PageObject> pageObject) {
+ // Take ownership of the PageObject
+ pageObjects_.push_back(std::move(pageObject));
+ }
+
+ void RemoveObject(int index) {
+ auto it = pageObjects_.begin();
+ std::advance(it, index);
+ pageObjects_.erase(it);
+ }
+
+ bool PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot) override;
+ ScopedFPDFAnnotation CreatePdfiumInstance(FPDF_DOCUMENT document, FPDF_PAGE page) override;
+ bool UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document) override;
+
+ private:
+ std::vector<std::unique_ptr<PageObject>> pageObjects_;
+};
+
+} // namespace pdfClient
+
+#endif \ No newline at end of file
diff --git a/pdf/framework/libs/pdfClient/jni_conversion.cc b/pdf/framework/libs/pdfClient/jni_conversion.cc
index b77559bb0..a81fd4d1d 100644
--- a/pdf/framework/libs/pdfClient/jni_conversion.cc
+++ b/pdf/framework/libs/pdfClient/jni_conversion.cc
@@ -16,17 +16,26 @@
#include "jni_conversion.h"
+#include <android/bitmap.h>
#include <string.h>
+#include "logging.h"
#include "rect.h"
+using pdfClient::Color;
using pdfClient::Document;
+using pdfClient::ImageObject;
using pdfClient::LinuxFileOps;
+using pdfClient::Matrix;
+using pdfClient::PageObject;
+using pdfClient::PathObject;
using pdfClient::Rectangle_i;
using pdfClient::SelectionBoundary;
using std::string;
using std::vector;
+#define LOG_TAG "jni_conversion"
+
namespace convert {
namespace {
@@ -42,7 +51,15 @@ static const char* kChoiceOption = "android/graphics/pdf/models/ListItem";
static const char* kGotoLinkDestination =
"android/graphics/pdf/content/PdfPageGotoLinkContent$Destination";
static const char* kGotoLink = "android/graphics/pdf/content/PdfPageGotoLinkContent";
-
+static const char* kPageObject = "android/graphics/pdf/component/PdfPageObject";
+static const char* kPathObject = "android/graphics/pdf/component/PdfPagePathObject";
+static const char* kImageObject = "android/graphics/pdf/component/PdfPageImageObject";
+
+static const char* kBitmap = "android/graphics/Bitmap";
+static const char* kBitmapConfig = "android/graphics/Bitmap$Config";
+static const char* kColor = "android/graphics/Color";
+static const char* kMatrix = "android/graphics/Matrix";
+static const char* kPath = "android/graphics/Path";
static const char* kRect = "android/graphics/Rect";
static const char* kRectF = "android/graphics/RectF";
static const char* kInteger = "java/lang/Integer";
@@ -117,6 +134,23 @@ jobject ToJavaList(JNIEnv* env, const vector<T>& input,
return java_list;
}
+// Copy a C++ vector to a java ArrayList, using the given function to convert.
+template <class T>
+jobject ToJavaList(JNIEnv* env, const vector<T*>& input,
+ jobject (*ToJavaObject)(JNIEnv* env, const T*)) {
+ static jclass arraylist_class = GetPermClassRef(env, kArrayList);
+ static jmethodID init = env->GetMethodID(arraylist_class, "<init>", "(I)V");
+ static jmethodID add = env->GetMethodID(arraylist_class, "add", funcsig("Z", kObject).c_str());
+
+ jobject java_list = env->NewObject(arraylist_class, init, input.size());
+ for (size_t i = 0; i < input.size(); i++) {
+ jobject java_object = ToJavaObject(env, input[i]);
+ env->CallBooleanMethod(java_list, add, java_object);
+ env->DeleteLocalRef(java_object);
+ }
+ return java_list;
+}
+
} // namespace
jobject ToJavaPdfDocument(JNIEnv* env, std::unique_ptr<Document> doc) {
@@ -324,4 +358,384 @@ jobject ToJavaGotoLinks(JNIEnv* env, const vector<GotoLink>& links) {
return ToJavaList(env, links, &ToJavaGotoLink);
}
+jobject ToJavaBitmap(JNIEnv* env, void* buffer, int width, int height) {
+ // Find Java Bitmap class
+ static jclass bitmap_class = GetPermClassRef(env, kBitmap);
+
+ // Get createBitmap method ID
+ static jmethodID create_bitmap = env->GetStaticMethodID(
+ bitmap_class, "createBitmap", funcsig(kBitmap, "I", "I", kBitmapConfig).c_str());
+
+ // Get Bitmap.Config.ARGB_8888 field ID
+ static jclass bitmap_config_class = GetPermClassRef(env, kBitmapConfig);
+ static jfieldID argb8888_field =
+ env->GetStaticFieldID(bitmap_config_class, "ARGB_8888", sig(kBitmapConfig).c_str());
+ static jobject argb8888 =
+ env->NewGlobalRef(env->GetStaticObjectField(bitmap_config_class, argb8888_field));
+
+ // Create a Java Bitmap object
+ jobject java_bitmap =
+ env->CallStaticObjectMethod(bitmap_class, create_bitmap, width, height, argb8888);
+
+ // Lock the Bitmap pixels for copying
+ void* bitmap_pixels;
+ if (AndroidBitmap_lockPixels(env, java_bitmap, &bitmap_pixels) < 0) {
+ return NULL;
+ }
+
+ // Copy the buffer data into java Bitmap.
+ std::memcpy(bitmap_pixels, buffer, width * height); // 4 bytes per pixel (ARGB_8888)
+
+ // Unlock the Bitmap pixels
+ AndroidBitmap_unlockPixels(env, java_bitmap);
+
+ return java_bitmap;
+}
+
+jobject ToJavaColor(JNIEnv* env, Color color) {
+ // Find Java Color class
+ static jclass color_class = GetPermClassRef(env, kColor);
+
+ // Get valueOf method ID
+ static jmethodID value_of =
+ env->GetStaticMethodID(color_class, "valueOf", funcsig(kColor, "I").c_str());
+
+ // Get ARGB values from Native Color
+ uint A = color.a;
+ uint R = color.r;
+ uint G = color.g;
+ uint B = color.b;
+
+ // Make ARGB java color int
+ int java_color_int = (A & 0xFF) << 24 | (R & 0xFF) << 16 | (G & 0xFF) << 8 | (B & 0xFF);
+
+ // Create a Java Color Object.
+ jobject java_color = env->CallStaticObjectMethod(color_class, value_of, java_color_int);
+
+ return java_color;
+}
+
+jfloatArray ToJavaFloatArray(JNIEnv* env, const float arr[], size_t length) {
+ // Create Java float Array.
+ jfloatArray java_float_array = env->NewFloatArray(length);
+
+ // Copy data from the C++ float Array to the Java float Array
+ env->SetFloatArrayRegion(java_float_array, 0, length, arr);
+
+ return java_float_array;
+}
+
+jobject ToJavaMatrix(JNIEnv* env, const Matrix matrix) {
+ // Find Java Matrix class
+ static jclass matrix_class = GetPermClassRef(env, kMatrix);
+ // Get the constructor method ID
+ static jmethodID init = env->GetMethodID(matrix_class, "<init>", funcsig("V").c_str());
+
+ // Create Java Matrix object.
+ jobject java_matrix = env->NewObject(matrix_class, init);
+
+ // Create Transform Array.
+ float transform[9] = {matrix.a, matrix.c, matrix.e, matrix.b, matrix.d, matrix.f, 0, 0, 1};
+
+ // Convert to Java floatArray.
+ jfloatArray java_float_array =
+ ToJavaFloatArray(env, transform, sizeof(transform) / sizeof(transform[0]));
+
+ // Matrix setValues.
+ static jmethodID set_values = env->GetMethodID(matrix_class, "setValues", "([F)V");
+ env->CallVoidMethod(java_matrix, set_values, java_float_array);
+
+ return java_matrix;
+}
+
+jobject ToJavaPath(JNIEnv* env, const std::vector<PathObject::Segment>& segments) {
+ // Find Java Path class.
+ static jclass path_class = GetPermClassRef(env, kPath);
+ // Get the constructor methodID.
+ static jmethodID init = env->GetMethodID(path_class, "<init>", funcsig("V").c_str());
+
+ // Create Java Path object.
+ jobject java_path = env->NewObject(path_class, init);
+
+ // Set Path Segments in Java.
+ for (auto& segment : segments) {
+ switch (segment.command) {
+ case PathObject::Segment::Command::Move: {
+ static jmethodID move_to =
+ env->GetMethodID(path_class, "moveTo", funcsig("V", "F", "F").c_str());
+
+ env->CallVoidMethod(java_path, move_to, segment.x, segment.y);
+ break;
+ }
+ case PathObject::Segment::Command::Line: {
+ static jmethodID line_to =
+ env->GetMethodID(path_class, "lineTo", funcsig("V", "F", "F").c_str());
+
+ env->CallVoidMethod(java_path, line_to, segment.x, segment.y);
+ break;
+ }
+ default:
+ break;
+ }
+ // Check if segment isClosed.
+ if (segment.is_closed) {
+ static jmethodID close = env->GetMethodID(path_class, "close", funcsig("V").c_str());
+
+ env->CallVoidMethod(java_path, close);
+ }
+ }
+
+ return java_path;
+}
+
+jobject ToJavaPdfPageObject(JNIEnv* env, const PageObject* page_object) {
+ // Check for Native Supported Object.
+ if (!page_object) {
+ return NULL;
+ }
+
+ jobject java_page_object = NULL;
+
+ switch (page_object->GetType()) {
+ case PageObject::Type::Path: {
+ // Cast to PathObject
+ const PathObject* path_object = static_cast<const PathObject*>(page_object);
+
+ // Find Java PathObject Class.
+ static jclass path_object_class = GetPermClassRef(env, kPathObject);
+ // Get Constructor Id.
+ static jmethodID init_path =
+ env->GetMethodID(path_object_class, "<init>", funcsig("V", kPath).c_str());
+
+ // Create Java Path from Native PathSegments.
+ jobject java_path = ToJavaPath(env, path_object->segments);
+
+ // Create Java PathObject Instance.
+ java_page_object = env->NewObject(path_object_class, init_path, java_path);
+
+ // Set Java PathObject FillColor.
+ if (path_object->is_fill_mode) {
+ static jmethodID set_fill_color = env->GetMethodID(
+ path_object_class, "setFillColor", funcsig("V", kColor).c_str());
+
+ env->CallVoidMethod(java_page_object, set_fill_color,
+ ToJavaColor(env, path_object->fill_color));
+ }
+
+ // Set Java PathObject StrokeColor.
+ if (path_object->is_stroke) {
+ static jmethodID set_stroke_color = env->GetMethodID(
+ path_object_class, "setStrokeColor", funcsig("V", kColor).c_str());
+
+ env->CallVoidMethod(java_page_object, set_stroke_color,
+ ToJavaColor(env, path_object->stroke_color));
+ }
+
+ // Set Java Stroke Width.
+ static jmethodID set_stroke_width =
+ env->GetMethodID(path_object_class, "setStrokeWidth", "(F)V");
+ env->CallVoidMethod(java_page_object, set_stroke_width, path_object->stroke_width);
+
+ break;
+ }
+ case PageObject::Type::Image: {
+ // Cast to ImageObject
+ const ImageObject* image_object = static_cast<const ImageObject*>(page_object);
+
+ // Find Java ImageObject Class.
+ static jclass image_object_class = GetPermClassRef(env, kImageObject);
+ // Get Constructor Id.
+ static jmethodID init_image =
+ env->GetMethodID(image_object_class, "<init>", funcsig("V", kBitmap).c_str());
+
+ // Get Bitmap readable buffer from ImageObject Data.
+ void* buffer = image_object->GetBitmapReadableBuffer();
+
+ // Create Java Bitmap from Native Bitmap Buffer.
+ jobject java_bitmap =
+ ToJavaBitmap(env, buffer, image_object->width, image_object->height);
+
+ // Create Java ImageObject Instance.
+ java_page_object = env->NewObject(image_object_class, init_image, java_bitmap);
+
+ break;
+ }
+ default:
+ break;
+ }
+
+ // If no PageObject was created, return null
+ if (java_page_object == NULL) {
+ return NULL;
+ }
+
+ // Find Java PageObject class
+ static jclass page_object_class = GetPermClassRef(env, kPageObject);
+
+ // Set Java Matrix
+ static jmethodID set_matrix =
+ env->GetMethodID(page_object_class, "setMatrix", funcsig("V", kMatrix).c_str());
+ env->CallVoidMethod(java_page_object, set_matrix, ToJavaMatrix(env, page_object->matrix));
+
+ return java_page_object;
+}
+
+jobject ToJavaPdfPageObjects(JNIEnv* env, const vector<PageObject*>& page_objects) {
+ return ToJavaList(env, page_objects, &ToJavaPdfPageObject);
+}
+
+Color ToNativeColor(JNIEnv* env, jobject java_color) {
+ // Find Java Color class
+ static jclass color_class = GetPermClassRef(env, kColor);
+
+ // Get the color as an ARGB integer
+ jmethodID get_color_int = env->GetMethodID(color_class, "toArgb", funcsig("I").c_str());
+ jint java_color_int = env->CallIntMethod(java_color, get_color_int);
+
+ // Decoding RGBA components
+ unsigned int red = (java_color_int >> 16) & 0xFF;
+ unsigned int green = (java_color_int >> 8) & 0xFF;
+ unsigned int blue = java_color_int & 0xFF;
+ unsigned int alpha = (java_color_int >> 24) & 0xFF;
+
+ return Color(red, green, blue, alpha);
+}
+
+std::unique_ptr<PageObject> ToNativePageObject(JNIEnv* env, jobject java_page_object) {
+ // Find Java PageObject class and GetType
+ static jclass page_object_class = GetPermClassRef(env, kPageObject);
+ static jmethodID get_type = env->GetMethodID(page_object_class, "getPdfObjectType", "()I");
+ jint page_object_type = env->CallIntMethod(java_page_object, get_type);
+
+ // Pointer to PageObject
+ std::unique_ptr<PageObject> page_object = nullptr;
+
+ switch (static_cast<PageObject::Type>(page_object_type)) {
+ case PageObject::Type::Path: {
+ // Create PathObject Data Instance.
+ auto path_object = std::make_unique<PathObject>();
+
+ // Get Ref to Java PathObject Class.
+ static jclass path_object_class = GetPermClassRef(env, kPathObject);
+
+ // Get Path from Java PathObject.
+ static jmethodID to_path =
+ env->GetMethodID(path_object_class, "toPath", funcsig(kPath).c_str());
+ jobject java_path = env->CallObjectMethod(java_page_object, to_path);
+
+ // Find Java Path Class.
+ static jclass path_class = GetPermClassRef(env, kPath);
+
+ // Get the Approximate Array for the Path.
+ static jmethodID approximate = env->GetMethodID(path_class, "approximate", "(F)[F");
+ jfloatArray java_approximate =
+ (jfloatArray)env->CallObjectMethod(java_path, approximate);
+ const jsize size = env->GetArrayLength(java_approximate);
+
+ // Copy Java Array to Native Array
+ float path_approximate[size];
+ env->GetFloatArrayRegion(java_approximate, 0, size, path_approximate);
+
+ // Set PathObject Data PathSegments.
+ auto& segments = path_object->segments;
+ for (int i = 0; i < size; i += 3) {
+ if (i == 0 || path_approximate[i] == path_approximate[i - 3]) {
+ segments.emplace_back(PathObject::Segment::Command::Move,
+ path_approximate[i + 1], path_approximate[i + 2]);
+ } else {
+ segments.emplace_back(PathObject::Segment::Command::Line,
+ path_approximate[i + 1], path_approximate[i + 2]);
+ }
+ }
+
+ // Get Java PathObject Fill Color.
+ static jmethodID get_fill_color =
+ env->GetMethodID(path_object_class, "getFillColor", funcsig(kColor).c_str());
+ jobject java_fill_color = env->CallObjectMethod(java_page_object, get_fill_color);
+
+ // Set PathObject Data Fill Mode and Fill Color
+ path_object->is_fill_mode = (java_fill_color != NULL);
+ if (path_object->is_fill_mode) {
+ path_object->fill_color = ToNativeColor(env, java_fill_color);
+ }
+
+ // Get Java PathObject Stroke Color.
+ static jmethodID get_stroke_color =
+ env->GetMethodID(path_object_class, "getStrokeColor", funcsig(kColor).c_str());
+ jobject java_stroke_color = env->CallObjectMethod(java_page_object, get_stroke_color);
+
+ // Set PathObject Data Stroke Mode and Stroke Color.
+ path_object->is_stroke = (java_stroke_color != NULL);
+ if (path_object->is_stroke) {
+ path_object->stroke_color = ToNativeColor(env, java_stroke_color);
+ }
+
+ // Get Java PathObject Stroke Width.
+ static jmethodID get_stroke_width =
+ env->GetMethodID(path_object_class, "getStrokeWidth", funcsig("F").c_str());
+ jfloat stroke_width = env->CallFloatMethod(java_page_object, get_stroke_width);
+
+ // Set PathObject Data Stroke Width.
+ path_object->stroke_width = stroke_width;
+
+ page_object = std::move(path_object);
+ break;
+ }
+ case PageObject::Type::Image: {
+ // Create ImageObject Data Instance.
+ auto image_object = std::make_unique<ImageObject>();
+
+ // Get Ref to Java ImageObject Class.
+ static jclass image_object_class = GetPermClassRef(env, kImageObject);
+
+ // Get the bitmap from the Java ImageObject
+ static jmethodID get_bitmap =
+ env->GetMethodID(image_object_class, "getBitmap", funcsig(kBitmap).c_str());
+ jobject java_bitmap = env->CallObjectMethod(java_page_object, get_bitmap);
+
+ // Create an FPDF_BITMAP from the Android Bitmap
+ void* bitmap_pixels;
+ if (AndroidBitmap_lockPixels(env, java_bitmap, &bitmap_pixels) < 0) {
+ break;
+ }
+
+ AndroidBitmapInfo bitmap_info;
+ AndroidBitmap_getInfo(env, java_bitmap, &bitmap_info);
+ const int stride = bitmap_info.width * 4;
+
+ // Set ImageObject Data Bitmap
+ image_object->bitmap = ScopedFPDFBitmap(FPDFBitmap_CreateEx(
+ bitmap_info.width, bitmap_info.height, FPDFBitmap_BGRA, bitmap_pixels, stride));
+
+ // Unlock the Android Bitmap
+ AndroidBitmap_unlockPixels(env, java_bitmap);
+
+ page_object = std::move(image_object);
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (!page_object) {
+ return nullptr;
+ }
+
+ // Get Matrix from Java PageObject.
+ static jmethodID get_matrix = env->GetMethodID(page_object_class, "getMatrix", "()[F");
+ jfloatArray java_matrix_array =
+ (jfloatArray)env->CallObjectMethod(java_page_object, get_matrix);
+
+ // Copy Java Array to Native Array
+ float transform[9];
+ env->GetFloatArrayRegion(java_matrix_array, 0, 9, transform);
+
+ // Set PageObject Data Matrix.
+ page_object->matrix = {transform[0 /*kMScaleX*/], transform[3 /*kMSkewY*/],
+ transform[1 /*kMSkewX*/], transform[4 /*kMScaleY*/],
+ transform[2 /*kMTransX*/], transform[5 /*kMTransY*/]};
+
+ return page_object;
+}
+
} // namespace convert \ No newline at end of file
diff --git a/pdf/framework/libs/pdfClient/jni_conversion.h b/pdf/framework/libs/pdfClient/jni_conversion.h
index 51d8a8b57..b85aaaed2 100644
--- a/pdf/framework/libs/pdfClient/jni_conversion.h
+++ b/pdf/framework/libs/pdfClient/jni_conversion.h
@@ -25,13 +25,18 @@
#include "file.h"
#include "form_widget_info.h"
#include "page.h"
+#include "page_object.h"
#include "rect.h"
+using pdfClient::Color;
using pdfClient::Document;
using pdfClient::FormWidgetInfo;
using pdfClient::GotoLink;
using pdfClient::GotoLinkDest;
+using pdfClient::Matrix;
using pdfClient::Option;
+using pdfClient::PageObject;
+using pdfClient::PathObject;
using pdfClient::Rectangle_i;
using pdfClient::SelectionBoundary;
using pdfClient::Status;
@@ -108,6 +113,24 @@ jobject ToJavaGotoLink(JNIEnv* env, const GotoLink link);
jobject ToJavaGotoLinks(JNIEnv* env, const vector<GotoLink>& links);
+jobject ToJavaBitmap(JNIEnv* env, void* buffer, int width, int height);
+
+jobject ToJavaColor(JNIEnv* env, Color color);
+
+jfloatArray ToJavaFloatArray(JNIEnv* env, const float arr[], size_t length);
+
+jobject ToJavaMatrix(JNIEnv* env, const Matrix matrix);
+
+jobject ToJavaPath(JNIEnv* env, const std::vector<PathObject::Segment>& segments);
+
+jobject ToJavaPdfPageObject(JNIEnv* env, const PageObject* page_object);
+
+jobject ToJavaPdfPageObjects(JNIEnv* env, const vector<PageObject*>& page_objects);
+
+Color ToNativeColor(JNIEnv* env, jobject java_color);
+
+std::unique_ptr<PageObject> ToNativePageObject(JNIEnv* env, jobject java_page_object);
+
} // namespace convert
#endif // MEDIAPROVIDER_PDF_JNI_CONVERSION_H_ \ No newline at end of file
diff --git a/pdf/framework/libs/pdfClient/page.cc b/pdf/framework/libs/pdfClient/page.cc
index 7765f01b4..e02047dca 100644
--- a/pdf/framework/libs/pdfClient/page.cc
+++ b/pdf/framework/libs/pdfClient/page.cc
@@ -41,6 +41,7 @@
#define LOG_TAG "page"
+using pdfClient::Rectangle_f;
using std::vector;
namespace pdfClient {
@@ -445,38 +446,13 @@ std::vector<PageObject*> Page::GetPageObjects(bool refetch) {
int Page::AddPageObject(std::unique_ptr<PageObject> pageObject) {
// Create a scoped PDFium page object.
- ScopedFPDFPageObject scoped_page_object;
-
- // Handle different page object types.
- switch (pageObject->GetType()) {
- case PageObject::Type::Image: {
- ImageObject* imgObject = pageObject->AsImage();
-
- // Create a PDFium image object.
- scoped_page_object = ScopedFPDFPageObject(FPDFPageObj_NewImageObj(document_));
- if (!scoped_page_object) {
- return -1;
- }
- // Set the bitmap for the image object.
- if (!FPDFImageObj_SetBitmap(nullptr, 0, scoped_page_object.get(), imgObject->bitmap)) {
- return -1;
- }
- break;
- }
- default:
- break;
- }
+ ScopedFPDFPageObject scoped_page_object = pageObject->CreateFPDFInstance(document_);
// Check if a FPDF page object was created.
if (!scoped_page_object) {
return -1;
}
- // Set the matrix for the page object.
- if (!FPDFPageObj_SetMatrix(scoped_page_object.get(), &pageObject->matrix)) {
- return -1;
- }
-
// Insert the FPDF page object into the FPDF page.
FPDFPage_InsertObject(page_.get(), scoped_page_object.release());
FPDFPage_GenerateContent(page_.get());
@@ -508,32 +484,16 @@ bool Page::RemovePageObject(int index) {
}
bool Page::UpdatePageObject(int index, std::unique_ptr<PageObject> pageObject) {
+ // Check for valid index
if (index < 0 || index >= FPDFPage_CountObjects(page_.get())) {
return false;
}
+ // Get PDFium PageObject.
FPDF_PAGEOBJECT page_object = FPDFPage_GetObject(page_.get(), index);
- switch (pageObject->GetType()) {
- case PageObject::Type::Image: {
- ImageObject* imageObject = pageObject->AsImage();
-
- // Check for Type Correctness.
- if (FPDFPageObj_GetType(page_object) != FPDF_PAGEOBJ_IMAGE) {
- return false;
- }
- // Set the new bitmap.
- if (!FPDFImageObj_SetBitmap(nullptr, 0, page_object, imageObject->bitmap)) {
- return false;
- }
- break;
- }
- default:
- break;
- }
-
- // Set the updated matrix.
- if (!FPDFPageObj_SetMatrix(page_object, &pageObject->matrix)) {
+ // Update PDFium PageObject
+ if (!pageObject->UpdateFPDFInstance(page_object)) {
return false;
}
@@ -829,37 +789,145 @@ void Page::PopulatePageObjects(bool refetch) {
std::unique_ptr<PageObject> page_object_ = nullptr;
switch (type) {
+ case FPDF_PAGEOBJ_PATH: {
+ page_object_ = std::make_unique<PathObject>();
+ break;
+ }
case FPDF_PAGEOBJ_IMAGE: {
- auto image_object_ = std::make_unique<ImageObject>();
- // Get Bitmap from Image
- FPDF_BITMAP bitmap = FPDFImageObj_GetBitmap(page_object);
- if (bitmap) {
- image_object_->bitmap = bitmap;
- page_object_ = std::move(image_object_);
- }
+ page_object_ = std::make_unique<ImageObject>();
break;
}
default:
break;
}
- if (page_object_) {
- // Get Matrix Data
- FPDFPageObj_GetMatrix(page_object, &page_object_->matrix);
- // Get Fill Color Data
- FPDFPageObj_GetFillColor(page_object, &page_object_->fill_color.r,
- &page_object_->fill_color.g, &page_object_->fill_color.b,
- &page_object_->fill_color.a);
- // Get Stroke Color Data
- FPDFPageObj_GetStrokeColor(page_object, &page_object_->stroke_color.r,
- &page_object_->stroke_color.g, &page_object_->stroke_color.b,
- &page_object_->stroke_color.a);
- // Get Stroke Width Data
- FPDFPageObj_GetStrokeWidth(page_object, &page_object_->stroke_width);
-
+ // Populate PageObject From Page
+ if (page_object_ && page_object_->PopulateFromFPDFInstance(page_object)) {
page_objects_[index] = std::move(page_object_);
}
}
}
+std::vector<Annotation*> Page::GetPageAnnotations() {
+ PopulateAnnotations();
+
+ std::vector<Annotation*> result;
+
+ result.reserve(annotations_.size());
+ for (const auto& annotation : annotations_) {
+ result.push_back(annotation.get());
+ }
+
+ return result;
+}
+
+void Page::PopulateAnnotations() {
+ // If page_ is null
+ if (!page_) {
+ LOGE("Page is null");
+ return;
+ }
+
+ int num_of_annotations = FPDFPage_GetAnnotCount(page_.get());
+ annotations_.resize(num_of_annotations);
+
+ for (int annotation_index = 0; annotation_index < num_of_annotations; annotation_index++) {
+ ScopedFPDFAnnotation scoped_annot(FPDFPage_GetAnnot(page_.get(), annotation_index));
+ int annotationType = FPDFAnnot_GetSubtype(scoped_annot.get());
+
+ FS_RECTF rect;
+ if (!FPDFAnnot_GetRect(scoped_annot.get(), &rect)) {
+ LOGE("Failed to get the bounds of the stamp annotation");
+ break;
+ }
+ Rectangle_f bounds = Rectangle_f{rect.left, rect.top, rect.right, rect.bottom};
+
+ std::unique_ptr<Annotation> annotation = nullptr;
+
+ switch (annotationType) {
+ case FPDF_ANNOT_STAMP: {
+ annotation = std::make_unique<StampAnnotation>(bounds);
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+
+ if (!annotation || !annotation->PopulateFromPdfiumInstance(scoped_annot.get())) {
+ LOGE("Failed to create a pdfClient's instance of stamp annotation using pdfium "
+ "instance");
+ }
+
+ annotations_[annotation_index] = std::move(annotation);
+ }
+}
+
+int Page::AddPageAnnotation(std::unique_ptr<Annotation> annotation) {
+ ScopedFPDFAnnotation scoped_annot = annotation->CreatePdfiumInstance(document_, page_.get());
+
+ if (!scoped_annot) {
+ LOGE("Failed to add the given annotation to the page");
+ return -1;
+ }
+
+ FPDFPage_GenerateContent(page_.get());
+
+ // Add the object to the annotations_ list
+ annotations_.push_back(std::move(annotation));
+
+ // Return the index of added annotation
+ return FPDFPage_GetAnnotIndex(page_.get(), scoped_annot.get());
+}
+
+bool Page::RemovePageAnnotation(int index) {
+ PopulateAnnotations();
+ if (index >= annotations_.size() || index < 0) {
+ LOGE("Given index is out range for number of annotations on this page");
+ return false;
+ }
+ // Remove the annotation at given index
+ if (!FPDFPage_RemoveAnnot(page_.get(), index)) {
+ LOGE("Failed to remove the annotation at index - %d ", index);
+ return false;
+ }
+
+ FPDFPage_GenerateContent(page_.get());
+
+ // Remove from annotations_ list
+ annotations_.erase(annotations_.begin() + index);
+
+ return true;
+}
+
+bool Page::UpdatePageAnnotation(int index, std::unique_ptr<Annotation> annotation) {
+ PopulateAnnotations();
+ // Check for valid index
+ if (index < 0 || index >= annotations_.size()) {
+ return false;
+ }
+
+ // check if there in an annotation of supported type at given index
+ if (annotations_[index] == nullptr) {
+ return false;
+ }
+
+ // Get the pdfium annotation
+ ScopedFPDFAnnotation scoped_annot = ScopedFPDFAnnotation(FPDFPage_GetAnnot(page_.get(), index));
+
+ if (!scoped_annot) {
+ LOGE("Failed to get pdfium instance");
+ return false;
+ }
+
+ if (annotation->UpdatePdfiumInstance(scoped_annot.get(), document_)) {
+ LOGE("Failed to update pdfium annotation");
+ return false;
+ }
+
+ FPDFPage_GenerateContent(page_.get());
+
+ return true;
+}
+
} // namespace pdfClient \ No newline at end of file
diff --git a/pdf/framework/libs/pdfClient/page.h b/pdf/framework/libs/pdfClient/page.h
index 884a1df3c..93c885ebe 100644
--- a/pdf/framework/libs/pdfClient/page.h
+++ b/pdf/framework/libs/pdfClient/page.h
@@ -26,6 +26,7 @@
#include <utility>
#include <vector>
+#include "annotation.h"
#include "cpp/fpdf_scopers.h"
#include "form_filler.h"
#include "form_widget_info.h"
@@ -241,6 +242,20 @@ class Page {
// the Page, we only modify the PageObject's attributes.
bool UpdatePageObject(int index, std::unique_ptr<PageObject> page_object);
+ // Get all supported annotations. The list will contain null for the types of annotations
+ // which are not supported. Page will have ownership of annotations
+ std::vector<Annotation*> GetPageAnnotations();
+
+ // Add an annotation to the page
+ int AddPageAnnotation(std::unique_ptr<Annotation> annotation);
+
+ // Remove the annotation from the page at a given index
+ bool RemovePageAnnotation(int index);
+
+ // Update the attributes of the annotation on the Page. Ownership stays with
+ // the Page, we only modify the Annotation's attributes.
+ bool UpdatePageAnnotation(int index, std::unique_ptr<Annotation> annotation);
+
private:
// Convenience methods to access the variables dependent on an initialized
// ScopedFPDFTextPage. We lazy init text_page_ for efficiency because many
@@ -352,6 +367,11 @@ class Page {
// Populates page_objects_ with PageObjects on Page.
void PopulatePageObjects(bool refetch);
+
+ // Annotations
+ std::vector<std::unique_ptr<Annotation>> annotations_;
+
+ void PopulateAnnotations();
};
} // namespace pdfClient
diff --git a/pdf/framework/libs/pdfClient/page_object.cc b/pdf/framework/libs/pdfClient/page_object.cc
new file mode 100644
index 000000000..de2183b9b
--- /dev/null
+++ b/pdf/framework/libs/pdfClient/page_object.cc
@@ -0,0 +1,245 @@
+/*
+ * 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.
+ */
+
+#include "page_object.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "cpp/fpdf_scopers.h"
+#include "fpdf_edit.h"
+#include "fpdfview.h"
+#include "logging.h"
+
+#define LOG_TAG "page_object"
+
+namespace pdfClient {
+
+PageObject::PageObject(Type type) : type(type) {}
+
+PageObject::Type PageObject::GetType() const {
+ return type;
+}
+
+PageObject::~PageObject() = default;
+
+PathObject::PathObject() : PageObject(Type::Path) {}
+
+ScopedFPDFPageObject PathObject::CreateFPDFInstance(FPDF_DOCUMENT document) {
+ int segment_count = segments.size();
+ if (segment_count == 0) {
+ return nullptr;
+ }
+ // Get Start Points
+ float x = segments[0].x;
+ float y = segments[0].y;
+
+ // Create a scoped PDFium path object.
+ ScopedFPDFPageObject scoped_path_object(FPDFPageObj_CreateNewPath(x, y));
+ if (!scoped_path_object) {
+ return nullptr;
+ }
+
+ // Insert all segments into PDFium Path Object
+ for (int i = 1; i < segment_count; ++i) {
+ // Get EndPoint for current segment.
+ x = segments[i].x;
+ y = segments[i].y;
+ switch (segments[i].command) {
+ case Segment::Command::Move: {
+ FPDFPath_MoveTo(scoped_path_object.get(), x, y);
+ break;
+ }
+ case Segment::Command::Line: {
+ FPDFPath_LineTo(scoped_path_object.get(), x, y);
+ break;
+ }
+ default:
+ break;
+ }
+ if (segments[i].is_closed) {
+ FPDFPath_Close(scoped_path_object.get());
+ }
+ }
+
+ // Update attributes of PDFium path object.
+ if (!UpdateFPDFInstance(scoped_path_object.get())) {
+ return nullptr;
+ }
+
+ return scoped_path_object;
+}
+
+bool PathObject::UpdateFPDFInstance(FPDF_PAGEOBJECT path_object) {
+ if (!path_object) {
+ return false;
+ }
+
+ // Check for Type Correctness.
+ if (FPDFPageObj_GetType(path_object) != FPDF_PAGEOBJ_PATH) {
+ return false;
+ }
+
+ // Set the updated Draw Mode
+ int fill_mode = this->is_fill_mode ? FPDF_FILLMODE_WINDING : FPDF_FILLMODE_NONE;
+ if (!FPDFPath_SetDrawMode(path_object, fill_mode, is_stroke)) {
+ return false;
+ }
+
+ // Set the updated matrix.
+ if (!FPDFPageObj_SetMatrix(path_object, reinterpret_cast<FS_MATRIX*>(&matrix))) {
+ return false;
+ }
+
+ // Set the updated Stroke Width
+ FPDFPageObj_SetStrokeWidth(path_object, stroke_width);
+ // Set the updated Stroke Color
+ FPDFPageObj_SetStrokeColor(path_object, stroke_color.r, stroke_color.g, stroke_color.b,
+ stroke_color.a);
+ // Set the updated Fill Color
+ FPDFPageObj_SetFillColor(path_object, fill_color.r, fill_color.g, fill_color.b, fill_color.a);
+
+ return true;
+}
+
+bool PathObject::PopulateFromFPDFInstance(FPDF_PAGEOBJECT path_object) {
+ // Count the number of Segments in the Path
+ int segment_count = FPDFPath_CountSegments(path_object);
+ if (segment_count == 0) {
+ return false;
+ }
+
+ // Get Path Segments
+ for (int index = 0; index < segment_count; ++index) {
+ FPDF_PATHSEGMENT path_segment = FPDFPath_GetPathSegment(path_object, index);
+
+ // Get Type for the current Path Segment
+ int type = FPDFPathSegment_GetType(path_segment);
+ if (type == FPDF_SEGMENT_UNKNOWN || type == FPDF_SEGMENT_BEZIERTO) {
+ // Control point extraction of bezier curve is not supported by Pdfium as of now.
+ return false;
+ }
+
+ // Get EndPoint for the current Path Segment
+ float x, y;
+ FPDFPathSegment_GetPoint(path_segment, &x, &y);
+
+ // Get Close for the current Path Segment
+ bool is_closed = FPDFPathSegment_GetClose(path_segment);
+
+ // Add Segment to PageObject Data
+ if (type == FPDF_SEGMENT_LINETO) {
+ segments.emplace_back(Segment::Command::Line, x, y, is_closed);
+ } else {
+ segments.emplace_back(Segment::Command::Move, x, y, is_closed);
+ }
+ }
+
+ // Get Draw Mode
+ int fill_mode;
+ FPDF_BOOL stroke;
+ if (!FPDFPath_GetDrawMode(path_object, &fill_mode, &stroke)) {
+ LOGE("Path GetDrawMode Failed!");
+ return false;
+ }
+ this->is_fill_mode = fill_mode;
+ this->is_stroke = stroke;
+
+ // Get Matrix
+ if (!FPDFPageObj_GetMatrix(path_object, reinterpret_cast<FS_MATRIX*>(&matrix))) {
+ return false;
+ }
+
+ // Get Fill Color
+ FPDFPageObj_GetFillColor(path_object, &fill_color.r, &fill_color.g, &fill_color.b,
+ &fill_color.a);
+ // Get Stroke Color
+ FPDFPageObj_GetStrokeColor(path_object, &stroke_color.r, &stroke_color.g, &stroke_color.b,
+ &stroke_color.a);
+ // Get Stroke Width
+ FPDFPageObj_GetStrokeWidth(path_object, &stroke_width);
+
+ return true;
+}
+
+PathObject::~PathObject() = default;
+
+ImageObject::ImageObject() : PageObject(Type::Image) {}
+
+ScopedFPDFPageObject ImageObject::CreateFPDFInstance(FPDF_DOCUMENT document) {
+ // Create a scoped PDFium image object.
+ ScopedFPDFPageObject scoped_image_object(FPDFPageObj_NewImageObj(document));
+ if (!scoped_image_object) {
+ return nullptr;
+ }
+ // Update attributes of PDFium image object.
+ if (!UpdateFPDFInstance(scoped_image_object.get())) {
+ return nullptr;
+ }
+ return scoped_image_object;
+}
+
+bool ImageObject::UpdateFPDFInstance(FPDF_PAGEOBJECT image_object) {
+ if (!image_object) {
+ return false;
+ }
+
+ // Check for Type Correctness.
+ if (FPDFPageObj_GetType(image_object) != FPDF_PAGEOBJ_IMAGE) {
+ return false;
+ }
+
+ // Set the updated bitmap.
+ if (!FPDFImageObj_SetBitmap(nullptr, 0, image_object, bitmap.get())) {
+ return false;
+ }
+
+ // Set the updated matrix.
+ if (!FPDFPageObj_SetMatrix(image_object, reinterpret_cast<FS_MATRIX*>(&matrix))) {
+ return false;
+ }
+
+ width = FPDFBitmap_GetWidth(bitmap.get());
+ height = FPDFBitmap_GetHeight(bitmap.get());
+
+ return true;
+}
+
+bool ImageObject::PopulateFromFPDFInstance(FPDF_PAGEOBJECT image_object) {
+ // Get Bitmap
+ bitmap = ScopedFPDFBitmap(FPDFImageObj_GetBitmap(image_object));
+ if (bitmap.get() == nullptr) {
+ return false;
+ }
+
+ // Get Matrix
+ if (!FPDFPageObj_GetMatrix(image_object, reinterpret_cast<FS_MATRIX*>(&matrix))) {
+ return false;
+ }
+
+ width = FPDFBitmap_GetWidth(bitmap.get());
+ height = FPDFBitmap_GetHeight(bitmap.get());
+
+ return true;
+}
+
+void* ImageObject::GetBitmapReadableBuffer() const {
+ return FPDFBitmap_GetBuffer(bitmap.get());
+}
+
+ImageObject::~ImageObject() = default;
+
+} // namespace pdfClient \ No newline at end of file
diff --git a/pdf/framework/libs/pdfClient/page_object.h b/pdf/framework/libs/pdfClient/page_object.h
index b93c15cda..1fdd50784 100644
--- a/pdf/framework/libs/pdfClient/page_object.h
+++ b/pdf/framework/libs/pdfClient/page_object.h
@@ -19,14 +19,15 @@
#include <stdint.h>
+#include <vector>
+
+#include "cpp/fpdf_scopers.h"
#include "fpdfview.h"
typedef unsigned int uint;
namespace pdfClient {
-struct ImageObject;
-
struct Color {
uint r;
uint g;
@@ -40,39 +41,93 @@ struct Color {
static constexpr uint INVALID_COLOR = 256;
};
-struct PageObject {
+struct Matrix {
+ float a;
+ float b;
+ float c;
+ float d;
+ float e;
+ float f;
+};
+
+class PageObject {
+ public:
enum class Type {
Unknown = 0,
- Image,
+ Path = 2,
+ Image = 3,
};
- PageObject(Type t = Type::Unknown) : type(t) {}
-
- // Returns a pointer to the ImageObject if the PageObject is of type Image.
- // Returns nullptr otherwise.
- virtual ImageObject* AsImage() { return nullptr; }
+ Type GetType() const;
+ // Returns a FPDF Instance for a PageObject.
+ virtual ScopedFPDFPageObject CreateFPDFInstance(FPDF_DOCUMENT document) = 0;
+ // Updates the FPDF Instance of PageObject present on Page.
+ virtual bool UpdateFPDFInstance(FPDF_PAGEOBJECT page_object) = 0;
+ // Populates data from FPDFInstance of PageObject present on Page.
+ virtual bool PopulateFromFPDFInstance(FPDF_PAGEOBJECT page_object) = 0;
- Type GetType() { return type; }
+ virtual ~PageObject();
- virtual ~PageObject() = default;
-
- FS_MATRIX matrix; // Matrix used to scale, rotate, shear and translate the page object.
+ Matrix matrix; // Matrix used to scale, rotate, shear and translate the page object.
Color fill_color;
Color stroke_color;
float stroke_width = 1.0f;
+ protected:
+ PageObject(Type type = Type::Unknown);
+
private:
Type type;
};
-struct ImageObject : public PageObject {
- ImageObject() : PageObject(Type::Image) {}
+class PathObject : public PageObject {
+ public:
+ PathObject();
+
+ ScopedFPDFPageObject CreateFPDFInstance(FPDF_DOCUMENT document) override;
+ bool UpdateFPDFInstance(FPDF_PAGEOBJECT path_object) override;
+ bool PopulateFromFPDFInstance(FPDF_PAGEOBJECT path_object) override;
+
+ ~PathObject();
+
+ class Segment {
+ public:
+ enum class Command {
+ Unknown = 0,
+ Move,
+ Line,
+ };
+
+ Command command;
+ float x;
+ float y;
+ bool is_closed; // Checks if the path_segment is closed
+
+ Segment(Command command, float x, float y, bool is_closed = false)
+ : command(command), x(x), y(y), is_closed(is_closed) {}
+ };
+
+ bool is_fill_mode;
+ bool is_stroke;
+
+ std::vector<Segment> segments;
+};
+
+class ImageObject : public PageObject {
+ public:
+ ImageObject();
+
+ ScopedFPDFPageObject CreateFPDFInstance(FPDF_DOCUMENT document) override;
+ bool UpdateFPDFInstance(FPDF_PAGEOBJECT image_object) override;
+ bool PopulateFromFPDFInstance(FPDF_PAGEOBJECT image_object) override;
- ImageObject* AsImage() override { return this; }
+ void* GetBitmapReadableBuffer() const;
- ~ImageObject() { FPDFBitmap_Destroy(bitmap); }
+ ~ImageObject();
- FPDF_BITMAP bitmap;
+ int width;
+ int height;
+ ScopedFPDFBitmap bitmap;
};
} // namespace pdfClient
diff --git a/pdf/framework/libs/pdfClient/page_test.cc b/pdf/framework/libs/pdfClient/page_test.cc
index 5e6493fc6..a1e5007df 100644
--- a/pdf/framework/libs/pdfClient/page_test.cc
+++ b/pdf/framework/libs/pdfClient/page_test.cc
@@ -34,15 +34,20 @@
namespace {
+using ::pdfClient::Annotation;
+using ::pdfClient::Color;
using ::pdfClient::Document;
using ::pdfClient::ImageObject;
using ::pdfClient::Page;
using ::pdfClient::PageObject;
+using ::pdfClient::PathObject;
using ::pdfClient::Rectangle_i;
+using ::pdfClient::StampAnnotation;
static const std::string kTestdata = "testdata";
static const std::string kSekretNoPassword = "sekret_no_password.pdf";
-static const std::string kImageObject = "image_object.pdf";
+static const std::string kPageObject = "page_object.pdf";
+static const std::string kAnnotation = "annotation.pdf";
std::string GetTestDataDir() {
return android::base::GetExecutableDirectory();
@@ -205,19 +210,21 @@ TEST(Test, InvalidPageNumberTest) {
}
TEST(Test, GetPageObjectsTest) {
- Document doc(LoadTestDocument(kImageObject), false);
+ Document doc(LoadTestDocument(kPageObject), false);
std::shared_ptr<Page> page = doc.GetPage(0);
std::vector<PageObject*> pageObjects = page->GetPageObjects();
// Check for PageObjects size.
- ASSERT_EQ(1, pageObjects.size());
- // Check for the PageObject to be ImageObject.
+ ASSERT_EQ(2, pageObjects.size());
+ // Check for the first PageObject to be ImageObject.
ASSERT_EQ(PageObject::Type::Image, pageObjects[0]->GetType());
+ // Check for the second PageObject to be PathObject.
+ ASSERT_EQ(PageObject::Type::Path, pageObjects[1]->GetType());
}
-TEST(Test, AddPageObjectTest) {
- Document doc(LoadTestDocument(kImageObject), false);
+TEST(Test, AddImagePageObjectTest) {
+ Document doc(LoadTestDocument(kPageObject), false);
std::shared_ptr<Page> page = doc.GetPage(0);
std::vector<PageObject*> initialPageObjects = page->GetPageObjects();
@@ -226,9 +233,8 @@ TEST(Test, AddPageObjectTest) {
auto imageObject = std::make_unique<ImageObject>();
// Create FPDF Bitmap.
- FPDF_BITMAP bitmap = FPDFBitmap_Create(6000, 4000, 1);
- FPDFBitmap_FillRect(bitmap, 0, 0, 3000, 4000, 0xFF000000);
- imageObject->bitmap = bitmap;
+ imageObject->bitmap = ScopedFPDFBitmap(FPDFBitmap_Create(100, 100, 1));
+ FPDFBitmap_FillRect(imageObject->bitmap.get(), 0, 0, 100, 100, 0xFF000000);
// Set Matrix.
imageObject->matrix = {1.0f, 0, 0, 1.0f, 0, 0};
@@ -241,10 +247,53 @@ TEST(Test, AddPageObjectTest) {
// Assert that the size has increased by one.
ASSERT_EQ(initialPageObjects.size() + 1, updatedPageObjects.size());
+ // Check for the first PageObject to be ImageObject.
+ ASSERT_EQ(PageObject::Type::Image, updatedPageObjects[0]->GetType());
+ // Check for the second PageObject to be PathObject.
+ ASSERT_EQ(PageObject::Type::Path, updatedPageObjects[1]->GetType());
+ // Check for the first PageObject to be ImageObject.
+ ASSERT_EQ(PageObject::Type::Image, updatedPageObjects[2]->GetType());
+}
+
+TEST(Test, AddPathPageObject) {
+ Document doc(LoadTestDocument(kPageObject), false);
+ std::shared_ptr<Page> page = doc.GetPage(0);
+
+ std::vector<PageObject*> initialPageObjects = page->GetPageObjects();
+
+ // Create Path Object.
+ auto pathObject = std::make_unique<PathObject>();
+
+ // Command Simple Path
+ pathObject->segments.emplace_back(PathObject::Segment::Command::Move, 0.0f, 0.0f);
+ pathObject->segments.emplace_back(PathObject::Segment::Command::Line, 100.0f, 150.0f);
+ pathObject->segments.emplace_back(PathObject::Segment::Command::Line, 150.0f, 150.0f);
+
+ // Set Draw Mode
+ pathObject->is_fill_mode = false;
+ pathObject->is_stroke = true;
+
+ // Set PathObject Matrix.
+ pathObject->matrix = {1.0f, 0, 0, 1.0f, 0, 0};
+
+ // Add the page object.
+ ASSERT_EQ(page->AddPageObject(std::move(pathObject)), initialPageObjects.size());
+
+ // Get Updated PageObjects
+ std::vector<PageObject*> updatedPageObjects = page->GetPageObjects(true);
+
+ // Assert that the size has increased by one.
+ ASSERT_EQ(initialPageObjects.size() + 1, updatedPageObjects.size());
+ // Check for the first PageObject to be ImageObject.
+ ASSERT_EQ(PageObject::Type::Image, updatedPageObjects[0]->GetType());
+ // Check for the second PageObject to be PathObject.
+ ASSERT_EQ(PageObject::Type::Path, updatedPageObjects[1]->GetType());
+ // Check for the first PageObject to be PathObject.
+ ASSERT_EQ(PageObject::Type::Path, updatedPageObjects[2]->GetType());
}
TEST(Test, RemovePageObjectTest) {
- Document doc(LoadTestDocument(kImageObject), false);
+ Document doc(LoadTestDocument(kPageObject), false);
std::shared_ptr<Page> page = doc.GetPage(0);
@@ -258,8 +307,8 @@ TEST(Test, RemovePageObjectTest) {
ASSERT_EQ(initialPageObjects.size() - 1, updatedPageObjects.size());
}
-TEST(Test, UpdatePageObjectTest) {
- Document doc(LoadTestDocument(kImageObject), false);
+TEST(Test, UpdateImagePageObjectTest) {
+ Document doc(LoadTestDocument(kPageObject), false);
std::shared_ptr<Page> page = doc.GetPage(0);
// Get initial page objects.
@@ -269,9 +318,8 @@ TEST(Test, UpdatePageObjectTest) {
auto imageObject = std::make_unique<ImageObject>();
// Create FPDF Bitmap.
- FPDF_BITMAP bitmap = FPDFBitmap_Create(6000, 4000, 1);
- FPDFBitmap_FillRect(bitmap, 0, 0, 6000, 4000, 0xFF0000FF);
- imageObject->bitmap = bitmap;
+ imageObject->bitmap = ScopedFPDFBitmap(FPDFBitmap_Create(100, 110, 1));
+ FPDFBitmap_FillRect(imageObject->bitmap.get(), 0, 0, 100, 110, 0xFF0000FF);
// Set Matrix.
imageObject->matrix = {2.0f, 0, 0, 2.0f, 0, 0};
@@ -286,8 +334,10 @@ TEST(Test, UpdatePageObjectTest) {
ASSERT_EQ(initialPageObjects.size(), updatedPageObjects.size());
// Check for updated bitmap.
- ASSERT_EQ(FPDFBitmap_GetWidth(updatedPageObjects[0]->AsImage()->bitmap), 6000);
- ASSERT_EQ(FPDFBitmap_GetHeight(updatedPageObjects[0]->AsImage()->bitmap), 4000);
+ ASSERT_EQ(FPDFBitmap_GetWidth(static_cast<ImageObject*>(updatedPageObjects[0])->bitmap.get()),
+ 100);
+ ASSERT_EQ(FPDFBitmap_GetHeight(static_cast<ImageObject*>(updatedPageObjects[0])->bitmap.get()),
+ 110);
// Check for updated matrix.
ASSERT_EQ(updatedPageObjects[0]->matrix.a, 2.0f);
@@ -298,4 +348,157 @@ TEST(Test, UpdatePageObjectTest) {
ASSERT_EQ(updatedPageObjects[0]->matrix.f, 0.0f);
}
+TEST(Test, UpdatePathPageObjectTest) {
+ Document doc(LoadTestDocument(kPageObject), false);
+ std::shared_ptr<Page> page = doc.GetPage(0);
+
+ // Get initial page objects.
+ std::vector<PageObject*> initialPageObjects = page->GetPageObjects();
+
+ // Create Path Object.
+ auto pathObject = std::make_unique<PathObject>();
+
+ // Update fill Color.
+ pathObject->fill_color = Color(255, 0, 0, 255);
+
+ // Update Draw Mode.
+ pathObject->is_fill_mode = true;
+ pathObject->is_stroke = false;
+
+ // Set Matrix.
+ pathObject->matrix = {2.0f, 0, 0, 2.0f, 0, 0};
+
+ // Update the page object.
+ EXPECT_TRUE(page->UpdatePageObject(1, std::move(pathObject)));
+
+ // Get the updated page objects.
+ std::vector<PageObject*> updatedPageObjects = page->GetPageObjects(true);
+
+ // Check for updated fill Color.
+ ASSERT_EQ(updatedPageObjects[1]->fill_color.r, 255);
+ ASSERT_EQ(updatedPageObjects[1]->fill_color.b, 0);
+ ASSERT_EQ(updatedPageObjects[1]->fill_color.g, 0);
+ ASSERT_EQ(updatedPageObjects[1]->fill_color.a, 255);
+
+ // Check for updated Draw Mode.
+ ASSERT_EQ(static_cast<PathObject*>(updatedPageObjects[1])->is_fill_mode, true);
+ ASSERT_EQ(static_cast<PathObject*>(updatedPageObjects[1])->is_stroke, false);
+
+ // Check for updated matrix.
+ ASSERT_EQ(updatedPageObjects[1]->matrix.a, 2.0f);
+ ASSERT_EQ(updatedPageObjects[1]->matrix.b, 0.0f);
+ ASSERT_EQ(updatedPageObjects[1]->matrix.c, 0.0f);
+ ASSERT_EQ(updatedPageObjects[1]->matrix.d, 2.0f);
+ ASSERT_EQ(updatedPageObjects[1]->matrix.e, 0.0f);
+ ASSERT_EQ(updatedPageObjects[1]->matrix.f, 0.0f);
+}
+
+TEST(Test, GetPageAnnotationsTest) {
+ Document doc(LoadTestDocument(kAnnotation), false);
+
+ std::shared_ptr<Page> page = doc.GetPage(0);
+ std::vector<Annotation*> annotations = page->GetPageAnnotations();
+
+ // Check for number of annotations
+ ASSERT_EQ(1, annotations.size());
+
+ // Check for the first Annotation to be StampAnnotation.
+ ASSERT_EQ(Annotation::Type::Stamp, annotations[0]->GetType());
+
+ StampAnnotation* stamp_annotation = static_cast<StampAnnotation*>(annotations[0]);
+ std::vector<PageObject*> pageObjects = stamp_annotation->GetObjects();
+
+ // Check for number of page objects inside stamp annotation
+ ASSERT_EQ(2, pageObjects.size());
+
+ // Check for the first PageObject to be ImageObject.
+ ASSERT_EQ(PageObject::Type::Image, pageObjects[0]->GetType());
+ // Check for the second PageObject to be PathObject.
+ ASSERT_EQ(PageObject::Type::Path, pageObjects[1]->GetType());
+}
+
+TEST(Test, AddStampAnnotationTest) {
+ Document doc(LoadTestDocument(kAnnotation), false);
+ std::shared_ptr<Page> page = doc.GetPage(0);
+
+ std::vector<Annotation*> initialAnnotations = page->GetPageAnnotations();
+
+ // Bounds for stamp annotation
+ Rectangle_f bounds = pdfClient::Rectangle_f{0, 300, 200, 0};
+ // Create Stamp Annotation
+ auto stampAnnotation = std::make_unique<StampAnnotation>(bounds);
+
+ // Insert image page object
+ auto imageObject = std::make_unique<ImageObject>();
+
+ // Create FPDF Bitmap.
+ imageObject->bitmap = ScopedFPDFBitmap(FPDFBitmap_Create(100, 100, 1));
+ FPDFBitmap_FillRect(imageObject->bitmap.get(), 0, 0, 100, 100, 0xFF000000);
+
+ // Set Matrix.
+ imageObject->matrix = {1.0f, 0, 0, 1.0f, 0, 0};
+
+ // Add the page object.
+ stampAnnotation->AddObject(std::move(imageObject));
+
+ // Create Path Object.
+ auto pathObject = std::make_unique<PathObject>();
+
+ // Command Simple Path
+ pathObject->segments.emplace_back(PathObject::Segment::Command::Move, 0.0f, 0.0f);
+ pathObject->segments.emplace_back(PathObject::Segment::Command::Line, 100.0f, 150.0f);
+ pathObject->segments.emplace_back(PathObject::Segment::Command::Line, 150.0f, 150.0f);
+
+ // Set Draw Mode
+ pathObject->is_fill_mode = false;
+ pathObject->is_stroke = true;
+
+ // Set PathObject Matrix.
+ pathObject->matrix = {1.0f, 0, 0, 1.0f, 0, 0};
+
+ // Add the page object.
+ stampAnnotation->AddObject(std::move(pathObject));
+
+ // Add the stamp annotation.
+ ASSERT_EQ(page->AddPageAnnotation(std::move(stampAnnotation)), initialAnnotations.size());
+
+ // Get Updated annotations
+ std::vector<Annotation*> updatedAnnotations = page->GetPageAnnotations();
+
+ // Assert that the size has increased by one.
+ ASSERT_EQ(initialAnnotations.size() + 1, updatedAnnotations.size());
+ // Check for the first Annotation to be StampAnnotation
+ ASSERT_EQ(Annotation::Type::Stamp, updatedAnnotations[0]->GetType());
+ // Check for the second Annotation to be StampAnnotation.
+ ASSERT_EQ(Annotation::Type::Stamp, updatedAnnotations[1]->GetType());
+
+ // Check for the page objects inside stamp annotation
+ StampAnnotation* stamp_annotation = static_cast<StampAnnotation*>(updatedAnnotations[1]);
+ std::vector<PageObject*> pageObjects = stamp_annotation->GetObjects();
+
+ // Check for number of page objects inside stamp annotation
+ ASSERT_EQ(2, pageObjects.size());
+
+ // Check for the first PageObject to be ImageObject.
+ ASSERT_EQ(PageObject::Type::Image, pageObjects[0]->GetType());
+ // Check for the second PageObject to be PathObject.
+ ASSERT_EQ(PageObject::Type::Path, pageObjects[1]->GetType());
+}
+
+TEST(Test, RemovePageAnnotationTest) {
+ Document doc(LoadTestDocument(kAnnotation), false);
+
+ std::shared_ptr<Page> page = doc.GetPage(0);
+
+ std::vector<Annotation*> initialAnnotations = page->GetPageAnnotations();
+
+ EXPECT_TRUE(initialAnnotations.size() > 0);
+ // Remove an annotation
+ EXPECT_TRUE(page->RemovePageAnnotation(0));
+ // Get Updated annotations after removal
+ std::vector<Annotation*> updatedAnnotations = page->GetPageAnnotations();
+
+ ASSERT_EQ(initialAnnotations.size() - 1, updatedAnnotations.size());
+}
+
} // namespace \ No newline at end of file
diff --git a/pdf/framework/libs/pdfClient/pdf_document_jni.cc b/pdf/framework/libs/pdfClient/pdf_document_jni.cc
index 01e8c58bb..3cc604d31 100644
--- a/pdf/framework/libs/pdfClient/pdf_document_jni.cc
+++ b/pdf/framework/libs/pdfClient/pdf_document_jni.cc
@@ -319,8 +319,8 @@ Java_android_graphics_pdf_PdfDocumentProxy_isPdfLinearized(JNIEnv* env, jobject
return doc->IsLinearized();
}
-JNIEXPORT jint JNICALL Java_android_graphics_pdf_PdfDocumentProxy_getFormType(JNIEnv* env,
- jobject jPdfDocument) {
+JNIEXPORT jint JNICALL Java_android_graphics_pdf_PdfDocumentProxy_getFormType(
+ JNIEnv* env, jobject jPdfDocument) {
std::unique_lock<std::mutex> lock(mutex_);
Document* doc = convert::GetPdfDocPtr(env, jPdfDocument);
return doc->GetFormType();
@@ -446,3 +446,63 @@ JNIEXPORT jobject JNICALL Java_android_graphics_pdf_PdfDocumentProxy_setFormFiel
doc->ReleaseRetainedPage(pageNum);
return convert::ToJavaRects(env, invalid_rects);
}
+
+JNIEXPORT jint JNICALL Java_android_graphics_pdf_PdfDocumentProxy_addPageObject(
+ JNIEnv* env, jobject jPdfDocument, jint pageNum, jobject jPageObject) {
+ std::unique_lock<std::mutex> lock(mutex_);
+ Document* doc = convert::GetPdfDocPtr(env, jPdfDocument);
+ std::shared_ptr<Page> page = doc->GetPage(pageNum, true);
+
+ std::unique_ptr<PageObject> page_object = convert::ToNativePageObject(env, jPageObject);
+
+ if (!page_object) {
+ return -1;
+ }
+
+ int new_object_index = page->AddPageObject(std::move(page_object));
+
+ doc->ReleaseRetainedPage(pageNum);
+ return new_object_index;
+}
+
+JNIEXPORT jobject JNICALL Java_android_graphics_pdf_PdfDocumentProxy_getPageObjects(
+ JNIEnv* env, jobject jPdfDocument, jint pageNum) {
+ std::unique_lock<std::mutex> lock(mutex_);
+ Document* doc = convert::GetPdfDocPtr(env, jPdfDocument);
+ std::shared_ptr<Page> page = doc->GetPage(pageNum, true);
+
+ std::vector<PageObject*> page_objects = page->GetPageObjects();
+
+ doc->ReleaseRetainedPage(pageNum);
+ return convert::ToJavaPdfPageObjects(env, page_objects);
+}
+
+JNIEXPORT jboolean JNICALL Java_android_graphics_pdf_PdfDocumentProxy_removePageObject(
+ JNIEnv* env, jobject jPdfDocument, jint pageNum, jint index) {
+ std::unique_lock<std::mutex> lock(mutex_);
+ Document* doc = convert::GetPdfDocPtr(env, jPdfDocument);
+ std::shared_ptr<Page> page = doc->GetPage(pageNum, true);
+
+ bool removed = page->RemovePageObject(index);
+
+ doc->ReleaseRetainedPage(pageNum);
+ return removed;
+}
+
+JNIEXPORT jboolean JNICALL Java_android_graphics_pdf_PdfDocumentProxy_updatePageObject(
+ JNIEnv* env, jobject jPdfDocument, jint pageNum, jint index, jobject jPageObject) {
+ std::unique_lock<std::mutex> lock(mutex_);
+ Document* doc = convert::GetPdfDocPtr(env, jPdfDocument);
+ std::shared_ptr<Page> page = doc->GetPage(pageNum, true);
+
+ std::unique_ptr<PageObject> page_object = convert::ToNativePageObject(env, jPageObject);
+
+ if (!page_object) {
+ return false;
+ }
+
+ bool updated = page->UpdatePageObject(index, std::move(page_object));
+
+ doc->ReleaseRetainedPage(pageNum);
+ return updated;
+}
diff --git a/pdf/framework/libs/pdfClient/pdf_document_jni.h b/pdf/framework/libs/pdfClient/pdf_document_jni.h
index e6cad3c57..e71937de2 100644
--- a/pdf/framework/libs/pdfClient/pdf_document_jni.h
+++ b/pdf/framework/libs/pdfClient/pdf_document_jni.h
@@ -109,6 +109,18 @@ JNIEXPORT jobject JNICALL Java_android_graphics_pdf_PdfDocumentProxy_setFormFiel
JNIEnv* env, jobject jPdfDocument, jint pageNum, jint annotationIndex,
jintArray jSelectedIndices);
+JNIEXPORT jint JNICALL Java_android_graphics_pdf_PdfDocumentProxy_addPageObject(
+ JNIEnv* env, jobject jPdfDocument, jint pageNum, jobject jPageObject);
+
+JNIEXPORT jobject JNICALL Java_android_graphics_pdf_PdfDocumentProxy_getPageObjects(
+ JNIEnv* env, jobject jPdfDocument, jint pageNum);
+
+JNIEXPORT jboolean JNICALL Java_android_graphics_pdf_PdfDocumentProxy_removePageObject(
+ JNIEnv* env, jobject jPdfDocument, jint pageNum, jint index);
+
+JNIEXPORT jboolean JNICALL Java_android_graphics_pdf_PdfDocumentProxy_updatePageObject(
+ JNIEnv* env, jobject jPdfDocument, jint pageNum, jint index, jobject jPageObject);
+
#ifdef __cplusplus
}
#endif
diff --git a/pdf/framework/libs/pdfClient/rect.h b/pdf/framework/libs/pdfClient/rect.h
index bb708f76b..d8d28144a 100644
--- a/pdf/framework/libs/pdfClient/rect.h
+++ b/pdf/framework/libs/pdfClient/rect.h
@@ -33,6 +33,12 @@ struct Point_d {
double y;
};
+// A point with float precision
+struct Point_f {
+ float x;
+ float y;
+};
+
// A rectangle with integer precision
struct Rectangle_i {
// The x-coordinate of the left-top corner (lesser value).
@@ -69,6 +75,24 @@ struct Rectangle_d {
Point_d Center() const { return Point_d{(left + right) / 2, (top + bottom) / 2}; }
};
+// A rectangle with float precision
+struct Rectangle_f {
+ // The x-coordinate of the left-top corner (lesser value).
+ float left;
+ // The y-coordinate of the left-top corner (lesser value).
+ float top;
+ // The x-coordinate of the right-bottom corner (greater value).
+ float right;
+ // The y-coordinate of the right-bottom corner (greater value).
+ float bottom;
+
+ float Width() const { return right - left; }
+
+ float Height() const { return bottom - top; }
+
+ Point_f Center() const { return Point_f{(left + right) / 2, (top + bottom) / 2}; }
+};
+
// Check if two points are equal:
inline bool operator==(const Point_i& lhs, const Point_i& rhs) {
return lhs.x == rhs.x && lhs.y == rhs.y;
@@ -127,6 +151,8 @@ Rectangle_d Intersect(const Rectangle_d& lhs, const Rectangle_d& rhs);
Rectangle_i Union(const Rectangle_i& lhs, const Rectangle_i& rhs);
Rectangle_d Union(const Rectangle_d& lhs, const Rectangle_d& rhs);
+Rectangle_f FloatRect(const float x1, const float y1, const float x2, const float y2);
+
} // namespace pdfClient
#endif // MEDIAPROVIDER_PDF_JNI_PDFCLIENT_RECT_H_ \ No newline at end of file
diff --git a/pdf/framework/libs/pdfClient/testdata/annotation.pdf b/pdf/framework/libs/pdfClient/testdata/annotation.pdf
new file mode 100644
index 000000000..e87600e8c
--- /dev/null
+++ b/pdf/framework/libs/pdfClient/testdata/annotation.pdf
Binary files differ
diff --git a/pdf/framework/libs/pdfClient/testdata/image_object.pdf b/pdf/framework/libs/pdfClient/testdata/image_object.pdf
deleted file mode 100755
index 36d9ffbdd..000000000
--- a/pdf/framework/libs/pdfClient/testdata/image_object.pdf
+++ /dev/null
Binary files differ
diff --git a/pdf/framework/libs/pdfClient/testdata/page_object.pdf b/pdf/framework/libs/pdfClient/testdata/page_object.pdf
new file mode 100644
index 000000000..81215acb2
--- /dev/null
+++ b/pdf/framework/libs/pdfClient/testdata/page_object.pdf
Binary files differ
diff --git a/photopicker/res/values-af/feature_privacy_explainer_strings.xml b/photopicker/res/values-af/feature_privacy_explainer_strings.xml
index c6f0850a1..1a73240f7 100644
--- a/photopicker/res/values-af/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-af/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> sal net toegang hê tot die foto’s wat jy kies"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Kies foto’s en video’s waartoe jy <xliff:g id="APP_NAME">%1$s</xliff:g> toegang gee"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Hierdie app"</string>
</resources>
diff --git a/photopicker/res/values-am/feature_privacy_explainer_strings.xml b/photopicker/res/values-am/feature_privacy_explainer_strings.xml
index 355eac219..5298da10b 100644
--- a/photopicker/res/values-am/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-am/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> እርስዎ ለመረጧቸው ፎቶዎች ብቻ መዳረሻ ይኖረዋል"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> እንዲደርስ እርስዎ የፈቀዷቸውን ፎቶዎች እና ቪድዮዎች ይምረጡ"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ይህ መተግበሪያ"</string>
</resources>
diff --git a/photopicker/res/values-ar/feature_privacy_explainer_strings.xml b/photopicker/res/values-ar/feature_privacy_explainer_strings.xml
index e9f23706a..deaa3ca86 100644
--- a/photopicker/res/values-ar/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-ar/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"سيصبح بإمكان \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" الوصول إلى الصور التي تختارها فقط"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"اختّر الصور والفيديوهات التي سيكون بإمكان \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" الوصول إليها"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"هذا التطبيق"</string>
</resources>
diff --git a/photopicker/res/values-as/feature_privacy_explainer_strings.xml b/photopicker/res/values-as/feature_privacy_explainer_strings.xml
index 72d16f3af..b8f9f248e 100644
--- a/photopicker/res/values-as/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-as/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g>এ কেৱল আপুনি বাছনি কৰা ফট’হে এক্সেছ কৰিব পাৰিব"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"আপুনি <xliff:g id="APP_NAME">%1$s</xliff:g>ক এক্সেছ কৰিবলৈ অনুমতি দিয়া ফট’ আৰু ভিডিঅ’ বাছনি কৰক"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"এইটো এপ্‌"</string>
</resources>
diff --git a/photopicker/res/values-az/feature_privacy_explainer_strings.xml b/photopicker/res/values-az/feature_privacy_explainer_strings.xml
index e5a816748..ecd21864c 100644
--- a/photopicker/res/values-az/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-az/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> yalnız seçdiyiniz fotolara daxil ola biləcək"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> tətbiqinə giriş imkanı verdiyiniz foto və videoları seçin"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Bu tətbiq"</string>
</resources>
diff --git a/photopicker/res/values-b+sr+Latn/feature_privacy_explainer_strings.xml b/photopicker/res/values-b+sr+Latn/feature_privacy_explainer_strings.xml
index 965a66bf9..a6e598bef 100644
--- a/photopicker/res/values-b+sr+Latn/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-b+sr+Latn/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> će imati pristup samo slikama koje izaberete"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Izaberite slike i video snimke kojima <xliff:g id="APP_NAME">%1$s</xliff:g> može da pristupi"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ova aplikacija"</string>
</resources>
diff --git a/photopicker/res/values-be/feature_privacy_explainer_strings.xml b/photopicker/res/values-be/feature_privacy_explainer_strings.xml
index 41f9b1819..7cba83370 100644
--- a/photopicker/res/values-be/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-be/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Праграма \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" будзе мець доступ толькі да выбраных вамі фота"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Выберыце фота і відэа, да якіх праграма \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" можа атрымліваць доступ"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Гэта праграма"</string>
</resources>
diff --git a/photopicker/res/values-bg/feature_privacy_explainer_strings.xml b/photopicker/res/values-bg/feature_privacy_explainer_strings.xml
index 33a1653dd..6d9ae89d7 100644
--- a/photopicker/res/values-bg/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-bg/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> ще осъществява достъп само до избраните от вас снимки"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Изберете снимки и видеоклипове, до които разрешавате да осъществява достъп <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Това приложение"</string>
</resources>
diff --git a/photopicker/res/values-bn/feature_privacy_explainer_strings.xml b/photopicker/res/values-bn/feature_privacy_explainer_strings.xml
index b3625eb36..19e44d2c8 100644
--- a/photopicker/res/values-bn/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-bn/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> শুধুমাত্র আপনার বেছে নেওয়া ফটো অ্যাক্সেস করবে"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"সেইসব ফটো ও ভিডিও বেছে নিন যা অ্যাক্সেস করার অনুমতি <xliff:g id="APP_NAME">%1$s</xliff:g> অ্যাপকে দিয়েছেন"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"এই অ্যাপ"</string>
</resources>
diff --git a/photopicker/res/values-bs/feature_privacy_explainer_strings.xml b/photopicker/res/values-bs/feature_privacy_explainer_strings.xml
index 5713f6dfb..31fae4994 100644
--- a/photopicker/res/values-bs/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-bs/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> će imati pristup samo fotografijama koje odaberete"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Odaberite fotografije i videozapise kojima aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> može pristupati"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ova aplikacija"</string>
</resources>
diff --git a/photopicker/res/values-ca/feature_privacy_explainer_strings.xml b/photopicker/res/values-ca/feature_privacy_explainer_strings.xml
index 1829dcceb..758561c25 100644
--- a/photopicker/res/values-ca/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-ca/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> només tindrà accés a les fotos que seleccionis"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecciona les fotos i els vídeos als quals permets que accedeixi <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Aquesta aplicació"</string>
</resources>
diff --git a/photopicker/res/values-cs/feature_privacy_explainer_strings.xml b/photopicker/res/values-cs/feature_privacy_explainer_strings.xml
index 488206440..190c9f19c 100644
--- a/photopicker/res/values-cs/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-cs/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Aplikace <xliff:g id="APP_NAME">%1$s</xliff:g> má přístup jen k fotkám, které vyberete"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Vyberte fotky a videa, ke kterým aplikaci <xliff:g id="APP_NAME">%1$s</xliff:g> chcete povolit přístup"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Tato aplikace"</string>
</resources>
diff --git a/photopicker/res/values-da/feature_privacy_explainer_strings.xml b/photopicker/res/values-da/feature_privacy_explainer_strings.xml
index f427fc24c..586f60011 100644
--- a/photopicker/res/values-da/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-da/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> tilgår kun de billeder, du vælger"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Vælg de billeder og videoer, som du vil give <xliff:g id="APP_NAME">%1$s</xliff:g> tilladelse til at tilgå"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Denne app"</string>
</resources>
diff --git a/photopicker/res/values-de/feature_privacy_explainer_strings.xml b/photopicker/res/values-de/feature_privacy_explainer_strings.xml
index 283139ba1..088f6852d 100644
--- a/photopicker/res/values-de/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-de/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> greift nur auf die von dir ausgewählten Fotos zu"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Auswählen, auf welche Fotos und Videos <xliff:g id="APP_NAME">%1$s</xliff:g> zugreifen darf"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Diese App"</string>
</resources>
diff --git a/photopicker/res/values-el/feature_privacy_explainer_strings.xml b/photopicker/res/values-el/feature_privacy_explainer_strings.xml
index ab1ffe8ca..38ef7c922 100644
--- a/photopicker/res/values-el/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-el/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Η εφαρμογή <xliff:g id="APP_NAME">%1$s</xliff:g> θα έχει πρόσβαση μόνο στις φωτογραφίες που επιλέγετε"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Επιλέξτε τις φωτογραφίες και τα βίντεο στα οποία παραχωρείτε πρόσβαση στην εφαρμογή <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Αυτή η εφαρμογή"</string>
</resources>
diff --git a/photopicker/res/values-en-rAU/feature_privacy_explainer_strings.xml b/photopicker/res/values-en-rAU/feature_privacy_explainer_strings.xml
index 665ee7b79..59f3a29f9 100644
--- a/photopicker/res/values-en-rAU/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-en-rAU/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> will only have access to the photos that you select"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Select photos and videos that you allow <xliff:g id="APP_NAME">%1$s</xliff:g> to access"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"This app"</string>
</resources>
diff --git a/photopicker/res/values-en-rCA/feature_privacy_explainer_strings.xml b/photopicker/res/values-en-rCA/feature_privacy_explainer_strings.xml
index 47f8044d6..914eeb91f 100644
--- a/photopicker/res/values-en-rCA/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-en-rCA/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> will only have access to the photos you select"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Select photos and videos that you allow <xliff:g id="APP_NAME">%1$s</xliff:g> to access"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"This app"</string>
</resources>
diff --git a/photopicker/res/values-en-rGB/feature_privacy_explainer_strings.xml b/photopicker/res/values-en-rGB/feature_privacy_explainer_strings.xml
index 665ee7b79..59f3a29f9 100644
--- a/photopicker/res/values-en-rGB/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-en-rGB/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> will only have access to the photos that you select"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Select photos and videos that you allow <xliff:g id="APP_NAME">%1$s</xliff:g> to access"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"This app"</string>
</resources>
diff --git a/photopicker/res/values-en-rIN/feature_privacy_explainer_strings.xml b/photopicker/res/values-en-rIN/feature_privacy_explainer_strings.xml
index 665ee7b79..59f3a29f9 100644
--- a/photopicker/res/values-en-rIN/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-en-rIN/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> will only have access to the photos that you select"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Select photos and videos that you allow <xliff:g id="APP_NAME">%1$s</xliff:g> to access"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"This app"</string>
</resources>
diff --git a/photopicker/res/values-es-rUS/feature_privacy_explainer_strings.xml b/photopicker/res/values-es-rUS/feature_privacy_explainer_strings.xml
index 36ba9ed57..585531173 100644
--- a/photopicker/res/values-es-rUS/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-es-rUS/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> solo tendrá acceso a las fotos que selecciones"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecciona fotos y videos a los que permites que <xliff:g id="APP_NAME">%1$s</xliff:g> acceda"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Esta app"</string>
</resources>
diff --git a/photopicker/res/values-es/feature_privacy_explainer_strings.xml b/photopicker/res/values-es/feature_privacy_explainer_strings.xml
index a30bdda0d..0d34b9d01 100644
--- a/photopicker/res/values-es/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-es/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> solo tendrá acceso a las fotos que selecciones"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecciona las fotos y los vídeos a los que <xliff:g id="APP_NAME">%1$s</xliff:g> podrá tener acceso"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Esta aplicación"</string>
</resources>
diff --git a/photopicker/res/values-et/feature_privacy_explainer_strings.xml b/photopicker/res/values-et/feature_privacy_explainer_strings.xml
index 5dbd864df..29eee25c6 100644
--- a/photopicker/res/values-et/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-et/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> pääseb juurde ainult teie valitud fotodele"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Valige fotod ja videod, millele lubate rakendusel <xliff:g id="APP_NAME">%1$s</xliff:g> juurde pääseda"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"See rakendus"</string>
</resources>
diff --git a/photopicker/res/values-eu/feature_privacy_explainer_strings.xml b/photopicker/res/values-eu/feature_privacy_explainer_strings.xml
index 9e414c9d5..7bc63bf15 100644
--- a/photopicker/res/values-eu/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-eu/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Hautatzen dituzun argazkiak erabiltzeko baimena baino ez du izango <xliff:g id="APP_NAME">%1$s</xliff:g> aplikazioak"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Hautatu <xliff:g id="APP_NAME">%1$s</xliff:g> erabili ahalko dituen argazki eta bideoak"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"aplikazio honek"</string>
</resources>
diff --git a/photopicker/res/values-fa/feature_privacy_explainer_strings.xml b/photopicker/res/values-fa/feature_privacy_explainer_strings.xml
index 57d1fd666..0c9f5673a 100644
--- a/photopicker/res/values-fa/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-fa/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"‫<xliff:g id="APP_NAME">%1$s</xliff:g> فقط به عکس‌هایی که شما انتخاب می‌کنید دسترسی خواهد داشت"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"عکس‌ها و ویدیوهایی را انتخاب کنید که به <xliff:g id="APP_NAME">%1$s</xliff:g> اجازه می‌دهید به آن‌ها دسترسی داشته باشد"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"این برنامه"</string>
</resources>
diff --git a/photopicker/res/values-fi/feature_privacy_explainer_strings.xml b/photopicker/res/values-fi/feature_privacy_explainer_strings.xml
index a6446f1d5..338dc92bd 100644
--- a/photopicker/res/values-fi/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-fi/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> saa pääsyn vain valitsemiisi kuviin"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Valitse kuvat ja videot, joihin <xliff:g id="APP_NAME">%1$s</xliff:g> saa pääsyn"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Tämä sovellus"</string>
</resources>
diff --git a/photopicker/res/values-fr-rCA/feature_privacy_explainer_strings.xml b/photopicker/res/values-fr-rCA/feature_privacy_explainer_strings.xml
index c50bd9247..9d17988af 100644
--- a/photopicker/res/values-fr-rCA/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-fr-rCA/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> accédera uniquement aux photos que vous sélectionnez"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Sélectionnez les photos et les vidéos auxquelles vous autorisez <xliff:g id="APP_NAME">%1$s</xliff:g> à accéder"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Cette appli"</string>
</resources>
diff --git a/photopicker/res/values-fr/feature_privacy_explainer_strings.xml b/photopicker/res/values-fr/feature_privacy_explainer_strings.xml
index 6340fbf9b..993feb2bd 100644
--- a/photopicker/res/values-fr/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-fr/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> n\'aura accès qu\'aux photos que vous sélectionnez"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Sélectionnez les photos et les vidéos auxquelles vous autorisez <xliff:g id="APP_NAME">%1$s</xliff:g> à accéder"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Cette appli"</string>
</resources>
diff --git a/photopicker/res/values-gl/feature_privacy_explainer_strings.xml b/photopicker/res/values-gl/feature_privacy_explainer_strings.xml
index 48361a6b9..54935d083 100644
--- a/photopicker/res/values-gl/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-gl/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> só terá acceso ás fotos que selecciones"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecciona as fotos e os vídeos aos que pode acceder <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Esta aplicación"</string>
</resources>
diff --git a/photopicker/res/values-gu/feature_privacy_explainer_strings.xml b/photopicker/res/values-gu/feature_privacy_explainer_strings.xml
index f5f78f0ff..99aa54a9b 100644
--- a/photopicker/res/values-gu/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-gu/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> માત્ર તમે પસંદ કરેલા ફોટાનો ઍક્સેસ જ ધરાવશે"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"તમે <xliff:g id="APP_NAME">%1$s</xliff:g>ને જે ફોટા અને વીડિયો ઍક્સેસ કરવાની મંજૂરી આપી હોય તેને પસંદ કરો"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"આ ઍપ"</string>
</resources>
diff --git a/photopicker/res/values-hi/feature_privacy_explainer_strings.xml b/photopicker/res/values-hi/feature_privacy_explainer_strings.xml
index 163028100..a37358364 100644
--- a/photopicker/res/values-hi/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-hi/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g>, आपकी चुनी गई फ़ोटो ही ऐक्सेस कर पाएगा"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"उन फ़ोटो और वीडियो को चुनें जिनका ऐक्सेस <xliff:g id="APP_NAME">%1$s</xliff:g> को दिया है"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"इस ऐप्लिकेशन से"</string>
</resources>
diff --git a/photopicker/res/values-hr/feature_privacy_explainer_strings.xml b/photopicker/res/values-hr/feature_privacy_explainer_strings.xml
index 25256f2d2..3428e9bef 100644
--- a/photopicker/res/values-hr/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-hr/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> imat će pristup samo fotografijama koje odaberete"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Odaberite fotografije i videozapise za koje želite da aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> ima pristup"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ova aplikacija"</string>
</resources>
diff --git a/photopicker/res/values-hu/feature_privacy_explainer_strings.xml b/photopicker/res/values-hu/feature_privacy_explainer_strings.xml
index 10c1831d6..6feeaae0b 100644
--- a/photopicker/res/values-hu/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-hu/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"A(z) <xliff:g id="APP_NAME">%1$s</xliff:g> csak az Ön által kiválasztott fotókhoz férhet majd hozzá"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Válassza ki azokat a fotókat és videókat, amelyekhez engedélyezi a(z) <xliff:g id="APP_NAME">%1$s</xliff:g> számára a hozzáférést"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ez az alkalmazás"</string>
</resources>
diff --git a/photopicker/res/values-hy/feature_privacy_explainer_strings.xml b/photopicker/res/values-hy/feature_privacy_explainer_strings.xml
index cfd0fcd50..24d7ab0e4 100644
--- a/photopicker/res/values-hy/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-hy/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> հավելվածին հասանելի կլինեն միայն ձեր ընտրած լուսանկարները"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Ընտրեք լուսանկարներ և տեսանյութեր, որոնք ուզում եք հասանելի դարձնել <xliff:g id="APP_NAME">%1$s</xliff:g> հավելվածին"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Այս"</string>
</resources>
diff --git a/photopicker/res/values-in/feature_privacy_explainer_strings.xml b/photopicker/res/values-in/feature_privacy_explainer_strings.xml
index 890312ac1..d5cd0feb2 100644
--- a/photopicker/res/values-in/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-in/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> hanya akan memiliki akses ke foto yang Anda pilih"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Pilih foto dan video yang Anda izinkan untuk diakses <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Aplikasi ini"</string>
</resources>
diff --git a/photopicker/res/values-is/feature_privacy_explainer_strings.xml b/photopicker/res/values-is/feature_privacy_explainer_strings.xml
index b2a04fc43..b79b77a43 100644
--- a/photopicker/res/values-is/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-is/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> fær aðeins aðgang að myndunum sem þú velur"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Veldu myndir og vídeó sem þú vilt leyfa <xliff:g id="APP_NAME">%1$s</xliff:g> að hafa aðgang að"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Þetta forrit"</string>
</resources>
diff --git a/photopicker/res/values-it/feature_privacy_explainer_strings.xml b/photopicker/res/values-it/feature_privacy_explainer_strings.xml
index d56287719..d13de860a 100644
--- a/photopicker/res/values-it/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-it/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> accederà solo alle foto che selezioni"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Seleziona foto e video a cui l\'app <xliff:g id="APP_NAME">%1$s</xliff:g> può accedere"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Questa app"</string>
</resources>
diff --git a/photopicker/res/values-iw/feature_category_grid_strings.xml b/photopicker/res/values-iw/feature_category_grid_strings.xml
new file mode 100644
index 000000000..92e1c4ee1
--- /dev/null
+++ b/photopicker/res/values-iw/feature_category_grid_strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_categories_nav_button_label" msgid="988315710756227148">"אוספים"</string>
+</resources>
diff --git a/photopicker/res/values-iw/feature_privacy_explainer_strings.xml b/photopicker/res/values-iw/feature_privacy_explainer_strings.xml
index 61ad525a4..5ae8eb3fd 100644
--- a/photopicker/res/values-iw/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-iw/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"‫<xliff:g id="APP_NAME">%1$s</xliff:g> תקבל גישה רק לתמונות שבחרת"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"בחירה של תמונות וסרטונים שתהיה ל-<xliff:g id="APP_NAME">%1$s</xliff:g> גישה אליהם"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"האפליקציה הזו"</string>
</resources>
diff --git a/photopicker/res/values-ja/feature_privacy_explainer_strings.xml b/photopicker/res/values-ja/feature_privacy_explainer_strings.xml
index c15fe4849..90816b010 100644
--- a/photopicker/res/values-ja/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-ja/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g>は選択された写真にのみアクセスできます"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g>にアクセスを許可する写真と動画を選択してください"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"このアプリ"</string>
</resources>
diff --git a/photopicker/res/values-ka/feature_privacy_explainer_strings.xml b/photopicker/res/values-ka/feature_privacy_explainer_strings.xml
index aa5aa6428..188e8d4b7 100644
--- a/photopicker/res/values-ka/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-ka/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g>-ს მხოლოდ თქვენ მიერ არჩეულ ფოტოებზე ექნება წვდომა"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"აირჩიეთ ფოტოები და ვიდეოები, რომლებზეც <xliff:g id="APP_NAME">%1$s</xliff:g>-ს წვდომის უფლებას აძლევთ"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ეს აპი"</string>
</resources>
diff --git a/photopicker/res/values-kk/feature_privacy_explainer_strings.xml b/photopicker/res/values-kk/feature_privacy_explainer_strings.xml
index 77e91d579..14c943280 100644
--- a/photopicker/res/values-kk/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-kk/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> қолданбасы тек сіз таңдаған фотосуреттерді пайдалана алады."</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> қолданбасына пайдалануға рұқсат беретін фотосуреттер мен бейнелерді таңдаңыз."</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Осы қолданба"</string>
</resources>
diff --git a/photopicker/res/values-km/feature_privacy_explainer_strings.xml b/photopicker/res/values-km/feature_privacy_explainer_strings.xml
index 5ca1c2007..3815858db 100644
--- a/photopicker/res/values-km/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-km/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> នឹងមានសិទ្ធិចូលប្រើប្រាស់បានតែរូបថតដែលអ្នកជ្រើសរើសប៉ុណ្ណោះ"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"ជ្រើសរើសរូបថត និងវីដេអូដែលអ្នកអនុញ្ញាតឱ្យ <xliff:g id="APP_NAME">%1$s</xliff:g> ចូលប្រើ"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"កម្មវិធីនេះ"</string>
</resources>
diff --git a/photopicker/res/values-kn/feature_privacy_explainer_strings.xml b/photopicker/res/values-kn/feature_privacy_explainer_strings.xml
index 2c8c5d28f..3406684e7 100644
--- a/photopicker/res/values-kn/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-kn/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"ನೀವು ಆಯ್ಕೆ ಮಾಡಿದ ಫೋಟೋಗಳಿಗೆ ಮಾತ್ರ <xliff:g id="APP_NAME">%1$s</xliff:g> ಆ್ಯಕ್ಸೆಸ್ ಅನ್ನು ಹೊಂದಿರುತ್ತದೆ"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"ನೀವು <xliff:g id="APP_NAME">%1$s</xliff:g> ಗೆ ಆ್ಯಕ್ಸೆಸ್‌ ಮಾಡಲು ಅನುಮತಿಸುವ ಫೋಟೋಗಳು ಮತ್ತು ವೀಡಿಯೊಗಳನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ಈ ಆ್ಯಪ್"</string>
</resources>
diff --git a/photopicker/res/values-ko/feature_privacy_explainer_strings.xml b/photopicker/res/values-ko/feature_privacy_explainer_strings.xml
index 76113be8f..d8d456271 100644
--- a/photopicker/res/values-ko/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-ko/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g>에서 내가 선택한 사진에만 액세스합니다."</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g>에 액세스하도록 허용할 사진과 동영상을 선택하세요"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"이 앱"</string>
</resources>
diff --git a/photopicker/res/values-ky/feature_privacy_explainer_strings.xml b/photopicker/res/values-ky/feature_privacy_explainer_strings.xml
index 3c18ce1e1..4d931bcc7 100644
--- a/photopicker/res/values-ky/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-ky/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> сиз тандаган сүрөттөрдү гана колдонот"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> колдоно алган сүрөттөр менен видеолорду тандаңыз"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ушул колдонмо"</string>
</resources>
diff --git a/photopicker/res/values-lo/feature_privacy_explainer_strings.xml b/photopicker/res/values-lo/feature_privacy_explainer_strings.xml
index 79a2caee3..efbabcb08 100644
--- a/photopicker/res/values-lo/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-lo/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> ຈະມີສິດເຂົ້າເຖິງໄດ້ສະເພາະຮູບພາບທີ່ທ່ານເລືອກເທົ່ານັ້ນ"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"ເລືອກຮູບພາບ ແລະ ວິດີໂອທີ່ທ່ານອະນຸຍາດໃຫ້ <xliff:g id="APP_NAME">%1$s</xliff:g> ເຂົ້າເຖິງ"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ແອັບນີ້"</string>
</resources>
diff --git a/photopicker/res/values-lt/feature_privacy_explainer_strings.xml b/photopicker/res/values-lt/feature_privacy_explainer_strings.xml
index 94868fab6..b299df4db 100644
--- a/photopicker/res/values-lt/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-lt/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"„<xliff:g id="APP_NAME">%1$s</xliff:g>“ gali pasiekti tik jūsų pasirinktas nuotraukas"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Pasirinkite nuotraukas ir vaizdo įrašus, kuriuos galės pasiekti <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ši programa"</string>
</resources>
diff --git a/photopicker/res/values-lv/feature_privacy_explainer_strings.xml b/photopicker/res/values-lv/feature_privacy_explainer_strings.xml
index 1f7a848e1..3ed1583fb 100644
--- a/photopicker/res/values-lv/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-lv/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Lietotne <xliff:g id="APP_NAME">%1$s</xliff:g> varēs piekļūt tikai jūsu atlasītajiem fotoattēliem."</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Atlasiet fotoattēlus un videoklipus, kam <xliff:g id="APP_NAME">%1$s</xliff:g> drīkst piekļūt."</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Šī lietotne"</string>
</resources>
diff --git a/photopicker/res/values-mk/feature_privacy_explainer_strings.xml b/photopicker/res/values-mk/feature_privacy_explainer_strings.xml
index 2535b901e..95e30b263 100644
--- a/photopicker/res/values-mk/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-mk/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> ќе има пристап само до фотографиите што ќе ги изберете"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Изберете фотографии и видеа до коишто дозволувате да пристапи <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Оваа апликација"</string>
</resources>
diff --git a/photopicker/res/values-ml/feature_privacy_explainer_strings.xml b/photopicker/res/values-ml/feature_privacy_explainer_strings.xml
index 04d4917d9..7c27aceb4 100644
--- a/photopicker/res/values-ml/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-ml/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"നിങ്ങൾ തിരഞ്ഞെടുക്കുന്ന ഫോട്ടോകളിലേക്ക് മാത്രമേ <xliff:g id="APP_NAME">%1$s</xliff:g> എന്നതിന് ആക്‌സസ് ഉണ്ടാകൂ"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> എന്നതിനെ നിങ്ങൾ ആക്‌സസ് ചെയ്യാൻ അനുവദിക്കുന്ന ഫോട്ടോകളും വീഡിയോകളും തിരഞ്ഞെടുക്കുക"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ഈ ആപ്പ്"</string>
</resources>
diff --git a/photopicker/res/values-mn/feature_privacy_explainer_strings.xml b/photopicker/res/values-mn/feature_privacy_explainer_strings.xml
index 773cc4354..0bc45ed5a 100644
--- a/photopicker/res/values-mn/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-mn/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> зөвхөн таны сонгосон зурагт хандах эрхтэй байх болно"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g>-д хандахыг зөвшөөрөх зураг болон видеогоо сонгоно уу"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Энэ апп"</string>
</resources>
diff --git a/photopicker/res/values-mr/feature_privacy_explainer_strings.xml b/photopicker/res/values-mr/feature_privacy_explainer_strings.xml
index 4bf35929b..41bbbb0bb 100644
--- a/photopicker/res/values-mr/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-mr/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> हे फक्त तुम्ही निवडलेले फोटो अ‍ॅक्सेस करू शकेल"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"तुम्ही <xliff:g id="APP_NAME">%1$s</xliff:g> ला अ‍ॅक्सेस करण्याची अनुमती देत असलेले फोटो आणि व्हिडिओ निवडा"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"हे अ‍ॅप"</string>
</resources>
diff --git a/photopicker/res/values-ms/feature_privacy_explainer_strings.xml b/photopicker/res/values-ms/feature_privacy_explainer_strings.xml
index 2357caca6..f074bdc5c 100644
--- a/photopicker/res/values-ms/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-ms/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> hanya akan mengakses foto yang anda pilih"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Pilih foto dan video yang boleh diakses oleh <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Apl ini"</string>
</resources>
diff --git a/photopicker/res/values-my/feature_privacy_explainer_strings.xml b/photopicker/res/values-my/feature_privacy_explainer_strings.xml
index fc7ef7e51..1ead222f7 100644
--- a/photopicker/res/values-my/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-my/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> သည် သင်ရွေးသော ဓာတ်ပုံများကိုသာ သုံးခွင့်ရှိမည်"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> အား သင်သုံးခွင့်ပြုသော ဓာတ်ပုံနှင့် ဗီဒီယိုများကို ရွေးပါ"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ဤအက်ပ်"</string>
</resources>
diff --git a/photopicker/res/values-nb/feature_privacy_explainer_strings.xml b/photopicker/res/values-nb/feature_privacy_explainer_strings.xml
index 904396ce1..95ad3ca7d 100644
--- a/photopicker/res/values-nb/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-nb/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> har bare tilgang til bildene du velger"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Velg bilder og videoer du vil at <xliff:g id="APP_NAME">%1$s</xliff:g> skal kunne bruke"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Denne appen"</string>
</resources>
diff --git a/photopicker/res/values-ne/feature_privacy_explainer_strings.xml b/photopicker/res/values-ne/feature_privacy_explainer_strings.xml
index e5de14c55..f1aa17a53 100644
--- a/photopicker/res/values-ne/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-ne/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> तपाईंले चयन गरेका फोटोहरू मात्र एक्सेस गर्न सक्ने छ"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"तपाईंले <xliff:g id="APP_NAME">%1$s</xliff:g> लाई एक्सेस दिनुभएका फोटो र भिडियोहरू चयन गर्नुहोस्"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"यो एप"</string>
</resources>
diff --git a/photopicker/res/values-nl/feature_privacy_explainer_strings.xml b/photopicker/res/values-nl/feature_privacy_explainer_strings.xml
index 345f89e58..3c6bb3109 100644
--- a/photopicker/res/values-nl/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-nl/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> heeft alleen toegang tot de foto\'s die je selecteert"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecteer foto\'s en video\'s waartoe je <xliff:g id="APP_NAME">%1$s</xliff:g> toegang wilt geven"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Deze app"</string>
</resources>
diff --git a/photopicker/res/values-or/feature_privacy_explainer_strings.xml b/photopicker/res/values-or/feature_privacy_explainer_strings.xml
index 8ee3b1f8c..69ce63f19 100644
--- a/photopicker/res/values-or/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-or/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> ପାଖରେ କେବଳ ଆପଣ ଚୟନ କରିଥିବା ଫଟୋଗୁଡ଼ିକର ଆକ୍ସେସ ରହିବ"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"ଆପଣ <xliff:g id="APP_NAME">%1$s</xliff:g>କୁ ଆକ୍ସେସ କରିବାକୁ ଅନୁମତି ଦେଇଥିବା ଫଟୋ ଏବଂ ଭିଡିଓଗୁଡ଼ିକୁ ଚୟନ କରନ୍ତୁ"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ଏହି ଆପ"</string>
</resources>
diff --git a/photopicker/res/values-pa/feature_privacy_explainer_strings.xml b/photopicker/res/values-pa/feature_privacy_explainer_strings.xml
index f9358a6f0..1360579e6 100644
--- a/photopicker/res/values-pa/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-pa/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> ਕੋਲ ਸਿਰਫ਼ ਤੁਹਾਡੀਆਂ ਚੁਣੀਆਂ ਹੋਈਆਂ ਫ਼ੋਟੋਆਂ ਤੱਕ ਪਹੁੰਚ ਹੋਵੇਗੀ"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"ਉਹ ਫ਼ੋਟੋਆਂ ਅਤੇ ਵੀਡੀਓ ਚੁਣੋ, ਜਿਨ੍ਹਾਂ ਤੱਕ ਤੁਸੀਂ <xliff:g id="APP_NAME">%1$s</xliff:g> ਨੂੰ ਪਹੁੰਚ ਕਰਨ ਦੀ ਆਗਿਆ ਦੇਣੀ ਹੈ"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ਇਹ ਐਪ"</string>
</resources>
diff --git a/photopicker/res/values-pl/feature_privacy_explainer_strings.xml b/photopicker/res/values-pl/feature_privacy_explainer_strings.xml
index b0d3add3e..30d8c603d 100644
--- a/photopicker/res/values-pl/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-pl/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Aplikacja <xliff:g id="APP_NAME">%1$s</xliff:g> ma dostęp tylko do wybranych przez Ciebie zdjęć"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Wybierz zdjęcia i filmy, które chcesz udostępnić aplikacji <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ta aplikacja"</string>
</resources>
diff --git a/photopicker/res/values-pt-rBR/feature_privacy_explainer_strings.xml b/photopicker/res/values-pt-rBR/feature_privacy_explainer_strings.xml
index 097af5b89..dff23efb8 100644
--- a/photopicker/res/values-pt-rBR/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-pt-rBR/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"O app <xliff:g id="APP_NAME">%1$s</xliff:g> só terá acesso às fotos que você selecionar"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecione fotos e vídeos que o app <xliff:g id="APP_NAME">%1$s</xliff:g> pode acessar"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Este app"</string>
</resources>
diff --git a/photopicker/res/values-pt-rPT/feature_privacy_explainer_strings.xml b/photopicker/res/values-pt-rPT/feature_privacy_explainer_strings.xml
index afb43db56..2ae133ada 100644
--- a/photopicker/res/values-pt-rPT/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-pt-rPT/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"A app <xliff:g id="APP_NAME">%1$s</xliff:g> só vai ter acesso às fotos selecionadas por si"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecione as fotos e os vídeos aos quais a app <xliff:g id="APP_NAME">%1$s</xliff:g> pode aceder"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Esta app"</string>
</resources>
diff --git a/photopicker/res/values-pt/feature_privacy_explainer_strings.xml b/photopicker/res/values-pt/feature_privacy_explainer_strings.xml
index 097af5b89..dff23efb8 100644
--- a/photopicker/res/values-pt/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-pt/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"O app <xliff:g id="APP_NAME">%1$s</xliff:g> só terá acesso às fotos que você selecionar"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecione fotos e vídeos que o app <xliff:g id="APP_NAME">%1$s</xliff:g> pode acessar"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Este app"</string>
</resources>
diff --git a/photopicker/res/values-ro/feature_privacy_explainer_strings.xml b/photopicker/res/values-ro/feature_privacy_explainer_strings.xml
index 1fb5f5bac..6161bc0a0 100644
--- a/photopicker/res/values-ro/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-ro/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> poate accesa numai fotografiile selectate de tine"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selectează fotografiile și videoclipurile la care <xliff:g id="APP_NAME">%1$s</xliff:g> poate avea acces"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Această aplicație"</string>
</resources>
diff --git a/photopicker/res/values-ru/feature_privacy_explainer_strings.xml b/photopicker/res/values-ru/feature_privacy_explainer_strings.xml
index 7c2b3c74c..1af1259f5 100644
--- a/photopicker/res/values-ru/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-ru/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> сможет получать доступ только к выбранным фотографиям."</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Выберите фотографии и видео, к которым <xliff:g id="APP_NAME">%1$s</xliff:g> может получить доступ."</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Это приложение"</string>
</resources>
diff --git a/photopicker/res/values-si/feature_privacy_explainer_strings.xml b/photopicker/res/values-si/feature_privacy_explainer_strings.xml
index b43c88511..95012a92a 100644
--- a/photopicker/res/values-si/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-si/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> හට ඔබ තෝරන ඡායාරූප වලට පමණක් ප්‍රවේශ විය හැකි වනු ඇත"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"ඔබ <xliff:g id="APP_NAME">%1$s</xliff:g> හට ප්‍රවේශ වීමට ඉඩ දෙන ඡායාරූප සහ වීඩියෝ තෝරන්න"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"මෙම යෙදුම"</string>
</resources>
diff --git a/photopicker/res/values-sk/feature_privacy_explainer_strings.xml b/photopicker/res/values-sk/feature_privacy_explainer_strings.xml
index 327db51d9..5002a8115 100644
--- a/photopicker/res/values-sk/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-sk/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Aplikácia <xliff:g id="APP_NAME">%1$s</xliff:g> bude mať prístup iba k fotkám, ktoré vyberiete"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Vyberte fotky a videá, ku ktorým má mať aplikácia <xliff:g id="APP_NAME">%1$s</xliff:g> prístup"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Táto aplikácia"</string>
</resources>
diff --git a/photopicker/res/values-sl/feature_privacy_explainer_strings.xml b/photopicker/res/values-sl/feature_privacy_explainer_strings.xml
index 7a9ed4db4..7f776fe6f 100644
--- a/photopicker/res/values-sl/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-sl/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> bo lahko dostopala samo do fotografij, ki jih izberete"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Izberite fotografije in videoposnetke, do katerih lahko dostopa aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ta aplikacija"</string>
</resources>
diff --git a/photopicker/res/values-sq/feature_privacy_explainer_strings.xml b/photopicker/res/values-sq/feature_privacy_explainer_strings.xml
index 754bf3f6f..39b07fa77 100644
--- a/photopicker/res/values-sq/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-sq/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"\"<xliff:g id="APP_NAME">%1$s</xliff:g>\" do të ketë qasje vetëm te fotografitë që zgjedh ti"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Zgjidh fotografitë dhe videot që lejon që të qaset \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ky aplikacion"</string>
</resources>
diff --git a/photopicker/res/values-sr/feature_privacy_explainer_strings.xml b/photopicker/res/values-sr/feature_privacy_explainer_strings.xml
index 56e0cc428..874220706 100644
--- a/photopicker/res/values-sr/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-sr/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> ће имати приступ само сликама које изаберете"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Изаберите слике и видео снимке којима <xliff:g id="APP_NAME">%1$s</xliff:g> може да приступи"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ова апликација"</string>
</resources>
diff --git a/photopicker/res/values-sv/feature_privacy_explainer_strings.xml b/photopicker/res/values-sv/feature_privacy_explainer_strings.xml
index 9304ec9e6..99ce01e52 100644
--- a/photopicker/res/values-sv/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-sv/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> får endast åtkomst till fotona du väljer"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Välj foton och videor som du ger <xliff:g id="APP_NAME">%1$s</xliff:g> åtkomst till"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Den här appen"</string>
</resources>
diff --git a/photopicker/res/values-sw/feature_privacy_explainer_strings.xml b/photopicker/res/values-sw/feature_privacy_explainer_strings.xml
index d57b030ec..f0df75553 100644
--- a/photopicker/res/values-sw/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-sw/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> itaweza tu kufikia picha utakazochagua"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Chagua picha na video unazoiruhusu <xliff:g id="APP_NAME">%1$s</xliff:g> kufikia"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Programu hii"</string>
</resources>
diff --git a/photopicker/res/values-ta/feature_privacy_explainer_strings.xml b/photopicker/res/values-ta/feature_privacy_explainer_strings.xml
index a742ce013..9c94c2b67 100644
--- a/photopicker/res/values-ta/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-ta/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"நீங்கள் தேர்ந்தெடுக்கும் படங்களை மட்டுமே <xliff:g id="APP_NAME">%1$s</xliff:g> ஆப்ஸால் அணுக முடியும்"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> ஆப்ஸ் அணுகுவதற்கு நீங்கள் அனுமதியளிக்கும் படங்களையும் வீடியோக்களையும் தேர்ந்தெடுங்கள்"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"இந்த ஆப்ஸ்"</string>
</resources>
diff --git a/photopicker/res/values-te/feature_privacy_explainer_strings.xml b/photopicker/res/values-te/feature_privacy_explainer_strings.xml
index 9e75f9a4b..b2e682385 100644
--- a/photopicker/res/values-te/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-te/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g>‌కు మీరు ఎంచుకునే ఫోటోలకు మాత్రమే యాక్సెస్ ఉంటుంది"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> యాక్సెస్ చేయడానికి మీరు అనుమతించే ఫోటోలను, వీడియోలను ఎంచుకోండి"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ఈ యాప్"</string>
</resources>
diff --git a/photopicker/res/values-th/feature_privacy_explainer_strings.xml b/photopicker/res/values-th/feature_privacy_explainer_strings.xml
index ad8cb30cf..5cac14b42 100644
--- a/photopicker/res/values-th/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-th/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> จะเข้าถึงได้เฉพาะรูปภาพที่คุณเลือกเท่านั้น"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"เลือกรูปภาพและวิดีโอที่คุณอนุญาตให้<xliff:g id="APP_NAME">%1$s</xliff:g>เข้าถึง"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"แอปนี้"</string>
</resources>
diff --git a/photopicker/res/values-tl/feature_privacy_explainer_strings.xml b/photopicker/res/values-tl/feature_privacy_explainer_strings.xml
index 7e53c88ea..6b0951428 100644
--- a/photopicker/res/values-tl/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-tl/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Magkakaroon lang ng access ang <xliff:g id="APP_NAME">%1$s</xliff:g> sa mga larawang pipiliin mo"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Pumili ng mga larawan at video na pinapayagan mong ma-access ng <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"App na ito"</string>
</resources>
diff --git a/photopicker/res/values-tr/feature_privacy_explainer_strings.xml b/photopicker/res/values-tr/feature_privacy_explainer_strings.xml
index 74afb1779..55b72df58 100644
--- a/photopicker/res/values-tr/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-tr/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> yalnızca seçtiğiniz fotoğraflara erişebilir"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> uygulamasının erişmesine izin verdiğiniz fotoğraf ve videoları seçin"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Bu uygulama"</string>
</resources>
diff --git a/photopicker/res/values-uk/feature_privacy_explainer_strings.xml b/photopicker/res/values-uk/feature_privacy_explainer_strings.xml
index a22c8cfdf..2d03b7ccd 100644
--- a/photopicker/res/values-uk/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-uk/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Додаток <xliff:g id="APP_NAME">%1$s</xliff:g> матиме доступ лише до вибраних вами фотографій"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Виберіть фотографії і відео, до яких додаток <xliff:g id="APP_NAME">%1$s</xliff:g> зможе отримувати доступ"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Цей додаток"</string>
</resources>
diff --git a/photopicker/res/values-ur/feature_privacy_explainer_strings.xml b/photopicker/res/values-ur/feature_privacy_explainer_strings.xml
index 9d00a5212..703c8cc08 100644
--- a/photopicker/res/values-ur/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-ur/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> صرف آپ کے منتخب کردہ تصاویر تک رسائی حاصل کرے گی"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"وہ تصاویر اور ویڈیوز منتخب کریں جن تک آپ <xliff:g id="APP_NAME">%1$s</xliff:g> کو رسائی حاصل کرنے کی اجازت دیتے ہیں"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"یہ ایپ"</string>
</resources>
diff --git a/photopicker/res/values-uz/feature_privacy_explainer_strings.xml b/photopicker/res/values-uz/feature_privacy_explainer_strings.xml
index 8de1375b0..0866c72a9 100644
--- a/photopicker/res/values-uz/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-uz/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> faqat siz tanlagan suratlarga kira oladi"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> ilovasi kira olishi uchun video va suratlarni tanlang"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Bu ilova"</string>
</resources>
diff --git a/photopicker/res/values-vi/feature_privacy_explainer_strings.xml b/photopicker/res/values-vi/feature_privacy_explainer_strings.xml
index cc470c4b0..f36f67b59 100644
--- a/photopicker/res/values-vi/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-vi/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> sẽ chỉ có quyền truy cập vào những ảnh bạn chọn"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Chọn những ảnh và video mà bạn cho phép <xliff:g id="APP_NAME">%1$s</xliff:g> truy cập"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ứng dụng này"</string>
</resources>
diff --git a/photopicker/res/values-zh-rCN/feature_privacy_explainer_strings.xml b/photopicker/res/values-zh-rCN/feature_privacy_explainer_strings.xml
index ce0ec7759..22e39a2f2 100644
--- a/photopicker/res/values-zh-rCN/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-zh-rCN/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"“<xliff:g id="APP_NAME">%1$s</xliff:g>”仅有权访问您选择的照片"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"选择您允许“<xliff:g id="APP_NAME">%1$s</xliff:g>”访问的照片和视频"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"此应用"</string>
</resources>
diff --git a/photopicker/res/values-zh-rHK/feature_privacy_explainer_strings.xml b/photopicker/res/values-zh-rHK/feature_privacy_explainer_strings.xml
index 473ad61a8..0130ebc7a 100644
--- a/photopicker/res/values-zh-rHK/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-zh-rHK/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"「<xliff:g id="APP_NAME">%1$s</xliff:g>」只可存取你選取的相片"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"選取允許「<xliff:g id="APP_NAME">%1$s</xliff:g>」存取的相片和影片"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"此應用程式"</string>
</resources>
diff --git a/photopicker/res/values-zh-rTW/feature_privacy_explainer_strings.xml b/photopicker/res/values-zh-rTW/feature_privacy_explainer_strings.xml
index 4d71eb132..c7bf1c219 100644
--- a/photopicker/res/values-zh-rTW/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-zh-rTW/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"「<xliff:g id="APP_NAME">%1$s</xliff:g>」只能存取你選取的相片"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"請選取允許「<xliff:g id="APP_NAME">%1$s</xliff:g>」存取的相片和影片"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"這個應用程式"</string>
</resources>
diff --git a/photopicker/res/values-zu/feature_privacy_explainer_strings.xml b/photopicker/res/values-zu/feature_privacy_explainer_strings.xml
index 35224e752..a79a1dc11 100644
--- a/photopicker/res/values-zu/feature_privacy_explainer_strings.xml
+++ b/photopicker/res/values-zu/feature_privacy_explainer_strings.xml
@@ -18,6 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_privacy_explainer" msgid="4607406928962678061">"I-<xliff:g id="APP_NAME">%1$s</xliff:g> izokwazi ukufinyelela kuphela izithombe ozikhethayo"</string>
- <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Khetha izithombe namavidiyo avumela i-<xliff:g id="APP_NAME">%1$s</xliff:g> ukuba ifinyelele"</string>
+ <!-- no translation found for photopicker_privacy_explainer_permission_mode (6875197264505249268) -->
+ <skip />
<string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Le app"</string>
</resources>
diff --git a/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt b/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt
index c413b7f73..a7537b004 100644
--- a/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt
+++ b/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt
@@ -426,7 +426,7 @@ class DataServiceImpl(
Log.v(
DataService.TAG,
- "Created an album media paging source that queries " + "$availableProviders",
+ "Created an album media paging source that queries $availableProviders",
)
albumMap[album.id] = albumMediaPagingSource
@@ -454,7 +454,7 @@ class DataServiceImpl(
Log.v(
DataService.TAG,
- "Created an album paging source that queries " + "$availableProviders",
+ "Created an album paging source that queries $availableProviders",
)
albumPagingSources.add(albumPagingSource)
@@ -512,7 +512,7 @@ class DataServiceImpl(
Log.v(
DataService.TAG,
- "Created a media paging source that queries database for" + "preview items.",
+ "Created a media paging source that queries database for preview items.",
)
mediaPagingSources.add(mediaPagingSource)
mediaPagingSource
diff --git a/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt b/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt
index 4733523dc..d0c873e84 100644
--- a/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt
+++ b/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt
@@ -29,6 +29,9 @@ import com.android.modules.utils.build.SdkLevel
import com.android.photopicker.core.configuration.PhotopickerConfiguration
import com.android.photopicker.data.model.CollectionInfo
import com.android.photopicker.data.model.Group
+import com.android.photopicker.data.model.GroupPageKey
+import com.android.photopicker.data.model.Icon
+import com.android.photopicker.data.model.KeyToCategoryType
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaPageKey
import com.android.photopicker.data.model.MediaSource
@@ -39,15 +42,16 @@ import com.android.photopicker.features.search.model.SearchSuggestion
import com.android.photopicker.features.search.model.SearchSuggestionType
/**
- * A client class that is reponsible for holding logic required to interact with [MediaProvider].
+ * A client class that is responsible for holding logic required to interact with [MediaProvider].
*
* It typically fetches data from [MediaProvider] using content queries and call methods.
*/
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"
+ private const val MEDIA_SETS_INIT_CALL_METHOD: String = "picker_media_sets_init_call"
+ private const val MEDIA_SET_CONTENTS_INIT_CALL_METHOD: String =
+ "picker_media_in_media_set_init"
private const val EXTRA_MIME_TYPES = "mime_types"
private const val EXTRA_INTENT_ACTION = "intent_action"
private const val EXTRA_PROVIDERS = "providers"
@@ -56,6 +60,10 @@ 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 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"
+ const val SEARCH_PROVIDER_AUTHORITIES = "search_provider_authorities"
const val SEARCH_REQUEST_ID = "search_request_id"
}
@@ -76,6 +84,32 @@ open class MediaProviderClient {
}
/**
+ * Contains all mandatory keys required to make a Category and Album query that are not present
+ * in [MediaQuery] already.
+ */
+ private enum class CategoryAndAlbumQuery(val key: String) {
+ PARENT_CATEGORY_ID("parent_category_id")
+ }
+
+ /**
+ * Contains all mandatory keys required to make a Media Set query that are not present in
+ * [MediaQuery] already.
+ */
+ private enum class MediaSetsQuery(val key: String) {
+ PARENT_CATEGORY_ID("parent_category_id"),
+ PARENT_CATEGORY_AUTHORITY("parent_category_authority"),
+ }
+
+ /**
+ * Contains all mandatory keys required to make a Media Set contents query that are not present
+ * in [MediaQuery] already.
+ */
+ private enum class MediaSetContentsQuery(val key: String) {
+ PARENT_MEDIA_SET_PICKER_ID("media_set_picker_id"),
+ PARENT_MEDIA_SET_AUTHORITY("media_set_picker_authority"),
+ }
+
+ /**
* Contains all optional and mandatory keys for data in the Available Providers query response.
*/
enum class AvailableProviderResponse(val key: String) {
@@ -156,6 +190,28 @@ open class MediaProviderClient {
SUGGESTION_TYPE("suggestion_type"),
}
+ enum class GroupResponse(val key: String) {
+ MEDIA_GROUP("media_group"),
+ /** Identifier received from CMP. This cannot be null. */
+ GROUP_ID("group_id"),
+ /** Identifier used in Picker Backend, if any. */
+ PICKER_ID("picker_id"),
+ DISPLAY_NAME("display_name"),
+ AUTHORITY("authority"),
+ UNWRAPPED_COVER_URI("unwrapped_cover_uri"),
+ ADDITIONAL_UNWRAPPED_COVER_URI_1("additional_cover_uri_1"),
+ ADDITIONAL_UNWRAPPED_COVER_URI_2("additional_cover_uri_2"),
+ ADDITIONAL_UNWRAPPED_COVER_URI_3("additional_cover_uri_3"),
+ CATEGORY_TYPE("category_type"),
+ IS_LEAF_CATEGORY("is_leaf_category"),
+ }
+
+ enum class GroupType() {
+ CATEGORY,
+ MEDIA_SET,
+ ALBUM,
+ }
+
/** Fetch available [Provider]-s from the Media Provider process. */
fun fetchAvailableProviders(contentResolver: ContentResolver): List<Provider> {
try {
@@ -225,8 +281,8 @@ open class MediaProviderClient {
cursor?.let {
LoadResult.Page(
data = cursor.getListOfMedia(),
- prevKey = cursor.getPrevPageKey(),
- nextKey = cursor.getNextPageKey(),
+ prevKey = cursor.getPrevMediaPageKey(),
+ nextKey = cursor.getNextMediaPageKey(),
itemsBefore =
cursor.getItemsBeforeCount() ?: LoadResult.Page.COUNT_UNDEFINED,
)
@@ -276,8 +332,8 @@ open class MediaProviderClient {
cursor?.let {
LoadResult.Page(
data = cursor.getListOfMedia(),
- prevKey = cursor.getPrevPageKey(),
- nextKey = cursor.getNextPageKey(),
+ prevKey = cursor.getPrevMediaPageKey(),
+ nextKey = cursor.getNextMediaPageKey(),
itemsBefore =
cursor.getItemsBeforeCount() ?: LoadResult.Page.COUNT_UNDEFINED,
)
@@ -331,8 +387,8 @@ open class MediaProviderClient {
cursor?.let {
LoadResult.Page(
data = cursor.getListOfMedia(),
- prevKey = cursor.getPrevPageKey(),
- nextKey = cursor.getNextPageKey(),
+ prevKey = cursor.getPrevMediaPageKey(),
+ nextKey = cursor.getNextMediaPageKey(),
)
}
?: throw IllegalStateException(
@@ -377,8 +433,8 @@ open class MediaProviderClient {
cursor?.let {
LoadResult.Page(
data = cursor.getListOfAlbums(),
- prevKey = cursor.getPrevPageKey(),
- nextKey = cursor.getNextPageKey(),
+ prevKey = cursor.getPrevMediaPageKey(),
+ nextKey = cursor.getNextMediaPageKey(),
)
}
?: throw IllegalStateException(
@@ -427,8 +483,8 @@ open class MediaProviderClient {
cursor?.let {
LoadResult.Page(
data = cursor.getListOfMedia(),
- prevKey = cursor.getPrevPageKey(),
- nextKey = cursor.getNextPageKey(),
+ prevKey = cursor.getPrevMediaPageKey(),
+ nextKey = cursor.getNextMediaPageKey(),
)
}
?: throw IllegalStateException(
@@ -542,6 +598,9 @@ open class MediaProviderClient {
}
}
+ /**
+ * Fetches a list of search suggestions from MediaProvider filtered by the input prefix string.
+ */
suspend fun fetchSearchSuggestions(
resolver: ContentResolver,
prefix: String,
@@ -571,6 +630,157 @@ open class MediaProviderClient {
}
/**
+ * Fetches a list of categories and albums from MediaProvider filtered by the input list of
+ * available providers, mime types and parent category id.
+ */
+ suspend fun fetchCategoriesAndAlbums(
+ pageKey: GroupPageKey,
+ pageSize: Int,
+ contentResolver: ContentResolver,
+ availableProviders: List<Provider>,
+ parentCategoryId: String?,
+ config: PhotopickerConfiguration,
+ cancellationSignal: CancellationSignal?,
+ ): LoadResult<GroupPageKey, Group> {
+ val input: Bundle =
+ bundleOf(
+ MediaQuery.PICKER_ID.key to pageKey.pickerId,
+ MediaQuery.PAGE_SIZE.key to pageSize,
+ MediaQuery.PROVIDERS.key to
+ ArrayList<String>().apply {
+ availableProviders.forEach { provider -> add(provider.authority) }
+ },
+ EXTRA_MIME_TYPES to config.mimeTypes,
+ EXTRA_INTENT_ACTION to config.action,
+ CategoryAndAlbumQuery.PARENT_CATEGORY_ID.key to parentCategoryId,
+ )
+ try {
+ return contentResolver
+ .query(
+ getCategoryUri(parentCategoryId),
+ /* projection */ null,
+ input,
+ cancellationSignal,
+ )
+ .use { cursor ->
+ cursor?.let {
+ LoadResult.Page(
+ data = cursor.getListOfCategoriesAndAlbums(availableProviders),
+ prevKey = cursor.getPrevGroupPageKey(),
+ nextKey = cursor.getNextGroupPageKey(),
+ )
+ }
+ ?: throw IllegalStateException(
+ "Received a null response from Content Provider"
+ )
+ }
+ } catch (e: RuntimeException) {
+ throw RuntimeException(
+ "Could not fetch categories and albums for parent category $parentCategoryId",
+ e,
+ )
+ }
+ }
+
+ /**
+ * Fetches a list of media sets from MediaProvider filtered by the input list of available
+ * providers, mime types and parent category id.
+ */
+ suspend fun fetchMediaSets(
+ pageKey: GroupPageKey,
+ pageSize: Int,
+ contentResolver: ContentResolver,
+ availableProviders: List<Provider>,
+ parentCategory: Group.Category,
+ config: PhotopickerConfiguration,
+ cancellationSignal: CancellationSignal?,
+ ): LoadResult<GroupPageKey, Group.MediaSet> {
+ val input: Bundle =
+ bundleOf(
+ MediaQuery.PICKER_ID.key to pageKey.pickerId,
+ MediaQuery.PAGE_SIZE.key to pageSize,
+ MediaQuery.PROVIDERS.key to
+ ArrayList<String>().apply {
+ availableProviders.forEach { provider -> add(provider.authority) }
+ },
+ EXTRA_MIME_TYPES to config.mimeTypes,
+ EXTRA_INTENT_ACTION to config.action,
+ MediaSetsQuery.PARENT_CATEGORY_ID.key to parentCategory.id,
+ MediaSetsQuery.PARENT_CATEGORY_AUTHORITY.key to parentCategory.authority,
+ )
+ try {
+ return contentResolver
+ .query(MEDIA_SETS_URI, /* projection */ null, input, cancellationSignal)
+ .use { cursor ->
+ cursor?.let {
+ LoadResult.Page(
+ data = cursor.getListOfMediaSets(availableProviders),
+ prevKey = cursor.getPrevGroupPageKey(),
+ nextKey = cursor.getNextGroupPageKey(),
+ )
+ }
+ ?: throw IllegalStateException(
+ "Received a null response from Content Provider"
+ )
+ }
+ } catch (e: RuntimeException) {
+ throw RuntimeException(
+ "Could not fetch media sets for parent category ${parentCategory.id}",
+ e,
+ )
+ }
+ }
+
+ /**
+ * Fetches a list of media items in a media set from MediaProvider filtered by the input list of
+ * available providers, mime types and parent media set id.
+ */
+ suspend fun fetchMediaSetContents(
+ pageKey: MediaPageKey,
+ pageSize: Int,
+ contentResolver: ContentResolver,
+ availableProviders: List<Provider>,
+ parentMediaSet: Group.MediaSet,
+ config: PhotopickerConfiguration,
+ cancellationSignal: CancellationSignal?,
+ ): LoadResult<MediaPageKey, Media> {
+ val input: Bundle =
+ bundleOf(
+ MediaQuery.PICKER_ID.key to pageKey.pickerId,
+ MediaQuery.PAGE_SIZE.key to pageSize,
+ MediaQuery.PROVIDERS.key to
+ ArrayList<String>().apply {
+ availableProviders.forEach { provider -> add(provider.authority) }
+ },
+ EXTRA_MIME_TYPES to config.mimeTypes,
+ EXTRA_INTENT_ACTION to config.action,
+ MediaSetContentsQuery.PARENT_MEDIA_SET_PICKER_ID.key to parentMediaSet.pickerId,
+ MediaSetContentsQuery.PARENT_MEDIA_SET_AUTHORITY.key to parentMediaSet.authority,
+ )
+ try {
+ return contentResolver
+ .query(MEDIA_SET_CONTENTS_URI, /* projection */ null, input, cancellationSignal)
+ .use { cursor ->
+ cursor?.let {
+ LoadResult.Page(
+ data = cursor.getListOfMedia(),
+ prevKey = cursor.getPrevMediaPageKey(),
+ nextKey = cursor.getNextMediaPageKey(),
+ )
+ }
+ ?: throw IllegalStateException(
+ "Received a null response from Content Provider"
+ )
+ }
+ } catch (e: RuntimeException) {
+ throw RuntimeException(
+ "Could not fetch media set contents for parent media set ${parentMediaSet.id}",
+ e,
+ )
+ }
+ }
+
+ /**
* Send a refresh media request to MediaProvider. This is a signal for MediaProvider to refresh
* its cache, if required.
*/
@@ -617,6 +827,67 @@ open class MediaProviderClient {
}
/**
+ * Send a refresh media sets request to MediaProvider. This is a signal for MediaProvider to
+ * refresh its cache for the given parent category id and authority, if required.
+ */
+ suspend fun refreshMediaSets(
+ contentResolver: ContentResolver,
+ category: Group.Category,
+ config: PhotopickerConfiguration,
+ ) {
+ val extras =
+ bundleOf(
+ EXTRA_MIME_TYPES to config.mimeTypes,
+ MediaSetsQuery.PARENT_CATEGORY_ID.key to category.id,
+ MediaSetsQuery.PARENT_CATEGORY_AUTHORITY.key to category.authority,
+ )
+
+ try {
+ contentResolver.call(
+ MEDIA_PROVIDER_AUTHORITY,
+ MEDIA_SETS_INIT_CALL_METHOD,
+ /* arg */ null,
+ extras,
+ )
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Could not send refresh media sets call to Media Provider $extras", e)
+ }
+ }
+
+ /**
+ * Send a refresh media set contents request to MediaProvider. This is a signal for
+ * MediaProvider to refresh its cache for the given parent media set id and authority, if
+ * required.
+ */
+ suspend fun refreshMediaSetContents(
+ contentResolver: ContentResolver,
+ mediaSet: Group.MediaSet,
+ config: PhotopickerConfiguration,
+ ) {
+ val extras =
+ bundleOf(
+ EXTRA_MIME_TYPES to config.mimeTypes,
+ MediaSetContentsQuery.PARENT_MEDIA_SET_PICKER_ID.key to mediaSet.id,
+ MediaSetContentsQuery.PARENT_MEDIA_SET_AUTHORITY.key to mediaSet.authority,
+ )
+
+ try {
+ contentResolver.call(
+ MEDIA_PROVIDER_AUTHORITY,
+ MEDIA_SET_CONTENTS_INIT_CALL_METHOD,
+ /* arg */ null,
+ extras,
+ )
+ } catch (e: RuntimeException) {
+ Log.e(
+ TAG,
+ "Could not send refresh media set contents call to Media Provider $extras",
+ e,
+ )
+ }
+ }
+
+ /**
* Creates a search request with the data source.
*
* The data source is expected to return a search request id associated with the request.
@@ -678,6 +949,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>()
@@ -835,10 +1143,10 @@ open class MediaProviderClient {
}
/**
- * Extracts the previous page key from the given [Cursor]. In case the cursor contains the
+ * Extracts the previous media page key from the given [Cursor]. In case the cursor contains the
* contents of the first page, the previous page key will be null.
*/
- private fun Cursor.getPrevPageKey(): MediaPageKey? {
+ private fun Cursor.getPrevMediaPageKey(): MediaPageKey? {
val id: Long = extras.getLong(MediaResponseExtras.PREV_PAGE_ID.key, Long.MIN_VALUE)
val date: Long =
extras.getLong(MediaResponseExtras.PREV_PAGE_DATE_TAKEN.key, Long.MIN_VALUE)
@@ -850,10 +1158,10 @@ open class MediaProviderClient {
}
/**
- * Extracts the next page key from the given [Cursor]. In case the cursor contains the contents
- * of the last page, the next page key will be null.
+ * Extracts the next media page key from the given [Cursor]. In case the cursor contains the
+ * contents of the last page, the next page key will be null.
*/
- private fun Cursor.getNextPageKey(): MediaPageKey? {
+ private fun Cursor.getNextMediaPageKey(): MediaPageKey? {
val id: Long = extras.getLong(MediaResponseExtras.NEXT_PAGE_ID.key, Long.MIN_VALUE)
val date: Long =
extras.getLong(MediaResponseExtras.NEXT_PAGE_DATE_TAKEN.key, Long.MIN_VALUE)
@@ -865,6 +1173,32 @@ open class MediaProviderClient {
}
/**
+ * Extracts the previous group page key from the given [Cursor]. In case the cursor contains the
+ * contents of the first page, the previous page key will be null.
+ */
+ private fun Cursor.getPrevGroupPageKey(): GroupPageKey? {
+ val id: Long = extras.getLong(MediaResponseExtras.PREV_PAGE_ID.key, Long.MIN_VALUE)
+ return if (id == Long.MIN_VALUE) {
+ null
+ } else {
+ GroupPageKey(pickerId = id)
+ }
+ }
+
+ /**
+ * Extracts the next group page key from the given [Cursor]. In case the cursor contains the
+ * contents of the last page, the next page key will be null.
+ */
+ private fun Cursor.getNextGroupPageKey(): GroupPageKey? {
+ val id: Long = extras.getLong(MediaResponseExtras.NEXT_PAGE_ID.key, Long.MAX_VALUE)
+ return if (id == Long.MAX_VALUE) {
+ null
+ } else {
+ GroupPageKey(pickerId = id)
+ }
+ }
+
+ /**
* Extracts the before items count from the given [Cursor]. In case the cursor does not contain
* this value, return null.
*/
@@ -882,6 +1216,8 @@ open class MediaProviderClient {
if (this.moveToFirst()) {
do {
val albumId = getString(getColumnIndexOrThrow(AlbumResponse.ALBUM_ID.key))
+ val coverUriString =
+ getString(getColumnIndexOrThrow(AlbumResponse.UNWRAPPED_COVER_URI.key))
result.add(
Group.Album(
id = albumId,
@@ -892,12 +1228,7 @@ open class MediaProviderClient {
getLong(getColumnIndexOrThrow(AlbumResponse.DATE_TAKEN.key)),
displayName =
getString(getColumnIndexOrThrow(AlbumResponse.ALBUM_NAME.key)),
- coverUri =
- Uri.parse(
- getString(
- getColumnIndexOrThrow(AlbumResponse.UNWRAPPED_COVER_URI.key)
- )
- ),
+ coverUri = coverUriString?.let { Uri.parse(it) } ?: Uri.parse(""),
coverMediaSource =
MediaSource.valueOf(
getString(
@@ -959,6 +1290,183 @@ open class MediaProviderClient {
return result
}
+ /** Creates a list of [Group.Category]-s and [Group.Album]-s from the given [Cursor]. */
+ private fun Cursor.getListOfCategoriesAndAlbums(
+ availableProviders: List<Provider>
+ ): List<Group> {
+ val result: MutableList<Group> = mutableListOf<Group>()
+ val authorityToSourceMap: Map<String, MediaSource> =
+ availableProviders.associate { provider -> provider.authority to provider.mediaSource }
+
+ if (this.moveToFirst()) {
+ do {
+ try {
+ val groupType = getString(getColumnIndexOrThrow(GroupResponse.MEDIA_GROUP.key))
+ when (groupType) {
+ GroupType.CATEGORY.name -> {
+ val icons: List<Icon> =
+ listOf<Icon?>(
+ this.getIcon(
+ authorityToSourceMap,
+ GroupResponse.UNWRAPPED_COVER_URI.key,
+ ),
+ this.getIcon(
+ authorityToSourceMap,
+ GroupResponse.ADDITIONAL_UNWRAPPED_COVER_URI_1.key,
+ ),
+ this.getIcon(
+ authorityToSourceMap,
+ GroupResponse.ADDITIONAL_UNWRAPPED_COVER_URI_2.key,
+ ),
+ this.getIcon(
+ authorityToSourceMap,
+ GroupResponse.ADDITIONAL_UNWRAPPED_COVER_URI_3.key,
+ ),
+ )
+ .filterNotNull()
+
+ result.add(
+ Group.Category(
+ id =
+ getString(
+ getColumnIndexOrThrow(GroupResponse.GROUP_ID.key)
+ ),
+ pickerId =
+ getLong(getColumnIndexOrThrow(GroupResponse.PICKER_ID.key)),
+ authority =
+ getString(
+ getColumnIndexOrThrow(GroupResponse.AUTHORITY.key)
+ ),
+ displayName =
+ getString(
+ getColumnIndexOrThrow(GroupResponse.DISPLAY_NAME.key)
+ ),
+ categoryType =
+ KeyToCategoryType[
+ getString(
+ getColumnIndexOrThrow(
+ GroupResponse.CATEGORY_TYPE.key
+ )
+ )]
+ ?: throw IllegalArgumentException(
+ "Could not recognize category type"
+ ),
+ icons = icons,
+ isLeafCategory =
+ getInt(
+ getColumnIndexOrThrow(
+ GroupResponse.IS_LEAF_CATEGORY.key
+ )
+ ) == 1,
+ )
+ )
+ }
+
+ GroupType.ALBUM.name -> {
+ val coverUriString =
+ getString(
+ getColumnIndexOrThrow(GroupResponse.UNWRAPPED_COVER_URI.key)
+ )
+ val coverUri = coverUriString?.let { Uri.parse(it) } ?: Uri.parse("")
+
+ result.add(
+ Group.Album(
+ id =
+ getString(
+ getColumnIndexOrThrow(GroupResponse.GROUP_ID.key)
+ ),
+ pickerId =
+ getLong(getColumnIndexOrThrow(GroupResponse.PICKER_ID.key)),
+ authority =
+ getString(
+ getColumnIndexOrThrow(GroupResponse.AUTHORITY.key)
+ ),
+ dateTakenMillisLong =
+ Long.MAX_VALUE, // This is not used and will soon be
+ // obsolete
+ displayName =
+ getString(
+ getColumnIndexOrThrow(GroupResponse.DISPLAY_NAME.key)
+ ),
+ coverUri = coverUri,
+ coverMediaSource =
+ coverUri?.let {
+ authorityToSourceMap[coverUri.getAuthority()]
+ } ?: MediaSource.LOCAL,
+ )
+ )
+ }
+
+ else -> {
+ Log.w(TAG, "Invalid group type: $groupType")
+ }
+ }
+ } catch (e: RuntimeException) {
+ Log.w(TAG, "Could not extract category or album from cursor, skipping it", e)
+ }
+ } while (moveToNext())
+ }
+
+ return result
+ }
+
+ /** Creates a list of [Group.MediaSet]-s from the given [Cursor]. */
+ private fun Cursor.getListOfMediaSets(
+ availableProviders: List<Provider>
+ ): List<Group.MediaSet> {
+ val result: MutableList<Group.MediaSet> = mutableListOf<Group.MediaSet>()
+ val authorityToSourceMap: Map<String, MediaSource> =
+ availableProviders.associate { provider -> provider.authority to provider.mediaSource }
+
+ if (this.moveToFirst()) {
+ do {
+ try {
+ result.add(
+ Group.MediaSet(
+ id = getString(getColumnIndexOrThrow(GroupResponse.GROUP_ID.key)),
+ pickerId = getLong(getColumnIndexOrThrow(GroupResponse.PICKER_ID.key)),
+ authority =
+ getString(getColumnIndexOrThrow(GroupResponse.AUTHORITY.key)),
+ displayName =
+ getString(getColumnIndexOrThrow(GroupResponse.DISPLAY_NAME.key)),
+ icon =
+ this.getIcon(
+ authorityToSourceMap,
+ GroupResponse.UNWRAPPED_COVER_URI.key,
+ ) ?: Icon(uri = Uri.parse(""), mediaSource = MediaSource.LOCAL),
+ )
+ )
+ } catch (e: RuntimeException) {
+ Log.w(TAG, "Could not extract media set from cursor, skipping it", e)
+ }
+ } while (moveToNext())
+ }
+
+ return result
+ }
+
+ /** Creates an [Icon] object from the current [Cursor] row. If an error occurs, returns null. */
+ private fun Cursor.getIcon(
+ authorityToSourceMap: Map<String, MediaSource>,
+ columnName: String,
+ ): Icon? {
+ var unwrappedUriString: String? = null
+
+ try {
+ unwrappedUriString = getString(getColumnIndexOrThrow(columnName))
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Could not get unwrapped uri $unwrappedUriString from cursor", e)
+ }
+
+ return unwrappedUriString?.let {
+ val unwrappedUri: Uri = Uri.parse(unwrappedUriString)
+ val authority: String? = unwrappedUri.getAuthority()
+ val mediaSource: MediaSource = authorityToSourceMap[authority] ?: MediaSource.LOCAL
+ val icon = Icon(unwrappedUri, mediaSource)
+ icon
+ }
+ }
+
/** Convert the input search suggestion type string to enum */
private fun getSearchSuggestionType(stringSuggestionType: String?): SearchSuggestionType {
requireNotNull(stringSuggestionType) { "Suggestion type is null" }
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/data/UriHelper.kt b/photopicker/src/com/android/photopicker/data/UriHelper.kt
index 9112d76f6..9dcf26e96 100644
--- a/photopicker/src/com/android/photopicker/data/UriHelper.kt
+++ b/photopicker/src/com/android/photopicker/data/UriHelper.kt
@@ -23,15 +23,18 @@ import android.provider.MediaStore
/** Provides URI constants and helper functions. */
internal const val MEDIA_PROVIDER_AUTHORITY = MediaStore.AUTHORITY
private const val UPDATE_PATH_SEGMENT = "update"
-private const val AVAILABLE_PROVIDERS_PATH_SEGMENT = "available_providers"
-private const val COLLECTION_INFO_SEGMENT = "collection_info"
-private const val MEDIA_PATH_SEGMENT = "media"
-private const val ALBUM_PATH_SEGMENT = "album"
-private const val MEDIA_GRANTS_COUNT_PATH_SEGMENT = "media_grants_count"
+const val AVAILABLE_PROVIDERS_PATH_SEGMENT = "available_providers"
+const val COLLECTION_INFO_SEGMENT = "collection_info"
+const val MEDIA_PATH_SEGMENT = "media"
+const val ALBUM_PATH_SEGMENT = "album"
+const val MEDIA_GRANTS_COUNT_PATH_SEGMENT = "media_grants_count"
private const val PREVIEW_PATH_SEGMENT = "preview"
-private const val PRE_SELECTION_URI_PATH_SEGMENT = "pre_selection"
-private const val SEARCH_MEDIA_PATH_SEGMENT = "search_media"
-private const val SEARCH_SUGGESTIONS_PATH_SEGMENT = "search_suggestions"
+const val PRE_SELECTION_URI_PATH_SEGMENT = "pre_selection"
+const val SEARCH_MEDIA_PATH_SEGMENT = "search_media"
+const val SEARCH_SUGGESTIONS_PATH_SEGMENT = "search_suggestions"
+const val CATEGORIES_PATH_SEGMENT = "categories"
+const val MEDIA_SETS_PATH_SEGMENT = "media_sets"
+const val MEDIA_SET_CONTENTS_PATH_SEGMENT = "media_set_contents"
const val PICKER_SEGMENT = "picker"
const val PICKER_TRANSCODED_SEGMENT = "picker_transcoded"
@@ -109,6 +112,16 @@ fun getAlbumMediaUri(albumId: String): Uri {
val SEARCH_SUGGESTIONS_URI: Uri =
pickerUri.buildUpon().apply { appendPath(SEARCH_SUGGESTIONS_PATH_SEGMENT) }.build()
+/** URI that receives [ContentProvider] change notifications for search result updates. */
+val SEARCH_RESULTS_UPDATE_URI: Uri =
+ pickerUri
+ .buildUpon()
+ .apply {
+ appendPath(SEARCH_MEDIA_PATH_SEGMENT)
+ appendPath(UPDATE_PATH_SEGMENT)
+ }
+ .build()
+
fun getSearchResultsMediaUri(searchRequestId: Int): Uri {
return pickerUri
.buildUpon()
@@ -118,3 +131,18 @@ fun getSearchResultsMediaUri(searchRequestId: Int): Uri {
}
.build()
}
+
+fun getCategoryUri(parentCategoryId: String?): Uri {
+ return pickerUri
+ .buildUpon()
+ .apply {
+ appendPath(CATEGORIES_PATH_SEGMENT)
+ parentCategoryId?.let { appendPath(parentCategoryId) }
+ }
+ .build()
+}
+
+val MEDIA_SETS_URI = pickerUri.buildUpon().appendPath(MEDIA_SETS_PATH_SEGMENT).build()
+
+val MEDIA_SET_CONTENTS_URI =
+ pickerUri.buildUpon().appendPath(MEDIA_SET_CONTENTS_PATH_SEGMENT).build()
diff --git a/photopicker/src/com/android/photopicker/features/categorygrid/data/CategoryDataService.kt b/photopicker/src/com/android/photopicker/features/categorygrid/data/CategoryDataService.kt
index b40c4e921..e22843c4d 100644
--- a/photopicker/src/com/android/photopicker/features/categorygrid/data/CategoryDataService.kt
+++ b/photopicker/src/com/android/photopicker/features/categorygrid/data/CategoryDataService.kt
@@ -37,30 +37,17 @@ interface CategoryDataService {
}
/**
- * Creates a paging source that can load root level categories and albums. Root level categories
- * are the ones that have no parent (categories).
+ * Creates a paging source that can load categories and albums.
*
- * @param cancellationSignal An optional [CancellationSignal] that can be marked as cancelled
- * when the query results are no longer required.
- * @return The [PagingSource] that fetches a page using [GroupPageKey]. A page in the paging
- * source contains a [List] of [Group.Category] or [Group.Album] items.
- */
- fun getCategories(
- cancellationSignal: CancellationSignal? = null
- ): PagingSource<GroupPageKey, Group>
-
- /**
- * Creates a paging source that can load child categories. Child categories are the ones that
- * have a parent categories.
- *
- * @param category the parent [Category].
+ * @param category the parent [Category]. If the parent category is null then the method returns
+ * root categories.
* @param cancellationSignal An optional [CancellationSignal] that can be marked as cancelled
* when the query results are no longer required.
* @return The [PagingSource] that fetches a page using [GroupPageKey]. A page in the paging
* source contains a [List] of [Group.Category] items.
*/
fun getCategories(
- parentCategory: Group.Category,
+ parentCategory: Group.Category? = null,
cancellationSignal: CancellationSignal? = null,
): PagingSource<GroupPageKey, Group>
diff --git a/photopicker/src/com/android/photopicker/features/categorygrid/data/CategoryDataServiceImpl.kt b/photopicker/src/com/android/photopicker/features/categorygrid/data/CategoryDataServiceImpl.kt
new file mode 100644
index 000000000..1acd39dd6
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/features/categorygrid/data/CategoryDataServiceImpl.kt
@@ -0,0 +1,334 @@
+/*
+ * 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.photopicker.features.categorygrid.data
+
+import android.content.ContentResolver
+import android.os.CancellationSignal
+import android.util.Log
+import androidx.paging.PagingSource
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.data.DataService
+import com.android.photopicker.data.MediaProviderClient
+import com.android.photopicker.data.NotificationService
+import com.android.photopicker.data.model.Group
+import com.android.photopicker.data.model.GroupPageKey
+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.categorygrid.paging.CategoryAndAlbumPagingSource
+import com.android.photopicker.features.categorygrid.paging.MediaSetContentsPagingSource
+import com.android.photopicker.features.categorygrid.paging.MediaSetsPagingSource
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+/**
+ * Provides category feature data to the Photo Picker UI. The data comes from a [ContentProvider]
+ * called [MediaProvider].
+ *
+ * Underlying data changes in [MediaProvider] are observed using [ContentObservers]. When a change
+ * in data is observed, the data is re-fetched from the [MediaProvider] process and the new data is
+ * emitted to the [StateFlows]-s.
+ *
+ * @param dataService Core Picker's data service that provides data related to core functionality.
+ * @param config A [StateFlow] that emits [PhotopickerConfiguration] changes.
+ * @param scope The [CoroutineScope] the data flows will be shared in.
+ * @param dispatcher A [CoroutineDispatcher] to run the coroutines in.
+ * @param notificationService An instance of [NotificationService] responsible to listen to data
+ * change notifications.
+ * @param mediaProviderClient An instance of [MediaProviderClient] responsible to get data from
+ * MediaProvider.
+ * @param events Event bus for the current session.
+ */
+class CategoryDataServiceImpl(
+ private val dataService: DataService,
+ private val config: StateFlow<PhotopickerConfiguration>,
+ private val scope: CoroutineScope,
+ private val dispatcher: CoroutineDispatcher,
+ private val notificationService: NotificationService,
+ private val mediaProviderClient: MediaProviderClient,
+ private val events: Events,
+) : CategoryDataService {
+ private val cachedPagingSourceMutex = Mutex()
+ private var rootCategoryAndAlbumPagingSource: PagingSource<GroupPageKey, Group>? = null
+ private val childCategoryPagingSources:
+ MutableMap<Group.Category, PagingSource<GroupPageKey, Group>> =
+ mutableMapOf()
+ private val mediaSetPagingSources:
+ MutableMap<Group.Category, PagingSource<GroupPageKey, Group.MediaSet>> =
+ mutableMapOf()
+ private val mediaSetContentPagingSources:
+ MutableMap<Group.MediaSet, PagingSource<MediaPageKey, Media>> =
+ mutableMapOf()
+
+ init {
+ // Listen to available provider changes and clear category cache when required.
+ scope.launch(dispatcher) {
+ dataService.availableProviders.collect { providers: List<Provider> ->
+ Log.d(
+ CategoryDataService.TAG,
+ "Available providers have changed to $providers. " +
+ "Clearing category results cache.",
+ )
+
+ cachedPagingSourceMutex.withLock {
+ rootCategoryAndAlbumPagingSource?.invalidate()
+ childCategoryPagingSources.values.forEach { pagingSource ->
+ pagingSource.invalidate()
+ }
+ childCategoryPagingSources.clear()
+
+ mediaSetPagingSources.values.forEach { pagingSource ->
+ pagingSource.invalidate()
+ }
+ mediaSetPagingSources.clear()
+
+ mediaSetContentPagingSources.values.forEach { pagingSource ->
+ pagingSource.invalidate()
+ }
+ mediaSetContentPagingSources.clear()
+ }
+ }
+ }
+ }
+
+ override fun getCategories(
+ parentCategory: Group.Category?,
+ cancellationSignal: CancellationSignal?,
+ ): PagingSource<GroupPageKey, Group> = runBlocking {
+ return@runBlocking cachedPagingSourceMutex.withLock {
+ return@withLock when {
+ parentCategory == null &&
+ rootCategoryAndAlbumPagingSource != null &&
+ !rootCategoryAndAlbumPagingSource!!.invalid -> {
+ Log.d(
+ CategoryDataService.TAG,
+ "A valid paging source is available for root categories and albums. " +
+ "Not creating a new paging source.",
+ )
+
+ val pagingSource = rootCategoryAndAlbumPagingSource!!
+ // Register the new cancellation signal to be cancelled in the callback.
+ pagingSource.registerInvalidatedCallback { cancellationSignal?.cancel() }
+ pagingSource
+ }
+
+ parentCategory != null &&
+ childCategoryPagingSources.containsKey(parentCategory) &&
+ !childCategoryPagingSources[parentCategory]!!.invalid -> {
+ Log.d(
+ CategoryDataService.TAG,
+ "A valid paging source is available for category ${parentCategory.categoryType}. " +
+ "Not creating a new paging source.",
+ )
+
+ val pagingSource = childCategoryPagingSources[parentCategory]!!
+ // Register the new cancellation signal to be cancelled in the callback.
+ pagingSource.registerInvalidatedCallback { cancellationSignal?.cancel() }
+ pagingSource
+ }
+
+ else -> {
+ val availableProviders: List<Provider> = dataService.availableProviders.value
+ val contentResolver: ContentResolver = dataService.activeContentResolver.value
+ val pagingSource =
+ CategoryAndAlbumPagingSource(
+ contentResolver = contentResolver,
+ availableProviders = availableProviders,
+ parentCategoryId = parentCategory?.id,
+ mediaProviderClient = mediaProviderClient,
+ dispatcher = dispatcher,
+ configuration = config.value,
+ events = events,
+ cancellationSignal = cancellationSignal,
+ )
+ // Ensure that cancellation get propagated to the data source when the paging
+ // source
+ // is invalidated.
+ pagingSource.registerInvalidatedCallback { cancellationSignal?.cancel() }
+
+ Log.v(
+ CategoryDataService.TAG,
+ "Created a category paging source that queries $availableProviders for " +
+ "parent category id $parentCategory",
+ )
+
+ // Update paging source cache.
+ when {
+ parentCategory == null -> rootCategoryAndAlbumPagingSource = pagingSource
+ else -> childCategoryPagingSources[parentCategory] = pagingSource
+ }
+
+ pagingSource
+ }
+ }
+ }
+ }
+
+ override fun getMediaSets(
+ category: Group.Category,
+ cancellationSignal: CancellationSignal?,
+ ): PagingSource<GroupPageKey, Group.MediaSet> = runBlocking {
+ return@runBlocking cachedPagingSourceMutex.withLock {
+ if (
+ mediaSetPagingSources.containsKey(category) &&
+ !mediaSetPagingSources[category]!!.invalid
+ ) {
+ Log.d(
+ CategoryDataService.TAG,
+ "A valid paging source is available for media sets ${category.categoryType}. " +
+ "Not creating a new paging source.",
+ )
+ val pagingSource = mediaSetPagingSources[category]!!
+ // Register the new cancellation signal to be cancelled in the callback.
+ pagingSource.registerInvalidatedCallback { cancellationSignal?.cancel() }
+ pagingSource
+ } else {
+ refreshMediaSets(category)
+
+ val availableProviders: List<Provider> = dataService.availableProviders.value
+ val contentResolver: ContentResolver = dataService.activeContentResolver.value
+ val pagingSource =
+ MediaSetsPagingSource(
+ contentResolver = contentResolver,
+ availableProviders = availableProviders,
+ parentCategory = category,
+ mediaProviderClient = mediaProviderClient,
+ dispatcher = dispatcher,
+ configuration = config.value,
+ events = events,
+ cancellationSignal = cancellationSignal,
+ )
+ // Ensure that cancellation get propagated to the data source when the paging source
+ // is invalidated.
+ pagingSource.registerInvalidatedCallback { cancellationSignal?.cancel() }
+
+ Log.v(
+ CategoryDataService.TAG,
+ "Created a media source paging source that queries $availableProviders for " +
+ "parent category id $category",
+ )
+
+ mediaSetPagingSources[category] = pagingSource
+ pagingSource
+ }
+ }
+ }
+
+ override fun getMediaSetContents(
+ mediaSet: Group.MediaSet,
+ cancellationSignal: CancellationSignal?,
+ ): PagingSource<MediaPageKey, Media> = runBlocking {
+ return@runBlocking cachedPagingSourceMutex.withLock {
+ if (
+ mediaSetContentPagingSources.containsKey(mediaSet) &&
+ !mediaSetContentPagingSources[mediaSet]!!.invalid
+ ) {
+ Log.d(
+ CategoryDataService.TAG,
+ "A valid paging source is available for media set content ${mediaSet.id}. " +
+ "Not creating a new paging source.",
+ )
+ val pagingSource = mediaSetContentPagingSources[mediaSet]!!
+ // Register the new cancellation signal to be cancelled in the callback.
+ pagingSource.registerInvalidatedCallback { cancellationSignal?.cancel() }
+ pagingSource
+ } else {
+ refreshMediaSetContents(mediaSet)
+
+ val availableProviders: List<Provider> = dataService.availableProviders.value
+ val contentResolver: ContentResolver = dataService.activeContentResolver.value
+ val pagingSource =
+ MediaSetContentsPagingSource(
+ contentResolver = contentResolver,
+ availableProviders = availableProviders,
+ parentMediaSet = mediaSet,
+ mediaProviderClient = mediaProviderClient,
+ dispatcher = dispatcher,
+ configuration = config.value,
+ events = events,
+ cancellationSignal = cancellationSignal,
+ )
+ // Ensure that cancellation get propagated to the data source when the paging source
+ // is invalidated.
+ pagingSource.registerInvalidatedCallback { cancellationSignal?.cancel() }
+
+ Log.v(
+ CategoryDataService.TAG,
+ "Created a media source paging source that queries $availableProviders for " +
+ "parent media set id ${mediaSet.id}",
+ )
+
+ mediaSetContentPagingSources[mediaSet] = pagingSource
+ pagingSource
+ }
+ }
+ }
+
+ private suspend fun refreshMediaSets(category: Group.Category) {
+ val providers = dataService.availableProviders.value
+ val contentResolver = dataService.activeContentResolver.value
+ val isCategoryProviderAvailable =
+ providers.any { provider -> provider.authority == category.authority }
+
+ if (isCategoryProviderAvailable) {
+ Log.d(
+ CategoryDataService.TAG,
+ "Sending media sets refresh request to the data source" +
+ " for parent category ${category.categoryType}",
+ )
+
+ mediaProviderClient.refreshMediaSets(contentResolver, category, config.value)
+ } else {
+ Log.e(
+ CategoryDataService.TAG,
+ "Available providers $providers " +
+ "does not contain category authority ${category.authority}. " +
+ "Skip sending refresh media sets request.",
+ )
+ }
+ }
+
+ private suspend fun refreshMediaSetContents(mediaSet: Group.MediaSet) {
+ val providers = dataService.availableProviders.value
+ val contentResolver = dataService.activeContentResolver.value
+ val isMediaSetProviderAvailable =
+ providers.any { provider -> provider.authority == mediaSet.authority }
+
+ if (isMediaSetProviderAvailable) {
+ Log.d(
+ CategoryDataService.TAG,
+ "Sending media set contents refresh request to the data source" +
+ " for parent media set ${mediaSet.id}",
+ )
+
+ mediaProviderClient.refreshMediaSetContents(contentResolver, mediaSet, config.value)
+ } else {
+ Log.e(
+ CategoryDataService.TAG,
+ "Available providers $providers " +
+ "does not contain media set authority ${mediaSet.authority}. " +
+ "Skip sending refresh media set contents request.",
+ )
+ }
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/features/categorygrid/data/FakeCategoryDataServiceImpl.kt b/photopicker/src/com/android/photopicker/features/categorygrid/data/FakeCategoryDataServiceImpl.kt
deleted file mode 100644
index 858239176..000000000
--- a/photopicker/src/com/android/photopicker/features/categorygrid/data/FakeCategoryDataServiceImpl.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.photopicker.features.categorygrid.data
-
-import android.os.CancellationSignal
-import androidx.paging.PagingSource
-import com.android.photopicker.data.DataService
-import com.android.photopicker.data.model.Group
-import com.android.photopicker.data.model.GroupPageKey
-import com.android.photopicker.data.model.Media
-import com.android.photopicker.data.model.MediaPageKey
-
-/**
- * Placeholder for the actual [CategoryDataService] implementation class. This class can be used to
- * unblock and test UI development till we have the actual implementation in ready.
- */
-class FakeCategoryDataServiceImpl(val coreDataService: DataService) : CategoryDataService {
- override fun getCategories(
- cancellationSignal: CancellationSignal?
- ): PagingSource<GroupPageKey, Group> {
- TODO("Not yet implemented")
- }
-
- override fun getCategories(
- parentCategory: Group.Category,
- cancellationSignal: CancellationSignal?,
- ): PagingSource<GroupPageKey, Group> {
- TODO("Not yet implemented")
- }
-
- override fun getMediaSets(
- category: Group.Category,
- cancellationSignal: CancellationSignal?,
- ): PagingSource<GroupPageKey, Group.MediaSet> {
- TODO("Not yet implemented")
- }
-
- // Returns paging source for the main media grid.
- override fun getMediaSetContents(
- mediaSet: Group.MediaSet,
- cancellationSignal: CancellationSignal?,
- ): PagingSource<MediaPageKey, Media> {
- return coreDataService.mediaPagingSource()
- }
-}
diff --git a/photopicker/src/com/android/photopicker/features/categorygrid/inject/CategoryActivityRetainedModule.kt b/photopicker/src/com/android/photopicker/features/categorygrid/inject/CategoryActivityRetainedModule.kt
index 2c9e26bf5..59e34f073 100644
--- a/photopicker/src/com/android/photopicker/features/categorygrid/inject/CategoryActivityRetainedModule.kt
+++ b/photopicker/src/com/android/photopicker/features/categorygrid/inject/CategoryActivityRetainedModule.kt
@@ -17,14 +17,21 @@
package com.android.photopicker.features.categorygrid.inject
import android.util.Log
+import com.android.photopicker.core.Background
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.events.Events
import com.android.photopicker.data.DataService
+import com.android.photopicker.data.MediaProviderClient
+import com.android.photopicker.data.NotificationService
import com.android.photopicker.features.categorygrid.data.CategoryDataService
-import com.android.photopicker.features.categorygrid.data.FakeCategoryDataServiceImpl
+import com.android.photopicker.features.categorygrid.data.CategoryDataServiceImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.hilt.android.scopes.ActivityRetainedScoped
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
/**
* Injection Module for category feature specific dependencies, that provides access to objects
@@ -51,7 +58,15 @@ class CategoryActivityRetainedModule {
/** Provider for an implementation of [CategoryDataService]. */
@Provides
@ActivityRetainedScoped
- fun provideCategoryDataService(dataService: DataService): CategoryDataService {
+ fun provideCategoryDataService(
+ dataService: DataService,
+ configurationManager: ConfigurationManager,
+ @Background scope: CoroutineScope,
+ @Background dispatcher: CoroutineDispatcher,
+ mediaProviderClient: MediaProviderClient,
+ notificationService: NotificationService,
+ events: Events,
+ ): CategoryDataService {
if (::categoryDataService.isInitialized) {
return categoryDataService
} else {
@@ -61,8 +76,16 @@ class CategoryActivityRetainedModule {
" Initializing CategoryDataService.",
)
- // TODO(b/361043596): Switch with actual implementation when ready.
- categoryDataService = FakeCategoryDataServiceImpl(dataService)
+ categoryDataService =
+ CategoryDataServiceImpl(
+ dataService,
+ configurationManager.configuration,
+ scope,
+ dispatcher,
+ notificationService,
+ mediaProviderClient,
+ events,
+ )
return categoryDataService
}
}
diff --git a/photopicker/src/com/android/photopicker/features/categorygrid/inject/CategoryEmbeddedServiceModule.kt b/photopicker/src/com/android/photopicker/features/categorygrid/inject/CategoryEmbeddedServiceModule.kt
index 13b552f8a..503badaec 100644
--- a/photopicker/src/com/android/photopicker/features/categorygrid/inject/CategoryEmbeddedServiceModule.kt
+++ b/photopicker/src/com/android/photopicker/features/categorygrid/inject/CategoryEmbeddedServiceModule.kt
@@ -17,14 +17,21 @@
package com.android.photopicker.features.categorygrid.inject
import android.util.Log
+import com.android.photopicker.core.Background
import com.android.photopicker.core.EmbeddedServiceComponent
import com.android.photopicker.core.SessionScoped
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.events.Events
import com.android.photopicker.data.DataService
+import com.android.photopicker.data.MediaProviderClient
+import com.android.photopicker.data.NotificationService
import com.android.photopicker.features.categorygrid.data.CategoryDataService
-import com.android.photopicker.features.categorygrid.data.FakeCategoryDataServiceImpl
+import com.android.photopicker.features.categorygrid.data.CategoryDataServiceImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
/**
* Injection Module for category feature specific dependencies, that provides access to objects
@@ -49,7 +56,15 @@ class CategoryEmbeddedServiceModule {
/** Provider for an implementation of [CategoryDataService]. */
@Provides
@SessionScoped
- fun provideCategoryDataService(dataService: DataService): CategoryDataService {
+ fun provideCategoryDataService(
+ dataService: DataService,
+ configurationManager: ConfigurationManager,
+ @Background scope: CoroutineScope,
+ @Background dispatcher: CoroutineDispatcher,
+ mediaProviderClient: MediaProviderClient,
+ notificationService: NotificationService,
+ events: Events,
+ ): CategoryDataService {
if (::categoryDataService.isInitialized) {
return categoryDataService
} else {
@@ -59,8 +74,16 @@ class CategoryEmbeddedServiceModule {
" Initializing CategoryDataService.",
)
- // TODO(b/361043596): Switch with actual implementation when ready.
- categoryDataService = FakeCategoryDataServiceImpl(dataService)
+ categoryDataService =
+ CategoryDataServiceImpl(
+ dataService,
+ configurationManager.configuration,
+ scope,
+ dispatcher,
+ notificationService,
+ mediaProviderClient,
+ events,
+ )
return categoryDataService
}
}
diff --git a/photopicker/src/com/android/photopicker/features/categorygrid/paging/CategoryAndAlbumPagingSource.kt b/photopicker/src/com/android/photopicker/features/categorygrid/paging/CategoryAndAlbumPagingSource.kt
new file mode 100644
index 000000000..6b196604a
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/features/categorygrid/paging/CategoryAndAlbumPagingSource.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.photopicker.features.categorygrid.paging
+
+import android.content.ContentResolver
+import android.os.CancellationSignal
+import android.util.Log
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.data.MediaProviderClient
+import com.android.photopicker.data.model.Group
+import com.android.photopicker.data.model.GroupPageKey
+import com.android.photopicker.data.model.Provider
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/**
+ * This [PagingSource] class is responsible to providing paginated category and album data from
+ * Picker Backend by serving requests from Paging library. It sources data from a [ContentProvider]
+ * called [MediaProvider].
+ */
+class CategoryAndAlbumPagingSource(
+ private val contentResolver: ContentResolver,
+ private val availableProviders: List<Provider>,
+ private val parentCategoryId: String?,
+ private val mediaProviderClient: MediaProviderClient,
+ private val dispatcher: CoroutineDispatcher,
+ private val configuration: PhotopickerConfiguration,
+ private val events: Events,
+ private val cancellationSignal: CancellationSignal?,
+) : PagingSource<GroupPageKey, Group>() {
+ companion object {
+ val TAG: String = "PickerCategoryPagingSource"
+ }
+
+ override suspend fun load(params: LoadParams<GroupPageKey>): LoadResult<GroupPageKey, Group> {
+ val pageKey = params.key ?: GroupPageKey()
+ val pageSize = params.loadSize
+ // Switch to the background thread from the main thread using [withContext].
+ val result =
+ withContext(dispatcher) {
+ try {
+ if (availableProviders.isEmpty()) {
+ throw IllegalArgumentException("No available providers found.")
+ }
+
+ mediaProviderClient.fetchCategoriesAndAlbums(
+ pageKey,
+ pageSize,
+ contentResolver,
+ availableProviders,
+ parentCategoryId,
+ configuration,
+ cancellationSignal,
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Could not fetch page from Media provider", e)
+ LoadResult.Error(e)
+ }
+ }
+
+ return result
+ }
+
+ override fun getRefreshKey(state: PagingState<GroupPageKey, Group>): GroupPageKey? = null
+}
diff --git a/photopicker/src/com/android/photopicker/features/categorygrid/paging/MediaSetContentsPagingSource.kt b/photopicker/src/com/android/photopicker/features/categorygrid/paging/MediaSetContentsPagingSource.kt
new file mode 100644
index 000000000..b7d7b5b3e
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/features/categorygrid/paging/MediaSetContentsPagingSource.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.photopicker.features.categorygrid.paging
+
+import android.content.ContentResolver
+import android.os.CancellationSignal
+import android.util.Log
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.data.MediaProviderClient
+import com.android.photopicker.data.model.Group
+import com.android.photopicker.data.model.Media
+import com.android.photopicker.data.model.MediaPageKey
+import com.android.photopicker.data.model.Provider
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/**
+ * This [PagingSource] class is responsible to providing paginated media items in media set Picker
+ * Backend by serving requests from Paging library. It sources data from a [ContentProvider] called
+ * [MediaProvider].
+ */
+class MediaSetContentsPagingSource(
+ val contentResolver: ContentResolver,
+ private val availableProviders: List<Provider>,
+ private val parentMediaSet: Group.MediaSet,
+ private val mediaProviderClient: MediaProviderClient,
+ private val dispatcher: CoroutineDispatcher,
+ private val configuration: PhotopickerConfiguration,
+ private val events: Events,
+ private val cancellationSignal: CancellationSignal?,
+) : PagingSource<MediaPageKey, Media>() {
+ companion object {
+ val TAG: String = "PickerMediaSetContentPagingSource"
+ }
+
+ override suspend fun load(params: LoadParams<MediaPageKey>): LoadResult<MediaPageKey, Media> {
+ val pageKey = params.key ?: MediaPageKey()
+ val pageSize = params.loadSize
+ // Switch to the background thread from the main thread using [withContext].
+ val result =
+ withContext(dispatcher) {
+ try {
+ if (availableProviders.isEmpty()) {
+ throw IllegalArgumentException("No available providers found.")
+ }
+
+ mediaProviderClient.fetchMediaSetContents(
+ pageKey,
+ pageSize,
+ contentResolver,
+ availableProviders,
+ parentMediaSet,
+ configuration,
+ cancellationSignal,
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Could not fetch page from Media provider", e)
+ LoadResult.Error(e)
+ }
+ }
+
+ return result
+ }
+
+ override fun getRefreshKey(state: PagingState<MediaPageKey, Media>): MediaPageKey? = null
+}
diff --git a/photopicker/src/com/android/photopicker/features/categorygrid/paging/MediaSetsPagingSource.kt b/photopicker/src/com/android/photopicker/features/categorygrid/paging/MediaSetsPagingSource.kt
new file mode 100644
index 000000000..5b3752705
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/features/categorygrid/paging/MediaSetsPagingSource.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.photopicker.features.categorygrid.paging
+
+import android.content.ContentResolver
+import android.os.CancellationSignal
+import android.util.Log
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.data.MediaProviderClient
+import com.android.photopicker.data.model.Group
+import com.android.photopicker.data.model.GroupPageKey
+import com.android.photopicker.data.model.Provider
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/**
+ * This [PagingSource] class is responsible to providing paginated media sets data from Picker
+ * Backend by serving requests from Paging library. It sources data from a [ContentProvider] called
+ * [MediaProvider].
+ */
+class MediaSetsPagingSource(
+ val contentResolver: ContentResolver,
+ private val availableProviders: List<Provider>,
+ private val parentCategory: Group.Category,
+ private val mediaProviderClient: MediaProviderClient,
+ private val dispatcher: CoroutineDispatcher,
+ private val configuration: PhotopickerConfiguration,
+ private val events: Events,
+ private val cancellationSignal: CancellationSignal?,
+) : PagingSource<GroupPageKey, Group.MediaSet>() {
+ companion object {
+ val TAG: String = "PickerMediaSetPagingSource"
+ }
+
+ override suspend fun load(
+ params: LoadParams<GroupPageKey>
+ ): LoadResult<GroupPageKey, Group.MediaSet> {
+ val pageKey = params.key ?: GroupPageKey()
+ val pageSize = params.loadSize
+ // Switch to the background thread from the main thread using [withContext].
+ val result =
+ withContext(dispatcher) {
+ try {
+ if (availableProviders.isEmpty()) {
+ throw IllegalArgumentException("No available providers found.")
+ }
+
+ mediaProviderClient.fetchMediaSets(
+ pageKey,
+ pageSize,
+ contentResolver,
+ availableProviders,
+ parentCategory,
+ configuration,
+ cancellationSignal,
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Could not fetch page from Media provider", e)
+ LoadResult.Error(e)
+ }
+ }
+
+ return result
+ }
+
+ override fun getRefreshKey(state: PagingState<GroupPageKey, Group.MediaSet>): GroupPageKey? =
+ null
+}
diff --git a/photopicker/src/com/android/photopicker/features/search/Search.kt b/photopicker/src/com/android/photopicker/features/search/Search.kt
index 400d13c83..ea6b95e64 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
@@ -118,7 +118,7 @@ private val MEASUREMENT_SEARCH_BAR_HEIGHT = 56.dp
private val MEASUREMENT_SEARCH_BAR_PADDING =
PaddingValues(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 16.dp)
-private val FETCH_SUGGESTION_DEBOUNCE_DELAY = 300L // in milliseconds
+private val FETCH_SUGGESTION_DEBOUNCE_DELAY = 50L // in milliseconds
private val SUGGESTION_TITLE_PADDING =
PaddingValues(start = 32.dp, end = 32.dp, top = 12.dp, bottom = 12.dp)
@@ -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 -> {
@@ -633,11 +633,13 @@ private fun ShowSuggestions(
onSuggestionClick,
)
}
- item {
- Text(
- text = stringResource(R.string.photopicker_search_suggestions_text),
- modifier = Modifier.padding(SUGGESTION_TITLE_PADDING),
- )
+ if (faceSuggestions.isNotEmpty() || otherSuggestions.isNotEmpty()) {
+ item {
+ Text(
+ text = stringResource(R.string.photopicker_search_suggestions_text),
+ modifier = Modifier.padding(SUGGESTION_TITLE_PADDING),
+ )
+ }
}
if (faceSuggestions.size > 0) {
item {
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..9aacde80f 100644
--- a/photopicker/src/com/android/photopicker/features/search/data/SearchDataServiceImpl.kt
+++ b/photopicker/src/com/android/photopicker/features/search/data/SearchDataServiceImpl.kt
@@ -17,6 +17,8 @@
package com.android.photopicker.features.search.data
import android.content.ContentResolver
+import android.database.ContentObserver
+import android.net.Uri
import android.os.CancellationSignal
import android.util.Log
import androidx.paging.PagingSource
@@ -26,17 +28,23 @@ import com.android.photopicker.core.user.UserStatus
import com.android.photopicker.data.DataService
import com.android.photopicker.data.MediaProviderClient
import com.android.photopicker.data.NotificationService
+import com.android.photopicker.data.SEARCH_RESULTS_UPDATE_URI
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.Job
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
@@ -75,7 +83,7 @@ class SearchDataServiceImpl(
) : SearchDataService {
companion object {
// Timeout for receiving suggestions from the data source in milli seconds.
- private const val SUGGESTIONS_TIMEOUT: Long = 500
+ private const val SUGGESTIONS_TIMEOUT: Long = 1500
}
// An internal lock to allow thread-safe updates to the search request and results cache.
@@ -88,6 +96,14 @@ class SearchDataServiceImpl(
private val searchResultsPagingSources: MutableMap<Int, PagingSource<MediaPageKey, Media>> =
mutableMapOf()
+ // Callback flow that listens to changes in search results and emits the search request id when
+ // change is observed.
+ private var searchResultsUpdateCallbackFlow: Flow<Int>? = null
+
+ // Saves the current job that collects the [searchResultsUpdateCallbackFlow].
+ // Cancel this job when there is a change in the current profile's content resolver.
+ private var searchResultsUpdateCollectJob: Job? = null
+
init {
// Listen to available provider changes and clear search cache when required.
scope.launch(dispatcher) {
@@ -106,13 +122,42 @@ class SearchDataServiceImpl(
searchResultsPagingSources.clear()
searchRequestIdMap.clear()
}
+
+ _userSearchStateInfo.update { fetchSearchStateInfo() }
+ }
+ }
+
+ scope.launch(dispatcher) {
+ // Only observe the changes in the active content resolver
+ dataService.activeContentResolver.collect { activeContentResolver: ContentResolver ->
+ Log.d(SearchDataService.TAG, "Active content resolver has changed.")
+
+ // Stop collecting search results updates from previously initialized callback flow.
+ searchResultsUpdateCollectJob?.cancel()
+ searchResultsUpdateCallbackFlow = initSearchResultsUpdateFlow(activeContentResolver)
+
+ searchResultsUpdateCollectJob =
+ scope.launch(dispatcher) {
+ searchResultsUpdateCallbackFlow?.collect { searchRequestId: Int ->
+ Log.d(
+ SearchDataService.TAG,
+ "Search results update notification " +
+ "received for search request id $searchRequestId ",
+ )
+ searchResultsPagingSourceMutex.withLock {
+ searchResultsPagingSources[searchRequestId]?.invalidate()
+ }
+ }
+ }
}
}
}
- // 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
@@ -202,7 +247,7 @@ class SearchDataServiceImpl(
searchResultsPagingSourceMutex.withLock {
if (
searchResultsPagingSources.containsKey(searchRequestId) &&
- searchResultsPagingSources[searchRequestId]!!.invalid
+ !searchResultsPagingSources[searchRequestId]!!.invalid
) {
Log.d(
SearchDataService.TAG,
@@ -232,7 +277,8 @@ class SearchDataServiceImpl(
Log.d(
DataService.TAG,
- "Created a search results paging source that queries $availableProviders",
+ "Created a search results paging source that queries $availableProviders " +
+ "for search request id $searchRequestId",
)
searchResultsPagingSources[searchRequestId] = searchResultsPagingSource
@@ -287,4 +333,52 @@ 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
+ }
+
+ /**
+ * Creates a callback flow that emits search request id when an update in search results is
+ * observed using [ContentObserver] notifications.
+ */
+ private fun initSearchResultsUpdateFlow(resolver: ContentResolver): Flow<Int> = callbackFlow {
+ val observer =
+ object : ContentObserver(/* handler */ null) {
+ override fun onChange(selfChange: Boolean, uri: Uri?) {
+ // Verify that search request id is present in the URI
+ if (
+ uri?.pathSegments?.size == (1 + SEARCH_RESULTS_UPDATE_URI.pathSegments.size)
+ ) {
+ val searchRequestId: Int =
+ Integer.parseInt(uri.pathSegments[uri.pathSegments.size - 1] ?: "-1")
+ trySend(searchRequestId)
+ }
+ }
+ }
+
+ // Register the content observer callback.
+ notificationService.registerContentObserverCallback(
+ resolver,
+ SEARCH_RESULTS_UPDATE_URI,
+ /* notifyForDescendants */ true,
+ observer,
+ )
+
+ // Unregister when the flow is closed.
+ awaitClose { notificationService.unregisterContentObserverCallback(resolver, observer) }
+ }
}
diff --git a/photopicker/src/com/android/photopicker/features/search/data/SearchResultsPagingSource.kt b/photopicker/src/com/android/photopicker/features/search/data/SearchResultsPagingSource.kt
index 12938b520..88fce10fb 100644
--- a/photopicker/src/com/android/photopicker/features/search/data/SearchResultsPagingSource.kt
+++ b/photopicker/src/com/android/photopicker/features/search/data/SearchResultsPagingSource.kt
@@ -61,15 +61,25 @@ class SearchResultsPagingSource(
checkNotNull(searchRequestId) { "Search request id is invalid" }
- mediaProviderClient.fetchSearchResults(
- searchRequestId,
- pageKey,
- pageSize,
- contentResolver,
- availableProviders,
- configuration,
- cancellationSignal,
- )
+ val searchResults =
+ mediaProviderClient.fetchSearchResults(
+ searchRequestId,
+ pageKey,
+ pageSize,
+ contentResolver,
+ availableProviders,
+ configuration,
+ cancellationSignal,
+ )
+
+ if (searchResults is LoadResult.Page) {
+ Log.d(
+ TAG,
+ "Received ${searchResults.data.count()} search results from MP for $searchRequestId",
+ )
+ }
+
+ searchResults
} catch (e: Exception) {
Log.e(TAG, "Could not fetch search results page from Media provider", e)
LoadResult.Error(e)
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..5c96c8a80 100644
--- a/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt
@@ -23,8 +23,10 @@ import android.os.Bundle
import android.os.CancellationSignal
import android.test.mock.MockContentProvider
import androidx.core.os.bundleOf
+import com.android.photopicker.data.model.CategoryType
import com.android.photopicker.data.model.CollectionInfo
import com.android.photopicker.data.model.Group
+import com.android.photopicker.data.model.Icon
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaSource
import com.android.photopicker.data.model.Provider
@@ -103,6 +105,20 @@ val DEFAULT_SEARCH_SUGGESTIONS: List<SearchSuggestion> =
),
)
+val DEFAULT_CATEGORY: Group.Category =
+ createCategory(CategoryType.PEOPLE_AND_PETS, DEFAULT_PROVIDERS[0].authority)
+
+val DEFAULT_CATEGORIES_AND_ALBUMS: List<Group> =
+ listOf(
+ createAlbum("Favorites"),
+ createAlbum("Downloads"),
+ DEFAULT_CATEGORY,
+ createAlbum("CloudAlbum"),
+ )
+
+val DEFAULT_MEDIA_SETS: List<Group.MediaSet> =
+ listOf(createMediaSet("1"), createMediaSet("2"), createMediaSet("3"))
+
fun createMediaImage(pickerId: Long): Media {
return Media.Image(
mediaId = UUID.randomUUID().toString(),
@@ -122,11 +138,33 @@ fun createAlbum(albumId: String): Group.Album {
return Group.Album(
id = albumId,
pickerId = albumId.hashCode().toLong(),
- authority = "authority",
+ authority = DEFAULT_PROVIDERS[0].authority,
dateTakenMillisLong = Long.MAX_VALUE,
displayName = albumId,
- coverUri = Uri.parse("content://media/picker/authority/media/$albumId"),
- coverMediaSource = MediaSource.LOCAL,
+ coverUri = Uri.parse("content://test_authority/$albumId"),
+ coverMediaSource = DEFAULT_PROVIDERS[0].mediaSource,
+ )
+}
+
+fun createCategory(type: CategoryType, authority: String): Group.Category {
+ return Group.Category(
+ id = "test_id_" + type.name,
+ pickerId = 0,
+ authority = authority,
+ displayName = type.name,
+ categoryType = type,
+ icons = listOf(Icon(Uri.parse("content://test_authority/id"), MediaSource.LOCAL)),
+ isLeafCategory = true,
+ )
+}
+
+fun createMediaSet(mediaSetId: String): Group.MediaSet {
+ return Group.MediaSet(
+ id = mediaSetId,
+ pickerId = mediaSetId.hashCode().toLong(),
+ authority = DEFAULT_PROVIDERS[0].authority,
+ displayName = mediaSetId,
+ icon = Icon(Uri.parse("content://test_authority/$mediaSetId"), MediaSource.LOCAL),
)
}
@@ -138,6 +176,10 @@ 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,
+ var parentCategory: Group.Category = DEFAULT_CATEGORY,
+ var categoriesAndAlbums: List<Group> = DEFAULT_CATEGORIES_AND_ALBUMS,
+ var mediaSets: List<Group.MediaSet> = DEFAULT_MEDIA_SETS,
) : MockContentProvider() {
var lastRefreshMediaRequest: Bundle? = null
var TEST_GRANTS_COUNT = 2
@@ -149,19 +191,24 @@ class TestMediaProvider(
cancellationSignal: CancellationSignal?,
): Cursor? {
return when (uri.lastPathSegment) {
- "available_providers" -> getAvailableProviders()
- "collection_info" -> getCollectionInfo()
- "media" -> getMedia()
- "album" -> getAlbums()
- "media_grants_count" -> fetchMediaGrantsCount()
- "pre_selection" -> fetchFilteredMedia(queryArgs)
- "search_suggestions" -> getSearchSuggestions()
+ AVAILABLE_PROVIDERS_PATH_SEGMENT -> getAvailableProviders()
+ COLLECTION_INFO_SEGMENT -> getCollectionInfo()
+ MEDIA_PATH_SEGMENT -> getMedia()
+ ALBUM_PATH_SEGMENT -> getAlbums()
+ MEDIA_GRANTS_COUNT_PATH_SEGMENT -> fetchMediaGrantsCount()
+ PRE_SELECTION_URI_PATH_SEGMENT -> fetchFilteredMedia(queryArgs)
+ SEARCH_SUGGESTIONS_PATH_SEGMENT -> getSearchSuggestions()
+ CATEGORIES_PATH_SEGMENT -> getCategoriesAndAlbums()
+ MEDIA_SETS_PATH_SEGMENT -> getMediaSets()
+ MEDIA_SET_CONTENTS_PATH_SEGMENT -> getMedia()
else -> {
val pathSegments: MutableList<String> = uri.getPathSegments()
- if (pathSegments.size == 4 && pathSegments[2].equals("album")) {
+ if (pathSegments.size == 4 && pathSegments[2].equals(ALBUM_PATH_SEGMENT)) {
// Album media query
return getAlbumMedia(pathSegments[3])
- } else if (pathSegments.size == 4 && pathSegments[2].equals("search_media")) {
+ } else if (
+ pathSegments.size == 4 && pathSegments[2].equals(SEARCH_MEDIA_PATH_SEGMENT)
+ ) {
// Search results media query
return getMedia()
} else {
@@ -173,13 +220,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")
}
}
@@ -389,4 +445,86 @@ class TestMediaProvider(
}
return cursor
}
+
+ private fun getCategoriesAndAlbums(): Cursor {
+ val cursor =
+ MatrixCursor(
+ arrayOf(
+ MediaProviderClient.GroupResponse.MEDIA_GROUP.key,
+ MediaProviderClient.GroupResponse.GROUP_ID.key,
+ MediaProviderClient.GroupResponse.PICKER_ID.key,
+ MediaProviderClient.GroupResponse.DISPLAY_NAME.key,
+ MediaProviderClient.GroupResponse.AUTHORITY.key,
+ MediaProviderClient.GroupResponse.UNWRAPPED_COVER_URI.key,
+ MediaProviderClient.GroupResponse.ADDITIONAL_UNWRAPPED_COVER_URI_1.key,
+ MediaProviderClient.GroupResponse.ADDITIONAL_UNWRAPPED_COVER_URI_2.key,
+ MediaProviderClient.GroupResponse.ADDITIONAL_UNWRAPPED_COVER_URI_3.key,
+ MediaProviderClient.GroupResponse.CATEGORY_TYPE.key,
+ MediaProviderClient.GroupResponse.IS_LEAF_CATEGORY.key,
+ )
+ )
+ categoriesAndAlbums.forEach { group ->
+ when (group) {
+ is Group.Album ->
+ cursor.addRow(
+ arrayOf(
+ MediaProviderClient.GroupType.ALBUM.name,
+ group.id,
+ group.pickerId.toString(),
+ group.displayName,
+ group.authority,
+ group.coverUri.toString(),
+ /* additional uri */ null,
+ /* additional uri */ null,
+ /* additional uri */ null,
+ /* category type */ null,
+ /* is leaf category */ null,
+ )
+ )
+ is Group.Category ->
+ cursor.addRow(
+ arrayOf(
+ MediaProviderClient.GroupType.CATEGORY.name,
+ group.id,
+ group.pickerId.toString(),
+ group.displayName,
+ group.authority,
+ group.icons.getOrNull(0)?.getLoadableUri()?.toString(),
+ group.icons.getOrNull(1)?.getLoadableUri()?.toString(),
+ group.icons.getOrNull(2)?.getLoadableUri()?.toString(),
+ group.icons.getOrNull(3)?.getLoadableUri()?.toString(),
+ group.categoryType.key,
+ if (group.isLeafCategory) 1 else null,
+ )
+ )
+ else -> {}
+ }
+ }
+ return cursor
+ }
+
+ private fun getMediaSets(): Cursor {
+ val cursor =
+ MatrixCursor(
+ arrayOf(
+ MediaProviderClient.GroupResponse.GROUP_ID.key,
+ MediaProviderClient.GroupResponse.PICKER_ID.key,
+ MediaProviderClient.GroupResponse.DISPLAY_NAME.key,
+ MediaProviderClient.GroupResponse.AUTHORITY.key,
+ MediaProviderClient.GroupResponse.UNWRAPPED_COVER_URI.key,
+ )
+ )
+ mediaSets.forEach {
+ cursor.addRow(
+ arrayOf(
+ it.id,
+ it.pickerId.toString(),
+ it.displayName,
+ it.authority,
+ it.icon.getLoadableUri().toString(),
+ )
+ )
+ }
+ return cursor
+ }
}
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..3f26d98c2 100644
--- a/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt
@@ -27,11 +27,13 @@ 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
import com.android.photopicker.data.TestMediaProvider
import com.android.photopicker.data.model.Group
+import com.android.photopicker.data.model.GroupPageKey
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaPageKey
import com.android.photopicker.data.model.MediaSource
@@ -487,4 +489,135 @@ 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())
+ }
+
+ @Test
+ fun testFetchCategories() = runTest {
+ val mediaProviderClient = MediaProviderClient()
+
+ val categoriesLoadResult: LoadResult<GroupPageKey, Group> =
+ mediaProviderClient.fetchCategoriesAndAlbums(
+ pageKey = GroupPageKey(),
+ pageSize = 5,
+ contentResolver = testContentResolver,
+ availableProviders = testContentProvider.providers,
+ parentCategoryId = null,
+ config =
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ sessionId = sessionId,
+ ),
+ CancellationSignal(),
+ )
+
+ assertThat(categoriesLoadResult is LoadResult.Page).isTrue()
+
+ val categoriesAndAlbums: List<Group> = (categoriesLoadResult as LoadResult.Page).data
+
+ val expectedCategoriesAndAlbums = testContentProvider.categoriesAndAlbums
+ assertThat(categoriesAndAlbums.count()).isEqualTo(expectedCategoriesAndAlbums.count())
+ for (index in expectedCategoriesAndAlbums.indices) {
+ assertThat(categoriesAndAlbums[index]).isEqualTo(expectedCategoriesAndAlbums[index])
+ }
+ }
+
+ @Test
+ fun testFetchMediaSets() = runTest {
+ val mediaProviderClient = MediaProviderClient()
+
+ val mediaSetsLoadResult: LoadResult<GroupPageKey, Group.MediaSet> =
+ mediaProviderClient.fetchMediaSets(
+ pageKey = GroupPageKey(),
+ pageSize = 5,
+ contentResolver = testContentResolver,
+ availableProviders = testContentProvider.providers,
+ parentCategory = testContentProvider.parentCategory,
+ config =
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ sessionId = sessionId,
+ ),
+ cancellationSignal = CancellationSignal(),
+ )
+
+ assertThat(mediaSetsLoadResult is LoadResult.Page).isTrue()
+
+ val mediaSets: List<Group.MediaSet> = (mediaSetsLoadResult as LoadResult.Page).data
+
+ val expectedMediaSets = testContentProvider.mediaSets
+ assertThat(mediaSets.count()).isEqualTo(expectedMediaSets.count())
+ for (index in expectedMediaSets.indices) {
+ assertThat(mediaSets[index]).isEqualTo(expectedMediaSets[index])
+ }
+ }
+
+ @Test
+ fun testFetchMediaSetContents() = runTest {
+ val mediaProviderClient = MediaProviderClient()
+
+ val mediaSetContentsLoadResult: LoadResult<MediaPageKey, Media> =
+ mediaProviderClient.fetchMediaSetContents(
+ pageKey = MediaPageKey(),
+ pageSize = 5,
+ contentResolver = testContentResolver,
+ availableProviders = testContentProvider.providers,
+ parentMediaSet = testContentProvider.mediaSets[0],
+ config =
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ sessionId = sessionId,
+ ),
+ cancellationSignal = CancellationSignal(),
+ )
+
+ assertThat(mediaSetContentsLoadResult is LoadResult.Page).isTrue()
+
+ val media: List<Media> = (mediaSetContentsLoadResult as LoadResult.Page).data
+
+ val expectedMedia = testContentProvider.media
+ assertThat(media.count()).isEqualTo(expectedMedia.count())
+ for (index in expectedMedia.indices) {
+ assertThat(media[index]).isEqualTo(expectedMedia[index])
+ }
+ }
}
diff --git a/photopicker/tests/src/com/android/photopicker/features/categorygrid/data/CategoryDataServiceImplTest.kt b/photopicker/tests/src/com/android/photopicker/features/categorygrid/data/CategoryDataServiceImplTest.kt
new file mode 100644
index 000000000..c5656e27e
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/features/categorygrid/data/CategoryDataServiceImplTest.kt
@@ -0,0 +1,354 @@
+/*
+ * 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.features.categorygrid.data
+
+import android.content.ContentResolver
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.CancellationSignal
+import android.os.Parcel
+import android.os.UserHandle
+import androidx.paging.PagingSource
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.photopicker.core.configuration.provideTestConfigurationFlow
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.user.UserProfile
+import com.android.photopicker.core.user.UserStatus
+import com.android.photopicker.data.DataService
+import com.android.photopicker.data.DataServiceImpl
+import com.android.photopicker.data.MediaProviderClient
+import com.android.photopicker.data.TestMediaProvider
+import com.android.photopicker.data.TestNotificationServiceImpl
+import com.android.photopicker.data.TestPrefetchDataService
+import com.android.photopicker.data.model.Group
+import com.android.photopicker.data.model.GroupPageKey
+import com.android.photopicker.data.model.Media
+import com.android.photopicker.data.model.MediaPageKey
+import com.android.photopicker.data.model.MediaSource
+import com.android.photopicker.data.model.Provider
+import com.android.photopicker.features.categorygrid.data.CategoryDataService
+import com.android.photopicker.features.categorygrid.data.CategoryDataServiceImpl
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class CategoryDataServiceImplTest {
+
+ companion object {
+ private fun createUserHandle(userId: Int = 0): UserHandle {
+ val parcel = Parcel.obtain()
+ parcel.writeInt(userId)
+ parcel.setDataPosition(0)
+ val userHandle = UserHandle(parcel)
+ parcel.recycle()
+ return userHandle
+ }
+
+ private val userProfilePrimary: UserProfile =
+ UserProfile(handle = createUserHandle(0), profileType = UserProfile.ProfileType.PRIMARY)
+ }
+
+ private lateinit var testFeatureManager: FeatureManager
+ private lateinit var testContentProvider: TestMediaProvider
+ private lateinit var testContentResolver: ContentResolver
+ private lateinit var notificationService: TestNotificationServiceImpl
+ private lateinit var mediaProviderClient: MediaProviderClient
+ private lateinit var mockContext: Context
+ private lateinit var mockPackageManager: PackageManager
+ private lateinit var events: Events
+ private lateinit var userStatusFlow: MutableStateFlow<UserStatus>
+
+ @Before
+ fun setup() {
+ val scope = TestScope()
+ testContentProvider = TestMediaProvider()
+ testContentResolver = ContentResolver.wrap(testContentProvider)
+ notificationService = TestNotificationServiceImpl()
+ mediaProviderClient = MediaProviderClient()
+ mockContext = mock(Context::class.java)
+ mockPackageManager = mock(PackageManager::class.java)
+ val userStatus =
+ UserStatus(
+ activeUserProfile = userProfilePrimary,
+ allProfiles = listOf(userProfilePrimary),
+ activeContentResolver = testContentResolver,
+ )
+ testFeatureManager =
+ FeatureManager(
+ configuration = provideTestConfigurationFlow(scope = scope.backgroundScope),
+ scope = scope,
+ prefetchDataService = TestPrefetchDataService(),
+ registeredFeatures = setOf(),
+ coreEventsConsumed = setOf(),
+ coreEventsProduced = setOf(),
+ )
+ userStatusFlow = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = scope.backgroundScope,
+ provideTestConfigurationFlow(scope.backgroundScope),
+ testFeatureManager,
+ )
+ }
+
+ @Test
+ fun testCategoryPagingSourceCacheReuse() = runTest {
+ val dataService = getDataService(this)
+ val categoryDataService = getCategoryDataService(this, dataService)
+
+ advanceTimeBy(100)
+
+ val cancellationSignal = CancellationSignal()
+ val firstCategoryAndAlbumPagingSource: PagingSource<GroupPageKey, Group> =
+ categoryDataService.getCategories()
+ assertThat(firstCategoryAndAlbumPagingSource.invalid).isFalse()
+
+ // Check that the older paging source was cached and is reused.
+ val secondCategoryAndAlbumPagingSource: PagingSource<GroupPageKey, Group> =
+ categoryDataService.getCategories(cancellationSignal = cancellationSignal)
+ assertThat(secondCategoryAndAlbumPagingSource).isEqualTo(firstCategoryAndAlbumPagingSource)
+ assertThat(cancellationSignal.isCanceled()).isFalse()
+
+ firstCategoryAndAlbumPagingSource.invalidate()
+ assertThat(cancellationSignal.isCanceled()).isTrue()
+ }
+
+ @Test
+ fun testCategoryPagingSourceInvalidation() = runTest {
+ val dataService = getDataService(this)
+ val categoryDataService = getCategoryDataService(this, dataService)
+
+ val emissions = mutableListOf<List<Provider>>()
+ this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
+ advanceTimeBy(100)
+
+ assertThat(emissions.count()).isEqualTo(1)
+
+ val cancellationSignal = CancellationSignal()
+ val firstCategoryAndAlbumPagingSource: PagingSource<GroupPageKey, Group> =
+ categoryDataService.getCategories(cancellationSignal = cancellationSignal)
+ assertThat(firstCategoryAndAlbumPagingSource.invalid).isFalse()
+ assertThat(cancellationSignal.isCanceled()).isFalse()
+
+ updateActiveContentResolver()
+ advanceTimeBy(1000)
+
+ // Since the active user has changed, this should trigger a re-fetch of the active
+ // providers.
+ assertThat(emissions.count()).isEqualTo(2)
+
+ // Check that the old PagingSource has been invalidated.
+ assertThat(firstCategoryAndAlbumPagingSource.invalid).isTrue()
+
+ // Check that the CancellationSignal has been marked as cancelled.
+ assertThat(cancellationSignal.isCanceled()).isTrue()
+
+ // Check that the new PagingSource instance is valid.
+ val secondCategoryAndAlbumPagingSource: PagingSource<GroupPageKey, Group> =
+ categoryDataService.getCategories()
+ assertThat(secondCategoryAndAlbumPagingSource.invalid).isFalse()
+ }
+
+ @Test
+ fun testMediaSetsPagingSourceCacheReuse() = runTest {
+ val dataService = getDataService(this)
+ val categoryDataService = getCategoryDataService(this, dataService)
+
+ advanceTimeBy(100)
+
+ val cancellationSignal = CancellationSignal()
+ val firstMediaSetsPagingSource: PagingSource<GroupPageKey, Group.MediaSet> =
+ categoryDataService.getMediaSets(testContentProvider.parentCategory)
+ assertThat(firstMediaSetsPagingSource.invalid).isFalse()
+
+ // Check that the older paging source was cached and is reused.
+ val secondMediaSetsPagingSource: PagingSource<GroupPageKey, Group.MediaSet> =
+ categoryDataService.getMediaSets(testContentProvider.parentCategory, cancellationSignal)
+ assertThat(secondMediaSetsPagingSource).isEqualTo(firstMediaSetsPagingSource)
+ assertThat(cancellationSignal.isCanceled()).isFalse()
+
+ firstMediaSetsPagingSource.invalidate()
+ assertThat(cancellationSignal.isCanceled()).isTrue()
+ }
+
+ @Test
+ fun testMediaSetsPagingSourceInvalidation() = runTest {
+ val dataService = getDataService(this)
+ val categoryDataService = getCategoryDataService(this, dataService)
+
+ val emissions = mutableListOf<List<Provider>>()
+ this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
+ advanceTimeBy(100)
+
+ assertThat(emissions.count()).isEqualTo(1)
+
+ val cancellationSignal = CancellationSignal()
+ val firstMediaSetsPagingSource: PagingSource<GroupPageKey, Group.MediaSet> =
+ categoryDataService.getMediaSets(testContentProvider.parentCategory, cancellationSignal)
+ assertThat(firstMediaSetsPagingSource.invalid).isFalse()
+ assertThat(cancellationSignal.isCanceled()).isFalse()
+
+ updateActiveContentResolver()
+ advanceTimeBy(1000)
+
+ // Since the active user has changed, this should trigger a re-fetch of the active
+ // providers.
+ assertThat(emissions.count()).isEqualTo(2)
+
+ // Check that the old PagingSource has been invalidated.
+ assertThat(firstMediaSetsPagingSource.invalid).isTrue()
+
+ // Check that the CancellationSignal has been marked as cancelled.
+ assertThat(cancellationSignal.isCanceled()).isTrue()
+
+ // Check that the new PagingSource instance is valid.
+ val secondMediaSetsPagingSource: PagingSource<GroupPageKey, Group.MediaSet> =
+ categoryDataService.getMediaSets(testContentProvider.parentCategory)
+ assertThat(secondMediaSetsPagingSource.invalid).isFalse()
+ }
+
+ @Test
+ fun testMediaSetContentsPagingSourceCacheReuse() = runTest {
+ val dataService = getDataService(this)
+ val categoryDataService = getCategoryDataService(this, dataService)
+
+ advanceTimeBy(100)
+
+ val cancellationSignal = CancellationSignal()
+ val firstMediaSetContentsPagingSource: PagingSource<MediaPageKey, Media> =
+ categoryDataService.getMediaSetContents(testContentProvider.mediaSets[0])
+ assertThat(firstMediaSetContentsPagingSource.invalid).isFalse()
+
+ // Check that the older paging source was cached and is reused.
+ val secondMediaSetContentsPagingSource: PagingSource<MediaPageKey, Media> =
+ categoryDataService.getMediaSetContents(
+ testContentProvider.mediaSets[0].copy(),
+ cancellationSignal,
+ )
+ assertThat(secondMediaSetContentsPagingSource).isEqualTo(firstMediaSetContentsPagingSource)
+ assertThat(cancellationSignal.isCanceled()).isFalse()
+
+ firstMediaSetContentsPagingSource.invalidate()
+ assertThat(cancellationSignal.isCanceled()).isTrue()
+ }
+
+ @Test
+ fun testMediaSetContentsPagingSourceInvalidation() = runTest {
+ val dataService = getDataService(this)
+ val categoryDataService = getCategoryDataService(this, dataService)
+
+ val emissions = mutableListOf<List<Provider>>()
+ this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
+ advanceTimeBy(100)
+
+ assertThat(emissions.count()).isEqualTo(1)
+
+ val cancellationSignal = CancellationSignal()
+ val firstMediaSetContentsPagingSource: PagingSource<MediaPageKey, Media> =
+ categoryDataService.getMediaSetContents(
+ testContentProvider.mediaSets[0],
+ cancellationSignal,
+ )
+ assertThat(firstMediaSetContentsPagingSource.invalid).isFalse()
+ assertThat(cancellationSignal.isCanceled()).isFalse()
+
+ updateActiveContentResolver()
+ advanceTimeBy(1000)
+
+ // Since the active user has changed, this should trigger a re-fetch of the active
+ // providers.
+ assertThat(emissions.count()).isEqualTo(2)
+
+ // Check that the old PagingSource has been invalidated.
+ assertThat(firstMediaSetContentsPagingSource.invalid).isTrue()
+
+ // Check that the CancellationSignal has been marked as cancelled.
+ assertThat(cancellationSignal.isCanceled()).isTrue()
+
+ // Check that the new PagingSource instance is valid.
+ val secondMediaSetContentsPagingSource: PagingSource<MediaPageKey, Media> =
+ categoryDataService.getMediaSetContents(testContentProvider.mediaSets[0])
+ assertThat(secondMediaSetContentsPagingSource.invalid).isFalse()
+ }
+
+ private fun getDataService(scope: TestScope): DataService {
+ return DataServiceImpl(
+ userStatus = userStatusFlow,
+ scope = scope.backgroundScope,
+ notificationService = notificationService,
+ mediaProviderClient = mediaProviderClient,
+ dispatcher = StandardTestDispatcher(scope.testScheduler),
+ config = provideTestConfigurationFlow(scope.backgroundScope),
+ featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
+ )
+ }
+
+ private fun getCategoryDataService(
+ scope: TestScope,
+ dataService: DataService,
+ ): CategoryDataService {
+ return CategoryDataServiceImpl(
+ dataService = dataService,
+ config = provideTestConfigurationFlow(scope.backgroundScope),
+ scope = scope.backgroundScope,
+ notificationService = notificationService,
+ mediaProviderClient = mediaProviderClient,
+ dispatcher = StandardTestDispatcher(scope.testScheduler),
+ events = events,
+ )
+ }
+
+ private fun updateActiveContentResolver() {
+ val updatedContentProvider = TestMediaProvider()
+ val updatedContentResolver: ContentResolver = ContentResolver.wrap(updatedContentProvider)
+ updatedContentProvider.providers =
+ mutableListOf(
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ ),
+ Provider(
+ authority = "cloud_authority",
+ mediaSource = MediaSource.REMOTE,
+ uid = 0,
+ displayName = "",
+ ),
+ )
+ userStatusFlow.update { it.copy(activeContentResolver = updatedContentResolver) }
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/features/categorygrid/data/TestCategoryDataServiceImpl.kt b/photopicker/tests/src/com/android/photopicker/features/categorygrid/data/TestCategoryDataServiceImpl.kt
index e39b7b7a3..04f84382b 100644
--- a/photopicker/tests/src/com/android/photopicker/features/categorygrid/data/TestCategoryDataServiceImpl.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/categorygrid/data/TestCategoryDataServiceImpl.kt
@@ -26,13 +26,7 @@ import com.android.photopicker.features.categorygrid.data.CategoryDataService
class TestCategoryDataServiceImpl : CategoryDataService {
override fun getCategories(
- cancellationSignal: CancellationSignal?
- ): PagingSource<GroupPageKey, Group> {
- TODO("Not yet implemented")
- }
-
- override fun getCategories(
- parentCategory: Group.Category,
+ parentCategory: Group.Category?,
cancellationSignal: CancellationSignal?,
): PagingSource<GroupPageKey, Group> {
TODO("Not yet implemented")
diff --git a/photopicker/tests/src/com/android/photopicker/features/search/SearchDataServiceImplTest.kt b/photopicker/tests/src/com/android/photopicker/features/search/SearchDataServiceImplTest.kt
index 92da23322..11c08e9c4 100644
--- a/photopicker/tests/src/com/android/photopicker/features/search/SearchDataServiceImplTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/search/SearchDataServiceImplTest.kt
@@ -19,6 +19,7 @@ package com.android.photopicker.features.search
import android.content.ContentResolver
import android.content.Context
import android.content.pm.PackageManager
+import android.net.Uri
import android.os.CancellationSignal
import android.os.Parcel
import android.os.UserHandle
@@ -65,6 +66,9 @@ import org.mockito.Mockito.mock
class SearchDataServiceImplTest {
companion object {
+ private val searchMediaUpdateUri =
+ Uri.parse("content://media/picker_internal/v2/search_media/update")
+
private fun createUserHandle(userId: Int = 0): UserHandle {
val parcel = Parcel.obtain()
parcel.writeInt(userId)
@@ -202,4 +206,67 @@ class SearchDataServiceImplTest {
searchDataService.getSearchResults(searchText)
assertThat(secondSearchResultsPagingSource.invalid).isFalse()
}
+
+ @Test
+ fun testOnUpdateSearchResultsNotification() = runTest {
+ val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
+
+ val dataService: DataService =
+ DataServiceImpl(
+ userStatus = userStatusFlow,
+ scope = this.backgroundScope,
+ notificationService = notificationService,
+ mediaProviderClient = mediaProviderClient,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ config = provideTestConfigurationFlow(this.backgroundScope),
+ featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
+ )
+
+ val searchDataService: SearchDataService =
+ SearchDataServiceImpl(
+ dataService = dataService,
+ userStatus = userStatusFlow,
+ photopickerConfiguration = provideTestConfigurationFlow(this.backgroundScope),
+ scope = this.backgroundScope,
+ notificationService = notificationService,
+ mediaProviderClient = mediaProviderClient,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ events = events,
+ )
+
+ advanceTimeBy(100)
+
+ val searchText: String = "search_query"
+ val cancellationSignal = CancellationSignal()
+ val firstSearchResultsPagingSource: PagingSource<MediaPageKey, Media> =
+ searchDataService.getSearchResults(searchText = searchText, cancellationSignal)
+ assertThat(firstSearchResultsPagingSource.invalid).isFalse()
+
+ val searchResultsUpdateUri: Uri =
+ searchMediaUpdateUri
+ .buildUpon()
+ .apply { appendPath(testContentProvider.searchRequestId.toString()) }
+ .build()
+ // Send an update notification
+ notificationService.dispatchChangeToObservers(searchResultsUpdateUri)
+ advanceTimeBy(100)
+
+ // Check that the first media paging source was marked as invalid
+ assertThat(firstSearchResultsPagingSource.invalid).isTrue()
+
+ // Check that the a new PagingSource instance was created which is still valid
+ val secondSearchResultsPagingSource: PagingSource<MediaPageKey, Media> =
+ searchDataService.getSearchResults(searchText = searchText, cancellationSignal)
+ assertThat(secondSearchResultsPagingSource).isNotEqualTo(firstSearchResultsPagingSource)
+ assertThat(secondSearchResultsPagingSource.invalid).isFalse()
+ }
}
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/FilesOwnershipUtils.java b/src/com/android/providers/media/FilesOwnershipUtils.java
index 04eee5c19..a0e2ee5fb 100644
--- a/src/com/android/providers/media/FilesOwnershipUtils.java
+++ b/src/com/android/providers/media/FilesOwnershipUtils.java
@@ -109,7 +109,7 @@ public class FilesOwnershipUtils {
int rowsAffected = db.update(FILES_TABLE_NAME, contentValues, whereClause,
whereArgs.toArray(String[]::new));
- Log.d(TAG, "Set owner package name to null for " + rowsAffected + " items for "
+ Log.i(TAG, "Set owner package name to null for " + rowsAffected + " items for "
+ "packages " + Arrays.toString(packages));
db.execSQL("DROP TABLE " + TEMP_TABLE_NAME);
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 69736a15a..bc3127c5b 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -3981,7 +3981,8 @@ public class MediaProvider extends ContentProvider {
mPickerSyncController.getCloudProvider(), mPickerDataLayer);
}
if (table == PICKER_INTERNAL_V2) {
- return PickerUriResolverV2.query(getContext().getApplicationContext(), uri, queryArgs);
+ return PickerUriResolverV2.query(
+ getContext().getApplicationContext(), uri, queryArgs, signal);
}
final DatabaseHelper helper = getDatabaseForUri(uri);
@@ -7154,6 +7155,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);
}
@@ -7233,6 +7237,7 @@ public class MediaProvider extends ContentProvider {
int userId;
List<Uri> uris = null;
String[] packageNames;
+ int packageUid;
if (checkPermissionShell(caller)) {
// If the caller is the shell, the accepted parameter is EXTRA_PACKAGE_NAME
// (as string).
@@ -7241,7 +7246,14 @@ public class MediaProvider extends ContentProvider {
"Missing required extras arguments: EXTRA_URI or"
+ " EXTRA_PACKAGE_NAME");
}
- packageNames = new String[]{extras.getString(Intent.EXTRA_PACKAGE_NAME)};
+ String packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME);
+ packageNames = new String[]{packageName};
+ try {
+ packageUid = mPackageManager.getPackageUid(packageName, 0);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "No packageUid found for packageName " + packageName, e);
+ throw new RuntimeException(e);
+ }
// Uris are not a requirement for revoke all call
if (!isCallForRevokeAll) {
uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI)));
@@ -7251,7 +7263,7 @@ public class MediaProvider extends ContentProvider {
userId = UserHandle.myUserId();
} else if (checkPermissionSelf(caller) || isCallerPhotoPicker()) {
final PackageManager pm = getContext().getPackageManager();
- final int packageUid = extras.getInt(Intent.EXTRA_UID);
+ packageUid = extras.getInt(Intent.EXTRA_UID);
packageNames = pm.getPackagesForUid(packageUid);
// Get the userId from packageUid as the initiator could be a cloned app, which
// accesses Media via MP of its parent user and Binder's callingUid reflects
@@ -7267,12 +7279,12 @@ public class MediaProvider extends ContentProvider {
getSecurityExceptionMessage("revoke media grants"));
}
- if (isCallForRevokeAll && !isOwnedPhotosEnabled(caller)) {
+ if (isCallForRevokeAll && !isOwnedPhotosEnabled(packageUid)) {
mMediaGrants.removeAllMediaGrantsForPackages(packageNames, "user de-selections",
userId);
} else if (uris != null) {
mMediaGrants.removeMediaGrantsForPackage(packageNames, uris, userId);
- if (isOwnedPhotosEnabled(caller)) {
+ if (isOwnedPhotosEnabled(packageUid)) {
mFilesOwnershipUtils.removeOwnerPackageNameForUris(packageNames, uris,
userId);
}
@@ -7704,6 +7716,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/backupandrestore/BackupExecutor.java b/src/com/android/providers/media/backupandrestore/BackupExecutor.java
index 55b731020..fee5b4590 100644
--- a/src/com/android/providers/media/backupandrestore/BackupExecutor.java
+++ b/src/com/android/providers/media/backupandrestore/BackupExecutor.java
@@ -22,6 +22,7 @@ import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils
import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.BACKUP_DIRECTORY_NAME;
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.flags.Flags.enableBackupAndRestore;
import static com.android.providers.media.util.Logging.TAG;
import android.annotation.SuppressLint;
@@ -82,7 +83,6 @@ public final class BackupExecutor {
public BackupExecutor(Context context, DatabaseHelper databaseHelper) {
mContext = context;
mExternalDatabaseHelper = databaseHelper;
- mLevelDBInstance = LevelDBManager.getInstance(getBackupFilePath());
}
/**
@@ -92,10 +92,13 @@ public final class BackupExecutor {
* 3. Updates the new backed up generation number
*/
public void doBackup(CancellationSignal signal) {
- if (!SdkLevel.isAtLeastS()) {
+ if (!SdkLevel.isAtLeastS() || !enableBackupAndRestore()) {
return;
}
+ if (mLevelDBInstance == null) {
+ mLevelDBInstance = LevelDBManager.getInstance(getBackupFilePath());
+ }
final long lastBackedUpGenerationNumberFromLevelDb = getLastBackedUpGenerationNumber();
final long currentDbGenerationNumber = mExternalDatabaseHelper.runWithoutTransaction(
DatabaseHelper::getGeneration);
@@ -269,7 +272,7 @@ public final class BackupExecutor {
* Removes entry for given file path from Backup.
*/
public void deleteBackupForPath(String path) {
- if (path != null) {
+ if (enableBackupAndRestore() && path != null && mLevelDBInstance != null) {
mLevelDBInstance.delete(path);
}
}
diff --git a/src/com/android/providers/media/photopicker/SearchState.java b/src/com/android/providers/media/photopicker/SearchState.java
index e6cbeabf3..9e41bb04c 100644
--- a/src/com/android/providers/media/photopicker/SearchState.java
+++ b/src/com/android/providers/media/photopicker/SearchState.java
@@ -23,6 +23,7 @@ import android.content.pm.PackageManager;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.ConfigStore;
@@ -45,9 +46,13 @@ public class SearchState {
*/
public boolean isCloudSearchEnabled(
@NonNull Context context,
- @NonNull String cloudAuthority) {
+ @Nullable String cloudAuthority) {
requireNonNull(context);
- requireNonNull(cloudAuthority);
+
+ if (cloudAuthority == null) {
+ Log.d(TAG, "Cloud authority received is null. Cloud search is disabled");
+ return false;
+ }
if (!isSearchFeatureEnabled(context)) {
Log.d(TAG, "Search feature is disabled on the device.");
@@ -111,7 +116,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/sync/PickerSearchProviderClient.java b/src/com/android/providers/media/photopicker/sync/PickerSearchProviderClient.java
index a51501634..ad73da140 100644
--- a/src/com/android/providers/media/photopicker/sync/PickerSearchProviderClient.java
+++ b/src/com/android/providers/media/photopicker/sync/PickerSearchProviderClient.java
@@ -84,9 +84,20 @@ public class PickerSearchProviderClient {
queryArgs.putString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN, resumePageToken);
queryArgs.putInt(CloudMediaProviderContract.EXTRA_SORT_ORDER, sortOrder);
- return mContext.getContentResolver().query(
+ Log.d(TAG, "Search results query sent to CMP: " + queryArgs);
+
+ final Cursor cursor = mContext.getContentResolver().query(
getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_SEARCH_MEDIA),
- null, queryArgs, cancellationSignal);
+ null, queryArgs, null);
+
+ if (cursor == null) {
+ Log.d(TAG, "Search results response from the CMP is null.");
+
+ } else {
+ Log.d(TAG, "Search results received from the CMP: " + cursor.getCount()
+ + " extras: " + cursor.getExtras());
+ }
+ return cursor;
}
/**
@@ -99,9 +110,21 @@ public class PickerSearchProviderClient {
final Bundle queryArgs = new Bundle();
queryArgs.putString(CloudMediaProviderContract.KEY_PREFIX_TEXT, requireNonNull(prefixText));
queryArgs.putInt(CloudMediaProviderContract.EXTRA_PAGE_SIZE, limit);
- return mContext.getContentResolver().query(
+
+ Log.d(TAG, "Search suggestions query sent to CMP: " + queryArgs);
+
+ final Cursor cursor = mContext.getContentResolver().query(
getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_SEARCH_SUGGESTION),
- null, queryArgs, cancellationSignal);
+ null, queryArgs, null);
+
+ if (cursor == null) {
+ Log.d(TAG, "Search suggestions response from the CMP is null.");
+
+ } else {
+ Log.d(TAG, "Search suggestions received from the CMP: " + cursor.getCount()
+ + " extras: " + cursor.getExtras());
+ }
+ return cursor;
}
/**
@@ -116,9 +139,21 @@ public class PickerSearchProviderClient {
queryArgs = new Bundle();
}
queryArgs.putString(CloudMediaProviderContract.KEY_PARENT_CATEGORY_ID, parentCategoryId);
- return mContext.getContentResolver().query(
+
+ Log.d(TAG, "Categories query sent to CMP: " + queryArgs);
+
+ final Cursor cursor = mContext.getContentResolver().query(
getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_MEDIA_CATEGORY),
- null, queryArgs, cancellationSignal);
+ null, queryArgs, null);
+
+ if (cursor == null) {
+ Log.d(TAG, "Categories response from the CMP is null.");
+
+ } else {
+ Log.d(TAG, "Categories received from the CMP: " + cursor.getCount()
+ + " extras: " + cursor.getExtras());
+ }
+ return cursor;
}
/**
@@ -134,9 +169,21 @@ public class PickerSearchProviderClient {
queryArgs.putString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN, nextPageToken);
queryArgs.putInt(CloudMediaProviderContract.EXTRA_PAGE_SIZE, pageSize);
queryArgs.putStringArray(Intent.EXTRA_MIME_TYPES, mimeTypes);
- return mContext.getContentResolver().query(
+
+ Log.d(TAG, "Media sets query sent to CMP: " + queryArgs);
+
+ final Cursor cursor = mContext.getContentResolver().query(
getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_MEDIA_SET),
- null, queryArgs, cancellationSignal);
+ null, queryArgs, null);
+
+ if (cursor == null) {
+ Log.d(TAG, "Media sets response from the CMP is null.");
+
+ } else {
+ Log.d(TAG, "Media sets received from the CMP: " + cursor.getCount()
+ + " extras: " + cursor.getExtras());
+ }
+ return cursor;
}
/**
@@ -158,9 +205,20 @@ public class PickerSearchProviderClient {
queryArgs.putInt(CloudMediaProviderContract.EXTRA_SORT_ORDER, sortOrder);
queryArgs.putStringArray(Intent.EXTRA_MIME_TYPES, mimeTypes);
- return mContext.getContentResolver().query(
+ Log.d(TAG, "Media set content query sent to CMP: " + queryArgs);
+
+ final Cursor cursor = mContext.getContentResolver().query(
getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_MEDIA_IN_MEDIA_SET),
- null, queryArgs, cancellationSignal);
+ null, queryArgs, null);
+
+ if (cursor == null) {
+ Log.d(TAG, "Media set contents response from the CMP is null.");
+
+ } else {
+ Log.d(TAG, "Media set contents received from the CMP: " + cursor.getCount()
+ + " extras: " + cursor.getExtras());
+ }
+ return cursor;
}
private Uri getCloudUriFromPath(String uriPath) {
@@ -185,6 +243,7 @@ public class PickerSearchProviderClient {
final CloudMediaProviderContract.Capabilities capabilities =
response.getParcelable(EXTRA_PROVIDER_CAPABILITIES);
requireNonNull(capabilities);
+ Log.d(TAG, "Capabilities received from CMP: " + capabilities);
return capabilities;
} catch (RuntimeException e) {
diff --git a/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java b/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java
index abc135d43..85f6fd181 100644
--- a/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java
+++ b/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java
@@ -46,6 +46,7 @@ import androidx.work.WorkerParameters;
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
+import com.android.providers.media.photopicker.v2.PickerNotificationSender;
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;
@@ -69,6 +70,7 @@ public class SearchResultsSyncWorker extends Worker {
public static final String SYNC_COMPLETE_RESUME_KEY = "SYNCED";
private final Context mContext;
private final CancellationSignal mCancellationSignal;
+ private boolean mMarkedSyncWorkAsComplete = false;
/**
* Creates an instance of the {@link Worker}.
@@ -190,7 +192,13 @@ public class SearchResultsSyncWorker extends Worker {
// Mark sync as completed after getting the first page to start returning
// search results to the UI.
- markSearchResultsSyncAsComplete(syncSource, getId());
+ if (mMarkedSyncWorkAsComplete) {
+ PickerNotificationSender
+ .notifySearchResultsChange(mContext, searchRequestId);
+ } else {
+ markSearchResultsSyncAsComplete(syncSource, getId());
+ mMarkedSyncWorkAsComplete = true;
+ }
}
}
} finally {
diff --git a/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java b/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java
index 819e94d8f..14b509cba 100644
--- a/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java
+++ b/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java
@@ -118,7 +118,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
- * This class handles Photo Picker content queries.\
+ * This class handles Photo Picker content queries.
*/
public class PickerDataLayerV2 {
private static final String TAG = "PickerDataLayerV2";
@@ -414,20 +414,23 @@ public class PickerDataLayerV2 {
// Add Pinned album and categories to the list of cursors in the order in which they
// should be displayed. Note that pinned albums can only be local and merged albums.
+ long index = 0;
for (Pair<MediaGroup, String> mediaGroup: PINNED_CATEGORIES_AND_ALBUMS_ORDER) {
final Cursor cursor;
-
switch (mediaGroup.first) {
case ALBUM:
final String albumId = mediaGroup.second;
if (MERGED_ALBUMS.contains(albumId)) {
- final Cursor albumsCursor = PickerMediaDatabaseUtil.getMergedAlbumsCursor(
+ final Cursor mergedAlbumCursor =
+ PickerMediaDatabaseUtil.getMergedAlbumsCursor(
albumId, appContext, queryArgs, database, effectiveLocalAuthority,
effectiveCloudAuthority);
- cursor = MediaGroupCursorUtils.getMediaGroupCursorForAlbums(albumsCursor);
+ cursor = MediaGroupCursorUtils.getMediaGroupCursorForAlbums(
+ mergedAlbumCursor, index);
} else if (LOCAL_ALBUMS.contains(albumId)) {
- final Cursor albumCursor = localAlbums.getOrDefault(albumId, null);
- cursor = MediaGroupCursorUtils.getMediaGroupCursorForAlbums(albumCursor);
+ final Cursor localAlbumCursor = localAlbums.getOrDefault(albumId, null);
+ cursor = MediaGroupCursorUtils.getMediaGroupCursorForAlbums(
+ localAlbumCursor, index);
} else {
Log.e(TAG, "Could not recognize pinned album id, skipping it : " + albumId);
cursor = null;
@@ -438,7 +441,7 @@ public class PickerDataLayerV2 {
switch (mediaGroup.second) {
case CloudMediaProviderContract.MEDIA_CATEGORY_TYPE_PEOPLE_AND_PETS:
cursor = MediaGroupCursorUtils.getMediaGroupCursorForCategories(
- categories, effectiveCloudAuthority);
+ categories, effectiveCloudAuthority, index);
break;
default:
Log.e(TAG, "Could not recognize pinned category type, skipping it : "
@@ -452,7 +455,10 @@ public class PickerDataLayerV2 {
cursor = null;
}
- allMediaGroupCursors.add(cursor);
+ if (cursor != null) {
+ index += cursor.getCount();
+ allMediaGroupCursors.add(cursor);
+ }
}
// Add cloud albums at the end.
@@ -462,7 +468,7 @@ public class PickerDataLayerV2 {
final Cursor cloudAlbumsCursor = getCloudAlbumsCursor(appContext, query,
effectiveLocalAuthority, effectiveCloudAuthority);
allMediaGroupCursors.add(
- MediaGroupCursorUtils.getMediaGroupCursorForAlbums(cloudAlbumsCursor));
+ MediaGroupCursorUtils.getMediaGroupCursorForAlbums(cloudAlbumsCursor, index));
} catch (RuntimeException ex) {
Log.w(TAG, "Cloud provider exception while fetching cloud albums cursor", ex);
}
@@ -678,7 +684,7 @@ public class PickerDataLayerV2 {
try {
cloudSearchSuggestions = cloudSuggestionsFuture.get(
- /* timeout */ 300, TimeUnit.MILLISECONDS);
+ /* timeout */ 1500, TimeUnit.MILLISECONDS);
cloudSuggestionsFuture.thenApplyAsync(
(suggestions) -> maybeCacheSearchSuggestions(query, suggestions),
executor);
@@ -1444,7 +1450,6 @@ public class PickerDataLayerV2 {
@NonNull Bundle extras, @NonNull Context appContext, @NonNull WorkManager workManager) {
requireNonNull(workManager);
-
MediaSetsSyncRequestParams mediaSetsSyncRequestParams =
new MediaSetsSyncRequestParams(extras);
final Set<String> providers = new HashSet<>(
@@ -1475,6 +1480,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
@@ -1634,7 +1669,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/PickerNotificationSender.java b/src/com/android/providers/media/photopicker/v2/PickerNotificationSender.java
index 3fe9770db..ecdc0e0cb 100644
--- a/src/com/android/providers/media/photopicker/v2/PickerNotificationSender.java
+++ b/src/com/android/providers/media/photopicker/v2/PickerNotificationSender.java
@@ -21,6 +21,7 @@ import static com.android.providers.media.photopicker.v2.PickerUriResolverV2.AVA
import static com.android.providers.media.photopicker.v2.PickerUriResolverV2.MEDIA_PATH_SEGMENT;
import static com.android.providers.media.photopicker.v2.PickerUriResolverV2.PICKER_INTERNAL_PATH_SEGMENT;
import static com.android.providers.media.photopicker.v2.PickerUriResolverV2.PICKER_V2_PATH_SEGMENT;
+import static com.android.providers.media.photopicker.v2.PickerUriResolverV2.SEARCH_RESULT_MEDIA_PATH_SEGMENT;
import static com.android.providers.media.photopicker.v2.PickerUriResolverV2.UPDATE_PATH_SEGMENT;
import static java.util.Objects.requireNonNull;
@@ -72,6 +73,15 @@ public class PickerNotificationSender {
.appendPath(UPDATE_PATH_SEGMENT)
.build();
+ private static final Uri SEARCH_RESULTS_UPDATE_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(MediaStore.AUTHORITY)
+ .appendPath(PICKER_INTERNAL_PATH_SEGMENT)
+ .appendPath(PICKER_V2_PATH_SEGMENT)
+ .appendPath(SEARCH_RESULT_MEDIA_PATH_SEGMENT)
+ .appendPath(UPDATE_PATH_SEGMENT)
+ .build();
+
/**
* Send media update notification to the registered {@link android.database.ContentObserver}-s.
* @param context The application context.
@@ -126,6 +136,22 @@ public class PickerNotificationSender {
}
}
+ /**
+ * Send search results update notification to the registered
+ * {@link android.database.ContentObserver}-s.
+ * @param context The application context.
+ * @param searchRequestId Search request ID corresponding for which the search results
+ * have updated.
+ */
+ public static void notifySearchResultsChange(
+ @NonNull Context context,
+ @NonNull int searchRequestId) {
+ Log.d(TAG, "Sending a notification for search results update " + searchRequestId);
+ context.getContentResolver().notifyChange(
+ getSearchResultsUpdateUri(searchRequestId),
+ /* observer= */ null);
+ }
+
private static Uri getAlbumMediaUpdateUri(
@NonNull String albumAuthority,
@NonNull String albumId) {
@@ -135,4 +161,11 @@ public class PickerNotificationSender {
.appendPath(requireNonNull(albumId))
.build();
}
+
+ private static Uri getSearchResultsUpdateUri(@NonNull int searchRequestId) {
+ return SEARCH_RESULTS_UPDATE_URI
+ .buildUpon()
+ .appendPath(Integer.toString(searchRequestId))
+ .build();
+ }
}
diff --git a/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2.java b/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2.java
index 120a8142f..cb2ec7135 100644
--- a/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2.java
+++ b/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2.java
@@ -23,6 +23,7 @@ import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
+import android.os.CancellationSignal;
import android.provider.MediaStore;
import androidx.annotation.IntDef;
@@ -41,12 +42,14 @@ public class PickerUriResolverV2 {
public static final String COLLECTION_INFO_PATH_SEGMENT = "collection_info";
public static final String MEDIA_PATH_SEGMENT = "media";
public static final String ALBUM_PATH_SEGMENT = "album";
+ public static final String SEARCH_RESULT_MEDIA_PATH_SEGMENT = "search_media";
public static final String UPDATE_PATH_SEGMENT = "update";
private static final String MEDIA_GRANTS_COUNT_PATH_SEGMENT = "media_grants_count";
private static final String PREVIEW_PATH_SEGMENT = "preview";
private static final String PRE_SELECTION_PATH_SEGMENT = "pre_selection";
- private static final String SEARCH_RESULT_MEDIA_PATH_SEGMENT = "search_media";
private static final String SEARCH_SUGGESTIONS_PATH_SEGMENT = "search_suggestions";
+ private static final String CATEGORIES_PATH_SEGMENT = "categories";
+ private static final String MEDIA_SETS_PATH_SEGMENT = "media_sets";
static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
@@ -60,6 +63,8 @@ public class PickerUriResolverV2 {
static final int PICKER_INTERNAL_PRE_SELECTION = 8;
static final int PICKER_INTERNAL_SEARCH_MEDIA = 9;
static final int PICKER_INTERNAL_SEARCH_SUGGESTIONS = 10;
+ static final int PICKER_INTERNAL_CATEGORIES = 11;
+ static final int PICKER_INTERNAL_MEDIA_SETS = 12;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
@@ -74,6 +79,8 @@ public class PickerUriResolverV2 {
PICKER_INTERNAL_PRE_SELECTION,
PICKER_INTERNAL_SEARCH_MEDIA,
PICKER_INTERNAL_SEARCH_SUGGESTIONS,
+ PICKER_INTERNAL_CATEGORIES,
+ PICKER_INTERNAL_MEDIA_SETS
})
private @interface PickerQuery {}
@@ -113,6 +120,12 @@ public class PickerUriResolverV2 {
sUriMatcher.addURI(MediaStore.AUTHORITY,
BASE_PICKER_PATH + SEARCH_SUGGESTIONS_PATH_SEGMENT,
PICKER_INTERNAL_SEARCH_SUGGESTIONS);
+ sUriMatcher.addURI(MediaStore.AUTHORITY,
+ BASE_PICKER_PATH + CATEGORIES_PATH_SEGMENT,
+ PICKER_INTERNAL_CATEGORIES);
+ sUriMatcher.addURI(MediaStore.AUTHORITY,
+ BASE_PICKER_PATH + MEDIA_SETS_PATH_SEGMENT,
+ PICKER_INTERNAL_MEDIA_SETS);
}
/**
@@ -123,7 +136,8 @@ public class PickerUriResolverV2 {
public static Cursor query(
@NonNull Context appContext,
@NonNull Uri uri,
- @Nullable Bundle queryArgs) {
+ @Nullable Bundle queryArgs,
+ @Nullable CancellationSignal cancellationSignal) {
@PickerQuery
final int query = sUriMatcher.match(uri);
@@ -160,8 +174,14 @@ public class PickerUriResolverV2 {
return PickerDataLayerV2.querySearchSuggestions(
appContext,
requireNonNull(queryArgs),
- null
- );
+ cancellationSignal);
+ case PICKER_INTERNAL_CATEGORIES:
+ return PickerDataLayerV2.queryCategoriesAndAlbums(
+ appContext,
+ requireNonNull(queryArgs),
+ cancellationSignal);
+ case PICKER_INTERNAL_MEDIA_SETS:
+ return PickerDataLayerV2.queryMediaSets(requireNonNull(queryArgs));
default:
throw new UnsupportedOperationException("Could not recognize content URI " + uri);
}
diff --git a/src/com/android/providers/media/photopicker/v2/model/MediaInMediaSetSyncRequestParams.java b/src/com/android/providers/media/photopicker/v2/model/MediaInMediaSetSyncRequestParams.java
index ee7f3304d..9e296ec63 100644
--- a/src/com/android/providers/media/photopicker/v2/model/MediaInMediaSetSyncRequestParams.java
+++ b/src/com/android/providers/media/photopicker/v2/model/MediaInMediaSetSyncRequestParams.java
@@ -23,22 +23,23 @@ import androidx.annotation.NonNull;
import java.util.Objects;
public class MediaInMediaSetSyncRequestParams {
+ public static final String KEY_PARENT_MEDIA_SET_AUTHORITY = "media_set_picker_authority";
+ public static final String KEY_PARENT_MEDIA_SET_PICKER_ID = "media_set_picker_id";
private final String mAuthority;
- private final String mMediaSetPickerId;
+ private final Long mMediaSetPickerId;
public MediaInMediaSetSyncRequestParams(@NonNull Bundle extras) {
Objects.requireNonNull(extras);
- mAuthority = extras.getString("authority");
- mMediaSetPickerId = extras.getString("media_set_picker_id");
+ mAuthority = extras.getString(KEY_PARENT_MEDIA_SET_AUTHORITY);
+ mMediaSetPickerId = extras.getLong(KEY_PARENT_MEDIA_SET_PICKER_ID);
Objects.requireNonNull(mAuthority);
- Objects.requireNonNull(mMediaSetPickerId);
}
public String getAuthority() {
return mAuthority;
}
- public String getMediaSetPickerId() {
+ public Long getMediaSetPickerId() {
return mMediaSetPickerId;
}
}
diff --git a/src/com/android/providers/media/photopicker/v2/model/MediaSetsSyncRequestParams.java b/src/com/android/providers/media/photopicker/v2/model/MediaSetsSyncRequestParams.java
index 821306bc3..029b034e1 100644
--- a/src/com/android/providers/media/photopicker/v2/model/MediaSetsSyncRequestParams.java
+++ b/src/com/android/providers/media/photopicker/v2/model/MediaSetsSyncRequestParams.java
@@ -27,6 +27,9 @@ import java.util.Objects;
provider.
*/
public class MediaSetsSyncRequestParams {
+ public static final String KEY_PARENT_CATEGORY_AUTHORITY = "parent_category_authority";
+ public static final String KEY_MIME_TYPES = "mime_types";
+ public static final String KEY_PARENT_CATEGORY_ID = "parent_category_id";
private final String mAuthority;
private final String mCategoryId;
@@ -34,9 +37,9 @@ public class MediaSetsSyncRequestParams {
public MediaSetsSyncRequestParams(@NonNull Bundle extras) {
Objects.requireNonNull(extras);
- mAuthority = extras.getString("authority");
- mMimeTypes = extras.getStringArray("mime_types");
- mCategoryId = extras.getString("category_id");
+ mAuthority = extras.getString(KEY_PARENT_CATEGORY_AUTHORITY);
+ mMimeTypes = extras.getStringArray(KEY_MIME_TYPES);
+ mCategoryId = extras.getString(KEY_PARENT_CATEGORY_ID);
}
public String getAuthority() {
diff --git a/src/com/android/providers/media/photopicker/v2/sqlite/MediaGroupCursorUtils.java b/src/com/android/providers/media/photopicker/v2/sqlite/MediaGroupCursorUtils.java
index aca3a2ad6..7d5c22447 100644
--- a/src/com/android/providers/media/photopicker/v2/sqlite/MediaGroupCursorUtils.java
+++ b/src/com/android/providers/media/photopicker/v2/sqlite/MediaGroupCursorUtils.java
@@ -142,10 +142,12 @@ public class MediaGroupCursorUtils {
/**
* @param cursor Input
* {@link com.android.providers.media.photopicker.v2.model.AlbumsCursorWrapper}
+ * @param index The index for the first album in the given albums cursor.
+ * The index value can be used to generate unique picker id for albums.
* @return Cursor with the columns {@link PickerSQLConstants.MediaGroupResponseColumns}.
*/
@Nullable
- public static Cursor getMediaGroupCursorForAlbums(@Nullable Cursor cursor) {
+ public static Cursor getMediaGroupCursorForAlbums(@Nullable Cursor cursor, long index) {
if (cursor == null) {
return null;
}
@@ -173,8 +175,9 @@ public class MediaGroupCursorUtils {
final String albumId = cursor.getString(cursor.getColumnIndexOrThrow(
PickerSQLConstants.AlbumResponse.ALBUM_ID.getColumnName()));
- final String pickerId = cursor.getString(cursor.getColumnIndexOrThrow(
- PickerSQLConstants.AlbumResponse.PICKER_ID.getColumnName()));
+ // Sets the picker id of the current album and increments the index for the
+ // next album.
+ final long pickerId = index++;
final String displayName = cursor.getString(cursor.getColumnIndexOrThrow(
PickerSQLConstants.AlbumResponse.ALBUM_NAME.getColumnName()));
@@ -209,12 +212,16 @@ public class MediaGroupCursorUtils {
/**
* @param cursor Input
* {@link CloudMediaProviderContract.MediaCategoryColumns} cursor.
+ * @param authority The authority of the category's CMP.
+ * @param index The index for the first category in the given categories cursor.
+ * The index value can be used to generate unique picker id for categories.
* @return Cursor with the columns {@link PickerSQLConstants.MediaGroupResponseColumns}.
*/
@Nullable
public static Cursor getMediaGroupCursorForCategories(
@Nullable Cursor cursor,
- @NonNull String authority) {
+ @NonNull String authority,
+ long index) {
if (cursor == null) {
return null;
}
@@ -296,7 +303,7 @@ public class MediaGroupCursorUtils {
response.addRow(new Object[]{
MediaGroup.CATEGORY.name(),
categoryId,
- /* pickerId */ null,
+ index,
displayName,
authority,
coverUri1,
diff --git a/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsCloudSubQuery.java b/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsCloudSubQuery.java
index d9c5a8e16..969394707 100644
--- a/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsCloudSubQuery.java
+++ b/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsCloudSubQuery.java
@@ -30,7 +30,7 @@ import java.util.Locale;
* Picker DB.
*/
public class MediaInMediaSetsCloudSubQuery extends MediaInMediaSetsSubQuery {
- public MediaInMediaSetsCloudSubQuery(Bundle queryArgs, String mediaSetPickerId) {
+ public MediaInMediaSetsCloudSubQuery(Bundle queryArgs, Long mediaSetPickerId) {
super(queryArgs, mediaSetPickerId);
}
diff --git a/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsLocalSubQuery.java b/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsLocalSubQuery.java
index d00e4306c..0e1df140f 100644
--- a/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsLocalSubQuery.java
+++ b/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsLocalSubQuery.java
@@ -31,7 +31,7 @@ import java.util.Locale;
* Picker DB.
*/
public class MediaInMediaSetsLocalSubQuery extends MediaInMediaSetsSubQuery {
- public MediaInMediaSetsLocalSubQuery(Bundle queryArgs, String mediaSetPickerId) {
+ public MediaInMediaSetsLocalSubQuery(Bundle queryArgs, Long mediaSetPickerId) {
super(queryArgs, mediaSetPickerId);
}
diff --git a/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsQuery.java b/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsQuery.java
index f0ba09f1a..09172721c 100644
--- a/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsQuery.java
+++ b/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsQuery.java
@@ -44,11 +44,8 @@ public class MediaInMediaSetsQuery {
final MediaInMediaSetsCloudSubQuery mCloudMediaSubquery;
- public MediaInMediaSetsQuery(Bundle queryArgs, @NonNull String mediaPickerSetId) {
+ public MediaInMediaSetsQuery(Bundle queryArgs, @NonNull Long mediaPickerSetId) {
Objects.requireNonNull(mediaPickerSetId);
- if (mediaPickerSetId.isEmpty()) {
- throw new RuntimeException("MediaSet pickerId to query media cannot be null");
- }
mIntentAction = queryArgs.getString("intent_action");
mProviders = new ArrayList<>(
Objects.requireNonNull(queryArgs.getStringArrayList("providers")));
diff --git a/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsSubQuery.java b/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsSubQuery.java
index 2e37e5255..ea536ee39 100644
--- a/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsSubQuery.java
+++ b/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsSubQuery.java
@@ -30,9 +30,9 @@ import java.util.Locale;
* and media table in Picker DB.
*/
public abstract class MediaInMediaSetsSubQuery extends MediaQuery {
- private final String mMediaSetPickerId;
+ private final Long mMediaSetPickerId;
- public MediaInMediaSetsSubQuery(Bundle queryArgs, String mediaSetPickerId) {
+ public MediaInMediaSetsSubQuery(Bundle queryArgs, Long mediaSetPickerId) {
super(queryArgs);
mMediaSetPickerId = mediaSetPickerId;
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..fc9945a29 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";
/**
@@ -351,13 +353,13 @@ public class PickerSQLConstants {
/** Source provider's authority. */
AUTHORITY("authority"),
/** Cover image Uri for the group. */
- UNWRAPPED_COVER_URI("cover_uri_1"),
+ UNWRAPPED_COVER_URI("unwrapped_cover_uri"),
/** Additional cover image Uri for the category. */
- ADDITIONAL_UNWRAPPED_COVER_URI_1("cover_uri_2"),
+ ADDITIONAL_UNWRAPPED_COVER_URI_1("additional_cover_uri_1"),
/** Additional cover image Uri for the category. */
- ADDITIONAL_UNWRAPPED_COVER_URI_2("cover_uri_3"),
+ ADDITIONAL_UNWRAPPED_COVER_URI_2("additional_cover_uri_2"),
/** Additional cover image Uri for the category. */
- ADDITIONAL_UNWRAPPED_COVER_URI_3("cover_uri_4"),
+ ADDITIONAL_UNWRAPPED_COVER_URI_3("additional_cover_uri_3"),
/** If the media group is category, this will be populated with the category type. */
CATEGORY_TYPE("category_type"),
/** True, if the media category is leaf category which contains media sets,
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 066fbd785..b33d21a96 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -475,17 +475,18 @@ public class ModernMediaScanner implements MediaScanner {
private int mDeleteCount;
/**
- * Tracks hidden directory and hidden subdirectories in a directory tree. A positive count
- * indicates that one or more of the current file's parents is a hidden directory.
- */
- private int mHiddenDirCount;
- /**
* Indicates if the nomedia directory tree is dirty. When a nomedia directory is dirty, we
* mark the top level nomedia as dirty. Hence if one of the sub directory in the nomedia
* directory is dirty, we consider the whole top level nomedia directory tree as dirty.
*/
private boolean mIsDirectoryTreeDirty;
+ /**
+ * Tracks hidden directory and hidden subdirectories in a directory tree.
+ */
+ private boolean mIsDirectoryTreeHidden = false;
+ private String mTopLevelHiddenDirectory;
+
Scan(File root, int reason) throws FileNotFoundException {
Trace.beginSection("Scanner.ctor");
@@ -551,16 +552,20 @@ public class ModernMediaScanner implements MediaScanner {
private void walkFileTree() {
mSignal.throwIfCanceled();
- final Pair<Boolean, Boolean> isDirScannableAndHidden =
- shouldScanPathAndIsPathHidden(mSingleFile ? mRoot.getParentFile() : mRoot);
+
+ File dirPath = mSingleFile ? mRoot.getParentFile() : mRoot;
+ final Pair<Boolean, Boolean> isDirScannableAndHidden = shouldScanPathAndIsPathHidden(
+ dirPath);
if (isDirScannableAndHidden.first) {
// This directory is scannable.
Trace.beginSection("Scanner.walkFileTree");
if (isDirScannableAndHidden.second) {
// This directory is hidden
- mHiddenDirCount++;
+ mIsDirectoryTreeHidden = true;
+ mTopLevelHiddenDirectory = dirPath.getAbsolutePath();
}
+
if (mSingleFile) {
acquireDirectoryLock(mRoot.getParentFile().toPath().toString());
}
@@ -842,8 +847,9 @@ public class ModernMediaScanner implements MediaScanner {
// overlap and confuse each other
acquireDirectoryLock(dir.toString());
- if (FileUtils.isDirectoryHidden(dir.toFile())) {
- mHiddenDirCount++;
+ if (!mIsDirectoryTreeHidden && FileUtils.isDirectoryHidden(dir.toFile())) {
+ mIsDirectoryTreeHidden = true;
+ mTopLevelHiddenDirectory = dir.toString();
}
// Scan this directory as a normal file so that "parent" database
@@ -1016,7 +1022,7 @@ public class ModernMediaScanner implements MediaScanner {
File file, String mimeType, int defaultMediaType) {
if (mimeType != null) {
return resolveMediaTypeFromFilePath(
- file, mimeType, /*isHidden*/ mHiddenDirCount > 0);
+ file, mimeType, /*isHidden*/ mIsDirectoryTreeHidden);
}
return defaultMediaType;
}
@@ -1075,8 +1081,18 @@ public class ModernMediaScanner implements MediaScanner {
// before releasing our lock below
applyPending();
- if (FileUtils.isDirectoryHidden(dir.toFile())) {
- mHiddenDirCount--;
+ boolean isDirHidden = FileUtils.isDirectoryHidden(dir.toFile());
+
+ if (isDirHidden && !mIsDirectoryTreeHidden) {
+ Log.w(TAG, "Hidden state of directory " + dir + " changed during active scan.");
+ }
+
+ if (mTopLevelHiddenDirectory != null && dir.toString().equals(
+ mTopLevelHiddenDirectory)) {
+ // Post visit the top level hidden directory being tracked. Reset hidden status
+ // for directory tree.
+ mIsDirectoryTreeHidden = false;
+ mTopLevelHiddenDirectory = null;
}
// Now that we're finished scanning this directory, release lock to
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/src/com/android/providers/media/MediaProviderForFuseTest.java b/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
index 216bcb9b1..ab3c4969f 100644
--- a/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
@@ -207,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);
@@ -256,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/photopicker/ItemsProviderTest.java b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
index f39da9c09..9e5317bd4 100644
--- a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
+++ b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
@@ -1423,7 +1423,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/sync/MediaInMediaSetsSyncWorkerTest.java b/tests/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorkerTest.java
index 509a3f6dd..6b256f555 100644
--- a/tests/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorkerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorkerTest.java
@@ -195,9 +195,11 @@ public class MediaInMediaSetsSyncWorkerTest {
assertEquals("Count of inserted media sets should be equal to the cursor size",
/*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
Bundle extras = new Bundle();
- extras.putString("authority", auth);
- extras.putString("category_id", categoryId);
- extras.putStringArray("mime_types", mimeTypes.toArray(new String[mimeTypes.size()]));
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, auth);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, categoryId);
+ extras.putStringArray(
+ MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ mimeTypes.toArray(new String[mimeTypes.size()]));
MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
Cursor fetchMediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
mDatabase, requestParams);
@@ -314,9 +316,10 @@ public class MediaInMediaSetsSyncWorkerTest {
assertEquals("Count of inserted media sets should be equal to the cursor size",
/*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
Bundle extras = new Bundle();
- extras.putString("authority", auth);
- extras.putString("category_id", categoryId);
- extras.putStringArray("mime_types", mimeTypes.toArray(new String[mimeTypes.size()]));
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, auth);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, categoryId);
+ extras.putStringArray(MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ mimeTypes.toArray(new String[mimeTypes.size()]));
MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
Cursor fetchMediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
mDatabase, requestParams);
@@ -432,9 +435,10 @@ public class MediaInMediaSetsSyncWorkerTest {
assertEquals("Count of inserted media sets should be equal to the cursor size",
/*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
Bundle extras = new Bundle();
- extras.putString("authority", auth);
- extras.putString("category_id", categoryId);
- extras.putStringArray("mime_types", mimeTypes.toArray(new String[mimeTypes.size()]));
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, auth);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, categoryId);
+ extras.putStringArray(MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ mimeTypes.toArray(new String[mimeTypes.size()]));
MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
Cursor fetchMediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
mDatabase, requestParams);
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 200242dfa..c9c972cb2 100644
--- a/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
@@ -68,6 +68,7 @@ import com.android.providers.media.photopicker.v2.model.MediaSetsSyncRequestPara
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
@@ -125,6 +126,7 @@ public class PickerSyncManagerTest {
assertThat(workRequest.getWorkSpec().expedited).isFalse();
}
+ @Ignore("b/387570966")
@Test
public void testSchedulePeriodicSyncs() {
setupPickerSyncManager(/* schedulePeriodicSyncs */ true);
@@ -162,6 +164,7 @@ public class PickerSyncManagerTest {
.isEqualTo(SYNC_LOCAL_AND_CLOUD);
}
+ @Ignore("b/387570966")
@Test
public void testPeriodicWorkIsScheduledOnDeviceConfigChanges() {
@@ -519,9 +522,10 @@ public class PickerSyncManagerTest {
String categoryId = "id";
String[] mimeTypes = new String[] { "image/*" };
Bundle extras = new Bundle();
- extras.putString("authority", SearchProvider.AUTHORITY);
- extras.putStringArray("mime_types", mimeTypes);
- extras.putString("category_id", categoryId);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY,
+ SearchProvider.AUTHORITY);
+ extras.putStringArray(MediaSetsSyncRequestParams.KEY_MIME_TYPES, mimeTypes);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, categoryId);
extras.putStringArrayList("providers", new ArrayList<>(List.of(
PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)));
@@ -563,9 +567,10 @@ public class PickerSyncManagerTest {
String categoryId = "id";
String[] mimeTypes = new String[] { "image/*" };
Bundle extras = new Bundle();
- extras.putString("authority", SearchProvider.AUTHORITY);
- extras.putStringArray("mime_types", mimeTypes);
- extras.putString("category_id", categoryId);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY,
+ SearchProvider.AUTHORITY);
+ extras.putStringArray(MediaSetsSyncRequestParams.KEY_MIME_TYPES, mimeTypes);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, categoryId);
extras.putStringArrayList("providers", new ArrayList<>(List.of(
SearchProvider.AUTHORITY)));
@@ -604,10 +609,12 @@ public class PickerSyncManagerTest {
public void testMediaInMediaSetSyncLocalProvider() {
setupPickerSyncManager(/*schedulePeriodicSyncs*/ false);
- String mediaSetPickerId = "id";
+ Long mediaSetPickerId = 1L;
Bundle extras = new Bundle();
- extras.putString("authority", SearchProvider.AUTHORITY);
- extras.putString("media_set_picker_id", mediaSetPickerId);
+ 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)));
@@ -633,7 +640,7 @@ public class PickerSyncManagerTest {
.getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
.isEqualTo(SYNC_LOCAL_ONLY);
assertThat(workRequest.getWorkSpec().input
- .getString(SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID))
+ .getLong(SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID, -1))
.isEqualTo(mediaSetPickerId);
assertThat(workRequest.getWorkSpec().input
.getString(SYNC_WORKER_INPUT_AUTHORITY))
@@ -644,10 +651,14 @@ public class PickerSyncManagerTest {
public void testMediaInMediaSetSyncCloudProvider() {
setupPickerSyncManager(/*schedulePeriodicSyncs*/ false);
- String mediaSetPickerId = "id";
+ Long mediaSetPickerId = 1L;
Bundle extras = new Bundle();
- extras.putString("authority", SearchProvider.AUTHORITY);
- extras.putString("media_set_picker_id", mediaSetPickerId);
+ 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)));
@@ -673,7 +684,7 @@ public class PickerSyncManagerTest {
.getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
.isEqualTo(SYNC_CLOUD_ONLY);
assertThat(workRequest.getWorkSpec().input
- .getString(SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID))
+ .getLong(SYNC_WORKER_INPUT_MEDIA_SET_PICKER_ID, -1))
.isEqualTo(mediaSetPickerId);
assertThat(workRequest.getWorkSpec().input
.getString(SYNC_WORKER_INPUT_AUTHORITY))
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 5799844b7..b08998029 100644
--- a/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java
+++ b/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java
@@ -117,6 +117,8 @@ 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;
@@ -838,9 +840,13 @@ public class PickerDataLayerV2Test {
SearchProvider.AUTHORITY, mimeTypes);
Bundle extras = new Bundle();
- extras.putString("authority", SearchProvider.AUTHORITY);
- extras.putStringArray("mime_types", new String[] { "image/*" });
- extras.putString("category_id", categoryId);
+ 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, categoryId);
extras.putStringArrayList("providers", new ArrayList<>(List.of(SearchProvider.AUTHORITY)));
try (Cursor mediaSets = PickerDataLayerV2.queryMediaSets(extras)) {
@@ -1030,7 +1036,7 @@ public class PickerDataLayerV2Test {
final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
- String mediaSetPickerId = "mediaSetPickerId";
+ Long mediaSetPickerId = 1L;
int cloudRowsInserted = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
mFacade.getDatabase(), List.of(
@@ -1057,8 +1063,12 @@ public class PickerDataLayerV2Test {
extras.putStringArrayList("providers",
new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
- extras.putString("media_set_picker_id", mediaSetPickerId);
- extras.putString("authority", LOCAL_PROVIDER);
+ 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)) {
@@ -2691,9 +2701,13 @@ public class PickerDataLayerV2Test {
doReturn(mMockFuture).when(mMockOperation).getResult();
Bundle extras = new Bundle();
- extras.putString("authority", SearchProvider.AUTHORITY);
- extras.putStringArray("mime_types", new String[] { "image/*" });
- extras.putString("category_id", "id");
+ 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);
@@ -2714,8 +2728,10 @@ public class PickerDataLayerV2Test {
doReturn(mMockFuture).when(mMockOperation).getResult();
Bundle extras = new Bundle();
- extras.putString("authority", SearchProvider.AUTHORITY);
- extras.putString("media_set_picker_id", "id");
+ 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);
@@ -2760,6 +2776,11 @@ public class PickerDataLayerV2Test {
.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")
@@ -2768,20 +2789,29 @@ public class PickerDataLayerV2Test {
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 groupd is people and pets category
+ // 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")
@@ -2790,21 +2820,24 @@ public class PickerDataLayerV2Test {
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 groupd is a cloud album
+ // 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
@@ -2812,6 +2845,11 @@ public class PickerDataLayerV2Test {
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);
}
}
@@ -2974,7 +3012,7 @@ public class PickerDataLayerV2Test {
}
private ContentValues getContentValues(
- String localId, String cloudId, String mediaSetPickerId) {
+ String localId, String cloudId, Long mediaSetPickerId) {
ContentValues contentValues = new ContentValues();
contentValues.put(
PickerSQLConstants.MediaInMediaSetsTableColumns.CLOUD_ID.getColumnName(), cloudId);
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
index b4eb2af3d..64db8598c 100644
--- a/tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsDatabaseUtilTest.java
+++ b/tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsDatabaseUtilTest.java
@@ -108,7 +108,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
- String mediaSetPickerId = "mediaSetPickerId";
+ Long mediaSetPickerId = 1L;
int cloudRowsInserted = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
mDatabase, List.of(
@@ -166,7 +166,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
- String mediaSetPickerId = "mediaSetPickerId";
+ Long mediaSetPickerId = 1L;
final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
mDatabase, List.of(
@@ -220,8 +220,8 @@ public class MediaInMediaSetsDatabaseUtilTest {
final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
- String mediaSetPickerId1 = "ms1";
- String mediaSetPickerId2 = "ms2";
+ Long mediaSetPickerId1 = 1L;
+ Long mediaSetPickerId2 = 2L;
final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
mDatabase, List.of(
@@ -273,7 +273,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
final Cursor cursor4 = getLocalMediaCursor(LOCAL_ID_4, dateTaken);
assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor4, 1);
- String mediaSetPickerId = "mediaSetPickerId";
+ Long mediaSetPickerId = 1L;
final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
mDatabase, List.of(
@@ -343,7 +343,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_3, LOCAL_ID_3, 0);
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
- String mediaSetPickerId = "mediaSetPickerId";
+ Long mediaSetPickerId = 1L;
final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
mDatabase, List.of(
@@ -402,7 +402,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
JPEG_IMAGE_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor4, 1);
- String mediaSetPickerId = "mediaSetPickerId";
+ Long mediaSetPickerId = 1L;
final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
mDatabase, List.of(
@@ -463,7 +463,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
final Cursor cursor4 = getLocalMediaCursor(LOCAL_ID_4, 0);
assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor4, 1);
- String mediaSetPickerId = "mediaSetPickerId";
+ Long mediaSetPickerId = 1L;
final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
mDatabase, List.of(
@@ -517,7 +517,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
final Cursor cursor4 = getLocalMediaCursor(LOCAL_ID_4, 0);
assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor4, 1);
- String mediaSetPickerId = "mediaSetPickerId";
+ Long mediaSetPickerId = 1L;
final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
mDatabase, List.of(
@@ -582,7 +582,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
final Cursor cursor3 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
- String mediaSetPickerId = "mediaSetPickerId";
+ Long mediaSetPickerId = 1L;
int cloudRowsInserted = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
mDatabase, List.of(
@@ -608,7 +608,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
}
private ContentValues getContentValues(
- String localId, String cloudId, String mediaSetPickerId) {
+ String localId, String cloudId, Long mediaSetPickerId) {
ContentValues contentValues = new ContentValues();
contentValues.put(
PickerSQLConstants.MediaInMediaSetsTableColumns.CLOUD_ID.getColumnName(), cloudId);
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
index c9e2a8170..d74fc9591 100644
--- a/tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaSetsDatabaseUtilsTest.java
+++ b/tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaSetsDatabaseUtilsTest.java
@@ -141,9 +141,11 @@ public class MediaSetsDatabaseUtilsTest {
.that(insertResult)
.isAtLeast(/* expected min row id */ 0);
Bundle extras = new Bundle();
- extras.putString("authority", mAuthority);
- extras.putString("category_id", mCategoryId);
- extras.putStringArray("mime_types", mimeTypes.toArray(new String[mimeTypes.size()]));
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, mAuthority);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, mCategoryId);
+ extras.putStringArray(
+ MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ mimeTypes.toArray(new String[mimeTypes.size()]));
MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
Cursor mediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
@@ -172,9 +174,11 @@ public class MediaSetsDatabaseUtilsTest {
assertEquals("Count of inserted media sets should be equal to the cursor size",
/*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
Bundle extras = new Bundle();
- extras.putString("authority", mAuthority);
- extras.putString("category_id", mCategoryId);
- extras.putStringArray("mime_types", mimeTypes.toArray(new String[mimeTypes.size()]));
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, mAuthority);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, mCategoryId);
+ extras.putStringArray(
+ MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ mimeTypes.toArray(new String[mimeTypes.size()]));
MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
Cursor fetchMediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
mDatabase, requestParams);
@@ -208,9 +212,11 @@ public class MediaSetsDatabaseUtilsTest {
assertEquals("Count of inserted media sets should be equal to the cursor size",
/*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
Bundle extras = new Bundle();
- extras.putString("authority", mAuthority);
- extras.putString("category_id", mCategoryId);
- extras.putStringArray("mime_types", mimeTypes.toArray(new String[mimeTypes.size()]));
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, mAuthority);
+ extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, mCategoryId);
+ extras.putStringArray(
+ MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ mimeTypes.toArray(new String[mimeTypes.size()]));
MediaSetsSyncRequestParams requestParams = new MediaSetsSyncRequestParams(extras);
Cursor fetchMediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
mDatabase, requestParams);