summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp1
-rw-r--r--TEST_MAPPING5
-rw-r--r--jni/RedactionInfo.cpp2
-rw-r--r--pdf/framework-v/java/android/graphics/pdf/PdfRenderer.java5
-rw-r--r--pdf/framework/api/current.txt61
-rw-r--r--pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java5
-rw-r--r--pdf/framework/java/android/graphics/pdf/component/FreeTextAnnotation.java25
-rw-r--r--pdf/framework/java/android/graphics/pdf/component/HighlightAnnotation.java32
-rw-r--r--pdf/framework/java/android/graphics/pdf/component/PdfAnnotation.java31
-rw-r--r--pdf/framework/java/android/graphics/pdf/component/PdfPageObjectRenderMode.java72
-rw-r--r--pdf/framework/java/android/graphics/pdf/component/PdfPagePathObject.java38
-rw-r--r--pdf/framework/java/android/graphics/pdf/component/PdfPageTextObject.java85
-rw-r--r--pdf/framework/java/android/graphics/pdf/component/PdfPageTextObjectFont.java95
-rw-r--r--pdf/framework/java/android/graphics/pdf/component/PdfPageTextObjectFontFamily.java60
-rw-r--r--pdf/framework/java/android/graphics/pdf/component/StampAnnotation.java28
-rw-r--r--pdf/framework/libs/pdfClient/annotation.cc78
-rw-r--r--pdf/framework/libs/pdfClient/annotation.h46
-rw-r--r--pdf/framework/libs/pdfClient/image_object.cc94
-rw-r--r--pdf/framework/libs/pdfClient/image_object.h49
-rw-r--r--pdf/framework/libs/pdfClient/jni_conversion.cc633
-rw-r--r--pdf/framework/libs/pdfClient/jni_conversion.h1
-rw-r--r--pdf/framework/libs/pdfClient/page.cc55
-rw-r--r--pdf/framework/libs/pdfClient/page_object.cc225
-rw-r--r--pdf/framework/libs/pdfClient/page_object.h95
-rw-r--r--pdf/framework/libs/pdfClient/page_test.cc241
-rw-r--r--pdf/framework/libs/pdfClient/path_object.cc210
-rw-r--r--pdf/framework/libs/pdfClient/path_object.h71
-rw-r--r--pdf/framework/libs/pdfClient/testdata/page_object.pdfbin2442 -> 1853 bytes
-rw-r--r--pdf/framework/libs/pdfClient/text_object.cc295
-rw-r--r--pdf/framework/libs/pdfClient/text_object.h105
-rw-r--r--photopicker/res/values-af/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-am/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-ar/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-as/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-az/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-b+sr+Latn/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-be/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-bg/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-bn/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-bs/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-ca/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-cs/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-da/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-de/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-el/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-en-rAU/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-en-rCA/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-en-rGB/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-en-rIN/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-es-rUS/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-es/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-et/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-eu/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-fa/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-fi/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-fr-rCA/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-fr/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-gl/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-gu/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-hi/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-hr/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-hu/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-hy/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-in/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-is/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-it/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-iw/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-ja/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-ka/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-kk/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-km/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-kn/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-ko/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-ky/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-lo/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-lt/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-lv/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-mk/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-ml/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-mn/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-mr/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-ms/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-my/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-nb/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-ne/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-nl/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-or/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-pa/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-pl/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-pt-rBR/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-pt-rPT/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-pt/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-ro/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-ru/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-si/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-sk/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-sl/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-sq/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-sr/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-sv/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-sw/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-ta/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-te/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-th/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-tl/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-tr/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-uk/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-ur/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-uz/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-vi/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-zh-rCN/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-zh-rHK/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-zh-rTW/feature_search_strings.xml1
-rw-r--r--photopicker/res/values-zu/feature_search_strings.xml1
-rw-r--r--photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt68
-rw-r--r--photopicker/src/com/android/photopicker/core/events/Dispatchers.kt64
-rw-r--r--photopicker/src/com/android/photopicker/core/user/UserMonitor.kt16
-rw-r--r--photopicker/src/com/android/photopicker/features/albumgrid/AlbumGrid.kt6
-rw-r--r--photopicker/src/com/android/photopicker/features/categorygrid/CategoryGrid.kt8
-rw-r--r--photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt8
-rw-r--r--photopicker/src/com/android/photopicker/features/search/Search.kt15
-rw-r--r--photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt26
-rw-r--r--photopicker/tests/src/com/android/photopicker/core/events/DispatchersTest.kt176
-rw-r--r--photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt510
-rw-r--r--photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt20
-rw-r--r--res/values-fi/strings.xml2
-rw-r--r--src/com/android/providers/media/LocalCallingIdentity.java5
-rw-r--r--src/com/android/providers/media/MediaProvider.java24
-rw-r--r--src/com/android/providers/media/photopicker/PickerSyncController.java80
-rw-r--r--src/com/android/providers/media/photopicker/data/PickerDbFacade.java101
-rw-r--r--src/com/android/providers/media/photopicker/data/UserManagerState.java450
-rw-r--r--src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorker.java36
-rw-r--r--src/com/android/providers/media/photopicker/sync/MediaSetsResetWorker.java40
-rw-r--r--src/com/android/providers/media/photopicker/sync/MediaSetsSyncWorker.java35
-rw-r--r--src/com/android/providers/media/photopicker/sync/PickerSearchProviderClient.java12
-rw-r--r--src/com/android/providers/media/photopicker/sync/PickerSyncManager.java22
-rw-r--r--src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java67
-rw-r--r--src/com/android/providers/media/photopicker/v2/PhotopickerSyncHelper.java65
-rw-r--r--src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java12
-rw-r--r--src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsDatabaseUtil.java31
-rw-r--r--src/com/android/providers/media/photopicker/v2/sqlite/MediaSetsDatabaseUtil.java62
-rw-r--r--src/com/android/providers/media/util/MimeTypeFixHandler.java64
-rw-r--r--src/com/android/providers/media/util/MimeUtils.java29
-rw-r--r--src/com/android/providers/media/util/PermissionUtils.java23
-rw-r--r--tests/Android.bp1
l---------tests/photopicker_res1
-rw-r--r--tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java66
-rw-r--r--tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java11
-rw-r--r--tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java651
-rw-r--r--tests/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorkerTest.java9
-rw-r--r--tests/src/com/android/providers/media/photopicker/sync/MediaSetsResetWorkerTest.java13
-rw-r--r--tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java18
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java5
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsDatabaseUtilTest.java52
-rw-r--r--tests/src/com/android/providers/media/photopicker/v2/sqlite/MediaSetsDatabaseUtilsTest.java65
-rw-r--r--tests/src/com/android/providers/media/photopickersearch/PickerSearchProviderClientTest.java5
-rw-r--r--tests/src/com/android/providers/media/util/MimeTypeFixHandlerTest.java30
-rw-r--r--tests/src/com/android/providers/media/util/PermissionUtilsTest.java24
158 files changed, 4456 insertions, 1399 deletions
diff --git a/Android.bp b/Android.bp
index 93d8daeb4..4dd572385 100644
--- a/Android.bp
+++ b/Android.bp
@@ -61,6 +61,7 @@ android_app {
resource_dirs: [
"res",
+ "photopicker/res",
],
srcs: [
":mediaprovider-sources",
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 1f37f6d72..9a38fb418 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -180,11 +180,6 @@
]
},
{
- // This is a typo and is tracked in b/155715039 but flaky on CF.
- // Will fix this once the root cause of flake is fixed.
- "name": "AdoptableHostTest"
- },
- {
"name": "CtsScopedStorageCoreHostTest",
"options": [
{
diff --git a/jni/RedactionInfo.cpp b/jni/RedactionInfo.cpp
index 384e59fed..faf9c4204 100644
--- a/jni/RedactionInfo.cpp
+++ b/jni/RedactionInfo.cpp
@@ -18,6 +18,8 @@
#include <android-base/logging.h>
+#include <algorithm>
+
using std::unique_ptr;
using std::vector;
diff --git a/pdf/framework-v/java/android/graphics/pdf/PdfRenderer.java b/pdf/framework-v/java/android/graphics/pdf/PdfRenderer.java
index 6c8a59066..0296fd01d 100644
--- a/pdf/framework-v/java/android/graphics/pdf/PdfRenderer.java
+++ b/pdf/framework-v/java/android/graphics/pdf/PdfRenderer.java
@@ -935,6 +935,11 @@ public final class PdfRenderer implements AutoCloseable {
/**
* Update the given {@link PdfPageObject} to the page.
* <p>
+ * Note: This method only updates the parameters of the PageObject whose setters
+ * are available. Attempting to update fields with no corresponding setters will
+ * have no effect.
+ *
+ * <p>
* {@link PdfRenderer#write} needs to be called to get the updated PDF stream after calling
* this method. {@link PdfRenderer.Page} instance can be closed before calling
* {@link PdfRenderer#write}.
diff --git a/pdf/framework/api/current.txt b/pdf/framework/api/current.txt
index 3715cbdef..feef20bcf 100644
--- a/pdf/framework/api/current.txt
+++ b/pdf/framework/api/current.txt
@@ -80,23 +80,25 @@ package android.graphics.pdf.component {
@FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_text_annotations") public final class FreeTextAnnotation extends android.graphics.pdf.component.PdfAnnotation {
ctor public FreeTextAnnotation(@NonNull android.graphics.RectF, @NonNull String);
method @ColorInt public int getBackgroundColor();
+ method @NonNull public android.graphics.RectF getBounds();
method @ColorInt public int getTextColor();
method @NonNull public String getTextContent();
method public void setBackgroundColor(@ColorInt int);
+ method public void setBounds(@NonNull android.graphics.RectF);
method public void setTextColor(@ColorInt int);
method public void setTextContent(@NonNull String);
}
@FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_annotations") public final class HighlightAnnotation extends android.graphics.pdf.component.PdfAnnotation {
- ctor public HighlightAnnotation(@NonNull android.graphics.RectF);
+ ctor public HighlightAnnotation(@NonNull java.util.List<android.graphics.RectF>);
+ method @NonNull public java.util.List<android.graphics.RectF> getBounds();
method @ColorInt public int getColor();
+ method public void setBounds(@NonNull java.util.List<android.graphics.RectF>);
method public void setColor(@ColorInt int);
}
@FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_annotations") public abstract class PdfAnnotation {
- method @NonNull public android.graphics.RectF getBounds();
method public int getPdfAnnotationType();
- method public void setBounds(@NonNull android.graphics.RectF);
}
@FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_annotations") public final class PdfAnnotationType {
@@ -119,6 +121,13 @@ package android.graphics.pdf.component {
method public void transform(float, float, float, float, float, float);
}
+ @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_page_objects") public final class PdfPageObjectRenderMode {
+ field public static final int FILL = 0; // 0x0
+ field public static final int FILL_STROKE = 2; // 0x2
+ field public static final int STROKE = 1; // 0x1
+ field public static final int UNKNOWN = -1; // 0xffffffff
+ }
+
@FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_page_objects") public final class PdfPageObjectType {
method public static boolean isValidType(int);
field public static final int IMAGE = 3; // 0x3
@@ -129,36 +138,58 @@ 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(@NonNull android.graphics.Path);
- method @Nullable public android.graphics.Color getFillColor();
- method @Nullable public android.graphics.Color getStrokeColor();
+ method @ColorInt public int getFillColor();
+ method public int getRenderMode();
+ method @ColorInt public int getStrokeColor();
method public float getStrokeWidth();
- method public void setFillColor(@Nullable android.graphics.Color);
- method public void setStrokeColor(@Nullable android.graphics.Color);
+ method public void setFillColor(@ColorInt int);
+ method public void setRenderMode(int);
+ method public void setStrokeColor(@ColorInt int);
method public void setStrokeWidth(float);
method @NonNull public android.graphics.Path toPath();
}
@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();
+ ctor public PdfPageTextObject(@NonNull String, @NonNull android.graphics.pdf.component.PdfPageTextObjectFont, float);
+ method @ColorInt public int getFillColor();
+ method @NonNull public android.graphics.pdf.component.PdfPageTextObjectFont getFont();
method public float getFontSize();
- method @NonNull public android.graphics.Color getStrokeColor();
+ method public int getRenderMode();
+ method @ColorInt public int getStrokeColor();
method public float getStrokeWidth();
method @NonNull public String getText();
- method @NonNull public android.graphics.Typeface getTypeface();
- method public void setFillColor(@Nullable android.graphics.Color);
- method public void setFontSize(float);
- method public void setStrokeColor(@NonNull android.graphics.Color);
+ method public void setFillColor(@ColorInt int);
+ method public void setRenderMode(int);
+ method public void setStrokeColor(@ColorInt int);
method public void setStrokeWidth(float);
method public void setText(@NonNull String);
- method public void setTypeface(@NonNull android.graphics.Typeface);
+ }
+
+ @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_text_objects") public class PdfPageTextObjectFont {
+ ctor public PdfPageTextObjectFont(int, boolean, boolean);
+ ctor public PdfPageTextObjectFont(@NonNull android.graphics.pdf.component.PdfPageTextObjectFont);
+ method public int getFontFamily();
+ method public boolean isBold();
+ method public boolean isItalic();
+ method public void setBold(boolean);
+ method public void setFontFamily(int);
+ method public void setItalic(boolean);
+ }
+
+ @FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_text_objects") public class PdfPageTextObjectFontFamily {
+ field public static final int COURIER = 0; // 0x0
+ field public static final int HELVETICA = 1; // 0x1
+ field public static final int SYMBOL = 2; // 0x2
+ field public static final int TIMES_NEW_ROMAN = 3; // 0x3
}
@FlaggedApi("android.graphics.pdf.flags.enable_edit_pdf_stamp_annotations") public final class StampAnnotation extends android.graphics.pdf.component.PdfAnnotation {
ctor public StampAnnotation(@NonNull android.graphics.RectF);
method public void addObject(@NonNull android.graphics.pdf.component.PdfPageObject);
+ method @NonNull public android.graphics.RectF getBounds();
method @NonNull public java.util.List<android.graphics.pdf.component.PdfPageObject> getObjects();
method public void removeObject(@IntRange(from=0) int);
+ method public void setBounds(@NonNull android.graphics.RectF);
}
}
diff --git a/pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java b/pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java
index d5081ae98..2ab35f1f3 100644
--- a/pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java
+++ b/pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java
@@ -768,6 +768,11 @@ public final class PdfRendererPreV implements AutoCloseable {
/**
* Update the given {@link PdfPageObject} to the page.
* <p>
+ * Note: This method only updates the parameters of the PageObject whose setters
+ * are available. Attempting to update fields with no corresponding setters will
+ * have no effect.
+ *
+ * <p>
* {@link PdfRenderer#write} needs to be called to get the updated PDF stream after calling
* this method. {@link PdfRenderer.Page} instance can be closed before calling
* {@link PdfRenderer#write}.
diff --git a/pdf/framework/java/android/graphics/pdf/component/FreeTextAnnotation.java b/pdf/framework/java/android/graphics/pdf/component/FreeTextAnnotation.java
index 32d0762f6..303dd3ff0 100644
--- a/pdf/framework/java/android/graphics/pdf/component/FreeTextAnnotation.java
+++ b/pdf/framework/java/android/graphics/pdf/component/FreeTextAnnotation.java
@@ -22,6 +22,7 @@ import android.annotation.NonNull;
import android.graphics.Color;
import android.graphics.RectF;
import android.graphics.pdf.flags.Flags;
+import android.graphics.pdf.utils.Preconditions;
/**
* Represents a free text annotation in a PDF document.
@@ -38,6 +39,7 @@ import android.graphics.pdf.flags.Flags;
*/
@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_TEXT_ANNOTATIONS)
public final class FreeTextAnnotation extends PdfAnnotation {
+ @NonNull private RectF mBounds;
@NonNull private String mTextContent;
private @ColorInt int mTextColor;
private @ColorInt int mBackgroundColor;
@@ -51,13 +53,34 @@ public final class FreeTextAnnotation extends PdfAnnotation {
* @param textContent The text content of the annotation
*/
public FreeTextAnnotation(@NonNull RectF bounds, @NonNull String textContent) {
- super(PdfAnnotationType.FREETEXT, bounds);
+ super(PdfAnnotationType.FREETEXT);
+ this.mBounds = bounds;
this.mTextContent = textContent;
this.mTextColor = Color.BLACK;
this.mBackgroundColor = Color.WHITE;
}
/**
+ * Sets the bounding rectangle of the freetext annotation.
+ *
+ * @param bounds The new bounding rectangle.
+ * @throws NullPointerException if given bounds is null
+ */
+ public void setBounds(@NonNull RectF bounds) {
+ Preconditions.checkNotNull(bounds, "Bounds should not be null");
+ this.mBounds = bounds;
+ }
+
+ /**
+ * Returns the bounding rectangle of the freetext annotation.
+ *
+ * @return The bounding rectangle.
+ */
+ @NonNull public RectF getBounds() {
+ return mBounds;
+ }
+
+ /**
* Sets the text content of the annotation.
*
* @param text The new text content.
diff --git a/pdf/framework/java/android/graphics/pdf/component/HighlightAnnotation.java b/pdf/framework/java/android/graphics/pdf/component/HighlightAnnotation.java
index d3f5d27af..0f2661735 100644
--- a/pdf/framework/java/android/graphics/pdf/component/HighlightAnnotation.java
+++ b/pdf/framework/java/android/graphics/pdf/component/HighlightAnnotation.java
@@ -22,6 +22,9 @@ import android.annotation.NonNull;
import android.graphics.Color;
import android.graphics.RectF;
import android.graphics.pdf.flags.Flags;
+import android.graphics.pdf.utils.Preconditions;
+
+import java.util.List;
/**
* Represents a highlight annotation in a PDF document.
@@ -31,6 +34,7 @@ import android.graphics.pdf.flags.Flags;
*/
@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_ANNOTATIONS)
public final class HighlightAnnotation extends PdfAnnotation {
+ @NonNull private List<RectF> mBounds;
private @ColorInt int mColor;
/**
@@ -40,12 +44,36 @@ public final class HighlightAnnotation extends PdfAnnotation {
*
* @param bounds The bounding rectangle of the annotation.
*/
- public HighlightAnnotation(@NonNull RectF bounds) {
- super(PdfAnnotationType.HIGHLIGHT, bounds);
+ public HighlightAnnotation(@NonNull List<RectF> bounds) {
+ super(PdfAnnotationType.HIGHLIGHT);
+ this.mBounds = bounds;
this.mColor = Color.YELLOW;
}
/**
+ * Sets the bounding rectangles of the highlight annotation. Each rect in the list mBounds
+ * represent an absolute position of highlight inside the page of the document
+ *
+ * @param bounds The new bounding rectangles.
+ * @throws NullPointerException if given bounds is null
+ * @throws IllegalArgumentException if the given bounds list is empty
+ */
+ public void setBounds(@NonNull List<RectF> bounds) {
+ Preconditions.checkNotNull(bounds, "Bounds should not be null");
+ Preconditions.checkArgument(!bounds.isEmpty(), "Bounds should not be empty");
+ this.mBounds = bounds;
+ }
+
+ /**
+ * Returns the bounding rectangles of the highlight annotation.
+ *
+ * @return The bounding rectangles.
+ */
+ @NonNull public List<RectF> getBounds() {
+ return mBounds;
+ }
+
+ /**
* Returns the highlight color of the annotation.
*
* @return The highlight color.
diff --git a/pdf/framework/java/android/graphics/pdf/component/PdfAnnotation.java b/pdf/framework/java/android/graphics/pdf/component/PdfAnnotation.java
index 0d510f277..fb4772218 100644
--- a/pdf/framework/java/android/graphics/pdf/component/PdfAnnotation.java
+++ b/pdf/framework/java/android/graphics/pdf/component/PdfAnnotation.java
@@ -17,8 +17,6 @@
package android.graphics.pdf.component;
import android.annotation.FlaggedApi;
-import android.annotation.NonNull;
-import android.graphics.RectF;
import android.graphics.pdf.flags.Flags;
import android.graphics.pdf.utils.Preconditions;
@@ -30,23 +28,19 @@ import android.graphics.pdf.utils.Preconditions;
*/
@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_ANNOTATIONS)
public abstract class PdfAnnotation {
- private int mType;
- @NonNull private RectF mBounds;
+ private final int mType;
/**
* Creates a new PDF annotation with the specified type and bounds.
*
- * @param type The type of annotation. See {@link PdfAnnotationType} for possible values.
- * @param bounds The bounding rectangle of the annotation.
+ * @param type The type of annotation. See {@link PdfAnnotationType} for possible values.
*/
- PdfAnnotation(@PdfAnnotationType.Type int type, @NonNull RectF bounds) {
- Preconditions.checkNotNull(bounds, "Bounds cannot be null");
+ PdfAnnotation(@PdfAnnotationType.Type int type) {
Preconditions.checkArgument(type == PdfAnnotationType.UNKNOWN
|| type == PdfAnnotationType.FREETEXT
|| type == PdfAnnotationType.HIGHLIGHT
|| type == PdfAnnotationType.STAMP, "Invalid Annotation Type");
this.mType = type;
- this.mBounds = bounds;
}
/**
@@ -57,23 +51,4 @@ public abstract class PdfAnnotation {
public @PdfAnnotationType.Type int getPdfAnnotationType() {
return mType;
}
-
- /**
- * Sets the bounding rectangle of the annotation.
- *
- * @param bounds The new bounding rectangle.
- */
- public void setBounds(@NonNull RectF bounds) {
- this.mBounds = bounds;
- }
-
- /**
- * Returns the bounding rectangle of the annotation.
- *
- * @return The bounding rectangle.
- */
- @NonNull public RectF getBounds() {
- return mBounds;
- }
-
}
diff --git a/pdf/framework/java/android/graphics/pdf/component/PdfPageObjectRenderMode.java b/pdf/framework/java/android/graphics/pdf/component/PdfPageObjectRenderMode.java
new file mode 100644
index 000000000..c8603bb85
--- /dev/null
+++ b/pdf/framework/java/android/graphics/pdf/component/PdfPageObjectRenderMode.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics.pdf.component;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.graphics.pdf.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Defines rendering modes for PDF page objects (fill, stroke, etc.).
+ *
+ * <p>
+ * This final class provides constants for specifying how graphical elements
+ * are rendered on a PDF page. It cannot be instantiated.
+ *
+ * <p>
+ * Rendering modes:
+ * <ul>
+ * <li>{@link #UNKNOWN}: Unknown mode.
+ * <li>{@link #FILL}: Fill object.
+ * <li>{@link #STROKE}: Stroke object.
+ * <li>{@link #FILL_STROKE}: Fill and stroke object. </ul>
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_PAGE_OBJECTS)
+public final class PdfPageObjectRenderMode {
+ // Private constructor
+ private PdfPageObjectRenderMode() {
+ }
+
+ /**
+ * Unknown Mode
+ */
+ public static final int UNKNOWN = -1;
+
+ /**
+ * Fill Mode
+ */
+ public static final int FILL = 0;
+
+ /**
+ * Stroke Mode
+ */
+ public static final int STROKE = 1;
+
+ /**
+ * FillStroke Mode
+ */
+ public static final int FILL_STROKE = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({UNKNOWN, FILL, STROKE, FILL_STROKE})
+ public @interface Type {
+ }
+}
diff --git a/pdf/framework/java/android/graphics/pdf/component/PdfPagePathObject.java b/pdf/framework/java/android/graphics/pdf/component/PdfPagePathObject.java
index 08ed9ee3a..d19c37ae9 100644
--- a/pdf/framework/java/android/graphics/pdf/component/PdfPagePathObject.java
+++ b/pdf/framework/java/android/graphics/pdf/component/PdfPagePathObject.java
@@ -16,10 +16,9 @@
package android.graphics.pdf.component;
+import android.annotation.ColorInt;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.graphics.Color;
import android.graphics.Path;
import android.graphics.pdf.flags.Flags;
@@ -31,9 +30,10 @@ import android.graphics.pdf.flags.Flags;
@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_PAGE_OBJECTS)
public final class PdfPagePathObject extends PdfPageObject {
private final Path mPath;
- private Color mStrokeColor;
+ private @ColorInt int mStrokeColor;
private float mStrokeWidth;
- private Color mFillColor;
+ private @ColorInt int mFillColor;
+ private @PdfPageObjectRenderMode.Type int mRenderMode;
/**
* Constructor for the PdfPagePathObject. Sets the object type
@@ -42,6 +42,7 @@ public final class PdfPagePathObject extends PdfPageObject {
public PdfPagePathObject(@NonNull Path path) {
super(PdfPageObjectType.PATH);
this.mPath = path;
+ this.mRenderMode = PdfPageObjectRenderMode.FILL;
}
/**
@@ -64,8 +65,7 @@ public final class PdfPagePathObject extends PdfPageObject {
*
* @return The stroke color of the object.
*/
- @Nullable
- public Color getStrokeColor() {
+ public @ColorInt int getStrokeColor() {
return mStrokeColor;
}
@@ -74,7 +74,7 @@ public final class PdfPagePathObject extends PdfPageObject {
*
* @param strokeColor The stroke color of the object.
*/
- public void setStrokeColor(@Nullable Color strokeColor) {
+ public void setStrokeColor(@ColorInt int strokeColor) {
this.mStrokeColor = strokeColor;
}
@@ -101,8 +101,7 @@ public final class PdfPagePathObject extends PdfPageObject {
*
* @return The fill color of the object.
*/
- @Nullable
- public Color getFillColor() {
+ public @ColorInt int getFillColor() {
return mFillColor;
}
@@ -111,8 +110,27 @@ public final class PdfPagePathObject extends PdfPageObject {
*
* @param fillColor The fill color of the object.
*/
- public void setFillColor(@Nullable Color fillColor) {
+ public void setFillColor(@ColorInt int fillColor) {
this.mFillColor = fillColor;
}
+ /**
+ * Returns the {@link PdfPageObjectRenderMode} of the object.
+ * Returns {@link PdfPageObjectRenderMode#FILL} by default
+ * if {@link PdfPagePathObject#mRenderMode} is not set.
+ *
+ * @return The {@link PdfPageObjectRenderMode} of the object.
+ */
+ public @PdfPageObjectRenderMode.Type int getRenderMode() {
+ return mRenderMode;
+ }
+
+ /**
+ * Sets the {@link PdfPageObjectRenderMode} of the object.
+ *
+ * @param renderMode The {@link PdfPageObjectRenderMode} to be set.
+ */
+ public void setRenderMode(@PdfPageObjectRenderMode.Type int renderMode) {
+ mRenderMode = renderMode;
+ }
}
diff --git a/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObject.java b/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObject.java
index aa311fd5d..17662308e 100644
--- a/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObject.java
+++ b/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObject.java
@@ -16,11 +16,9 @@
package android.graphics.pdf.component;
+import android.annotation.ColorInt;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.graphics.Color;
-import android.graphics.Typeface;
import android.graphics.pdf.flags.Flags;
/**
@@ -30,24 +28,29 @@ import android.graphics.pdf.flags.Flags;
@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_TEXT_OBJECTS)
public final class PdfPageTextObject extends PdfPageObject {
private String mText;
- private Typeface mTypeface;
- private float mFontSize;
- private Color mStrokeColor = new Color(); // Default is opaque black in the sRGB color space.
+ private final PdfPageTextObjectFont mFont;
+ private final float mFontSize;
+ private @ColorInt int mStrokeColor;
private float mStrokeWidth = 1.0f;
- private Color mFillColor;
+ private @ColorInt int mFillColor;
+ private @PdfPageObjectRenderMode.Type int mRenderMode;
/**
* Constructor for the PdfPageTextObject.
* Sets the object type to TEXT and initializes the text color to black.
*
- * @param typeface The font of the text.
+ * @param font The font of the text.
* @param fontSize The font size of the text.
*/
- public PdfPageTextObject(@NonNull String text, @NonNull Typeface typeface, float fontSize) {
+ public PdfPageTextObject(@NonNull String text, @NonNull PdfPageTextObjectFont font,
+ float fontSize) {
super(PdfPageObjectType.TEXT);
this.mText = text;
- this.mTypeface = typeface;
+ this.mFont = font;
this.mFontSize = fontSize;
+ if (Flags.enableEditPdfPageObjects()) {
+ this.mRenderMode = PdfPageObjectRenderMode.FILL;
+ }
}
/**
@@ -79,31 +82,31 @@ public final class PdfPageTextObject extends PdfPageObject {
}
/**
- * Sets the font size of the object.
+ * Returns the font of the text.
*
- * @param fontSize The font size to set.
+ * @return A copy of the font object.
*/
- public void setFontSize(float fontSize) {
- mFontSize = fontSize;
+ @NonNull
+ public PdfPageTextObjectFont getFont() {
+ return new PdfPageTextObjectFont(mFont);
}
/**
- * Returns the stroke color of the object.
+ * Returns the fill color of the object.
*
- * @return The stroke color of the object.
+ * @return The fill color of the object.
*/
- @NonNull
- public Color getStrokeColor() {
- return mStrokeColor;
+ public @ColorInt int getFillColor() {
+ return mFillColor;
}
/**
- * Sets the stroke color of the object.
+ * Sets the fill color of the object.
*
- * @param strokeColor The stroke color of the object.
+ * @param fillColor The fill color of the object.
*/
- public void setStrokeColor(@NonNull Color strokeColor) {
- this.mStrokeColor = strokeColor;
+ public void setFillColor(@ColorInt int fillColor) {
+ this.mFillColor = fillColor;
}
/**
@@ -121,44 +124,42 @@ public final class PdfPageTextObject extends PdfPageObject {
* @param strokeWidth The stroke width of the object.
*/
public void setStrokeWidth(float strokeWidth) {
- this.mStrokeWidth = strokeWidth;
+ mStrokeWidth = strokeWidth;
}
/**
- * Returns the font of the text.
+ * Returns the stroke color of the object.
*
- * @return The font.
+ * @return The stroke color of the object.
*/
- @NonNull
- public Typeface getTypeface() {
- return mTypeface;
+ public @ColorInt int getStrokeColor() {
+ return mStrokeColor;
}
/**
- * Sets the font of the text.
+ * Sets the stroke color of the object.
*
- * @param typeface The font to set.
+ * @param strokeColor The stroke color of the object.
*/
- public void setTypeface(@NonNull Typeface typeface) {
- this.mTypeface = typeface;
+ public void setStrokeColor(@ColorInt int strokeColor) {
+ this.mStrokeColor = strokeColor;
}
/**
- * Returns the fill color of the object.
+ * Returns the render mode of the object.
*
- * @return The fill color of the object.
+ * @return The render mode of the object.
*/
- @Nullable
- public Color getFillColor() {
- return mFillColor;
+ public @PdfPageObjectRenderMode.Type int getRenderMode() {
+ return mRenderMode;
}
/**
- * Sets the fill color of the object.
+ * Sets the render mode of the object.
*
- * @param fillColor The fill color of the object.
+ * @param renderMode The render mode to be set.
*/
- public void setFillColor(@Nullable Color fillColor) {
- this.mFillColor = fillColor;
+ public void setRenderMode(@PdfPageObjectRenderMode.Type int renderMode) {
+ mRenderMode = renderMode;
}
}
diff --git a/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObjectFont.java b/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObjectFont.java
new file mode 100644
index 000000000..c38baa156
--- /dev/null
+++ b/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObjectFont.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics.pdf.component;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.graphics.pdf.flags.Flags;
+
+@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_TEXT_OBJECTS)
+public class PdfPageTextObjectFont {
+ private @PdfPageTextObjectFontFamily.Type int mFontFamily;
+ private boolean mIsBold;
+ private boolean mIsItalic;
+
+ public PdfPageTextObjectFont(@PdfPageTextObjectFontFamily.Type int fontFamily,
+ boolean isBold, boolean isItalic) {
+ mFontFamily = fontFamily;
+ mIsBold = isBold;
+ mIsItalic = isItalic;
+ }
+
+ public PdfPageTextObjectFont(@NonNull PdfPageTextObjectFont font) {
+ this.mFontFamily = font.getFontFamily();
+ this.mIsBold = font.isBold();
+ this.mIsItalic = font.isItalic();
+ }
+
+ /**
+ * Returns the font-family which is of type {@link PdfPageTextObjectFontFamily}
+ *
+ * @return The font-family.
+ */
+ public @PdfPageTextObjectFontFamily.Type int getFontFamily() {
+ return mFontFamily;
+ }
+
+ /**
+ * Set the font family of the object.
+ *
+ * @param fontFamily The font family to be set.
+ */
+ public void setFontFamily(@PdfPageTextObjectFontFamily.Type int fontFamily) {
+ mFontFamily = fontFamily;
+ }
+
+ /**
+ * Determines if the text is bold.
+ *
+ * @return true if the text is bold, false otherwise.
+ */
+ public boolean isBold() {
+ return mIsBold;
+ }
+
+ /**
+ * Sets whether the text should be bold or not.
+ *
+ * @param bold true if the text should be bold, false otherwise.
+ */
+ public void setBold(boolean bold) {
+ mIsBold = bold;
+ }
+
+ /**
+ * Determines if the text is italic.
+ *
+ * @return true if the text is italic, false otherwise.
+ */
+ public boolean isItalic() {
+ return mIsItalic;
+ }
+
+ /**
+ * Set whether the text should be italic or not.
+ *
+ * @param italic true if the text should be italic, false otherwise.
+ */
+ public void setItalic(boolean italic) {
+ mIsItalic = italic;
+ }
+}
diff --git a/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObjectFontFamily.java b/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObjectFontFamily.java
new file mode 100644
index 000000000..4a40f3df5
--- /dev/null
+++ b/pdf/framework/java/android/graphics/pdf/component/PdfPageTextObjectFontFamily.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics.pdf.component;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.graphics.pdf.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The class holds the set of font families supported by {@link PdfPageTextObject}.
+ * The specified font families are standard font families defined
+ * in the PDF Spec 1.7 - Page 146.
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_TEXT_OBJECTS)
+public class PdfPageTextObjectFontFamily {
+ private PdfPageTextObjectFontFamily() {}
+
+ /**
+ * Courier Font
+ */
+ public static final int COURIER = 0;
+
+ /**
+ * Helvetica Font
+ */
+ public static final int HELVETICA = 1;
+
+ /**
+ * Symbol Font (Note: Renders only symbols)
+ */
+ public static final int SYMBOL = 2;
+
+ /**
+ * TimesNewRoman Font
+ */
+ public static final int TIMES_NEW_ROMAN = 3;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({COURIER, HELVETICA, SYMBOL, TIMES_NEW_ROMAN})
+ public @interface Type {
+ }
+}
diff --git a/pdf/framework/java/android/graphics/pdf/component/StampAnnotation.java b/pdf/framework/java/android/graphics/pdf/component/StampAnnotation.java
index 2663cd869..3f3279415 100644
--- a/pdf/framework/java/android/graphics/pdf/component/StampAnnotation.java
+++ b/pdf/framework/java/android/graphics/pdf/component/StampAnnotation.java
@@ -35,19 +35,43 @@ import java.util.List;
*/
@FlaggedApi(Flags.FLAG_ENABLE_EDIT_PDF_STAMP_ANNOTATIONS)
public final class StampAnnotation extends PdfAnnotation {
+ @NonNull private RectF mBounds;
@NonNull private List<PdfPageObject> mObjects;
/**
- * Creates a new stamp annotation with the specified bounds
+ * Creates a new stamp annotation with the specified bounds.
+ * <p>
+ * The list of page objects inside the stamp annotation will be empty by default
*
* @param bounds The bounding rectangle of the annotation.
*/
public StampAnnotation(@NonNull RectF bounds) {
- super(PdfAnnotationType.STAMP, bounds);
+ super(PdfAnnotationType.STAMP);
+ mBounds = bounds;
mObjects = new ArrayList<>();
}
/**
+ * Sets the bounding rectangle of the stamp annotation.
+ *
+ * @param bounds The new bounding rectangle.
+ * @throws NullPointerException if given bounds is null
+ */
+ public void setBounds(@NonNull RectF bounds) {
+ Preconditions.checkNotNull(bounds, "Bounds should not be null");
+ this.mBounds = bounds;
+ }
+
+ /**
+ * Returns the bounding rectangle of the stamp annotation.
+ *
+ * @return The bounding rectangle.
+ */
+ @NonNull public RectF getBounds() {
+ return mBounds;
+ }
+
+ /**
* Adds a PDF page object to the stamp annotation.
* <p>
* The page object should be a path, text or an image.
diff --git a/pdf/framework/libs/pdfClient/annotation.cc b/pdf/framework/libs/pdfClient/annotation.cc
index 20a6c8bb8..653f62ac6 100644
--- a/pdf/framework/libs/pdfClient/annotation.cc
+++ b/pdf/framework/libs/pdfClient/annotation.cc
@@ -18,7 +18,9 @@
#include <utils/pdf_strings.h>
+#include "image_object.h"
#include "logging.h"
+#include "path_object.h"
#define LOG_TAG "annotation"
@@ -33,7 +35,21 @@ std::vector<PageObject*> StampAnnotation::GetObjects() const {
return page_objects;
}
-bool StampAnnotation::PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot) {
+bool updateExistingBounds(FPDF_ANNOTATION fpdf_annot, size_t num_bounds,
+ std::vector<Rectangle_f> bounds) {
+ for (auto bound_index = 0; bound_index < num_bounds; bound_index++) {
+ Rectangle_f rect = bounds[bound_index];
+ FS_QUADPOINTSF quad_points = {rect.left, rect.top, rect.right, rect.top,
+ rect.left, rect.bottom, rect.right, rect.bottom};
+ if (!FPDFAnnot_SetAttachmentPoints(fpdf_annot, bound_index, &quad_points)) {
+ LOGD("Failed to update the bounds of highlight annotation");
+ return false;
+ }
+ }
+ return true;
+}
+
+bool StampAnnotation::PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_PAGE page) {
int num_of_objects = FPDFAnnot_GetObjectCount(fpdf_annot);
for (int object_index = 0; object_index < num_of_objects; object_index++) {
@@ -56,7 +72,7 @@ bool StampAnnotation::PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot) {
}
}
- if (page_object_ && !page_object_->PopulateFromFPDFInstance(page_object)) {
+ if (page_object_ && !page_object_->PopulateFromFPDFInstance(page_object, page)) {
LOGE("Failed to get all the data corresponding to object with index "
"%d ",
object_index);
@@ -96,7 +112,7 @@ ScopedFPDFAnnotation StampAnnotation::CreatePdfiumInstance(FPDF_DOCUMENT documen
std::vector<PageObject*> pageObjects = GetObjects();
for (auto pageObject : pageObjects) {
- ScopedFPDFPageObject scoped_page_object = pageObject->CreateFPDFInstance(document);
+ ScopedFPDFPageObject scoped_page_object = pageObject->CreateFPDFInstance(document, page);
if (!scoped_page_object) {
LOGE("Failed to create page object to add in the stamp annotation");
@@ -112,7 +128,8 @@ ScopedFPDFAnnotation StampAnnotation::CreatePdfiumInstance(FPDF_DOCUMENT documen
return scoped_annot;
}
-bool StampAnnotation::UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document) {
+bool StampAnnotation::UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document,
+ FPDF_PAGE page) {
if (FPDFAnnot_GetSubtype(fpdf_annot) != FPDF_ANNOT_STAMP) {
LOGE("Unsupported operation - can't update a stamp annotation with some other type of "
"annotation");
@@ -147,7 +164,7 @@ bool StampAnnotation::UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCU
// Rewrite
std::vector<PageObject*> newPageObjects = GetObjects();
for (auto pageObject : newPageObjects) {
- ScopedFPDFPageObject scoped_page_object = pageObject->CreateFPDFInstance(document);
+ ScopedFPDFPageObject scoped_page_object = pageObject->CreateFPDFInstance(document, page);
if (!scoped_page_object) {
LOGE("Failed to create new page object to add in the stamp annotation");
@@ -162,7 +179,7 @@ bool StampAnnotation::UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCU
return true;
}
-bool HighlightAnnotation::PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot) {
+bool HighlightAnnotation::PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_PAGE page) {
// Get color
unsigned int R;
unsigned int G;
@@ -189,30 +206,47 @@ ScopedFPDFAnnotation HighlightAnnotation::CreatePdfiumInstance(FPDF_DOCUMENT doc
return nullptr;
}
- if (!this->UpdatePdfiumInstance(scoped_annot.get(), document)) {
+ if (!this->UpdatePdfiumInstance(scoped_annot.get(), document, page)) {
LOGE("Failed to create highlight annotation with given parameters");
}
return scoped_annot;
}
-bool HighlightAnnotation::UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document) {
+bool HighlightAnnotation::UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document,
+ FPDF_PAGE page) {
if (FPDFAnnot_GetSubtype(fpdf_annot) != FPDF_ANNOT_HIGHLIGHT) {
LOGE("Unsupported operation - can't update a highlight annotation with some other type of "
"annotation");
return false;
}
- Rectangle_f annotation_bounds = this->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(fpdf_annot, &rect)) {
- LOGE("Highlight Annotation bounds couldn't be updated");
- return false;
+ auto old_num_bounds = FPDFAnnot_CountAttachmentPoints(fpdf_annot);
+ std::vector<Rectangle_f> bounds = GetBounds();
+ auto new_num_bounds = bounds.size();
+
+ if (old_num_bounds == new_num_bounds) {
+ if (!updateExistingBounds(fpdf_annot, old_num_bounds, bounds)) return false;
+ } else if (old_num_bounds < new_num_bounds) {
+ if (!updateExistingBounds(fpdf_annot, old_num_bounds, bounds)) return false;
+ for (auto bound_index = old_num_bounds; bound_index < new_num_bounds; bound_index++) {
+ Rectangle_f rect = bounds[bound_index];
+ FS_QUADPOINTSF quad_points = {rect.left, rect.top, rect.right, rect.top,
+ rect.left, rect.bottom, rect.right, rect.bottom};
+ if (!FPDFAnnot_AppendAttachmentPoints(fpdf_annot, &quad_points)) {
+ LOGD("Failed to update bounds of the highlight annotation");
+ return false;
+ }
+ }
+ } else {
+ if (!updateExistingBounds(fpdf_annot, new_num_bounds, bounds)) return false;
+ for (auto bound_index = new_num_bounds; bound_index < old_num_bounds; bound_index++) {
+ FS_QUADPOINTSF quad_points = {0, 0, 0, 0, 0, 0, 0, 0};
+ if (!FPDFAnnot_SetAttachmentPoints(fpdf_annot, bound_index, &quad_points)) {
+ LOGD("Failed to update bounds of the highlight annotation");
+ return false;
+ }
+ }
}
Color new_color = this->GetColor();
@@ -236,7 +270,7 @@ bool FreeTextAnnotation::GetTextContentFromPdfium(FPDF_ANNOTATION fpdf_annot,
return true;
}
-bool FreeTextAnnotation::PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot) {
+bool FreeTextAnnotation::PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_PAGE page) {
// Pass a empty buffer to get the length of the text contents.
unsigned long text_length = FPDFAnnot_GetStringValue(fpdf_annot, kContents, nullptr, 0);
if (text_length == 0) {
@@ -274,14 +308,15 @@ ScopedFPDFAnnotation FreeTextAnnotation::CreatePdfiumInstance(FPDF_DOCUMENT docu
return nullptr;
}
- if (!UpdatePdfiumInstance(scoped_annot.get(), document)) {
+ if (!UpdatePdfiumInstance(scoped_annot.get(), document, page)) {
LOGE("Failed to create FreeText Annotation with given parameters");
}
return scoped_annot;
}
-bool FreeTextAnnotation::UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document) {
+bool FreeTextAnnotation::UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document,
+ FPDF_PAGE page) {
if (FPDFAnnot_GetSubtype(fpdf_annot) != FPDF_ANNOT_FREETEXT) {
LOGE("Unsupported operation - can't update a freetext annotation with some other type of "
"annotation");
@@ -313,4 +348,5 @@ bool FreeTextAnnotation::UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_D
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
index c7cca3c6d..518c45c64 100644
--- a/pdf/framework/libs/pdfClient/annotation.h
+++ b/pdf/framework/libs/pdfClient/annotation.h
@@ -34,21 +34,18 @@ class Annotation {
public:
enum class Type { UNKNOWN = 0, FreeText = 1, Highlight = 2, Stamp = 3 };
- Annotation(Type type, const Rectangle_f& bounds) : type_(type), bounds_(bounds) {}
+ Annotation(Type type) : type_(type) {}
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 bool PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_PAGE page) = 0;
virtual ScopedFPDFAnnotation CreatePdfiumInstance(FPDF_DOCUMENT document, FPDF_PAGE page) = 0;
- virtual bool UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document) = 0;
+ virtual bool UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document,
+ FPDF_PAGE page) = 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
@@ -56,7 +53,10 @@ class Annotation {
// underlying pdfium page objects
class StampAnnotation : public Annotation {
public:
- StampAnnotation(const Rectangle_f& bounds) : Annotation(Type::Stamp, bounds) {}
+ StampAnnotation(const Rectangle_f& bounds) : Annotation(Type::Stamp) { bounds_ = bounds; }
+
+ Rectangle_f GetBounds() const { return bounds_; }
+ void SetBounds(Rectangle_f bounds) { bounds_ = bounds; }
// Return a const reference to the list
// Stamp annotation will have the ownership of the page objects inside it
@@ -73,33 +73,45 @@ class StampAnnotation : public Annotation {
pageObjects_.erase(it);
}
- bool PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot) override;
+ bool PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_PAGE page) override;
ScopedFPDFAnnotation CreatePdfiumInstance(FPDF_DOCUMENT document, FPDF_PAGE page) override;
- bool UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document) override;
+ bool UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document,
+ FPDF_PAGE page) override;
private:
+ Rectangle_f bounds_;
std::vector<std::unique_ptr<PageObject>> pageObjects_;
};
class HighlightAnnotation : public Annotation {
public:
- HighlightAnnotation(const Rectangle_f& bounds) : Annotation(Type::Highlight, bounds) {}
+ HighlightAnnotation(const std::vector<Rectangle_f>& bounds) : Annotation(Type::Highlight) {
+ bounds_ = bounds;
+ }
+
+ std::vector<Rectangle_f> GetBounds() const { return bounds_; }
+ void SetBounds(std::vector<Rectangle_f> bounds) { bounds_ = bounds; }
Color GetColor() const { return color_; }
void SetColor(Color color) { color_ = color; }
- bool PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot) override;
+ bool PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_PAGE page) override;
ScopedFPDFAnnotation CreatePdfiumInstance(FPDF_DOCUMENT document, FPDF_PAGE page) override;
- bool UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document) override;
+ bool UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document,
+ FPDF_PAGE page) override;
private:
+ std::vector<Rectangle_f> bounds_;
Color color_;
};
class FreeTextAnnotation : public Annotation {
public:
static constexpr const char* kContents = "Contents";
- FreeTextAnnotation(const Rectangle_f& bounds) : Annotation(Type::FreeText, bounds) {}
+ FreeTextAnnotation(const Rectangle_f& bounds) : Annotation(Type::FreeText) { bounds_ = bounds; }
+
+ Rectangle_f GetBounds() const { return bounds_; }
+ void SetBounds(Rectangle_f bounds) { bounds_ = bounds; }
std::wstring GetTextContent() const { return text_content_; }
void SetTextContent(std::wstring textContent) { text_content_ = textContent; }
@@ -110,11 +122,13 @@ class FreeTextAnnotation : public Annotation {
Color GetBackgroundColor() const { return background_color_; }
void SetBackgroundColor(Color color) { background_color_ = color; }
- bool PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot) override;
+ bool PopulateFromPdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_PAGE page) override;
ScopedFPDFAnnotation CreatePdfiumInstance(FPDF_DOCUMENT document, FPDF_PAGE page) override;
- bool UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document) override;
+ bool UpdatePdfiumInstance(FPDF_ANNOTATION fpdf_annot, FPDF_DOCUMENT document,
+ FPDF_PAGE page) override;
private:
+ Rectangle_f bounds_;
std::wstring text_content_;
Color text_color_;
Color background_color_;
diff --git a/pdf/framework/libs/pdfClient/image_object.cc b/pdf/framework/libs/pdfClient/image_object.cc
new file mode 100644
index 000000000..61bb85c69
--- /dev/null
+++ b/pdf/framework/libs/pdfClient/image_object.cc
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "image_object.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "fpdf_edit.h"
+#include "logging.h"
+
+#define LOG_TAG "image_object"
+
+namespace pdfClient {
+
+ImageObject::ImageObject() : PageObject(Type::Image) {}
+
+ScopedFPDFPageObject ImageObject::CreateFPDFInstance(FPDF_DOCUMENT document, FPDF_PAGE page) {
+ // 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(), page)) {
+ return nullptr;
+ }
+ return scoped_image_object;
+}
+
+bool ImageObject::UpdateFPDFInstance(FPDF_PAGEOBJECT image_object, FPDF_PAGE page) {
+ 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 (!SetDeviceToPageMatrix(image_object, page)) {
+ return false;
+ }
+
+ width_ = FPDFBitmap_GetWidth(bitmap_.get());
+ height_ = FPDFBitmap_GetHeight(bitmap_.get());
+
+ return true;
+}
+
+bool ImageObject::PopulateFromFPDFInstance(FPDF_PAGEOBJECT image_object, FPDF_PAGE page) {
+ // Get Bitmap
+ bitmap_ = ScopedFPDFBitmap(FPDFImageObj_GetBitmap(image_object));
+ if (bitmap_.get() == nullptr) {
+ return false;
+ }
+
+ // Get Matrix
+ if (!GetPageToDeviceMatrix(image_object, page)) {
+ 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/image_object.h b/pdf/framework/libs/pdfClient/image_object.h
new file mode 100644
index 000000000..2e3aa9541
--- /dev/null
+++ b/pdf/framework/libs/pdfClient/image_object.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEDIAPROVIDER_PDF_JNI_PDFCLIENT_IMAGE_OBJECT_H_
+#define MEDIAPROVIDER_PDF_JNI_PDFCLIENT_IMAGE_OBJECT_H_
+
+#include <stdint.h>
+
+#include "cpp/fpdf_scopers.h"
+#include "fpdfview.h"
+#include "page_object.h"
+
+typedef unsigned int uint;
+
+namespace pdfClient {
+
+class ImageObject : public PageObject {
+ public:
+ ImageObject();
+
+ ScopedFPDFPageObject CreateFPDFInstance(FPDF_DOCUMENT document, FPDF_PAGE page) override;
+ bool UpdateFPDFInstance(FPDF_PAGEOBJECT image_object, FPDF_PAGE page) override;
+ bool PopulateFromFPDFInstance(FPDF_PAGEOBJECT image_object, FPDF_PAGE page) override;
+
+ void* GetBitmapReadableBuffer() const;
+
+ ~ImageObject();
+
+ int width_ = 0;
+ int height_ = 0;
+ ScopedFPDFBitmap bitmap_;
+};
+
+} // namespace pdfClient
+
+#endif // MEDIAPROVIDER_PDF_JNI_PDFCLIENT_IMAGE_OBJECT_H_ \ 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 c88b5fd78..c132b6698 100644
--- a/pdf/framework/libs/pdfClient/jni_conversion.cc
+++ b/pdf/framework/libs/pdfClient/jni_conversion.cc
@@ -19,12 +19,16 @@
#include <android/bitmap.h>
#include <string.h>
+#include "image_object.h"
#include "logging.h"
#include "rect.h"
+#include "text_object.h"
using pdfClient::Annotation;
using pdfClient::Color;
using pdfClient::Document;
+using pdfClient::Font;
+using pdfClient::font_names;
using pdfClient::FreeTextAnnotation;
using pdfClient::HighlightAnnotation;
using pdfClient::ICoordinateConverter;
@@ -38,6 +42,7 @@ using pdfClient::Rectangle_f;
using pdfClient::Rectangle_i;
using pdfClient::SelectionBoundary;
using pdfClient::StampAnnotation;
+using pdfClient::TextObject;
using std::string;
using std::vector;
@@ -59,6 +64,8 @@ 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* kTextFont = "android/graphics/pdf/component/PdfPageTextObjectFont";
+static const char* kTextObject = "android/graphics/pdf/component/PdfPageTextObject";
static const char* kPathObject = "android/graphics/pdf/component/PdfPagePathObject";
static const char* kImageObject = "android/graphics/pdf/component/PdfPageImageObject";
static const char* kStampAnnotation = "android/graphics/pdf/component/StampAnnotation";
@@ -128,8 +135,31 @@ jobject ToJavaString(JNIEnv* env, const std::string& s) {
return env->NewStringUTF(s.c_str());
}
-jobject ToJavaString(JNIEnv* env, const std::wstring& s) {
- return env->NewString((jchar*)s.c_str(), s.length());
+jobject ToJavaString(JNIEnv* env, const std::wstring& ws) {
+ jsize len = ws.length();
+ jchar* jchars = new jchar[len + 1]; // Null Termination
+
+ for (size_t i = 0; i < len; ++i) {
+ jchars[i] = static_cast<jchar>(ws[i]);
+ }
+ jchars[len] = 0;
+
+ jstring result = env->NewString(jchars, len);
+
+ delete[] jchars;
+ return result;
+}
+
+std::wstring ToNativeWideString(JNIEnv* env, jstring java_string) {
+ std::wstring value;
+
+ const jchar* raw = env->GetStringChars(java_string, 0);
+ jsize len = env->GetStringLength(java_string);
+
+ value.assign(raw, raw + len);
+
+ env->ReleaseStringChars(java_string, raw);
+ return value;
}
// Copy a C++ vector to a java ArrayList, using the given function to convert.
@@ -150,6 +180,22 @@ jobject ToJavaList(JNIEnv* env, const vector<T>& input,
return java_list;
}
+template <class T>
+jobject ToJavaList(JNIEnv* env, const vector<T>& input, ICoordinateConverter* converter,
+ jobject (*ToJavaObject)(JNIEnv* env, const T&, ICoordinateConverter* converter)) {
+ 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], converter);
+ env->CallBooleanMethod(java_list, add, java_object);
+ env->DeleteLocalRef(java_object);
+ }
+ 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, ICoordinateConverter* converter,
@@ -438,35 +484,6 @@ jobject ToJavaBitmap(JNIEnv* env, void* buffer, int width, int height) {
return java_bitmap;
}
-jstring wstringToJstringUTF16(JNIEnv* env, const std::wstring& wstr) {
- jsize len = wstr.length();
- jchar* jchars = new jchar[len + 1]; // +1 for null terminator
-
- for (size_t i = 0; i < len; ++i) {
- jchars[i] = static_cast<jchar>(wstr[i]);
- }
- jchars[len] = 0;
-
- jstring result = env->NewString(jchars, len);
-
- delete[] jchars;
-
- return result;
-}
-
-std::wstring jStringToWstring(JNIEnv* env, jstring java_string) {
- std::wstring value;
-
- const jchar* raw = env->GetStringChars(java_string, 0);
- jsize len = env->GetStringLength(java_string);
-
- value.assign(raw, raw + len);
-
- env->ReleaseStringChars(java_string, raw);
-
- return value;
-}
-
int ToJavaColorInt(Color color) {
// Get ARGB values from Native Color
uint A = color.a;
@@ -548,14 +565,12 @@ jobject ToJavaPath(JNIEnv* env, const std::vector<PathObject::Segment>& segments
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, output.x, output.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, output.x, output.y);
break;
}
@@ -565,7 +580,6 @@ jobject ToJavaPath(JNIEnv* env, const std::vector<PathObject::Segment>& segments
// 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);
}
}
@@ -573,77 +587,131 @@ jobject ToJavaPath(JNIEnv* env, const std::vector<PathObject::Segment>& segments
return java_path;
}
-jobject ToJavaPdfPageObject(JNIEnv* env, const PageObject* page_object,
+jobject ToJavaPdfTextObject(JNIEnv* env, const TextObject* text_object) {
+ // Find Java PdfTextObject Class.
+ static jclass text_object_class = GetPermClassRef(env, kTextObject);
+
+ // Create Java Text String from TextObject Data String.
+ jobject java_string = ToJavaString(env, text_object->text_);
+
+ // Get Native Font Object Data.
+ int font_family = static_cast<int>(text_object->font_.GetFamily());
+ bool bold = text_object->font_.IsBold();
+ bool italic = text_object->font_.IsItalic();
+
+ // Create Java TextObjectFont Instance.
+ static jclass text_font_class = GetPermClassRef(env, kTextFont);
+ static jmethodID init_text_font =
+ env->GetMethodID(text_font_class, "<init>", funcsig("V", "I", "Z", "Z").c_str());
+ jobject java_font = env->NewObject(text_font_class, init_text_font, font_family, bold, italic);
+
+ // Create Java PdfTextObject Instance.
+ static jmethodID init_text_object = env->GetMethodID(
+ text_object_class, "<init>", funcsig("V", kString, kTextFont, "F").c_str());
+ float font_size = text_object->font_size_;
+ jobject java_text_object =
+ env->NewObject(text_object_class, init_text_object, java_string, java_font, font_size);
+
+ // Set Java PdfTextObject Render Mode.
+ int render_mode = static_cast<int>(text_object->render_mode_);
+ static jmethodID set_render_mode = env->GetMethodID(text_object_class, "setRenderMode", "(I)V");
+ env->CallVoidMethod(java_text_object, set_render_mode, render_mode);
+
+ // Set Java PdfTextObject Fill Color.
+ static jmethodID set_fill_color = env->GetMethodID(text_object_class, "setFillColor", "(I)V");
+ env->CallVoidMethod(java_text_object, set_fill_color, ToJavaColorInt(text_object->fill_color_));
+
+ // Set Java PdfTextObject Stroke Color.
+ static jmethodID set_stroke_color =
+ env->GetMethodID(text_object_class, "setStrokeColor", "(I)V");
+ env->CallVoidMethod(java_text_object, set_stroke_color,
+ ToJavaColorInt(text_object->stroke_color_));
+
+ // Set Java PdfTextObject Stroke Width.
+ static jmethodID set_stroke_width =
+ env->GetMethodID(text_object_class, "setStrokeWidth", "(F)V");
+ env->CallVoidMethod(java_text_object, set_stroke_width, text_object->stroke_width_);
+
+ return java_text_object;
+}
+
+jobject ToJavaPdfPathObject(JNIEnv* env, const PathObject* path_object,
ICoordinateConverter* converter) {
- // Check for Native Supported Object.
- if (!page_object) {
- return NULL;
- }
+ // Find Java PdfPathObject 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());
- jobject java_page_object = NULL;
+ // Create Java Path from Native PathSegments.
+ jobject java_path = ToJavaPath(env, path_object->segments_, converter);
- switch (page_object->GetType()) {
- case PageObject::Type::Path: {
- // Cast to PathObject
- const PathObject* path_object = static_cast<const PathObject*>(page_object);
+ // Create Java PdfPathObject Instance.
+ jobject java_path_object = env->NewObject(path_object_class, init_path, java_path);
- // 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());
+ // Set Java PdfPathObject FillColor.
+ if (path_object->is_fill_) {
+ static jmethodID set_fill_color =
+ env->GetMethodID(path_object_class, "setFillColor", funcsig("V", "I").c_str());
- // Create Java Path from Native PathSegments.
- jobject java_path = ToJavaPath(env, path_object->segments, converter);
+ env->CallVoidMethod(java_path_object, set_fill_color,
+ ToJavaColorInt(path_object->fill_color_));
+ }
- // Create Java PathObject Instance.
- java_page_object = env->NewObject(path_object_class, init_path, java_path);
+ // Set Java PdfPathObject StrokeColor.
+ if (path_object->is_stroke_) {
+ static jmethodID set_stroke_color =
+ env->GetMethodID(path_object_class, "setStrokeColor", funcsig("V", "I").c_str());
- // 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_path_object, set_stroke_color,
+ ToJavaColorInt(path_object->stroke_color_));
+ }
- env->CallVoidMethod(java_page_object, set_fill_color,
- ToJavaColor(env, path_object->fill_color));
- }
+ // Set Java Stroke Width.
+ static jmethodID set_stroke_width =
+ env->GetMethodID(path_object_class, "setStrokeWidth", "(F)V");
+ env->CallVoidMethod(java_path_object, set_stroke_width, path_object->stroke_width_);
- // 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());
+ return java_path_object;
+}
- env->CallVoidMethod(java_page_object, set_stroke_color,
- ToJavaColor(env, path_object->stroke_color));
- }
+jobject ToJavaPdfImageObject(JNIEnv* env, const ImageObject* image_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());
- // 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);
+ // Get Bitmap readable buffer from ImageObject Data.
+ void* buffer = image_object->GetBitmapReadableBuffer();
- break;
- }
- case PageObject::Type::Image: {
- // Cast to ImageObject
- const ImageObject* image_object = static_cast<const ImageObject*>(page_object);
+ // Create Java Bitmap from Native Bitmap Buffer.
+ jobject java_bitmap = ToJavaBitmap(env, buffer, image_object->width_, image_object->height_);
- // 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());
+ // Create Java PdfImageObject Instance.
+ jobject java_image_object = env->NewObject(image_object_class, init_image, java_bitmap);
- // Get Bitmap readable buffer from ImageObject Data.
- void* buffer = image_object->GetBitmapReadableBuffer();
+ return java_image_object;
+}
- // Create Java Bitmap from Native Bitmap Buffer.
- jobject java_bitmap =
- ToJavaBitmap(env, buffer, image_object->width, image_object->height);
+jobject ToJavaPdfPageObject(JNIEnv* env, const PageObject* page_object,
+ ICoordinateConverter* converter) {
+ // Check for Native Supported Object.
+ if (!page_object) {
+ return NULL;
+ }
- // Create Java ImageObject Instance.
- java_page_object = env->NewObject(image_object_class, init_image, java_bitmap);
+ jobject java_page_object = NULL;
+ switch (page_object->GetType()) {
+ case PageObject::Type::Path: {
+ const PathObject* path_object = static_cast<const PathObject*>(page_object);
+ java_page_object = ToJavaPdfPathObject(env, path_object, converter);
+ break;
+ }
+ case PageObject::Type::Image: {
+ const ImageObject* image_object = static_cast<const ImageObject*>(page_object);
+ java_page_object = ToJavaPdfImageObject(env, image_object);
break;
}
default:
@@ -655,13 +723,14 @@ jobject ToJavaPdfPageObject(JNIEnv* env, const PageObject* page_object,
return NULL;
}
- // Find Java PageObject class
+ // Find Java PageObject Class.
static jclass page_object_class = GetPermClassRef(env, kPageObject);
- // Set Java Matrix
+ // Set Java PdfPageObject 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));
+ env->CallVoidMethod(java_page_object, set_matrix,
+ ToJavaMatrix(env, page_object->device_matrix_));
return java_page_object;
}
@@ -692,120 +761,228 @@ Color ToNativeColor(JNIEnv* env, jobject java_color) {
return ToNativeColor(java_color_int);
}
-std::unique_ptr<PageObject> ToNativePageObject(JNIEnv* env, jobject java_page_object,
- ICoordinateConverter* converter) {
- // 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);
+std::unique_ptr<TextObject> ToNativeTextObject(JNIEnv* env, jobject java_text_object) {
+ // Create TextObject Data Instance.
+ auto text_object = std::make_unique<TextObject>();
- // Pointer to PageObject
- std::unique_ptr<PageObject> page_object = nullptr;
+ // Get Ref to Java PdfTextObject Class.
+ static jclass text_object_class = GetPermClassRef(env, kTextObject);
- 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");
- // The acceptable error while approximating a Path Curve with a line.
- static const float acceptable_error = 0.5f;
- jfloatArray java_approximate =
- (jfloatArray)env->CallObjectMethod(java_path, approximate, acceptable_error);
- 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) {
- // Get DeviceToPage Coordinates
- Point_f output =
- converter->DeviceToPage({path_approximate[i + 1], path_approximate[i + 2]});
- if (i == 0 || path_approximate[i] == path_approximate[i - 3]) {
- segments.emplace_back(PathObject::Segment::Command::Move, output.x, output.y);
- } else {
- segments.emplace_back(PathObject::Segment::Command::Line, output.x, output.y);
- }
- }
+ // Get Java PdfTextObject Font.
+ static jmethodID get_text_font =
+ env->GetMethodID(text_object_class, "getFont", funcsig(kTextFont).c_str());
+ jobject java_text_font = env->CallObjectMethod(java_text_object, get_text_font);
- // 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);
+ // Find Java PdfTextObjectFont Class.
+ static jclass text_font_class = GetPermClassRef(env, kTextFont);
- // 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 the Font Family for the PdfTextObjectFont.
+ static jmethodID get_font_family = env->GetMethodID(text_font_class, "getFontFamily", "()I");
+ jint font_family = env->CallIntMethod(java_text_font, get_font_family);
- // 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);
+ // Is PdfTextObjectFont Bold.
+ static jmethodID is_bold = env->GetMethodID(text_font_class, "isBold", "()Z");
+ jboolean bold = env->CallBooleanMethod(java_text_font, is_bold);
- // 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);
- }
+ // Is PdfTextObjectFont Italic.
+ static jmethodID is_italic = env->GetMethodID(text_font_class, "isItalic", "()Z");
+ jboolean italic = env->CallBooleanMethod(java_text_font, is_italic);
+
+ // Set TextObject Data Font.
+ if (font_family < 0 || font_family >= font_names.size()) {
+ return nullptr;
+ }
+ text_object->font_ =
+ Font(font_names[font_family], static_cast<Font::Family>(font_family), bold, italic);
+
+ // Get Java PdfTextObject font size.
+ static jmethodID get_font_size = env->GetMethodID(text_object_class, "getFontSize", "()F");
+ jfloat font_size = env->CallFloatMethod(java_text_object, get_font_size);
+
+ // Set TextObject Data font size.
+ text_object->font_size_ = font_size;
- // 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);
+ // Get Java PdfTextObject Text.
+ static jmethodID get_text =
+ env->GetMethodID(text_object_class, "getText", funcsig(kString).c_str());
+ jstring java_text = static_cast<jstring>(env->CallObjectMethod(java_text_object, get_text));
- // Set PathObject Data Stroke Width.
- path_object->stroke_width = stroke_width;
+ // Set TextObject Data Text.
+ text_object->text_ = ToNativeWideString(env, java_text);
- page_object = std::move(path_object);
+ // Get Java PdfTextObject RenderMode.
+ static jmethodID get_render_mode = env->GetMethodID(text_object_class, "getRenderMode", "()I");
+ jint render_mode = env->CallIntMethod(java_text_object, get_render_mode);
+
+ // Set TextObject Data RenderMode.
+ switch (static_cast<TextObject::RenderMode>(render_mode)) {
+ case TextObject::RenderMode::Fill: {
+ text_object->render_mode_ = TextObject::RenderMode::Fill;
break;
}
- case PageObject::Type::Image: {
- // Create ImageObject Data Instance.
- auto image_object = std::make_unique<ImageObject>();
+ case TextObject::RenderMode::Stroke: {
+ text_object->render_mode_ = TextObject::RenderMode::Stroke;
+ break;
+ }
+ case TextObject::RenderMode::FillStroke: {
+ text_object->render_mode_ = TextObject::RenderMode::FillStroke;
+ break;
+ }
+ default: {
+ text_object->render_mode_ = TextObject::RenderMode::Unknown;
+ break;
+ }
+ }
- // Get Ref to Java ImageObject Class.
- static jclass image_object_class = GetPermClassRef(env, kImageObject);
+ // Get Java PdfTextObject Fill Color.
+ static jmethodID get_fill_color = env->GetMethodID(text_object_class, "getFillColor", "()I");
+ jint java_fill_color = env->CallIntMethod(java_text_object, get_fill_color);
- // 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);
+ // Set TextObject Data Fill Color
+ text_object->fill_color_ = ToNativeColor(java_fill_color);
- // Create an FPDF_BITMAP from the Android Bitmap
- void* bitmap_pixels;
- if (AndroidBitmap_lockPixels(env, java_bitmap, &bitmap_pixels) < 0) {
- break;
- }
+ // Get Java PdfTextObject Stroke Color.
+ static jmethodID get_stroke_color =
+ env->GetMethodID(text_object_class, "getStrokeColor", "()I");
+ jint java_stroke_color = env->CallIntMethod(java_text_object, get_stroke_color);
+
+ // Set TextObject Data Stroke Color.
+ text_object->stroke_color_ = ToNativeColor(java_stroke_color);
+
+ // Get Java PdfTextObject Stroke Width.
+ static jmethodID get_stroke_width =
+ env->GetMethodID(text_object_class, "getStrokeWidth", "()F");
+ jfloat stroke_width = env->CallFloatMethod(java_text_object, get_stroke_width);
+
+ // Set TextObject Data Stroke Width.
+ text_object->stroke_width_ = stroke_width;
- AndroidBitmapInfo bitmap_info;
- AndroidBitmap_getInfo(env, java_bitmap, &bitmap_info);
- const int stride = bitmap_info.width * 4;
+ return text_object;
+}
+
+std::unique_ptr<PathObject> ToNativePathObject(JNIEnv* env, jobject java_path_object,
+ ICoordinateConverter* converter) {
+ // 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_path_object, to_path);
+
+ // Find Java Path Class.
+ static jclass path_class = GetPermClassRef(env, kPath);
- // Set ImageObject Data Bitmap
- image_object->bitmap = ScopedFPDFBitmap(FPDFBitmap_CreateEx(
- bitmap_info.width, bitmap_info.height, FPDFBitmap_BGRA, bitmap_pixels, stride));
+ // Get the Approximate Array for the Path.
+ static jmethodID approximate = env->GetMethodID(path_class, "approximate", "(F)[F");
+ // The acceptable error while approximating a Path Curve with a line.
+ static const float acceptable_error = 0.5f;
+ jfloatArray java_approximate =
+ (jfloatArray)env->CallObjectMethod(java_path, approximate, acceptable_error);
+ const jsize size = env->GetArrayLength(java_approximate);
- // Unlock the Android Bitmap
- AndroidBitmap_unlockPixels(env, java_bitmap);
+ // 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) {
+ // Get DeviceToPage Coordinates
+ Point_f output =
+ converter->DeviceToPage({path_approximate[i + 1], path_approximate[i + 2]});
+ if (i == 0 || path_approximate[i] == path_approximate[i - 3]) {
+ segments.emplace_back(PathObject::Segment::Command::Move, output.x, output.y);
+ } else {
+ segments.emplace_back(PathObject::Segment::Command::Line, output.x, output.y);
+ }
+ }
+
+ // Get Java PathObject Fill Color.
+ static jmethodID get_fill_color =
+ env->GetMethodID(path_object_class, "getFillColor", funcsig("I").c_str());
+ jint java_fill_color = env->CallIntMethod(java_path_object, get_fill_color);
+
+ // Set PathObject Data Fill Mode and Fill Color
+ path_object->is_fill_ = (java_fill_color != 0);
+ if (path_object->is_fill_) {
+ path_object->fill_color_ = ToNativeColor(java_fill_color);
+ }
+
+ // Get Java PathObject Stroke Color.
+ static jmethodID get_stroke_color =
+ env->GetMethodID(path_object_class, "getStrokeColor", funcsig("I").c_str());
+ jint java_stroke_color = env->CallIntMethod(java_path_object, get_stroke_color);
+
+ // Set PathObject Data Stroke Mode and Stroke Color.
+ path_object->is_stroke_ = (java_stroke_color != 0);
+ if (path_object->is_stroke_) {
+ path_object->stroke_color_ = ToNativeColor(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_path_object, get_stroke_width);
+
+ // Set PathObject Data Stroke Width.
+ path_object->stroke_width_ = stroke_width;
+
+ return path_object;
+}
+
+std::unique_ptr<ImageObject> ToNativeImageObject(JNIEnv* env, jobject java_image_object) {
+ // 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_image_object, get_bitmap);
+
+ // Create an FPDF_BITMAP from the Android Bitmap.
+ void* bitmap_pixels;
+ if (AndroidBitmap_lockPixels(env, java_bitmap, &bitmap_pixels) < 0) {
+ return nullptr;
+ }
+
+ 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);
+
+ return image_object;
+}
- page_object = std::move(image_object);
+std::unique_ptr<PageObject> ToNativePageObject(JNIEnv* env, jobject java_page_object,
+ ICoordinateConverter* converter) {
+ // 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: {
+ page_object = ToNativePathObject(env, java_page_object, converter);
+ break;
+ }
+ case PageObject::Type::Image: {
+ page_object = ToNativeImageObject(env, java_page_object);
break;
}
default:
@@ -826,9 +1003,9 @@ std::unique_ptr<PageObject> ToNativePageObject(JNIEnv* env, jobject java_page_ob
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*/]};
+ page_object->device_matrix_ = {transform[0 /*kMScaleX*/], transform[3 /*kMSkewY*/],
+ transform[1 /*kMSkewX*/], transform[4 /*kMScaleY*/],
+ transform[2 /*kMTransX*/], transform[5 /*kMTransY*/]};
return page_object;
}
@@ -840,9 +1017,9 @@ jobject ToJavaPageAnnotations(JNIEnv* env, const vector<Annotation*>& annotation
jobject ToJavaStampAnnotation(JNIEnv* env, const Annotation* annotation,
ICoordinateConverter* converter) {
- jobject java_bounds = ToJavaRectF(env, annotation->GetBounds(), converter);
// Cast to StampAnnotation
const StampAnnotation* stamp_annotation = static_cast<const StampAnnotation*>(annotation);
+ jobject java_bounds = ToJavaRectF(env, stamp_annotation->GetBounds(), converter);
// Find Java StampAnnotation Class.
static jclass stamp_annotation_class = GetPermClassRef(env, kStampAnnotation);
@@ -870,16 +1047,17 @@ jobject ToJavaStampAnnotation(JNIEnv* env, const Annotation* annotation,
jobject ToJavaHighlightAnnotation(JNIEnv* env, const Annotation* annotation,
ICoordinateConverter* converter) {
- jobject java_bounds = ToJavaRectF(env, annotation->GetBounds(), converter);
// Cast to HighlightAnnotation
const HighlightAnnotation* highlight_annotation =
static_cast<const HighlightAnnotation*>(annotation);
+ jobject java_bounds =
+ ToJavaList(env, highlight_annotation->GetBounds(), converter, &ToJavaRectF);
// Find Java HighlightAnnotation Class.
static jclass highlight_annotation_class = GetPermClassRef(env, kHighlightAnnotation);
// Get Constructor Id.
static jmethodID init =
- env->GetMethodID(highlight_annotation_class, "<init>", funcsig("V", kRectF).c_str());
+ env->GetMethodID(highlight_annotation_class, "<init>", funcsig("V", kList).c_str());
// Create Java HighlightAnnotation Instance.
jobject java_annotation = env->NewObject(highlight_annotation_class, init, java_bounds);
@@ -897,11 +1075,11 @@ jobject ToJavaHighlightAnnotation(JNIEnv* env, const Annotation* annotation,
jobject ToJavaFreeTextAnnotation(JNIEnv* env, const Annotation* annotation,
ICoordinateConverter* converter) {
- jobject java_bounds = ToJavaRectF(env, annotation->GetBounds(), converter);
-
// Cast to FreeText Annotation
const FreeTextAnnotation* freetext_annotation =
static_cast<const FreeTextAnnotation*>(annotation);
+
+ jobject java_bounds = ToJavaRectF(env, freetext_annotation->GetBounds(), converter);
// Find Java FreeTextAnnotation class.
static jclass freetext_annotation_class = GetPermClassRef(env, kFreeTextAnnotation);
// Get Constructor Id.
@@ -909,7 +1087,7 @@ jobject ToJavaFreeTextAnnotation(JNIEnv* env, const Annotation* annotation,
funcsig("V", kRectF, kString).c_str());
// Get Java String for text content.
- jobject java_string = wstringToJstringUTF16(env, freetext_annotation->GetTextContent());
+ jobject java_string = ToJavaString(env, freetext_annotation->GetTextContent());
// Create Java FreeTextAnnotation Object.
jobject java_freetext_annotation =
env->NewObject(freetext_annotation_class, init, java_bounds, java_string);
@@ -960,14 +1138,18 @@ jobject ToJavaPageAnnotation(JNIEnv* env, const Annotation* annotation,
}
std::unique_ptr<Annotation> ToNativeStampAnnotation(JNIEnv* env, jobject java_annotation,
- Rectangle_f native_bounds,
ICoordinateConverter* converter) {
- // Create StampAnnotation Instance.
- auto stamp_annotation = std::make_unique<StampAnnotation>(native_bounds);
-
// Get Ref to Java StampAnnotation Class.
static jclass stamp_annotation_class = GetPermClassRef(env, kStampAnnotation);
+ jmethodID get_bounds =
+ env->GetMethodID(stamp_annotation_class, "getBounds", funcsig(kRectF).c_str());
+ jobject java_bounds = env->CallObjectMethod(java_annotation, get_bounds);
+ Rectangle_f native_bounds = ToNativeRectF(env, java_bounds, converter);
+
+ // Create StampAnnotation Instance.
+ auto stamp_annotation = std::make_unique<StampAnnotation>(native_bounds);
+
// Get PdfPageObjects from stamp annotation
static jmethodID get_objects =
env->GetMethodID(stamp_annotation_class, "getObjects", funcsig(kList).c_str());
@@ -988,13 +1170,30 @@ std::unique_ptr<Annotation> ToNativeStampAnnotation(JNIEnv* env, jobject java_an
}
std::unique_ptr<Annotation> ToNativeHighlightAnnotation(JNIEnv* env, jobject java_annotation,
- Rectangle_f native_bounds) {
- // Create HighlightAnnotation Instance.
- auto highlight_annotation = std::make_unique<HighlightAnnotation>(native_bounds);
-
+ ICoordinateConverter* converter) {
// Get Ref to Java HighlightAnnotation Class.
static jclass highlight_annotation_class = GetPermClassRef(env, kHighlightAnnotation);
+ jmethodID get_bounds =
+ env->GetMethodID(highlight_annotation_class, "getBounds", funcsig(kList).c_str());
+ jobject java_bounds = env->CallObjectMethod(java_annotation, get_bounds);
+
+ vector<Rectangle_f> native_bounds;
+
+ jclass list_class = env->FindClass(kList);
+ jmethodID size_method = env->GetMethodID(list_class, "size", funcsig("I").c_str());
+ jmethodID get_method = env->GetMethodID(list_class, "get", funcsig(kObject, "I").c_str());
+
+ jint listSize = env->CallIntMethod(java_bounds, size_method);
+ for (int i = 0; i < listSize; i++) {
+ jobject java_bound = env->CallObjectMethod(java_bounds, get_method, i);
+ Rectangle_f native_bound = ToNativeRectF(env, java_bound, converter);
+ native_bounds.push_back(native_bound);
+ }
+
+ // Create HighlightAnnotation Instance.
+ auto highlight_annotation = std::make_unique<HighlightAnnotation>(native_bounds);
+
// Get and set highlight color
// Get methodId for getColor
@@ -1008,13 +1207,18 @@ std::unique_ptr<Annotation> ToNativeHighlightAnnotation(JNIEnv* env, jobject jav
}
std::unique_ptr<Annotation> ToNativeFreeTextAnnotation(JNIEnv* env, jobject java_annotation,
- Rectangle_f native_bounds) {
- // Create FreeTextAnnotation Instance.
- auto freetext_annotation = std::make_unique<FreeTextAnnotation>(native_bounds);
-
+ ICoordinateConverter* converter) {
// Get Ref to Java FreeTextAnnotation Class.
static jclass freetext_annotation_class = GetPermClassRef(env, kFreeTextAnnotation);
+ jmethodID get_bounds =
+ env->GetMethodID(freetext_annotation_class, "getBounds", funcsig(kRectF).c_str());
+ jobject java_bounds = env->CallObjectMethod(java_annotation, get_bounds);
+ Rectangle_f native_bounds = ToNativeRectF(env, java_bounds, converter);
+
+ // Create FreeTextAnnotation Instance.
+ auto freetext_annotation = std::make_unique<FreeTextAnnotation>(native_bounds);
+
// Get the TextContent from Java layer.
static jmethodID get_text_content =
env->GetMethodID(freetext_annotation_class, "getTextContent", funcsig(kString).c_str());
@@ -1022,7 +1226,7 @@ std::unique_ptr<Annotation> ToNativeFreeTextAnnotation(JNIEnv* env, jobject java
static_cast<jstring>(env->CallObjectMethod(java_annotation, get_text_content));
// Set the TextContent
- std::wstring native_text_content = jStringToWstring(env, java_text_content);
+ std::wstring native_text_content = ToNativeWideString(env, java_text_content);
freetext_annotation->SetTextContent(native_text_content);
// Get the text color
@@ -1050,24 +1254,19 @@ std::unique_ptr<Annotation> ToNativePageAnnotation(JNIEnv* env, jobject java_ann
env->GetMethodID(annotation_class, "getPdfAnnotationType", funcsig("I").c_str());
jint annotation_type = env->CallIntMethod(java_annotation, get_type);
- // 2. Get bounds
- jmethodID get_bounds = env->GetMethodID(annotation_class, "getBounds", funcsig(kRectF).c_str());
- jobject java_bounds = env->CallObjectMethod(java_annotation, get_bounds);
- Rectangle_f native_bounds = ToNativeRectF(env, java_bounds, converter);
-
std::unique_ptr<Annotation> annotation = nullptr;
switch (static_cast<Annotation::Type>(annotation_type)) {
case Annotation::Type::Stamp: {
- annotation = ToNativeStampAnnotation(env, java_annotation, native_bounds, converter);
+ annotation = ToNativeStampAnnotation(env, java_annotation, converter);
break;
}
case Annotation::Type::Highlight: {
- annotation = ToNativeHighlightAnnotation(env, java_annotation, native_bounds);
+ annotation = ToNativeHighlightAnnotation(env, java_annotation, converter);
break;
}
case Annotation::Type::FreeText: {
- annotation = ToNativeFreeTextAnnotation(env, java_annotation, native_bounds);
+ annotation = ToNativeFreeTextAnnotation(env, java_annotation, converter);
break;
}
default:
diff --git a/pdf/framework/libs/pdfClient/jni_conversion.h b/pdf/framework/libs/pdfClient/jni_conversion.h
index c7648b9e5..2b679405b 100644
--- a/pdf/framework/libs/pdfClient/jni_conversion.h
+++ b/pdf/framework/libs/pdfClient/jni_conversion.h
@@ -26,6 +26,7 @@
#include "form_widget_info.h"
#include "page.h"
#include "page_object.h"
+#include "path_object.h"
#include "rect.h"
using pdfClient::Annotation;
diff --git a/pdf/framework/libs/pdfClient/page.cc b/pdf/framework/libs/pdfClient/page.cc
index 58db94fda..faa84b34b 100644
--- a/pdf/framework/libs/pdfClient/page.cc
+++ b/pdf/framework/libs/pdfClient/page.cc
@@ -32,9 +32,12 @@
#include "fpdf_doc.h"
#include "fpdf_text.h"
#include "fpdfview.h"
+#include "image_object.h"
#include "logging.h"
#include "normalize.h"
+#include "path_object.h"
#include "rect.h"
+#include "text_object.h"
#include "utf.h"
#include "utils/annot_hider.h"
#include "utils/text.h"
@@ -462,7 +465,7 @@ 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 = pageObject->CreateFPDFInstance(document_);
+ ScopedFPDFPageObject scoped_page_object(pageObject->CreateFPDFInstance(document_, page_.get()));
// Check if a FPDF page object was created.
if (!scoped_page_object) {
@@ -509,7 +512,7 @@ bool Page::UpdatePageObject(int index, std::unique_ptr<PageObject> pageObject) {
FPDF_PAGEOBJECT page_object = FPDFPage_GetObject(page_.get(), index);
// Update PDFium PageObject
- if (!pageObject->UpdateFPDFInstance(page_object)) {
+ if (!pageObject->UpdateFPDFInstance(page_object, page_.get())) {
return false;
}
@@ -805,6 +808,10 @@ void Page::PopulatePageObjects(bool refetch) {
std::unique_ptr<PageObject> page_object_ = nullptr;
switch (type) {
+ case FPDF_PAGEOBJ_TEXT: {
+ page_object_ = std::make_unique<TextObject>();
+ break;
+ }
case FPDF_PAGEOBJ_PATH: {
page_object_ = std::make_unique<PathObject>();
break;
@@ -818,7 +825,7 @@ void Page::PopulatePageObjects(bool refetch) {
}
// Populate PageObject From Page
- if (page_object_ && page_object_->PopulateFromFPDFInstance(page_object)) {
+ if (page_object_ && page_object_->PopulateFromFPDFInstance(page_object, page_.get())) {
page_objects_[index] = std::move(page_object_);
}
}
@@ -851,25 +858,48 @@ void Page::PopulateAnnotations() {
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 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: {
+ FS_RECTF rect;
+ if (!FPDFAnnot_GetRect(scoped_annot.get(), &rect)) {
+ LOGE("Failed to get the bounds of the annotation");
+ break;
+ }
+ auto bounds = Rectangle_f{rect.left, rect.top, rect.right, rect.bottom};
annotation = std::make_unique<StampAnnotation>(bounds);
break;
}
case FPDF_ANNOT_HIGHLIGHT: {
+ vector<Rectangle_f> bounds;
+ auto num_bounds = FPDFAnnot_CountAttachmentPoints(scoped_annot.get());
+ if (num_bounds > 0) {
+ bounds.resize(num_bounds);
+ for (auto bound_index = 0; bound_index < num_bounds; bound_index++) {
+ FS_QUADPOINTSF quad_points;
+ if (!FPDFAnnot_GetAttachmentPoints(scoped_annot.get(), bound_index,
+ &quad_points)) {
+ LOGD("Failed to get quad points from pdfium");
+ break;
+ }
+
+ bounds[bound_index] = Rectangle_f(quad_points.x1, quad_points.y1,
+ quad_points.x2, quad_points.y4);
+ }
+ } else {
+ LOGD("Failed to find bounds for highlight annotation");
+ }
annotation = std::make_unique<HighlightAnnotation>(bounds);
break;
}
case FPDF_ANNOT_FREETEXT: {
+ FS_RECTF rect;
+ if (!FPDFAnnot_GetRect(scoped_annot.get(), &rect)) {
+ LOGE("Failed to get the bounds of the annotation");
+ break;
+ }
+ auto bounds = Rectangle_f{rect.left, rect.top, rect.right, rect.bottom};
annotation = std::make_unique<FreeTextAnnotation>(bounds);
break;
}
@@ -878,7 +908,8 @@ void Page::PopulateAnnotations() {
}
}
- if (!annotation || !annotation->PopulateFromPdfiumInstance(scoped_annot.get())) {
+ if (!annotation ||
+ !annotation->PopulateFromPdfiumInstance(scoped_annot.get(), page_.get())) {
LOGE("Failed to create a pdfClient's instance of annotation using pdfium "
"instance");
}
@@ -944,7 +975,7 @@ bool Page::UpdatePageAnnotation(int index, std::unique_ptr<Annotation> annotatio
return false;
}
- if (!annotation->UpdatePdfiumInstance(scoped_annot.get(), document_)) {
+ if (!annotation->UpdatePdfiumInstance(scoped_annot.get(), document_, page_.get())) {
LOGE("Failed to update pdfium annotation's instance");
return false;
}
diff --git a/pdf/framework/libs/pdfClient/page_object.cc b/pdf/framework/libs/pdfClient/page_object.cc
index de2183b9b..c8cfd8313 100644
--- a/pdf/framework/libs/pdfClient/page_object.cc
+++ b/pdf/framework/libs/pdfClient/page_object.cc
@@ -23,223 +23,70 @@
#include "fpdf_edit.h"
#include "fpdfview.h"
#include "logging.h"
+#include "rect.h"
#define LOG_TAG "page_object"
namespace pdfClient {
-PageObject::PageObject(Type type) : type(type) {}
+PageObject::PageObject(Type type) : type_(type) {}
PageObject::Type PageObject::GetType() const {
- return type;
+ 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) {
+bool PageObject::GetPageToDeviceMatrix(FPDF_PAGEOBJECT page_object, FPDF_PAGE page) {
+ Matrix page_matrix;
+ if (!FPDFPageObj_GetMatrix(page_object, reinterpret_cast<FS_MATRIX*>(&page_matrix))) {
+ LOGE("GetPageMatrix failed!");
return false;
}
- // Check for Type Correctness.
- if (FPDFPageObj_GetType(path_object) != FPDF_PAGEOBJ_PATH) {
- return false;
- }
+ // Set identity transformation for GetBounds.
+ Matrix identity = {1, 0, 0, 1, 0, 0};
+ FPDFPageObj_SetMatrix(page_object, reinterpret_cast<FS_MATRIX*>(&identity));
- // 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 Bounds.
+ Rectangle_f bounds;
+ FPDFPageObj_GetBounds(page_object, &bounds.left, &bounds.bottom, &bounds.right, &bounds.top);
- // 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;
+ // Reset the original page matrix.
+ FPDFPageObj_SetMatrix(page_object, reinterpret_cast<FS_MATRIX*>(&page_matrix));
- // Get Matrix
- if (!FPDFPageObj_GetMatrix(path_object, reinterpret_cast<FS_MATRIX*>(&matrix))) {
- return false;
- }
+ float page_height = FPDF_GetPageHeightF(page);
- // 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);
+ // Page to device matrix.
+ device_matrix_.a = page_matrix.a;
+ device_matrix_.b = (page_matrix.b != 0) ? -page_matrix.b : 0;
+ device_matrix_.c = (page_matrix.c != 0) ? -page_matrix.c : 0;
+ device_matrix_.d = page_matrix.d;
+ device_matrix_.e = page_matrix.e + ((bounds.top + bounds.bottom) * page_matrix.c);
+ device_matrix_.f = page_height - page_matrix.f - ((bounds.top + bounds.bottom) * page_matrix.d);
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) {
+bool PageObject::SetDeviceToPageMatrix(FPDF_PAGEOBJECT page_object, FPDF_PAGE page) {
+ // Reset Previous Transformation.
+ Matrix identity = {1, 0, 0, 1, 0, 0};
+ if (!FPDFPageObj_SetMatrix(page_object, reinterpret_cast<FS_MATRIX*>(&identity))) {
+ LOGE("SetMatrix failed!");
return false;
}
- // Check for Type Correctness.
- if (FPDFPageObj_GetType(image_object) != FPDF_PAGEOBJ_IMAGE) {
- return false;
- }
+ Rectangle_f bounds;
+ FPDFPageObj_GetBounds(page_object, &bounds.left, &bounds.bottom, &bounds.right, &bounds.top);
- // Set the updated bitmap.
- if (!FPDFImageObj_SetBitmap(nullptr, 0, image_object, bitmap.get())) {
- return false;
- }
+ float page_height = FPDF_GetPageHeightF(page);
- // 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());
+ FPDFPageObj_Transform(page_object, 1, 0, 0, 1, 0, -(bounds.top + bounds.bottom));
+ FPDFPageObj_Transform(page_object, device_matrix_.a, -device_matrix_.b, -device_matrix_.c,
+ device_matrix_.d, device_matrix_.e, -device_matrix_.f);
+ FPDFPageObj_Transform(page_object, 1, 0, 0, 1, 0, page_height);
return true;
}
-void* ImageObject::GetBitmapReadableBuffer() const {
- return FPDFBitmap_GetBuffer(bitmap.get());
-}
-
-ImageObject::~ImageObject() = default;
+PageObject::~PageObject() = 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 d06270b08..50df89616 100644
--- a/pdf/framework/libs/pdfClient/page_object.h
+++ b/pdf/framework/libs/pdfClient/page_object.h
@@ -19,7 +19,7 @@
#include <stdint.h>
-#include <vector>
+#include <algorithm>
#include "cpp/fpdf_scopers.h"
#include "fpdfview.h"
@@ -34,11 +34,12 @@ struct Color {
uint b;
uint a;
+ Color() : Color(0, 0, 0, 255) {}
Color(uint r, uint g, uint b, uint a) : r(r), g(g), b(b), a(a) {}
- Color() : Color(INVALID_COLOR, INVALID_COLOR, INVALID_COLOR, INVALID_COLOR) {}
- private:
- static constexpr uint INVALID_COLOR = 256;
+ bool operator==(const Color& other) const {
+ return r == other.r && g == other.g && b == other.b && a == other.a;
+ }
};
struct Matrix {
@@ -48,88 +49,56 @@ struct Matrix {
float d;
float e;
float f;
+
+ Matrix() {}
+ Matrix(float a, float b, float c, float d, float e, float f)
+ : a(a), b(b), c(c), d(d), e(e), f(f) {}
+
+ bool operator==(const Matrix& other) const {
+ return a == other.a && b == other.b && c == other.c && d == other.d && e == other.e &&
+ f == other.f;
+ }
+
+ float operator-(const Matrix& other) const {
+ return std::max({abs(a - other.a), abs(b - other.b), abs(c - other.c), abs(d - other.d),
+ abs(e - other.e), abs(f - other.f)});
+ }
};
class PageObject {
public:
enum class Type {
Unknown = 0,
+ Text = 1,
Path = 2,
Image = 3,
};
Type GetType() const;
// Returns a FPDF Instance for a PageObject.
- virtual ScopedFPDFPageObject CreateFPDFInstance(FPDF_DOCUMENT document) = 0;
+ virtual ScopedFPDFPageObject CreateFPDFInstance(FPDF_DOCUMENT document, FPDF_PAGE page) = 0;
// Updates the FPDF Instance of PageObject present on Page.
- virtual bool UpdateFPDFInstance(FPDF_PAGEOBJECT page_object) = 0;
+ virtual bool UpdateFPDFInstance(FPDF_PAGEOBJECT page_object, FPDF_PAGE page) = 0;
// Populates data from FPDFInstance of PageObject present on Page.
- virtual bool PopulateFromFPDFInstance(FPDF_PAGEOBJECT page_object) = 0;
+ virtual bool PopulateFromFPDFInstance(FPDF_PAGEOBJECT page_object, FPDF_PAGE page) = 0;
virtual ~PageObject();
- Matrix matrix; // Matrix used to scale, rotate, shear and translate the page object.
- Color fill_color;
- Color stroke_color;
- float stroke_width = 1.0f;
+ Matrix device_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;
-};
-
-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
+ virtual bool GetPageToDeviceMatrix(FPDF_PAGEOBJECT page_object, FPDF_PAGE page);
+ virtual bool SetDeviceToPageMatrix(FPDF_PAGEOBJECT page_object, FPDF_PAGE page);
- 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 = false;
- bool is_stroke = false;
-
- 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;
-
- void* GetBitmapReadableBuffer() const;
-
- ~ImageObject();
-
- int width = 0;
- int height = 0;
- ScopedFPDFBitmap bitmap;
+ private:
+ Type type_;
};
} // namespace pdfClient
-#endif // MEDIAPROVIDER_PDF_JNI_PDFCLIENT_PAGE_OBJECT_H_ \ No newline at end of file
+#endif // MEDIAPROVIDER_PDF_JNI_PDFCLIENT_PAGE_OBJECT_H_
diff --git a/pdf/framework/libs/pdfClient/page_test.cc b/pdf/framework/libs/pdfClient/page_test.cc
index 6f29621fe..79dfeea84 100644
--- a/pdf/framework/libs/pdfClient/page_test.cc
+++ b/pdf/framework/libs/pdfClient/page_test.cc
@@ -23,11 +23,13 @@
#include <memory>
#include <string>
-#include "page_object.h"
-
// Goes first due to conflicts.
#include "document.h"
+#include "image_object.h"
+#include "page_object.h"
+#include "path_object.h"
#include "rect.h"
+#include "text_object.h"
// #include "file/base/path.h"
#include "cpp/fpdf_scopers.h"
#include "fpdfview.h"
@@ -36,14 +38,22 @@ namespace {
using ::pdfClient::Annotation;
using ::pdfClient::Color;
+using ::pdfClient::CourierNew;
using ::pdfClient::Document;
+using ::pdfClient::Font;
+using ::pdfClient::font_names;
using ::pdfClient::FreeTextAnnotation;
+using ::pdfClient::Helvetica;
using ::pdfClient::ImageObject;
+using ::pdfClient::Matrix;
using ::pdfClient::Page;
using ::pdfClient::PageObject;
using ::pdfClient::PathObject;
using ::pdfClient::Rectangle_i;
using ::pdfClient::StampAnnotation;
+using ::pdfClient::Symbol;
+using ::pdfClient::TextObject;
+using ::pdfClient::TimesNewRoman;
static const std::string kTestdata = "testdata";
static const std::string kSekretNoPassword = "sekret_no_password.pdf";
@@ -217,11 +227,13 @@ TEST(Test, GetPageObjectsTest) {
std::vector<PageObject*> pageObjects = page->GetPageObjects();
// Check for PageObjects size.
- ASSERT_EQ(2, pageObjects.size());
+ ASSERT_EQ(3, 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());
+ // Check for the third PageObject to be TextObject.
+ ASSERT_EQ(PageObject::Type::Text, pageObjects[2]->GetType());
}
TEST(Test, AddImagePageObjectTest) {
@@ -234,11 +246,11 @@ TEST(Test, AddImagePageObjectTest) {
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);
+ 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};
+ imageObject->device_matrix_ = {1.0f, 0, 0, 1.0f, 0, 0};
// Add the page object.
ASSERT_EQ(page->AddPageObject(std::move(imageObject)), initialPageObjects.size());
@@ -252,8 +264,10 @@ TEST(Test, AddImagePageObjectTest) {
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());
+ // Check for the third PageObject to be TextObject.
+ ASSERT_EQ(PageObject::Type::Text, updatedPageObjects[2]->GetType());
+ // Check for the fourth PageObject to be ImageObject.
+ ASSERT_EQ(PageObject::Type::Image, updatedPageObjects[3]->GetType());
}
TEST(Test, AddPathPageObject) {
@@ -266,16 +280,16 @@ TEST(Test, AddPathPageObject) {
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);
+ 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;
+ pathObject->is_fill_ = false;
+ pathObject->is_stroke_ = true;
// Set PathObject Matrix.
- pathObject->matrix = {1.0f, 0, 0, 1.0f, 0, 0};
+ pathObject->device_matrix_ = {1.0f, 0, 0, 1.0f, 0, 0};
// Add the page object.
ASSERT_EQ(page->AddPageObject(std::move(pathObject)), initialPageObjects.size());
@@ -289,8 +303,72 @@ TEST(Test, AddPathPageObject) {
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());
+ // Check for the third PageObject to be TextObject.
+ ASSERT_EQ(PageObject::Type::Text, updatedPageObjects[2]->GetType());
+ // Check for the fourth PageObject to be PathObject.
+ ASSERT_EQ(PageObject::Type::Path, updatedPageObjects[3]->GetType());
+}
+
+TEST(Test, AddTextPageObject) {
+ Document doc(LoadTestDocument(kPageObject), false);
+ std::shared_ptr<Page> page = doc.GetPage(0);
+
+ std::vector<PageObject*> initialPageObjects = page->GetPageObjects();
+
+ int page_objects_size = initialPageObjects.size();
+
+ // Assert font_names vector contains font name as per right order.
+ ASSERT_EQ(font_names[0], CourierNew);
+ ASSERT_EQ(font_names[1], Helvetica);
+ ASSERT_EQ(font_names[2], Symbol);
+ ASSERT_EQ(font_names[3], TimesNewRoman);
+
+ for (int index = 0; index < font_names.size(); index++) {
+ // Create Text Object.
+ auto textObject = std::make_unique<TextObject>();
+
+ // Set Font.
+ textObject->font_ = Font(font_names[index], static_cast<Font::Family>(index), true, true);
+
+ // Set Font Size.
+ textObject->font_size_ = 10.0f;
+
+ // Set Text.
+ textObject->text_ = L"Hello World!";
+
+ // Set Text Render Mode.
+ textObject->render_mode_ = TextObject::RenderMode::Stroke;
+
+ // Set TextObject Color.
+ textObject->fill_color_ = Color(0, 0, 255, 255);
+
+ // Set TextObject Matrix.
+ textObject->device_matrix_ = {1.0f, 0, 0, 1.0f, 0, 0};
+
+ // Add the page object.
+ if (index != 2) {
+ ASSERT_EQ(page->AddPageObject(std::move(textObject)), page_objects_size++);
+ } else {
+ // Symbol-BoldItalic is not a font.
+ ASSERT_EQ(page->AddPageObject(std::move(textObject)), -1);
+ }
+ }
+
+ // Get Updated PageObjects
+ std::vector<PageObject*> updatedPageObjects = page->GetPageObjects(true);
+
+ // Assert that the size has increased by three.
+ ASSERT_EQ(initialPageObjects.size() + 3, 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 third PageObject to be TextObject.
+ ASSERT_EQ(PageObject::Type::Text, updatedPageObjects[2]->GetType());
+ // Check for the added PageObjects to be TextObjects.
+ for (int index = 3; index < page_objects_size; index++) {
+ ASSERT_EQ(PageObject::Type::Text, updatedPageObjects[index]->GetType());
+ }
}
TEST(Test, RemovePageObjectTest) {
@@ -319,11 +397,12 @@ TEST(Test, UpdateImagePageObjectTest) {
auto imageObject = std::make_unique<ImageObject>();
// Create FPDF Bitmap.
- imageObject->bitmap = ScopedFPDFBitmap(FPDFBitmap_Create(100, 110, 1));
- FPDFBitmap_FillRect(imageObject->bitmap.get(), 0, 0, 100, 110, 0xFF0000FF);
+ 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};
+ Matrix update_matrix = {2.0f, 3.0f, 4.0f, 2.0f, 1.0f, -2.0f};
+ imageObject->device_matrix_ = update_matrix;
// Update the page object.
EXPECT_TRUE(page->UpdatePageObject(0, std::move(imageObject)));
@@ -335,18 +414,13 @@ TEST(Test, UpdateImagePageObjectTest) {
ASSERT_EQ(initialPageObjects.size(), updatedPageObjects.size());
// Check for updated bitmap.
- ASSERT_EQ(FPDFBitmap_GetWidth(static_cast<ImageObject*>(updatedPageObjects[0])->bitmap.get()),
+ ASSERT_EQ(FPDFBitmap_GetWidth(static_cast<ImageObject*>(updatedPageObjects[0])->bitmap_.get()),
100);
- ASSERT_EQ(FPDFBitmap_GetHeight(static_cast<ImageObject*>(updatedPageObjects[0])->bitmap.get()),
+ ASSERT_EQ(FPDFBitmap_GetHeight(static_cast<ImageObject*>(updatedPageObjects[0])->bitmap_.get()),
110);
// Check for updated matrix.
- ASSERT_EQ(updatedPageObjects[0]->matrix.a, 2.0f);
- ASSERT_EQ(updatedPageObjects[0]->matrix.b, 0.0f);
- ASSERT_EQ(updatedPageObjects[0]->matrix.c, 0.0f);
- ASSERT_EQ(updatedPageObjects[0]->matrix.d, 2.0f);
- ASSERT_EQ(updatedPageObjects[0]->matrix.e, 0.0f);
- ASSERT_EQ(updatedPageObjects[0]->matrix.f, 0.0f);
+ ASSERT_EQ(updatedPageObjects[0]->device_matrix_, update_matrix);
}
TEST(Test, UpdatePathPageObjectTest) {
@@ -360,14 +434,16 @@ TEST(Test, UpdatePathPageObjectTest) {
auto pathObject = std::make_unique<PathObject>();
// Update fill Color.
- pathObject->fill_color = Color(255, 0, 0, 255);
+ Color update_fill_color = Color(255, 0, 0, 255);
+ pathObject->fill_color_ = update_fill_color;
// Update Draw Mode.
- pathObject->is_fill_mode = true;
- pathObject->is_stroke = false;
+ pathObject->is_fill_ = true;
+ pathObject->is_stroke_ = false;
// Set Matrix.
- pathObject->matrix = {2.0f, 0, 0, 2.0f, 0, 0};
+ Matrix update_matrix = {2.0f, -3.0f, 2.0f, 2.0f, -1.0f, 2.0f};
+ pathObject->device_matrix_ = update_matrix;
// Update the page object.
EXPECT_TRUE(page->UpdatePageObject(1, std::move(pathObject)));
@@ -376,22 +452,83 @@ TEST(Test, UpdatePathPageObjectTest) {
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);
+ ASSERT_EQ(updatedPageObjects[1]->fill_color_, update_fill_color);
// 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);
+ ASSERT_EQ(static_cast<PathObject*>(updatedPageObjects[1])->is_fill_, 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);
+ ASSERT_EQ(updatedPageObjects[1]->device_matrix_, update_matrix);
+}
+
+TEST(Test, UpdateTextPageObjectTest) {
+ Document doc(LoadTestDocument(kPageObject), false);
+ std::shared_ptr<Page> page = doc.GetPage(0);
+
+ // Get initial page objects.
+ std::vector<PageObject*> initialPageObjects = page->GetPageObjects();
+
+ // Check for third page object to be text object.
+ ASSERT_EQ(PageObject::Type::Text, initialPageObjects[2]->GetType());
+
+ // Check initial text object data.
+ TextObject* initialTextObject = static_cast<TextObject*>(initialPageObjects[2]);
+
+ // Check initial text.
+ ASSERT_EQ(initialTextObject->text_, L"Hello World");
+
+ // Check initial render mode.
+ ASSERT_EQ(initialTextObject->render_mode_, TextObject::RenderMode::Fill);
+
+ // Check for initial matrix.
+ Matrix initial_matrix(1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 759.424f);
+ ASSERT_LT(initialTextObject->device_matrix_ - initial_matrix, 0.01f);
+
+ // Check for initial fill color.
+ ASSERT_EQ(initialTextObject->fill_color_, Color(0, 255, 0, 255));
+
+ // Create Text Object.
+ auto textObject = std::make_unique<TextObject>();
+
+ // Update the text.
+ std::wstring update_text = L"Hello PDF!";
+ textObject->text_ = update_text;
+
+ // Set Text Render Mode.
+ textObject->render_mode_ = TextObject::RenderMode::FillStroke;
+
+ // Update text Color.
+ Color update_fill_color = Color(0, 0, 255, 255);
+ textObject->fill_color_ = update_fill_color;
+
+ // Set Matrix.
+ Matrix update_matrix = {2.0f, 5.0f, -2.0f, 3.0f, -4.0f, 10.0f};
+ textObject->device_matrix_ = update_matrix;
+
+ // Update the page object.
+ EXPECT_TRUE(page->UpdatePageObject(2, std::move(textObject)));
+
+ // Get the updated page objects.
+ std::vector<PageObject*> updatedPageObjects = page->GetPageObjects(true);
+
+ // Check for updated text.
+ ASSERT_EQ(static_cast<TextObject*>(updatedPageObjects[2])->text_, update_text);
+
+ // Check for updated text render mode.
+ ASSERT_EQ(static_cast<TextObject*>(updatedPageObjects[2])->render_mode_,
+ TextObject::RenderMode::FillStroke);
+
+ // Check for updated fill Color.
+ ASSERT_EQ(updatedPageObjects[2]->fill_color_, update_fill_color);
+
+ /*
+ * TextObject Transformation is dependent upon its bounds values.
+ * Pdfium calculation for bounds shows a little fluctuation which results
+ * in return matrix values to be not exactly the same. We should tolerate
+ * the difference which does not make any visible difference.
+ */
+ ASSERT_LT(updatedPageObjects[2]->device_matrix_ - update_matrix, 0.01f);
}
TEST(Test, GetPageAnnotationsTest) {
@@ -433,11 +570,11 @@ TEST(Test, AddStampAnnotationTest) {
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);
+ 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};
+ imageObject->device_matrix_ = {1.0f, 0, 0, 1.0f, 0, 0};
// Add the page object.
stampAnnotation->AddObject(std::move(imageObject));
@@ -446,16 +583,16 @@ TEST(Test, AddStampAnnotationTest) {
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);
+ 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;
+ pathObject->is_fill_ = false;
+ pathObject->is_stroke_ = true;
// Set PathObject Matrix.
- pathObject->matrix = {1.0f, 0, 0, 1.0f, 0, 0};
+ pathObject->device_matrix_ = {1.0f, 2.0f, 3.0f, 1.0f, 3.0f, 2.0f};
// Add the page object.
stampAnnotation->AddObject(std::move(pathObject));
diff --git a/pdf/framework/libs/pdfClient/path_object.cc b/pdf/framework/libs/pdfClient/path_object.cc
new file mode 100644
index 000000000..a0d68a95e
--- /dev/null
+++ b/pdf/framework/libs/pdfClient/path_object.cc
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "path_object.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "fpdf_edit.h"
+#include "logging.h"
+#include "rect.h"
+
+#define LOG_TAG "path_object"
+
+namespace pdfClient {
+
+PathObject::PathObject() : PageObject(Type::Path) {}
+
+ScopedFPDFPageObject PathObject::CreateFPDFInstance(FPDF_DOCUMENT document, FPDF_PAGE page) {
+ 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(), page)) {
+ return nullptr;
+ }
+
+ return scoped_path_object;
+}
+
+bool PathObject::UpdateFPDFInstance(FPDF_PAGEOBJECT path_object, FPDF_PAGE page) {
+ 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_ ? FPDF_FILLMODE_WINDING : FPDF_FILLMODE_NONE;
+ if (!FPDFPath_SetDrawMode(path_object, fill_mode, is_stroke_)) {
+ return false;
+ }
+
+ // Set the updated matrix.
+ if (!SetDeviceToPageMatrix(path_object, page)) {
+ 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, FPDF_PAGE page) {
+ // 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;
+ }
+ is_fill_ = fill_mode;
+ is_stroke_ = stroke;
+
+ // Get Matrix
+ if (!GetPageToDeviceMatrix(path_object, page)) {
+ 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;
+}
+
+bool PathObject::GetPageToDeviceMatrix(FPDF_PAGEOBJECT path_object, FPDF_PAGE page) {
+ Matrix page_matrix;
+ if (!FPDFPageObj_GetMatrix(path_object, reinterpret_cast<FS_MATRIX*>(&page_matrix))) {
+ LOGE("GetPageMatrix failed!");
+ return false;
+ }
+
+ float page_height = FPDF_GetPageHeightF(page);
+
+ // Page to device matrix.
+ device_matrix_.a = page_matrix.a;
+ device_matrix_.b = (page_matrix.b != 0) ? -page_matrix.b : 0;
+ device_matrix_.c = (page_matrix.c != 0) ? -page_matrix.c : 0;
+ device_matrix_.d = page_matrix.d;
+ device_matrix_.e = page_matrix.e + (page_height * page_matrix.c);
+ device_matrix_.f = page_height - page_matrix.f - (page_height * page_matrix.d);
+
+ return true;
+}
+
+bool PathObject::SetDeviceToPageMatrix(FPDF_PAGEOBJECT path_object, FPDF_PAGE page) {
+ // Reset Previous Transformation.
+ Matrix identity = {1, 0, 0, 1, 0, 0};
+ if (!FPDFPageObj_SetMatrix(path_object, reinterpret_cast<FS_MATRIX*>(&identity))) {
+ LOGE("SetMatrix failed!");
+ return false;
+ }
+
+ float page_height = FPDF_GetPageHeightF(page);
+
+ FPDFPageObj_Transform(path_object, 1, 0, 0, 1, 0, -page_height);
+ FPDFPageObj_Transform(path_object, device_matrix_.a, -device_matrix_.b, -device_matrix_.c,
+ device_matrix_.d, device_matrix_.e, -device_matrix_.f);
+ FPDFPageObj_Transform(path_object, 1, 0, 0, 1, 0, page_height);
+
+ return true;
+}
+
+PathObject::~PathObject() = default;
+
+} // namespace pdfClient \ No newline at end of file
diff --git a/pdf/framework/libs/pdfClient/path_object.h b/pdf/framework/libs/pdfClient/path_object.h
new file mode 100644
index 000000000..fee76f70e
--- /dev/null
+++ b/pdf/framework/libs/pdfClient/path_object.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEDIAPROVIDER_PDF_JNI_PDFCLIENT_PATH_OBJECT_H_
+#define MEDIAPROVIDER_PDF_JNI_PDFCLIENT_PATH_OBJECT_H_
+
+#include <stdint.h>
+
+#include <vector>
+
+#include "cpp/fpdf_scopers.h"
+#include "fpdfview.h"
+#include "page_object.h"
+
+typedef unsigned int uint;
+
+namespace pdfClient {
+
+class PathObject : public PageObject {
+ public:
+ PathObject();
+
+ ScopedFPDFPageObject CreateFPDFInstance(FPDF_DOCUMENT document, FPDF_PAGE page) override;
+ bool UpdateFPDFInstance(FPDF_PAGEOBJECT path_object, FPDF_PAGE page) override;
+ bool PopulateFromFPDFInstance(FPDF_PAGEOBJECT path_object, FPDF_PAGE page) 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_ = false;
+ bool is_stroke_ = false;
+
+ std::vector<Segment> segments_;
+
+ protected:
+ bool GetPageToDeviceMatrix(FPDF_PAGEOBJECT path_object, FPDF_PAGE page) override;
+ bool SetDeviceToPageMatrix(FPDF_PAGEOBJECT path_object, FPDF_PAGE page) override;
+};
+
+} // namespace pdfClient
+
+#endif // MEDIAPROVIDER_PDF_JNI_PDFCLIENT_PATH_OBJECT_H_ \ No newline at end of file
diff --git a/pdf/framework/libs/pdfClient/testdata/page_object.pdf b/pdf/framework/libs/pdfClient/testdata/page_object.pdf
index 81215acb2..46c6ac08f 100644
--- a/pdf/framework/libs/pdfClient/testdata/page_object.pdf
+++ b/pdf/framework/libs/pdfClient/testdata/page_object.pdf
Binary files differ
diff --git a/pdf/framework/libs/pdfClient/text_object.cc b/pdf/framework/libs/pdfClient/text_object.cc
new file mode 100644
index 000000000..ba1e6a599
--- /dev/null
+++ b/pdf/framework/libs/pdfClient/text_object.cc
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "text_object.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <utils/pdf_strings.h>
+
+#include <string>
+
+#include "cpp/fpdf_scopers.h"
+#include "fpdf_edit.h"
+#include "fpdfview.h"
+#include "logging.h"
+
+#define LOG_TAG "text_object"
+
+using FontFamily = pdfClient::Font::Family;
+
+namespace pdfClient {
+
+std::optional<Font> GetFont(FPDF_PAGEOBJECT text_object) {
+ // Get FPDF font.
+ FPDF_FONT font = FPDFTextObj_GetFont(text_object);
+
+ // Get buffer length.
+ unsigned long text_len = FPDFFont_GetBaseFontName(font, nullptr, 0);
+
+ // Get font name.
+ std::unique_ptr<char[]> p_font_name = std::make_unique<char[]>(text_len);
+ if (!FPDFFont_GetBaseFontName(font, p_font_name.get(), text_len)) {
+ LOGE("GetBaseFontName failed");
+ return std::nullopt;
+ }
+
+ // Find font index.
+ std::string font_name(p_font_name.get());
+ if (font_mapper.find(font_name) == font_mapper.end()) {
+ LOGE("Font not found in font_mapper %s", font_name.c_str());
+ return std::nullopt;
+ }
+
+ return font_mapper[font_name];
+}
+
+TextObject::RenderMode GetRenderMode(FPDF_TEXT_RENDERMODE render_mode) {
+ switch (render_mode) {
+ case FPDF_TEXTRENDERMODE_FILL: {
+ return TextObject::RenderMode::Fill;
+ }
+ case FPDF_TEXTRENDERMODE_STROKE: {
+ return TextObject::RenderMode::Stroke;
+ }
+ case FPDF_TEXTRENDERMODE_FILL_STROKE: {
+ return TextObject::RenderMode::FillStroke;
+ }
+ default: {
+ return TextObject::RenderMode::Unknown;
+ }
+ }
+}
+
+FPDF_TEXT_RENDERMODE GetRenderMode(TextObject::RenderMode render_mode) {
+ switch (render_mode) {
+ case TextObject::RenderMode::Fill: {
+ return FPDF_TEXTRENDERMODE_FILL;
+ }
+ case TextObject::RenderMode::Stroke: {
+ return FPDF_TEXTRENDERMODE_STROKE;
+ }
+ case TextObject::RenderMode::FillStroke: {
+ return FPDF_TEXTRENDERMODE_FILL_STROKE;
+ }
+ default: {
+ return FPDF_TEXTRENDERMODE_UNKNOWN;
+ }
+ }
+}
+
+std::optional<std::wstring> GetText(FPDF_PAGEOBJECT text_object, FPDF_PAGE page) {
+ // Get text page.
+ ScopedFPDFTextPage text_page(FPDFText_LoadPage(page));
+ if (!text_page) {
+ return std::nullopt;
+ }
+
+ // Get buffer length.
+ unsigned long text_len = FPDFTextObj_GetText(text_object, text_page.get(), nullptr, 0);
+
+ // Get text.
+ std::unique_ptr<FPDF_WCHAR[]> p_text_buffer = std::make_unique<FPDF_WCHAR[]>(text_len);
+ if (!FPDFTextObj_GetText(text_object, text_page.get(), p_text_buffer.get(), text_len)) {
+ LOGE("GetText failed");
+ return std::nullopt;
+ }
+
+ return pdfClient_utils::ToWideString(p_text_buffer.get(), text_len);
+}
+
+Font::Font(const std::string& font_name, Family family, bool bold, bool italic)
+ : font_name_(font_name), family_(family), bold_(bold), italic_(italic) {}
+
+std::string Font::GetName() {
+ std::string name = font_name_;
+
+ if (bold_ && italic_) {
+ name += BoldItalic;
+ } else if (bold_) {
+ name += Bold;
+ } else if (italic_) {
+ name += Italic;
+ }
+
+ return name;
+}
+
+TextObject::TextObject() : PageObject(Type::Text) {}
+
+ScopedFPDFPageObject TextObject::CreateFPDFInstance(FPDF_DOCUMENT document, FPDF_PAGE page) {
+ // Create a scoped Pdfium font object.
+ ScopedFPDFFont font(FPDFText_LoadStandardFont(document, font_.GetName().c_str()));
+ if (!font) {
+ LOGE("Font creation failed");
+ return nullptr;
+ }
+
+ // Create a scoped Pdfium text object.
+ ScopedFPDFPageObject scoped_text_object(
+ FPDFPageObj_CreateTextObj(document, font.get(), font_size_));
+ if (!scoped_text_object) {
+ LOGE("Object creation failed");
+ return nullptr;
+ }
+
+ // Update attributes of Pdfium text object.
+ if (!UpdateFPDFInstance(scoped_text_object.get(), page)) {
+ LOGE("Create update failed");
+ return nullptr;
+ }
+
+ return scoped_text_object;
+}
+
+bool TextObject::UpdateFPDFInstance(FPDF_PAGEOBJECT text_object, FPDF_PAGE page) {
+ if (!text_object) {
+ LOGE("Object NULL");
+ return false;
+ }
+
+ // Check for type correctness.
+ if (FPDFPageObj_GetType(text_object) != FPDF_PAGEOBJ_TEXT) {
+ LOGE("TypeCast failed");
+ return false;
+ }
+
+ // Set the updated text.
+ auto fpdf_text = pdfClient_utils::ToFPDFWideString(text_);
+ if (text_.size() == 0 || !FPDFText_SetText(text_object, fpdf_text.get())) {
+ LOGE("SetText failed");
+ return false;
+ }
+
+ // Set the updated text render mode.
+ if (!FPDFTextObj_SetTextRenderMode(text_object, GetRenderMode(render_mode_))) {
+ LOGE("SetTextRenderMode failed");
+ return false;
+ }
+
+ // Set the updated device matrix.
+ if (!SetDeviceToPageMatrix(text_object, page)) {
+ LOGE("SetMatrix failed");
+ return false;
+ }
+
+ // Set updated stroke width.
+ if (!FPDFPageObj_SetStrokeWidth(text_object, stroke_width_)) {
+ LOGE("SetStrokeWidth failed");
+ return false;
+ }
+
+ // Set updated stroke color.
+ if (!FPDFPageObj_SetStrokeColor(text_object, stroke_color_.r, stroke_color_.g, stroke_color_.b,
+ stroke_color_.a)) {
+ LOGE("SetStrokeColor failed");
+ return false;
+ }
+
+ // Set the updated fill color.
+ if (!FPDFPageObj_SetFillColor(text_object, fill_color_.r, fill_color_.g, fill_color_.b,
+ fill_color_.a)) {
+ LOGE("SetFillColor failed");
+ return false;
+ }
+
+ return true;
+}
+
+bool TextObject::PopulateFromFPDFInstance(FPDF_PAGEOBJECT text_object, FPDF_PAGE page) {
+ // Get font.
+ std::optional<Font> fontOpt = GetFont(text_object);
+ if (!fontOpt) {
+ LOGE("GetFont failed");
+ return false;
+ }
+ font_ = *fontOpt;
+
+ // Get font size.
+ if (!FPDFTextObj_GetFontSize(text_object, &font_size_)) {
+ LOGE("GetFontSize failed");
+ return false;
+ }
+
+ // Get text.
+ std::optional<std::wstring> textOpt = GetText(text_object, page);
+ if (!textOpt) {
+ LOGE("GetText failed");
+ return false;
+ }
+ text_ = *textOpt;
+
+ // Get render mode.
+ render_mode_ = GetRenderMode(FPDFTextObj_GetTextRenderMode(text_object));
+ if (render_mode_ == RenderMode::Unknown) {
+ LOGE("GetRenderMode unknown");
+ return false;
+ }
+
+ // Get device matrix.
+ if (!GetPageToDeviceMatrix(text_object, page)) {
+ LOGE("GetMatrix failed");
+ return false;
+ }
+
+ // Get stroke width.
+ if (!FPDFPageObj_GetStrokeWidth(text_object, &stroke_width_)) {
+ LOGE("GetStrokeWidth failed");
+ return false;
+ }
+
+ // Get stroke color.
+ if (!FPDFPageObj_GetStrokeColor(text_object, &stroke_color_.r, &stroke_color_.g,
+ &stroke_color_.b, &stroke_color_.a)) {
+ LOGE("GetStrokeColor failed");
+ return false;
+ }
+
+ // Get fill color.
+ if (!FPDFPageObj_GetFillColor(text_object, &fill_color_.r, &fill_color_.g, &fill_color_.b,
+ &fill_color_.a)) {
+ LOGE("GetFillColor failed");
+ return false;
+ }
+
+ return true;
+}
+
+TextObject::~TextObject() = default;
+
+// Font Mapper
+std::unordered_map<std::string, Font> font_mapper = {
+ {Courier, Font(Courier, FontFamily::Courier)},
+ {Courier + Bold, Font(Courier, FontFamily::Courier, true)},
+ {Courier + Oblique, Font(Courier, FontFamily::Courier, false, true)},
+ {Courier + BoldOblique, Font(Courier, FontFamily::Courier, true, true)},
+
+ {Helvetica, Font(Helvetica, FontFamily::Helvetica)},
+ {Helvetica + Bold, Font(Helvetica, FontFamily::Helvetica, true)},
+ {Helvetica + Oblique, Font(Helvetica, FontFamily::Helvetica, false, true)},
+ {Helvetica + BoldOblique, Font(Helvetica, FontFamily::Helvetica, true, true)},
+
+ {TimesRoman, Font(TimesRoman, FontFamily::TimesRoman)},
+ {Times + Bold, Font(Times, FontFamily::TimesRoman, true)},
+ {Times + Italic, Font(Times, FontFamily::TimesRoman, false, true)},
+ {Times + BoldItalic, Font(Times, FontFamily::TimesRoman, true, true)},
+
+ {Symbol, Font(Symbol, FontFamily::Symbol)}};
+
+// Standard Font Names.
+std::vector<std::string> font_names = {CourierNew, Helvetica, Symbol, TimesNewRoman};
+
+} // namespace pdfClient \ No newline at end of file
diff --git a/pdf/framework/libs/pdfClient/text_object.h b/pdf/framework/libs/pdfClient/text_object.h
new file mode 100644
index 000000000..84a150b6b
--- /dev/null
+++ b/pdf/framework/libs/pdfClient/text_object.h
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEDIAPROVIDER_PDF_JNI_PDFCLIENT_TEXT_OBJECT_H_
+#define MEDIAPROVIDER_PDF_JNI_PDFCLIENT_TEXT_OBJECT_H_
+
+#include <stdint.h>
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "cpp/fpdf_scopers.h"
+#include "fpdfview.h"
+#include "page_object.h"
+
+namespace pdfClient {
+
+class Font {
+ public:
+ enum class Family {
+ Unknown = -1,
+ Courier,
+ Helvetica,
+ Symbol,
+ TimesRoman,
+ };
+
+ Font() : family_(Family::Unknown), bold_(false), italic_(false) {};
+ Font(const std::string& font_name, Family family = Family::Unknown, bool bold = false,
+ bool italic = false);
+
+ std::string GetName();
+ Family GetFamily() const { return family_; }
+ bool IsBold() const { return bold_; }
+ bool IsItalic() const { return italic_; }
+
+ private:
+ std::string font_name_;
+ Family family_;
+ bool bold_;
+ bool italic_;
+};
+
+class TextObject : public PageObject {
+ public:
+ TextObject();
+
+ ScopedFPDFPageObject CreateFPDFInstance(FPDF_DOCUMENT document, FPDF_PAGE page) override;
+ bool UpdateFPDFInstance(FPDF_PAGEOBJECT text_object, FPDF_PAGE page) override;
+ bool PopulateFromFPDFInstance(FPDF_PAGEOBJECT text_object, FPDF_PAGE page) override;
+
+ ~TextObject();
+
+ enum class RenderMode {
+ Unknown = -1,
+ Fill,
+ Stroke,
+ FillStroke,
+ };
+
+ Font font_;
+ float font_size_;
+ RenderMode render_mode_;
+ std::wstring text_;
+};
+
+// Define font names as constants
+const std::string Courier = "Courier";
+const std::string CourierNew = "CourierNew";
+const std::string Helvetica = "Helvetica";
+const std::string Symbol = "Symbol";
+const std::string Times = "Times";
+const std::string TimesRoman = "Times-Roman";
+const std::string TimesNewRoman = "TimesNewRoman";
+
+// Define font variants as constants
+const std::string Bold = "-Bold";
+const std::string Italic = "-Italic";
+const std::string Oblique = "-Oblique";
+const std::string BoldItalic = "-BoldItalic";
+const std::string BoldOblique = "-BoldOblique";
+
+// Font Mapper
+extern std::unordered_map<std::string, Font> font_mapper;
+
+// Standard Font Names
+extern std::vector<std::string> font_names;
+
+} // namespace pdfClient
+
+#endif // MEDIAPROVIDER_PDF_JNI_PDFCLIENT_TEXT_OBJECT_H_ \ No newline at end of file
diff --git a/photopicker/res/values-af/feature_search_strings.xml b/photopicker/res/values-af/feature_search_strings.xml
index 0a008ed65..59aac4dae 100644
--- a/photopicker/res/values-af/feature_search_strings.xml
+++ b/photopicker/res/values-af/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Probeer om vir soortgelyke woorde te soek"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Voorstelle"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Soektog gedeaktiveer."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Probeer soortgelyke woorde soek.\n\nSlegs items wat gerugsteun is sal in jou soekresultate verskyn."</string>
</resources>
diff --git a/photopicker/res/values-am/feature_search_strings.xml b/photopicker/res/values-am/feature_search_strings.xml
index 5638946f3..11a6b815f 100644
--- a/photopicker/res/values-am/feature_search_strings.xml
+++ b/photopicker/res/values-am/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"ተመሳሳይ ቃላትን ለመፈለግ ይሞክሩ"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"ጥቆማዎች"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"ፍለጋ ተሰናክሏል።"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"ተመሳሳይ ቃላትን ለመፈለግ ይሞክሩ።\n\nምትኬ የተቀመጠላቸው ንጥሎች ብቻ በእርስዎ የፍለጋ ውጤቶች ውስጥ ይታያሉ።"</string>
</resources>
diff --git a/photopicker/res/values-ar/feature_search_strings.xml b/photopicker/res/values-ar/feature_search_strings.xml
index b2ac5ad30..2e3a56a6b 100644
--- a/photopicker/res/values-ar/feature_search_strings.xml
+++ b/photopicker/res/values-ar/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"جرِّب البحث عن كلمات مُشابهة"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"اقتراحات"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"تم إيقاف البحث."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"يُرجى تجربة البحث عن كلمات مُشابهة.\n\nستظهر العناصر التي تم الاحتفاظ بنسخة احتياطية منها فقط في نتائج البحث."</string>
</resources>
diff --git a/photopicker/res/values-as/feature_search_strings.xml b/photopicker/res/values-as/feature_search_strings.xml
index 63ce1d998..fab3251e7 100644
--- a/photopicker/res/values-as/feature_search_strings.xml
+++ b/photopicker/res/values-as/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"একেধৰণৰ শব্দবোৰ সন্ধান কৰি চাওক"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"পৰামৰ্শ"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"সন্ধান কৰাটো অক্ষম কৰা আছে।"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"একেধৰণৰ শব্দ সন্ধান কৰি চাওক।\n\nকেৱল বেক আপ লোৱা বস্তু আপোনাৰ সন্ধানৰ ফলাফলত ওলাব।"</string>
</resources>
diff --git a/photopicker/res/values-az/feature_search_strings.xml b/photopicker/res/values-az/feature_search_strings.xml
index 94f0f0e2f..07f9a666b 100644
--- a/photopicker/res/values-az/feature_search_strings.xml
+++ b/photopicker/res/values-az/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Bənzər sözləri axtarmağa çalışın"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Təkliflər"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Axtarış deaktiv edildi."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Bənzər sözləri axtarın.\n\nYalnız yedəklənmiş elementlər axtarış nəticələrinizdə görünəcək."</string>
</resources>
diff --git a/photopicker/res/values-b+sr+Latn/feature_search_strings.xml b/photopicker/res/values-b+sr+Latn/feature_search_strings.xml
index 9bdc6550d..e1fca4c09 100644
--- a/photopicker/res/values-b+sr+Latn/feature_search_strings.xml
+++ b/photopicker/res/values-b+sr+Latn/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Potražite slične reči"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Predlozi"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Pretraga je onemogućena."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Potražite slične reči.\n\nU rezultatima pretrage se prikazuju samo stavke za koje je napravljena rezervna kopija."</string>
</resources>
diff --git a/photopicker/res/values-be/feature_search_strings.xml b/photopicker/res/values-be/feature_search_strings.xml
index c90fc7b44..e90985e6c 100644
--- a/photopicker/res/values-be/feature_search_strings.xml
+++ b/photopicker/res/values-be/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Паспрабуйце пашукаць падобныя словы"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Прапановы"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Пошук адключаны."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Паспрабуйце пошук па падобных словах.\n\nУ выніках пошуку будуць паказаны толькі тыя элементы, для якіх была створана рэзервовая копія."</string>
</resources>
diff --git a/photopicker/res/values-bg/feature_search_strings.xml b/photopicker/res/values-bg/feature_search_strings.xml
index 988ab4f53..09da09249 100644
--- a/photopicker/res/values-bg/feature_search_strings.xml
+++ b/photopicker/res/values-bg/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Опитайте да потърсите подобни думи"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Предложения"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Търсенето е деактивирано."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Опитайте да потърсите подобни думи.\n\nВ резултатите от търсенето ви ще се показват само елементите, за които е създадено резервно копие."</string>
</resources>
diff --git a/photopicker/res/values-bn/feature_search_strings.xml b/photopicker/res/values-bn/feature_search_strings.xml
index 155d50f33..5abccfcf8 100644
--- a/photopicker/res/values-bn/feature_search_strings.xml
+++ b/photopicker/res/values-bn/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"একই ধরনের শব্দ সার্চ করার চেষ্টা করুন"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"সাজেশন"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"সার্চ বন্ধ করা হয়েছে।"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"একই ধরনের শব্দ দিয়ে সার্চ করে দেখুন।\n\nযেসব আইটেমের ব্যাক-আপ নেওয়া আছে শুধু সেগুলিই আপনার সার্চ ফলাফলে দেখা যাবে।"</string>
</resources>
diff --git a/photopicker/res/values-bs/feature_search_strings.xml b/photopicker/res/values-bs/feature_search_strings.xml
index e10cb46e3..71409a901 100644
--- a/photopicker/res/values-bs/feature_search_strings.xml
+++ b/photopicker/res/values-bs/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Pokušajte pretražiti slične riječi"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Prijedlozi"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Pretraživanje je onemogućeno."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Pokušajte pretražiti slične riječi.\n\nU rezultatima pretraživanja će se prikazati samo stavke čija je sigurnosna kopija napravljena."</string>
</resources>
diff --git a/photopicker/res/values-ca/feature_search_strings.xml b/photopicker/res/values-ca/feature_search_strings.xml
index e10120469..c244a44c2 100644
--- a/photopicker/res/values-ca/feature_search_strings.xml
+++ b/photopicker/res/values-ca/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Prova de cercar paraules similars"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Suggeriments"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Cerca desactivada."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Prova de cercar paraules similars.\n\nNomés els elements de què s\'hagi creat una còpia de seguretat es mostraran als resultats de la cerca."</string>
</resources>
diff --git a/photopicker/res/values-cs/feature_search_strings.xml b/photopicker/res/values-cs/feature_search_strings.xml
index 07fddb9c5..6a32db6c1 100644
--- a/photopicker/res/values-cs/feature_search_strings.xml
+++ b/photopicker/res/values-cs/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Zkuste vyhledat podobná slova"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Návrhy"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Vyhledávání vypnuto."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Zkuste vyhledat podobná slova.\n\nVe výsledcích vyhledávání se zobrazí jen zálohované položky."</string>
</resources>
diff --git a/photopicker/res/values-da/feature_search_strings.xml b/photopicker/res/values-da/feature_search_strings.xml
index 54db9cc6c..84d9c7f1c 100644
--- a/photopicker/res/values-da/feature_search_strings.xml
+++ b/photopicker/res/values-da/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Prøv at søge efter lignende ord"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Forslag"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Søgning er deaktiveret."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Prøv at søge efter lignende ord.\n\nDet er kun elementer, der er sikkerhedskopieret, som vises i dine søgeresultater."</string>
</resources>
diff --git a/photopicker/res/values-de/feature_search_strings.xml b/photopicker/res/values-de/feature_search_strings.xml
index cbbeedece..c55207103 100644
--- a/photopicker/res/values-de/feature_search_strings.xml
+++ b/photopicker/res/values-de/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Versuche es mit einer Suche anhand ähnlicher Wörter"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Vorschläge"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Suche deaktiviert."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Versuche, nach ähnlichen Wörtern zu suchen.\n\nIn den Suchergebnissen werden nur gesicherte Elemente angezeigt."</string>
</resources>
diff --git a/photopicker/res/values-el/feature_search_strings.xml b/photopicker/res/values-el/feature_search_strings.xml
index 0d735c6f4..916169727 100644
--- a/photopicker/res/values-el/feature_search_strings.xml
+++ b/photopicker/res/values-el/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Δοκιμάστε να αναζητήσετε παρόμοιες λέξεις"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Προτάσεις"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Η αναζήτηση απενεργοποιήθηκε."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Δοκιμάστε να αναζητήσετε παρόμοιες λέξεις.\n\nΣτα αποτελέσματα αναζήτησης, θα εμφανίζονται μόνο τα στοιχεία για τα οποία έχουν δημιουργηθεί αντίγραφα ασφαλείας."</string>
</resources>
diff --git a/photopicker/res/values-en-rAU/feature_search_strings.xml b/photopicker/res/values-en-rAU/feature_search_strings.xml
index 48623b657..e14f928e6 100644
--- a/photopicker/res/values-en-rAU/feature_search_strings.xml
+++ b/photopicker/res/values-en-rAU/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Try searching for similar words"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Suggestions"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Search disabled."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Try searching for similar words.\n\nOnly items that are backed up will appear in your search results."</string>
</resources>
diff --git a/photopicker/res/values-en-rCA/feature_search_strings.xml b/photopicker/res/values-en-rCA/feature_search_strings.xml
index 48623b657..e14f928e6 100644
--- a/photopicker/res/values-en-rCA/feature_search_strings.xml
+++ b/photopicker/res/values-en-rCA/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Try searching for similar words"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Suggestions"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Search disabled."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Try searching for similar words.\n\nOnly items that are backed up will appear in your search results."</string>
</resources>
diff --git a/photopicker/res/values-en-rGB/feature_search_strings.xml b/photopicker/res/values-en-rGB/feature_search_strings.xml
index 48623b657..e14f928e6 100644
--- a/photopicker/res/values-en-rGB/feature_search_strings.xml
+++ b/photopicker/res/values-en-rGB/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Try searching for similar words"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Suggestions"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Search disabled."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Try searching for similar words.\n\nOnly items that are backed up will appear in your search results."</string>
</resources>
diff --git a/photopicker/res/values-en-rIN/feature_search_strings.xml b/photopicker/res/values-en-rIN/feature_search_strings.xml
index 48623b657..e14f928e6 100644
--- a/photopicker/res/values-en-rIN/feature_search_strings.xml
+++ b/photopicker/res/values-en-rIN/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Try searching for similar words"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Suggestions"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Search disabled."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Try searching for similar words.\n\nOnly items that are backed up will appear in your search results."</string>
</resources>
diff --git a/photopicker/res/values-es-rUS/feature_search_strings.xml b/photopicker/res/values-es-rUS/feature_search_strings.xml
index 97bc545d4..1a841ca92 100644
--- a/photopicker/res/values-es-rUS/feature_search_strings.xml
+++ b/photopicker/res/values-es-rUS/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Intenta buscar palabras similares"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Sugerencias"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Búsqueda inhabilitada."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Intenta buscar palabras similares.\n\nSolo los elementos a los que se les haya creado una copia de seguridad aparecerán en los resultados de la búsqueda."</string>
</resources>
diff --git a/photopicker/res/values-es/feature_search_strings.xml b/photopicker/res/values-es/feature_search_strings.xml
index 4efc06bd5..817e08a08 100644
--- a/photopicker/res/values-es/feature_search_strings.xml
+++ b/photopicker/res/values-es/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Prueba a buscar palabras similares"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Sugerencias"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Búsqueda inhabilitada."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Prueba a buscar palabras similares.\n\nSolo los elementos con copia de seguridad aparecerán en los resultados de búsqueda."</string>
</resources>
diff --git a/photopicker/res/values-et/feature_search_strings.xml b/photopicker/res/values-et/feature_search_strings.xml
index 924881fea..4f1bdb6e6 100644
--- a/photopicker/res/values-et/feature_search_strings.xml
+++ b/photopicker/res/values-et/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Proovige otsida sarnaseid sõnu"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Soovitused"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Otsing keelatud"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Proovige otsida sarnaseid sõnu.\n\nTeie otsingutulemustes kuvatakse ainult varundatud üksused."</string>
</resources>
diff --git a/photopicker/res/values-eu/feature_search_strings.xml b/photopicker/res/values-eu/feature_search_strings.xml
index cf4346e81..8c2a4f2ed 100644
--- a/photopicker/res/values-eu/feature_search_strings.xml
+++ b/photopicker/res/values-eu/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Bilatu antzeko hitzak"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Iradokizunak"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Bilaketa desgaituta dago."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Bilatu antzeko hitzak.\n\nBabeskopia eginda daukaten elementuak soilik agertuko dira bilaketa-emaitzetan."</string>
</resources>
diff --git a/photopicker/res/values-fa/feature_search_strings.xml b/photopicker/res/values-fa/feature_search_strings.xml
index 8e2d1cb2e..b8a289f4e 100644
--- a/photopicker/res/values-fa/feature_search_strings.xml
+++ b/photopicker/res/values-fa/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"کلمه‌های مشابه را جستجو کنید"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"پیشنهادها"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"جستجو غیرفعال شد."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"کلمات مشابه را جستجو کنید.\n\nفقط مواردی که پشتیبان‌گیری شده‌اند در نتایج جستجو نمایش داده می‌شوند."</string>
</resources>
diff --git a/photopicker/res/values-fi/feature_search_strings.xml b/photopicker/res/values-fi/feature_search_strings.xml
index e59c884f2..566bdfd41 100644
--- a/photopicker/res/values-fi/feature_search_strings.xml
+++ b/photopicker/res/values-fi/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Kokeile hakea samankaltaisia sanoja"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Ehdotukset"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Haku poistettu käytöstä."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Kokeile hakea samankaltaisia sanoja.\n\nVain varmuuskopioidut kohteet näkyvät hakutuloksissa."</string>
</resources>
diff --git a/photopicker/res/values-fr-rCA/feature_search_strings.xml b/photopicker/res/values-fr-rCA/feature_search_strings.xml
index 2f4e78c42..2f41f7ea6 100644
--- a/photopicker/res/values-fr-rCA/feature_search_strings.xml
+++ b/photopicker/res/values-fr-rCA/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Essayez de rechercher des mots semblables"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Suggestions"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Recherche désactivée."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Essayez de chercher des mots semblables.\n\nSeuls les éléments sauvegardés apparaîtront dans vos résultats de recherche."</string>
</resources>
diff --git a/photopicker/res/values-fr/feature_search_strings.xml b/photopicker/res/values-fr/feature_search_strings.xml
index 6295e278d..2e534d6ac 100644
--- a/photopicker/res/values-fr/feature_search_strings.xml
+++ b/photopicker/res/values-fr/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Essayez de rechercher des mots similaires"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Suggestions"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Recherche désactivée."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Essayez de rechercher des mots similaires.\n\nSeuls les éléments sauvegardés apparaîtront dans vos résultats de recherche."</string>
</resources>
diff --git a/photopicker/res/values-gl/feature_search_strings.xml b/photopicker/res/values-gl/feature_search_strings.xml
index ecbc5bd17..611818610 100644
--- a/photopicker/res/values-gl/feature_search_strings.xml
+++ b/photopicker/res/values-gl/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Proba a buscar palabras semellantes"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Suxestións"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"A busca está desactivada."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Proba a buscar palabras similares.\n\nNos resultados da busca só aparecerán os elementos dos que se fixesen copias de seguranza."</string>
</resources>
diff --git a/photopicker/res/values-gu/feature_search_strings.xml b/photopicker/res/values-gu/feature_search_strings.xml
index a6f7575c7..e3fe03c89 100644
--- a/photopicker/res/values-gu/feature_search_strings.xml
+++ b/photopicker/res/values-gu/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"સમાન શબ્દો શોધવાનો પ્રયાસ કરો"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"સૂચનો"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"શોધવાની સુવિધા બંધ કરેલી છે."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"આના જેવા શબ્દો શોધવાનો પ્રયાસ કરો.\n\nતમારા શોધ પરિણામોમાં માત્ર તે જ આઇટમ દેખાશે કે જેનું બૅકઅપ લેવામાં આવ્યું છે."</string>
</resources>
diff --git a/photopicker/res/values-hi/feature_search_strings.xml b/photopicker/res/values-hi/feature_search_strings.xml
index 70628fe72..a1fc2f56a 100644
--- a/photopicker/res/values-hi/feature_search_strings.xml
+++ b/photopicker/res/values-hi/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"मिलते-जुलते शब्द खोजें"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"सुझाव"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"खोज करने की सुविधा बंद की गई है."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"मिलते-जुलते शब्द खोजें.\n\nआपके खोज के नतीजों में सिर्फ़ वे आइटम दिखेंगे जिनका बैक अप लिया गया है."</string>
</resources>
diff --git a/photopicker/res/values-hr/feature_search_strings.xml b/photopicker/res/values-hr/feature_search_strings.xml
index 0b2b07e73..477f7c31e 100644
--- a/photopicker/res/values-hr/feature_search_strings.xml
+++ b/photopicker/res/values-hr/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Pokušajte potražiti slične riječi"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Prijedlozi"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Pretraživanje je onemogućeno."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Pokušajte pretražiti slične riječi.\n\nU rezultatima pretraživanja prikazivat će se samo sigurnosno kopirane stavke."</string>
</resources>
diff --git a/photopicker/res/values-hu/feature_search_strings.xml b/photopicker/res/values-hu/feature_search_strings.xml
index 216f5352a..de2afb4e5 100644
--- a/photopicker/res/values-hu/feature_search_strings.xml
+++ b/photopicker/res/values-hu/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Próbálkozzon hasonló szavak keresésével"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Javaslatok"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Keresés letiltva."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Próbálkozzon hasonló szavak keresésével.\n\nCsak azok az elemek jelennek meg a keresési találatok között, amelyekről készült biztonsági másolat."</string>
</resources>
diff --git a/photopicker/res/values-hy/feature_search_strings.xml b/photopicker/res/values-hy/feature_search_strings.xml
index 3b11e9055..8047dd2f2 100644
--- a/photopicker/res/values-hy/feature_search_strings.xml
+++ b/photopicker/res/values-hy/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Փորձեք որոնել նմանատիպ բառեր"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Առաջարկներ"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Որոնումն անջատված է։"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Փորձեք որոնել նմանատիպ բառեր։\n\nՁեր որոնման արդյունքներում կհայտնվեն միայն պահուստավորված տարրեր։"</string>
</resources>
diff --git a/photopicker/res/values-in/feature_search_strings.xml b/photopicker/res/values-in/feature_search_strings.xml
index 3d61b0a00..a88db050f 100644
--- a/photopicker/res/values-in/feature_search_strings.xml
+++ b/photopicker/res/values-in/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Coba telusuri kata yang serupa"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Saran"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Penelusuran nonaktif."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Coba telusuri kata yang serupa.\n\nHanya item yang dicadangkan yang akan muncul di hasil penelusuran Anda."</string>
</resources>
diff --git a/photopicker/res/values-is/feature_search_strings.xml b/photopicker/res/values-is/feature_search_strings.xml
index a23c423ea..ce86a8838 100644
--- a/photopicker/res/values-is/feature_search_strings.xml
+++ b/photopicker/res/values-is/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Prófaðu að leita að svipuðum orðum"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Tillögur"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Slökkt á leit."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Prófaðu að leita að svipuðum orðum.\n\nEingöngu öryggisafrituð atriði munu birtast í leitarniðurstöðunum."</string>
</resources>
diff --git a/photopicker/res/values-it/feature_search_strings.xml b/photopicker/res/values-it/feature_search_strings.xml
index 8e435b211..4e699e0e0 100644
--- a/photopicker/res/values-it/feature_search_strings.xml
+++ b/photopicker/res/values-it/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Prova a cercare parole simili"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Suggerimenti"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Ricerca disattivata."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Prova a cercare parole simili.\n\nNei risultati di ricerca verranno visualizzati solo gli elementi di cui è stato eseguito il backup."</string>
</resources>
diff --git a/photopicker/res/values-iw/feature_search_strings.xml b/photopicker/res/values-iw/feature_search_strings.xml
index 45c65a9d2..dbdcc4360 100644
--- a/photopicker/res/values-iw/feature_search_strings.xml
+++ b/photopicker/res/values-iw/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"אפשר לנסות לחפש מילים דומות"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"הצעות"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"החיפוש מושבת."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"אפשר לנסות לחפש מילים דומות.\n\nרק פריטים שגובו יופיעו בתוצאות החיפוש."</string>
</resources>
diff --git a/photopicker/res/values-ja/feature_search_strings.xml b/photopicker/res/values-ja/feature_search_strings.xml
index 6ff343663..9376ed952 100644
--- a/photopicker/res/values-ja/feature_search_strings.xml
+++ b/photopicker/res/values-ja/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"似た言葉を検索してみてください"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"候補"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"検索が無効になっています。"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"似た言葉を検索してみてください。\n\nバックアップされたアイテムのみが検索結果に表示されます。"</string>
</resources>
diff --git a/photopicker/res/values-ka/feature_search_strings.xml b/photopicker/res/values-ka/feature_search_strings.xml
index f84d2f8b2..2147c5e35 100644
--- a/photopicker/res/values-ka/feature_search_strings.xml
+++ b/photopicker/res/values-ka/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"ცადეთ მსგავსი სიტყვების მოძიება"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"შეთავაზებები"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"ძიება გათიშულია."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"ცადეთ მსგავსი სიტყვების ძიება.\n\nთქვენს ძიების შედეგებში გამოჩნდება მხოლოდ ის ერთეულები, რომელთა სარეზერვო ასლიც შექმნილია."</string>
</resources>
diff --git a/photopicker/res/values-kk/feature_search_strings.xml b/photopicker/res/values-kk/feature_search_strings.xml
index bb77223e9..dd81f0064 100644
--- a/photopicker/res/values-kk/feature_search_strings.xml
+++ b/photopicker/res/values-kk/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Ұқсас сөздерді іздеп көріңіз."</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Ұсыныстар"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Іздеу функциясы өшірулі."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Ұқсас сөздерді іздеп көріңіз.\n\nСақтық көшірмесі жасалған элементтер ғана іздеу нәтижелеріңізде көрінеді."</string>
</resources>
diff --git a/photopicker/res/values-km/feature_search_strings.xml b/photopicker/res/values-km/feature_search_strings.xml
index 977d13f42..5777d04e4 100644
--- a/photopicker/res/values-km/feature_search_strings.xml
+++ b/photopicker/res/values-km/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"សាកល្បងស្វែងរកពាក្យស្រដៀងគ្នា"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"ការណែនាំ"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"បាន​បិទ​ការ​ស្វែងរក។"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"សាកល្បងស្វែងរកពាក្យស្រដៀងគ្នា។\n\nមានតែធាតុដែលត្រូវបានបម្រុងទុកប៉ុណ្ណោះដែលនឹងបង្ហាញនៅក្នុងលទ្ធផល​ស្វែងរករបស់អ្នក។"</string>
</resources>
diff --git a/photopicker/res/values-kn/feature_search_strings.xml b/photopicker/res/values-kn/feature_search_strings.xml
index b9e4a7342..ec6a859cb 100644
--- a/photopicker/res/values-kn/feature_search_strings.xml
+++ b/photopicker/res/values-kn/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"ಅದೇ ರೀತಿಯ ಪದಗಳನ್ನು ಹುಡುಕಲು ಪ್ರಯತ್ನಿಸಿ"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"ಸಲಹೆಗಳು"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"ಹುಡುಕಾಟ ಫೀಚರ್‌ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"ಅದೇ ರೀತಿಯ ಪದಗಳನ್ನು ಹುಡುಕಲು ಪ್ರಯತ್ನಿಸಿ.\n\nಬ್ಯಾಕಪ್ ಮಾಡಲಾದ ಐಟಂಗಳು ಮಾತ್ರ ನಿಮ್ಮ ಹುಡುಕಾಟ ಫಲಿತಾಂಶಗಳಲ್ಲಿ ಕಾಣಿಸಿಕೊಳ್ಳುತ್ತವೆ."</string>
</resources>
diff --git a/photopicker/res/values-ko/feature_search_strings.xml b/photopicker/res/values-ko/feature_search_strings.xml
index ba8e91a61..52273ca0a 100644
--- a/photopicker/res/values-ko/feature_search_strings.xml
+++ b/photopicker/res/values-ko/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"비슷한 단어를 검색해 보세요."</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"추천"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"검색이 사용 중지되었습니다."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"비슷한 단어를 검색해 보세요.\n\n백업된 항목만 검색 결과에 표시됩니다."</string>
</resources>
diff --git a/photopicker/res/values-ky/feature_search_strings.xml b/photopicker/res/values-ky/feature_search_strings.xml
index 18a01a912..432e2dcb2 100644
--- a/photopicker/res/values-ky/feature_search_strings.xml
+++ b/photopicker/res/values-ky/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Окшош сөздөрдү издеп көрүңүз"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Сунуштар"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Издөө өчүрүлдү."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Окшош сөздөрдү издеп көрүңүз.\n\nТабылган нерселерде камдык көчүрмөсү сакталган нерселер гана көрүнөт."</string>
</resources>
diff --git a/photopicker/res/values-lo/feature_search_strings.xml b/photopicker/res/values-lo/feature_search_strings.xml
index ab12d63aa..4ba4b6130 100644
--- a/photopicker/res/values-lo/feature_search_strings.xml
+++ b/photopicker/res/values-lo/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"ລອງຊອກຫາຄຳທີ່ຄ້າຍກັນ"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"ການແນະນຳ"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"ການຊອກຫາປິດການນຳໃຊ້ຢູ່."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"ລອງຊອກຫາຄຳທີ່ຄ້າຍກັນ.\n\nມີພຽງລາຍການທີ່ສຳຮອງຂໍ້ມູນໄວ້ເທົ່ານັ້ນທີ່ຈະປາກົດໃນຜົນການຊອກຫາຂອງທ່ານ."</string>
</resources>
diff --git a/photopicker/res/values-lt/feature_search_strings.xml b/photopicker/res/values-lt/feature_search_strings.xml
index 2333566c5..d8c82afa6 100644
--- a/photopicker/res/values-lt/feature_search_strings.xml
+++ b/photopicker/res/values-lt/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Pabandykite ieškoti panašių žodžių"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Pasiūlymai"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Paieška išjungta."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Pabandykite ieškoti panašių žodžių.\n\nPaieškos rezultatuose bus rodomi tik tie elementai, kurių atsarginės kopijos yra sukurtos."</string>
</resources>
diff --git a/photopicker/res/values-lv/feature_search_strings.xml b/photopicker/res/values-lv/feature_search_strings.xml
index c2fc5b1ce..68e74f0b8 100644
--- a/photopicker/res/values-lv/feature_search_strings.xml
+++ b/photopicker/res/values-lv/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Mēģiniet meklēt līdzīgus vārdus"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Ieteikumi"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Meklēšana atspējota."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Mēģiniet meklēt līdzīgus vārdus.\n\nMeklēšanas rezultātos tiks rādīti tikai dublēti vienumi."</string>
</resources>
diff --git a/photopicker/res/values-mk/feature_search_strings.xml b/photopicker/res/values-mk/feature_search_strings.xml
index 07ef55612..74506fb43 100644
--- a/photopicker/res/values-mk/feature_search_strings.xml
+++ b/photopicker/res/values-mk/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Обидете се да пребарувате слични зборови"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Предлози"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Пребарувањето е оневозможено."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Обидете се со пребарување слични зборови.\n\nВо резултатите од пребарувањето ќе се појавуваат само ставки за кои е направен бекап."</string>
</resources>
diff --git a/photopicker/res/values-ml/feature_search_strings.xml b/photopicker/res/values-ml/feature_search_strings.xml
index 8720a872c..5f5c3583b 100644
--- a/photopicker/res/values-ml/feature_search_strings.xml
+++ b/photopicker/res/values-ml/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"സമാനമായ വാക്കുകൾ തിരഞ്ഞുനോക്കൂ"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"നിർദ്ദേശങ്ങൾ"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"തിരയൽ പ്രവർത്തനരഹിതമാക്കി."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"സമാനമായ വാക്കുകൾ തിരഞ്ഞുനോക്കൂ.\n\nബാക്കപ്പെടുത്ത ഇനങ്ങൾ മാത്രമേ നിങ്ങളുടെ തിരയൽ ഫലങ്ങളിൽ ദൃശ്യമാകൂ."</string>
</resources>
diff --git a/photopicker/res/values-mn/feature_search_strings.xml b/photopicker/res/values-mn/feature_search_strings.xml
index cc13ebcc7..edf143e4f 100644
--- a/photopicker/res/values-mn/feature_search_strings.xml
+++ b/photopicker/res/values-mn/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Төстэй үг хайж үзнэ үү"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Зөвлөмж"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Хайлтыг идэвхгүй болгосон."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Төстэй үг хайж үзнэ үү.\n\nЗөвхөн нөөцөлсөн зүйл таны хайлтын илэрцэд гарч ирнэ."</string>
</resources>
diff --git a/photopicker/res/values-mr/feature_search_strings.xml b/photopicker/res/values-mr/feature_search_strings.xml
index e0969983f..cc0d2c8cf 100644
--- a/photopicker/res/values-mr/feature_search_strings.xml
+++ b/photopicker/res/values-mr/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"समानार्थी शब्द शोधण्याचा प्रयत्न करा"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"सूचना"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"शोध बंद केला आहे."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"समानार्थी शब्द शोधण्याचा प्रयत्न करा.\n\nफक्त बॅकअप घेतलेले आयटम तुमच्या शोध परिणामांमध्ये दिसतील."</string>
</resources>
diff --git a/photopicker/res/values-ms/feature_search_strings.xml b/photopicker/res/values-ms/feature_search_strings.xml
index 9f3142b88..243aca8f0 100644
--- a/photopicker/res/values-ms/feature_search_strings.xml
+++ b/photopicker/res/values-ms/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Cuba cari perkataan yang serupa"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Cadangan"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Carian dilumpuhkan."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Cuba cari perkataan yang serupa.\n\nHanya item yang disandarkan akan dipaparkan dalam hasil carian anda."</string>
</resources>
diff --git a/photopicker/res/values-my/feature_search_strings.xml b/photopicker/res/values-my/feature_search_strings.xml
index 2c2057591..35e0e84b8 100644
--- a/photopicker/res/values-my/feature_search_strings.xml
+++ b/photopicker/res/values-my/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"အလားတူစာလုံးများကို ရှာကြည့်ပါ"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"အကြံပြုချက်များ"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"ရှာဖွေမှုကို ပိတ်ထားသည်။"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"အလားတူစာလုံးများကို ရှာကြည့်ပါ။\n\nအရန်သိမ်းထားသော ဖိုင်များသာ သင့်ရှာဖွေမှုရလဒ်များတွင် ပေါ်လာပါမည်။"</string>
</resources>
diff --git a/photopicker/res/values-nb/feature_search_strings.xml b/photopicker/res/values-nb/feature_search_strings.xml
index ff80a3ea1..d715bf403 100644
--- a/photopicker/res/values-nb/feature_search_strings.xml
+++ b/photopicker/res/values-nb/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Prøv å søke etter lignende ord"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Forslag"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Søkefunksjonen er slått av."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Prøv å søke etter lignende ord.\n\nBare elementer som er sikkerhetskopiert, vises i søkeresultatene."</string>
</resources>
diff --git a/photopicker/res/values-ne/feature_search_strings.xml b/photopicker/res/values-ne/feature_search_strings.xml
index dd8c0b030..6ea628017 100644
--- a/photopicker/res/values-ne/feature_search_strings.xml
+++ b/photopicker/res/values-ne/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"उस्तै शब्दहरू खोजी हेर्नुहोस्"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"सुझावहरू"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"खोज गर्ने सुविधा अफ गरिएको छ।"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"उस्तै शब्दहरू खोजी हेर्नुहोस्।\n\nब्याकअप गरिएका सामग्री मात्र तपाईंका खोज परिणाममा देखिने छन्।"</string>
</resources>
diff --git a/photopicker/res/values-nl/feature_search_strings.xml b/photopicker/res/values-nl/feature_search_strings.xml
index 4a21667cc..098ac1459 100644
--- a/photopicker/res/values-nl/feature_search_strings.xml
+++ b/photopicker/res/values-nl/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Zoek naar vergelijkbare woorden"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Suggesties"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Zoekfunctie staat uit"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Zoek naar vergelijkbare woorden.\n\nAlleen items waarvan een back-up is gemaakt, worden in de zoekresultaten getoond."</string>
</resources>
diff --git a/photopicker/res/values-or/feature_search_strings.xml b/photopicker/res/values-or/feature_search_strings.xml
index b7c035aa2..36b5f6f65 100644
--- a/photopicker/res/values-or/feature_search_strings.xml
+++ b/photopicker/res/values-or/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"ସମାନ ଶବ୍ଦଗୁଡ଼ିକ ସର୍ଚ୍ଚ କରିବାକୁ ଚେଷ୍ଟା କରନ୍ତୁ"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"ପରାମର୍ଶ"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"ସର୍ଚ୍ଚକୁ ଅକ୍ଷମ କରାଯାଇଛି।"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"ସମାନ ଶବ୍ଦଗୁଡ଼ିକ ସର୍ଚ୍ଚ କରିବାକୁ ଚେଷ୍ଟା କରନ୍ତୁ।\n\nକେବଳ ବେକଅପ ନିଆଯାଇଥିବା ଆଇଟମଗୁଡ଼ିକ ଆପଣଙ୍କ ସର୍ଚ୍ଚ ଫଳାଫଳରେ ଦେଖାଯିବ।"</string>
</resources>
diff --git a/photopicker/res/values-pa/feature_search_strings.xml b/photopicker/res/values-pa/feature_search_strings.xml
index 0b75aae3d..a0dfe3ecf 100644
--- a/photopicker/res/values-pa/feature_search_strings.xml
+++ b/photopicker/res/values-pa/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"ਮਿਲਦੇ-ਜੁਲਦੇ ਸ਼ਬਦ ਖੋਜ ਕੇ ਦੇਖੋ"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"ਸੁਝਾਅ"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"ਖੋਜ ਬੰਦ ਹੈ।"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"ਮਿਲਦੇ-ਜੁਲਦੇ ਸ਼ਬਦ ਖੋਜ ਕੇ ਦੇਖੋ।\n\nਸਿਰਫ਼ ਉਹੀ ਆਈਟਮਾਂ ਤੁਹਾਡੇ ਖੋਜ ਨਤੀਜਿਆਂ ਵਿੱਚ ਦਿਖਾਈ ਦੇਣਗੀਆਂ ਜਿਨ੍ਹਾਂ ਦਾ ਬੈਕਅੱਪ ਲਿਆ ਗਿਆ ਹੈ।"</string>
</resources>
diff --git a/photopicker/res/values-pl/feature_search_strings.xml b/photopicker/res/values-pl/feature_search_strings.xml
index 97f2406b4..b8deda6c4 100644
--- a/photopicker/res/values-pl/feature_search_strings.xml
+++ b/photopicker/res/values-pl/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Spróbuj wyszukać podobne słowa"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Sugestie"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Wyszukiwanie wyłączone."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Spróbuj wyszukać podobne słowa.\n\nW wynikach wyszukiwania będą wyświetlane tylko elementy z utworzoną kopią zapasową."</string>
</resources>
diff --git a/photopicker/res/values-pt-rBR/feature_search_strings.xml b/photopicker/res/values-pt-rBR/feature_search_strings.xml
index 6b59fa098..f945c6ed0 100644
--- a/photopicker/res/values-pt-rBR/feature_search_strings.xml
+++ b/photopicker/res/values-pt-rBR/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Tente pesquisar palavras semelhantes"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Sugestões"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Pesquisa desativada."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Tente pesquisar palavras semelhantes.\n\nApenas os itens salvos em backup vão aparecer nos resultados da pesquisa."</string>
</resources>
diff --git a/photopicker/res/values-pt-rPT/feature_search_strings.xml b/photopicker/res/values-pt-rPT/feature_search_strings.xml
index c2bfdba41..3ff4679e7 100644
--- a/photopicker/res/values-pt-rPT/feature_search_strings.xml
+++ b/photopicker/res/values-pt-rPT/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Experimente pesquisar palavras semelhantes"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Sugestões"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Pesquisa desativada."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Experimente pesquisar palavras semelhantes.\n\nApenas os itens com cópia de segurança aparecem nos resultados da pesquisa."</string>
</resources>
diff --git a/photopicker/res/values-pt/feature_search_strings.xml b/photopicker/res/values-pt/feature_search_strings.xml
index 6b59fa098..f945c6ed0 100644
--- a/photopicker/res/values-pt/feature_search_strings.xml
+++ b/photopicker/res/values-pt/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Tente pesquisar palavras semelhantes"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Sugestões"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Pesquisa desativada."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Tente pesquisar palavras semelhantes.\n\nApenas os itens salvos em backup vão aparecer nos resultados da pesquisa."</string>
</resources>
diff --git a/photopicker/res/values-ro/feature_search_strings.xml b/photopicker/res/values-ro/feature_search_strings.xml
index 436899085..c995a946b 100644
--- a/photopicker/res/values-ro/feature_search_strings.xml
+++ b/photopicker/res/values-ro/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Încearcă să cauți cuvinte similare"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Sugestii"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Căutarea a fost dezactivată."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Încearcă să cauți cuvinte similare.\n\nDoar elementele pentru care s-a făcut backup vor apărea în rezultatele căutării."</string>
</resources>
diff --git a/photopicker/res/values-ru/feature_search_strings.xml b/photopicker/res/values-ru/feature_search_strings.xml
index 5d5c6db5e..1f440f476 100644
--- a/photopicker/res/values-ru/feature_search_strings.xml
+++ b/photopicker/res/values-ru/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Попробуйте искать похожие слова"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Подсказки"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Поиск отключен."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Попробуйте найти похожие слова.\n\nВ результатах поиска появятся только резервные копии объектов."</string>
</resources>
diff --git a/photopicker/res/values-si/feature_search_strings.xml b/photopicker/res/values-si/feature_search_strings.xml
index eb5c9b021..d30f9a4fd 100644
--- a/photopicker/res/values-si/feature_search_strings.xml
+++ b/photopicker/res/values-si/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"සමාන වචන සෙවීමට උත්සාහ කරන්න"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"යෝජනා"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"සෙවීම අබලයි."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"සමාන වචන සෙවීමට උත්සාහ කරන්න.\n\nඔබේ සෙවීම් ප්‍රතිඵලවල දිස් වනු ඇත්තේ උපස්ථ කරන ලද අයිතම පමණි."</string>
</resources>
diff --git a/photopicker/res/values-sk/feature_search_strings.xml b/photopicker/res/values-sk/feature_search_strings.xml
index d7dac5ccf..eca7fe4b4 100644
--- a/photopicker/res/values-sk/feature_search_strings.xml
+++ b/photopicker/res/values-sk/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Skúste vyhľadať podobné slová"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Návrhy"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Vyhľadávanie je zakázané."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Skúste vyhľadať podobné slová.\n\nVo výsledkoch vyhľadávania sa zobrazia iba položky, ktoré sú zálohované."</string>
</resources>
diff --git a/photopicker/res/values-sl/feature_search_strings.xml b/photopicker/res/values-sl/feature_search_strings.xml
index b1f8ac518..cc89de21e 100644
--- a/photopicker/res/values-sl/feature_search_strings.xml
+++ b/photopicker/res/values-sl/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Poskusite poiskati podobne besede"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Predlogi"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Iskanje je onemogočeno."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Poskusite poiskati podobne besede.\n\nV rezultatih iskanja bodo prikazani samo elementi, ki so varnostno kopirani."</string>
</resources>
diff --git a/photopicker/res/values-sq/feature_search_strings.xml b/photopicker/res/values-sq/feature_search_strings.xml
index 53752c22e..509bcaf55 100644
--- a/photopicker/res/values-sq/feature_search_strings.xml
+++ b/photopicker/res/values-sq/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Provo të kërkosh për fjalë të ngjashme"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Sugjerime"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Kërkimi është çaktivizuar."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Provo të kërkosh për fjalë të ngjashme.\n\nNë rezultatet e tua të kërkimit do të shfaqen vetëm artikujt që janë rezervuar."</string>
</resources>
diff --git a/photopicker/res/values-sr/feature_search_strings.xml b/photopicker/res/values-sr/feature_search_strings.xml
index 7a3cdaf82..6780bcc83 100644
--- a/photopicker/res/values-sr/feature_search_strings.xml
+++ b/photopicker/res/values-sr/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Потражите сличне речи"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Предлози"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Претрага је онемогућена."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Потражите сличне речи.\n\nУ резултатима претраге се приказују само ставке за које је направљена резервна копија."</string>
</resources>
diff --git a/photopicker/res/values-sv/feature_search_strings.xml b/photopicker/res/values-sv/feature_search_strings.xml
index b2b179617..b3a31fb07 100644
--- a/photopicker/res/values-sv/feature_search_strings.xml
+++ b/photopicker/res/values-sv/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Testa att söka efter liknande ord"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Förslag"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Sökning inaktiverad."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Testa att söka efter liknande ord.\n\nEndast objekt som har säkerhetskopierats visas i sökresultaten."</string>
</resources>
diff --git a/photopicker/res/values-sw/feature_search_strings.xml b/photopicker/res/values-sw/feature_search_strings.xml
index 054dc3e49..2b06b16b1 100644
--- a/photopicker/res/values-sw/feature_search_strings.xml
+++ b/photopicker/res/values-sw/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Jaribu kutafuta maneno yanayofanana"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Mapendekezo"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Umezima kipengele cha utafutaji."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Jaribu kutafuta maneno yanayofanana.\n\nVipengee ulivyohifadhia nakala pekee ndivyo vitaonekana kwenye matokeo yako ya utafutaji."</string>
</resources>
diff --git a/photopicker/res/values-ta/feature_search_strings.xml b/photopicker/res/values-ta/feature_search_strings.xml
index 2e0f629a1..e6b6f5796 100644
--- a/photopicker/res/values-ta/feature_search_strings.xml
+++ b/photopicker/res/values-ta/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"ஒரே மாதிரியான வார்த்தைகளைத் தேடுங்கள்"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"பரிந்துரைகள்"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"தேடல் அம்சம் முடக்கப்பட்டுள்ளது."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"ஒரே மாதிரியான வார்த்தைகளைத் தேடுங்கள்.\n\nகாப்புப் பிரதி எடுக்கப்பட்டவை மட்டுமே உங்கள் தேடல் முடிவுகளில் காட்டப்படும்."</string>
</resources>
diff --git a/photopicker/res/values-te/feature_search_strings.xml b/photopicker/res/values-te/feature_search_strings.xml
index 1de73c3ee..5e2208def 100644
--- a/photopicker/res/values-te/feature_search_strings.xml
+++ b/photopicker/res/values-te/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"ఒకే రకమైన పదాల కోసం సెర్చ్ చేయడానికి ట్రై చేయండి"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"సూచనలు"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"సెర్చ్ డిజేబుల్ అయింది."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"ఒకే రకమైన పదాలను సెర్చ్ చేసి చూడండి.\n\nబ్యాకప్ అయిన ఐటెమ్‌లు మాత్రమే మీ సెర్చ్ ఫలితాల్లో కనిపిస్తాయి."</string>
</resources>
diff --git a/photopicker/res/values-th/feature_search_strings.xml b/photopicker/res/values-th/feature_search_strings.xml
index 1bde329ae..8e2e33b9c 100644
--- a/photopicker/res/values-th/feature_search_strings.xml
+++ b/photopicker/res/values-th/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"ลองค้นหาคำที่คล้ายกัน"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"คำแนะนำ"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"การค้นหาปิดอยู่"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"ลองค้นหาคำที่คล้ายกัน\n\nเฉพาะรายการที่สำรองข้อมูลไว้เท่านั้นที่จะปรากฏในผลการค้นหา"</string>
</resources>
diff --git a/photopicker/res/values-tl/feature_search_strings.xml b/photopicker/res/values-tl/feature_search_strings.xml
index 69d537ee3..7b4f35a29 100644
--- a/photopicker/res/values-tl/feature_search_strings.xml
+++ b/photopicker/res/values-tl/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Subukang maghanap ng mga katulad na salita"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Mga Suhestyon"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Naka-disable ang paghahanap"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Subukang maghanap ng mga katulad na salita.\n\nAng mga item lang na na-back up ang lalabas sa iyong mga resulta ng paghahanap."</string>
</resources>
diff --git a/photopicker/res/values-tr/feature_search_strings.xml b/photopicker/res/values-tr/feature_search_strings.xml
index b31569881..096c0a296 100644
--- a/photopicker/res/values-tr/feature_search_strings.xml
+++ b/photopicker/res/values-tr/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Benzer kelimeleri aramayı deneyin"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Öneriler"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Arama devre dışı bırakıldı"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Benzer kelimeleri aramayı deneyin.\n\nYalnızca yedeklenen öğeler arama sonuçlarınızda gösterilir."</string>
</resources>
diff --git a/photopicker/res/values-uk/feature_search_strings.xml b/photopicker/res/values-uk/feature_search_strings.xml
index 76a1bf936..e2f61e51b 100644
--- a/photopicker/res/values-uk/feature_search_strings.xml
+++ b/photopicker/res/values-uk/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Спробуйте пошукати за схожими словами"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Пропозиції"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Пошук вимкнено."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Спробуйте пошукати за схожими словами.\n\nУ результатах пошуку відображатимуться лише фотографії, для яких створено резервні копії."</string>
</resources>
diff --git a/photopicker/res/values-ur/feature_search_strings.xml b/photopicker/res/values-ur/feature_search_strings.xml
index 186fdd20e..151010d9f 100644
--- a/photopicker/res/values-ur/feature_search_strings.xml
+++ b/photopicker/res/values-ur/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"ملتے جلتے الفاظ تلاش کرنے کی کوشش کریں"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"تجاویز"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"تلاش غیر فعال ہے۔"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"ملتے جلتے الفاظ تلاش کرنے کی کوشش کریں۔\n\nآپ کے تلاش کے نتائج میں صرف وہ آئٹمز ظاہر ہوں گے جن کا بیک اپ لیا گیا ہے۔"</string>
</resources>
diff --git a/photopicker/res/values-uz/feature_search_strings.xml b/photopicker/res/values-uz/feature_search_strings.xml
index 1824a3302..11d23d060 100644
--- a/photopicker/res/values-uz/feature_search_strings.xml
+++ b/photopicker/res/values-uz/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Oʻxshash soʻzlarni qidiring"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Takliflar"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Qidiruv faolsizlantirildi."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Oʻxshash soʻzlarni qidiring.\n\nQidiruv natijalarida faqat zaxiralanganlar chiqadi."</string>
</resources>
diff --git a/photopicker/res/values-vi/feature_search_strings.xml b/photopicker/res/values-vi/feature_search_strings.xml
index 27b4277d5..f70b7a3ae 100644
--- a/photopicker/res/values-vi/feature_search_strings.xml
+++ b/photopicker/res/values-vi/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Hãy thử tìm những từ tương tự"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Nội dung đề xuất"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Tính năng tìm kiếm đã tắt."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Hãy thử tìm các từ tương tự.\n\nChỉ những mục đã được sao lưu mới xuất hiện trong kết quả tìm kiếm của bạn."</string>
</resources>
diff --git a/photopicker/res/values-zh-rCN/feature_search_strings.xml b/photopicker/res/values-zh-rCN/feature_search_strings.xml
index c4cb150af..9f1c53608 100644
--- a/photopicker/res/values-zh-rCN/feature_search_strings.xml
+++ b/photopicker/res/values-zh-rCN/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"试试搜索类似字词"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"建议"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"搜索功能已停用。"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"请尝试搜索类似字词。\n\n只有已备份的内容才会显示在搜索结果中。"</string>
</resources>
diff --git a/photopicker/res/values-zh-rHK/feature_search_strings.xml b/photopicker/res/values-zh-rHK/feature_search_strings.xml
index 48aa842e1..b66480442 100644
--- a/photopicker/res/values-zh-rHK/feature_search_strings.xml
+++ b/photopicker/res/values-zh-rHK/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"請嘗試搜尋類似字詞"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"建議"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"搜尋已停用。"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"請嘗試搜尋類似字詞。\n\n搜尋結果只會顯示已備份的項目。"</string>
</resources>
diff --git a/photopicker/res/values-zh-rTW/feature_search_strings.xml b/photopicker/res/values-zh-rTW/feature_search_strings.xml
index c9d5b2cc6..77aa7ae1e 100644
--- a/photopicker/res/values-zh-rTW/feature_search_strings.xml
+++ b/photopicker/res/values-zh-rTW/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"請嘗試搜尋相似的字詞"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"建議"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"搜尋功能已停用。"</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"請試著搜尋相似的字詞。\n\n搜尋結果只會顯示已備份的項目。"</string>
</resources>
diff --git a/photopicker/res/values-zu/feature_search_strings.xml b/photopicker/res/values-zu/feature_search_strings.xml
index c28fb27bd..a03a9ce46 100644
--- a/photopicker/res/values-zu/feature_search_strings.xml
+++ b/photopicker/res/values-zu/feature_search_strings.xml
@@ -24,4 +24,5 @@
<string name="photopicker_search_result_empty_state_body" msgid="1732249539645056618">"Zama ukusesha amagama afanayo"</string>
<string name="photopicker_search_suggestions_text" msgid="1758802235973720586">"Iziphakamiso"</string>
<string name="photopicker_search_disabled_hint" msgid="4049451515025551462">"Usesho lukhutshaziwe."</string>
+ <string name="photopicker_search_result_empty_state_message" msgid="2834353361038395730">"Zama ukusesha amagama afanayo.\n\nYizinto ezenzelwe ikhophi kuphela ezizovela emiphumeleni yakho yosesho."</string>
</resources>
diff --git a/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt b/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt
index 2b169311e..74bde6db9 100644
--- a/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt
+++ b/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt
@@ -22,6 +22,7 @@ import android.content.pm.ResolveInfo
import android.media.ApplicationMediaCapabilities
import android.net.Uri
import android.os.SystemProperties
+import android.os.UserHandle
import android.provider.MediaStore
import android.util.Log
import com.android.photopicker.core.navigation.PhotopickerDestinations
@@ -88,16 +89,21 @@ data class PhotopickerConfiguration(
/**
* Use the internal Intent to see if the Intent can be resolved as a
- * CrossProfileIntentForwarderActivity
+ * CrossProfileIntentForwarderActivity for the target user.
*
* This method exists to limit the visibility of the intent field, but [UserMonitor] requires
* the intent to check for CrossProfileIntentForwarder's. Rather than exposing intent as a
* public field, this method can be called to do the check, if an Intent exists.
*
+ * @param packageManager the PM of the process owner
+ * @param handle the [UserHandle] of the target user
* @return Whether the current Intent Photopicker may be running under has a matching
* CrossProfileIntentForwarderActivity
*/
- fun doesCrossProfileIntentForwarderExists(packageManager: PackageManager): Boolean {
+ fun doesCrossProfileIntentForwarderExists(
+ packageManager: PackageManager,
+ targetUserHandle: UserHandle,
+ ): Boolean {
val intentToCheck: Intent? =
when (runtimeEnv) {
@@ -121,9 +127,61 @@ data class PhotopickerConfiguration(
packageManager.queryIntentActivities(it, PackageManager.MATCH_DEFAULT_ONLY)) {
info?.let {
if (it.isCrossProfileIntentForwarderActivity()) {
- // This profile can handle cross profile content
- // from the current context profile
- return true
+
+ /*
+ * IMPORTANT: This is a reflection based hack to ensure the profile is actually
+ * the installer of the CrossProfileIntentForwardingActivity.
+ *
+ * ResolveInfo.targetUserId exists, but is a hidden API not available to
+ * mainline modules, and no such API exists, so it is accessed via reflection
+ * below. All exceptions are caught to protect against reflection related
+ * issues such as:
+ * NoSuchFieldException / IllegalAccessException / SecurityException.
+ *
+ * In the event of an exception, the code fails "closed" for the current
+ * profile to avoid showing content that should not be visible.
+ */
+ val activityTargetUserId =
+ try {
+ val property =
+ it::class.java.getDeclaredField("targetUserId").apply {
+ isAccessible = true
+ }
+ property?.get(it) as? Int
+ } catch (e: Exception) {
+ when (e) {
+ is NoSuchFieldException,
+ is IllegalAccessException,
+ is SecurityException -> {
+ Log.e(
+ ConfigurationManager.TAG,
+ "Could not reflect targetUserId field for cross " +
+ "profile checks.",
+ )
+ // Any time we are unable to obtain the cross profile
+ // targetUserId, fail closed by returning false.
+ return@doesCrossProfileIntentForwarderExists false
+ }
+ else -> {
+ Log.e(
+ ConfigurationManager.TAG,
+ "Exception occurred during cross profile checks",
+ e,
+ )
+ null
+ }
+ }
+ }
+ if (activityTargetUserId == targetUserHandle.getIdentifier()) {
+ Log.d(
+ ConfigurationManager.TAG,
+ "Found matching CrossProfileIntentForwarderActivity for " +
+ "targetUserId ${targetUserHandle.getIdentifier()}",
+ )
+ // This profile can handle cross profile content
+ // from the current context profile
+ return true
+ }
}
}
}
diff --git a/photopicker/src/com/android/photopicker/core/events/Dispatchers.kt b/photopicker/src/com/android/photopicker/core/events/Dispatchers.kt
index 3ec63d3f3..1984a4335 100644
--- a/photopicker/src/com/android/photopicker/core/events/Dispatchers.kt
+++ b/photopicker/src/com/android/photopicker/core/events/Dispatchers.kt
@@ -49,18 +49,18 @@ fun dispatchReportPhotopickerApiInfoEvent(
val sessionId = photopickerConfiguration.sessionId
// We always launch the picker in collapsed state. We track the state change as UI event.
val pickerSize = Telemetry.PickerSize.COLLAPSED
- val mediaFilters =
- photopickerConfiguration.mimeTypes
- .map { mimeType ->
- when {
- mimeType.contains("image") && mimeType.contains("video") ->
- Telemetry.MediaType.PHOTO_VIDEO
- mimeType.startsWith("image/") -> Telemetry.MediaType.PHOTO
- mimeType.startsWith("video/") -> Telemetry.MediaType.VIDEO
- else -> Telemetry.MediaType.UNSET_MEDIA_TYPE
- }
- }
- .ifEmpty { listOf(Telemetry.MediaType.UNSET_MEDIA_TYPE) }
+ val mimeTypes = photopickerConfiguration.mimeTypes
+ val mediaFilter =
+ when {
+ mimeTypes.size > 1 &&
+ mimeTypes.any { it.startsWith("image/") } &&
+ mimeTypes.any { it.startsWith("video/") } -> Telemetry.MediaType.PHOTO_VIDEO
+ mimeTypes.size == 1 && mimeTypes.first().startsWith("image/") ->
+ Telemetry.MediaType.PHOTO
+ mimeTypes.size == 1 && mimeTypes.first().startsWith("video/") ->
+ Telemetry.MediaType.VIDEO
+ else -> Telemetry.MediaType.UNSET_MEDIA_TYPE
+ }
val maxPickedItemsCount = photopickerConfiguration.selectionLimit
val selectedTab =
when (photopickerConfiguration.startDestination) {
@@ -80,28 +80,26 @@ fun dispatchReportPhotopickerApiInfoEvent(
val isCloudSearchEnabled = lazyFeatureManager.get().isFeatureEnabled(SearchFeature::class.java)
// TODO(b/376822503): Update when search is added
val isLocalSearchEnabled = false
- for (mediaFilter in mediaFilters) {
- coroutineScope.launch {
- lazyEvents
- .get()
- .dispatch(
- Event.ReportPhotopickerApiInfo(
- dispatcherToken = dispatcherToken,
- sessionId = sessionId,
- pickerIntentAction = pickerIntentAction,
- pickerSize = pickerSize,
- mediaFilter = mediaFilter,
- maxPickedItemsCount = maxPickedItemsCount,
- selectedTab = selectedTab,
- selectedAlbum = selectedAlbum,
- isOrderedSelectionSet = isOrderedSelectionSet,
- isAccentColorSet = isAccentColorSet,
- isDefaultTabSet = isDefaultTabSet,
- isCloudSearchEnabled = isCloudSearchEnabled,
- isLocalSearchEnabled = isLocalSearchEnabled,
- )
+ coroutineScope.launch {
+ lazyEvents
+ .get()
+ .dispatch(
+ Event.ReportPhotopickerApiInfo(
+ dispatcherToken = dispatcherToken,
+ sessionId = sessionId,
+ pickerIntentAction = pickerIntentAction,
+ pickerSize = pickerSize,
+ mediaFilter = mediaFilter,
+ maxPickedItemsCount = maxPickedItemsCount,
+ selectedTab = selectedTab,
+ selectedAlbum = selectedAlbum,
+ isOrderedSelectionSet = isOrderedSelectionSet,
+ isAccentColorSet = isAccentColorSet,
+ isDefaultTabSet = isDefaultTabSet,
+ isCloudSearchEnabled = isCloudSearchEnabled,
+ isLocalSearchEnabled = isLocalSearchEnabled,
)
- }
+ )
}
}
diff --git a/photopicker/src/com/android/photopicker/core/user/UserMonitor.kt b/photopicker/src/com/android/photopicker/core/user/UserMonitor.kt
index a751e5382..15c1b403c 100644
--- a/photopicker/src/com/android/photopicker/core/user/UserMonitor.kt
+++ b/photopicker/src/com/android/photopicker/core/user/UserMonitor.kt
@@ -311,6 +311,11 @@ class UserMonitor(
*/
private fun getIsCrossProfileAllowedForHandle(handle: UserHandle): Boolean {
+ // Early exit conditions
+ if (handle == processOwnerUserHandle) {
+ return true
+ }
+
// First, check if cross profile is delegated to parent profile
if (SdkLevel.isAtLeastV()) {
val properties: UserProperties = userManager.getUserProperties(handle)
@@ -323,14 +328,21 @@ class UserMonitor(
properties.getCrossProfileContentSharingStrategy() ==
UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT
) {
- return true
+ val parent = userManager.getProfileParent(handle)
+
+ parent?.let {
+ return getIsCrossProfileAllowedForHandle(it)
+ }
+
+ // Couldn't resolve parent, fail closed.
+ return false
}
}
// As a last resort, no applicable cross profile information found, so inspect the current
// configuration and if there is an intent set, try to see
// if there is a matching CrossProfileIntentForwarder
- return configuration.value.doesCrossProfileIntentForwarderExists(packageManager)
+ return configuration.value.doesCrossProfileIntentForwarderExists(packageManager, handle)
}
/**
diff --git a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGrid.kt b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGrid.kt
index 7cec24324..a281c5944 100644
--- a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGrid.kt
+++ b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGrid.kt
@@ -38,8 +38,6 @@ import com.android.photopicker.R
import com.android.photopicker.core.components.MediaGridItem
import com.android.photopicker.core.components.mediaGrid
import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
-import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
-import com.android.photopicker.core.embedded.LocalEmbeddedState
import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.LocalEvents
import com.android.photopicker.core.events.Telemetry
@@ -81,10 +79,6 @@ fun AlbumGrid(viewModel: AlbumGridViewModel = obtainViewModel()) {
val events = LocalEvents.current
val scope = rememberCoroutineScope()
- val isEmbedded =
- LocalPhotopickerConfiguration.current.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED
- val isExpanded = LocalEmbeddedState.current?.isExpanded ?: false
-
// Use the expanded layout any time the Width is Medium or larger.
val isExpandedScreen: Boolean =
when (LocalWindowSizeClass.current.widthSizeClass) {
diff --git a/photopicker/src/com/android/photopicker/features/categorygrid/CategoryGrid.kt b/photopicker/src/com/android/photopicker/features/categorygrid/CategoryGrid.kt
index 0c3270ab5..f6703268c 100644
--- a/photopicker/src/com/android/photopicker/features/categorygrid/CategoryGrid.kt
+++ b/photopicker/src/com/android/photopicker/features/categorygrid/CategoryGrid.kt
@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
@@ -47,8 +48,6 @@ import com.android.photopicker.R
import com.android.photopicker.core.components.MediaGridItem
import com.android.photopicker.core.components.mediaGrid
import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
-import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
-import com.android.photopicker.core.embedded.LocalEmbeddedState
import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.LocalEvents
import com.android.photopicker.core.events.Telemetry
@@ -92,10 +91,6 @@ fun CategoryGrid(viewModel: CategoryGridViewModel = obtainViewModel()) {
val events = LocalEvents.current
val scope = rememberCoroutineScope()
- val isEmbedded =
- LocalPhotopickerConfiguration.current.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED
- val isExpanded = LocalEmbeddedState.current?.isExpanded ?: false
-
// Use the expanded layout any time the Width is Medium or larger.
val isExpandedScreen: Boolean =
when (LocalWindowSizeClass.current.widthSizeClass) {
@@ -243,6 +238,7 @@ fun CategoryButton(modifier: Modifier) {
imageVector =
ImageVector.vectorResource(R.drawable.photopicker_category_icon),
contentDescription = null,
+ modifier = Modifier.size(18.dp),
)
Spacer(Modifier.width(8.dp))
Text(
diff --git a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt
index f5dfa8eec..b61675737 100644
--- a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt
+++ b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt
@@ -28,12 +28,12 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Image
-import androidx.compose.material.icons.outlined.PhotoAlbum
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@@ -385,7 +385,11 @@ fun PhotoGridNavButton(modifier: Modifier) {
when {
categoryFeatureEnabled && searchFeatureEnabled -> {
Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(imageVector = Icons.Outlined.PhotoAlbum, contentDescription = null)
+ Icon(
+ imageVector = Icons.Outlined.Image,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ )
Spacer(Modifier.width(8.dp))
Text(
stringResource(R.string.photopicker_photos_nav_button_label),
diff --git a/photopicker/src/com/android/photopicker/features/search/Search.kt b/photopicker/src/com/android/photopicker/features/search/Search.kt
index 1e6cb0a6c..0f2c619be 100644
--- a/photopicker/src/com/android/photopicker/features/search/Search.kt
+++ b/photopicker/src/com/android/photopicker/features/search/Search.kt
@@ -479,10 +479,15 @@ private fun SearchInput(
}
@Composable
-private fun RequestFocusOnResume(focusRequester: FocusRequester, focused: Boolean) {
+private fun RequestFocusOnResume(
+ focusRequester: FocusRequester,
+ focused: Boolean,
+ viewModel: SearchViewModel = obtainViewModel(),
+) {
val lifecycleOwner = LocalLifecycleOwner.current
+ val searchState by viewModel.searchState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
- when (focused) {
+ when (focused && searchState is SearchState.Inactive) {
true ->
lifecycleOwner.repeatOnLifecycle(state = Lifecycle.State.RESUMED) {
focusRequester.requestFocus()
@@ -789,7 +794,11 @@ fun SuggestionItem(suggestion: SearchSuggestion) {
}
val text = suggestion.displayText ?: ""
Text(text = text, modifier = Modifier.padding(start = MEASUREMENT_LARGE_PADDING).weight(1f))
- if (suggestion.type != SearchSuggestionType.FACE && suggestion.icon != null) {
+ if (
+ suggestion.type != SearchSuggestionType.FACE &&
+ suggestion.type != SearchSuggestionType.HISTORY &&
+ suggestion.icon != null
+ ) {
ShowSuggestionIcon(suggestion, Modifier.size(MEASUREMENT_OTHER_ICON).clip(CircleShape))
}
}
diff --git a/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt b/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt
index 0a36050d3..5b40fa827 100644
--- a/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt
@@ -77,6 +77,17 @@ import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
class BannerManagerImplTest {
+ /**
+ * Class that exposes the @hide api [targetUserId] in order to supply proper values for
+ * reflection based code that is inspecting this field.
+ *
+ * @property targetUserId
+ */
+ private class ReflectedResolveInfo(@JvmField val targetUserId: Int) : ResolveInfo() {
+
+ override fun isCrossProfileIntentForwarderActivity(): Boolean = true
+ }
+
// Isolate the test device by providing a test wrapper around device config so that the
// tests can control the flag values that are returned.
val deviceConfigProxy = TestDeviceConfigProxyImpl()
@@ -149,8 +160,7 @@ class BannerManagerImplTest {
whenever(mockUserManager.isManagedProfile(USER_ID_MANAGED)) { true }
whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { USER_HANDLE_PRIMARY }
- val mockResolveInfo = mock(ResolveInfo::class.java)
- whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true }
+ val mockResolveInfo = ReflectedResolveInfo(USER_ID_MANAGED)
whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
listOf(mockResolveInfo)
}
@@ -160,13 +170,13 @@ class BannerManagerImplTest {
resources.getDrawable(R.drawable.android, /* theme= */ null)
}
whenever(mockUserManager.getProfileLabel()) { PLATFORM_PROVIDED_PROFILE_LABEL }
- whenever(mockUserManager.getUserProperties(USER_HANDLE_PRIMARY))
- @JvmSerializableLambda {
- UserProperties.Builder().build()
- }
+ whenever(
+ mockUserManager.getUserProperties(USER_HANDLE_PRIMARY)
+ ) @JvmSerializableLambda { UserProperties.Builder().build() }
// By default, allow managed profile to be available
- whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED))
- @JvmSerializableLambda {
+ whenever(
+ mockUserManager.getUserProperties(USER_HANDLE_MANAGED)
+ ) @JvmSerializableLambda {
UserProperties.Builder()
.setCrossProfileContentSharingStrategy(
UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT
diff --git a/photopicker/tests/src/com/android/photopicker/core/events/DispatchersTest.kt b/photopicker/tests/src/com/android/photopicker/core/events/DispatchersTest.kt
index 0b99adf27..90afd5811 100644
--- a/photopicker/tests/src/com/android/photopicker/core/events/DispatchersTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/events/DispatchersTest.kt
@@ -154,10 +154,9 @@ class DispatchersTest {
mockSystemService(mockContext, UserManager::class.java) { mockUserManager }
if (SdkLevel.isAtLeastV()) {
- whenever(mockUserManager.getUserProperties(any(UserHandle::class.java)))
- @JvmSerializableLambda {
- UserProperties.Builder().build()
- }
+ whenever(
+ mockUserManager.getUserProperties(any(UserHandle::class.java))
+ ) @JvmSerializableLambda { UserProperties.Builder().build() }
whenever(mockUserManager.getUserBadge()) {
InstrumentationRegistry.getInstrumentation()
.context
@@ -358,12 +357,176 @@ class DispatchersTest {
}
@Test
- fun testDispatchReportPhotopickerApiInfoEvent() = runTest {
+ fun testDispatchReportPhotopickerApiInfoEventWithPhotoMimeType() = runTest {
+ // Setup
+ setup(testScope = this)
+
+ val mimeTypeList = arrayListOf("image/jpg")
+ val telemetryMimeTypeMapping = Telemetry.MediaType.PHOTO
+
+ val pickerIntentAction = Telemetry.PickerIntentAction.ACTION_PICK_IMAGES
+ val cloudSearch = lazyFeatureManager.get().isFeatureEnabled(SearchFeature::class.java)
+ val photopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(value = "")
+ sessionId(value = sessionId)
+ callingPackageUid(value = packageUid)
+ runtimeEnv(value = PhotopickerRuntimeEnv.EMBEDDED)
+ mimeTypes(mimeTypeList)
+ }
+
+ val expectedEvent =
+ Event.ReportPhotopickerApiInfo(
+ dispatcherToken = FeatureToken.CORE.token,
+ sessionId = sessionId,
+ pickerIntentAction = pickerIntentAction,
+ pickerSize = Telemetry.PickerSize.COLLAPSED,
+ mediaFilter = telemetryMimeTypeMapping,
+ maxPickedItemsCount = 1,
+ selectedTab = Telemetry.SelectedTab.UNSET_SELECTED_TAB,
+ selectedAlbum = Telemetry.SelectedAlbum.UNSET_SELECTED_ALBUM,
+ isOrderedSelectionSet = false,
+ isAccentColorSet = false,
+ isDefaultTabSet = false,
+ isCloudSearchEnabled = cloudSearch,
+ isLocalSearchEnabled = false,
+ )
+
+ // Action
+ dispatchReportPhotopickerApiInfoEvent(
+ coroutineScope = backgroundScope,
+ lazyEvents = lazyEvents,
+ photopickerConfiguration = photopickerConfiguration,
+ pickerIntentAction = pickerIntentAction,
+ lazyFeatureManager = lazyFeatureManager,
+ )
+ advanceTimeBy(delayTimeMillis = 50)
+
+ // Assert
+ assertThat(eventsDispatched).contains(expectedEvent)
+ assertThat(expectedEvent.mediaFilter).isEqualTo(telemetryMimeTypeMapping)
+ }
+
+ @Test
+ fun testDispatchReportPhotopickerApiInfoEventWithVideoMimeType() = runTest {
+ // Setup
+ setup(testScope = this)
+
+ val mimeTypeList = arrayListOf("video/jpg")
+ val telemetryMimeTypeMapping = Telemetry.MediaType.VIDEO
+
+ val pickerIntentAction = Telemetry.PickerIntentAction.ACTION_PICK_IMAGES
+ val cloudSearch = lazyFeatureManager.get().isFeatureEnabled(SearchFeature::class.java)
+ val photopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(value = "")
+ sessionId(value = sessionId)
+ callingPackageUid(value = packageUid)
+ runtimeEnv(value = PhotopickerRuntimeEnv.EMBEDDED)
+ mimeTypes(mimeTypeList)
+ }
+
+ val expectedEvent =
+ Event.ReportPhotopickerApiInfo(
+ dispatcherToken = FeatureToken.CORE.token,
+ sessionId = sessionId,
+ pickerIntentAction = pickerIntentAction,
+ pickerSize = Telemetry.PickerSize.COLLAPSED,
+ mediaFilter = telemetryMimeTypeMapping,
+ maxPickedItemsCount = 1,
+ selectedTab = Telemetry.SelectedTab.UNSET_SELECTED_TAB,
+ selectedAlbum = Telemetry.SelectedAlbum.UNSET_SELECTED_ALBUM,
+ isOrderedSelectionSet = false,
+ isAccentColorSet = false,
+ isDefaultTabSet = false,
+ isCloudSearchEnabled = cloudSearch,
+ isLocalSearchEnabled = false,
+ )
+
+ // Action
+ dispatchReportPhotopickerApiInfoEvent(
+ coroutineScope = backgroundScope,
+ lazyEvents = lazyEvents,
+ photopickerConfiguration = photopickerConfiguration,
+ pickerIntentAction = pickerIntentAction,
+ lazyFeatureManager = lazyFeatureManager,
+ )
+ advanceTimeBy(delayTimeMillis = 50)
+
+ // Assert
+ assertThat(eventsDispatched).contains(expectedEvent)
+ assertThat(expectedEvent.mediaFilter).isEqualTo(telemetryMimeTypeMapping)
+ }
+
+ @Test
+ fun testDispatchReportPhotopickerApiInfoEventWithBothPhotoAndVideoMimeType() = runTest {
+ // Setup
+ setup(testScope = this)
+
+ val mimeTypeList = arrayListOf("image/jpg", "video/mp4")
+ val telemetryMimeTypeMapping = Telemetry.MediaType.PHOTO_VIDEO
+
+ val pickerIntentAction = Telemetry.PickerIntentAction.ACTION_PICK_IMAGES
+ val cloudSearch = lazyFeatureManager.get().isFeatureEnabled(SearchFeature::class.java)
+ val photopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(value = "")
+ sessionId(value = sessionId)
+ callingPackageUid(value = packageUid)
+ runtimeEnv(value = PhotopickerRuntimeEnv.EMBEDDED)
+ mimeTypes(mimeTypeList)
+ }
+
+ val expectedEvent =
+ Event.ReportPhotopickerApiInfo(
+ dispatcherToken = FeatureToken.CORE.token,
+ sessionId = sessionId,
+ pickerIntentAction = pickerIntentAction,
+ pickerSize = Telemetry.PickerSize.COLLAPSED,
+ mediaFilter = telemetryMimeTypeMapping,
+ maxPickedItemsCount = 1,
+ selectedTab = Telemetry.SelectedTab.UNSET_SELECTED_TAB,
+ selectedAlbum = Telemetry.SelectedAlbum.UNSET_SELECTED_ALBUM,
+ isOrderedSelectionSet = false,
+ isAccentColorSet = false,
+ isDefaultTabSet = false,
+ isCloudSearchEnabled = cloudSearch,
+ isLocalSearchEnabled = false,
+ )
+
+ // Action
+ dispatchReportPhotopickerApiInfoEvent(
+ coroutineScope = backgroundScope,
+ lazyEvents = lazyEvents,
+ photopickerConfiguration = photopickerConfiguration,
+ pickerIntentAction = pickerIntentAction,
+ lazyFeatureManager = lazyFeatureManager,
+ )
+ advanceTimeBy(delayTimeMillis = 50)
+
+ // Assert
+ assertThat(eventsDispatched).contains(expectedEvent)
+ assertThat(expectedEvent.mediaFilter).isEqualTo(telemetryMimeTypeMapping)
+ }
+
+ @Test
+ fun testDispatchReportPhotopickerApiInfoEventWithDefaultPhotoAndVideoMimeType() = runTest {
// Setup
setup(testScope = this)
+ val mimeTypeList = arrayListOf("image/*", "video/*")
+ val telemetryMimeTypeMapping = Telemetry.MediaType.PHOTO_VIDEO
+
val pickerIntentAction = Telemetry.PickerIntentAction.ACTION_PICK_IMAGES
val cloudSearch = lazyFeatureManager.get().isFeatureEnabled(SearchFeature::class.java)
+ val photopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(value = "")
+ sessionId(value = sessionId)
+ callingPackageUid(value = packageUid)
+ runtimeEnv(value = PhotopickerRuntimeEnv.EMBEDDED)
+ mimeTypes(mimeTypeList)
+ }
val expectedEvent =
Event.ReportPhotopickerApiInfo(
@@ -371,7 +534,7 @@ class DispatchersTest {
sessionId = sessionId,
pickerIntentAction = pickerIntentAction,
pickerSize = Telemetry.PickerSize.COLLAPSED,
- mediaFilter = Telemetry.MediaType.PHOTO,
+ mediaFilter = telemetryMimeTypeMapping,
maxPickedItemsCount = 1,
selectedTab = Telemetry.SelectedTab.UNSET_SELECTED_TAB,
selectedAlbum = Telemetry.SelectedAlbum.UNSET_SELECTED_ALBUM,
@@ -394,5 +557,6 @@ class DispatchersTest {
// Assert
assertThat(eventsDispatched).contains(expectedEvent)
+ assertThat(expectedEvent.mediaFilter).isEqualTo(telemetryMimeTypeMapping)
}
}
diff --git a/photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt b/photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt
index 0c0b9a938..adca148b4 100644
--- a/photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt
@@ -67,6 +67,17 @@ import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
class UserMonitorTest {
+ /**
+ * Class that exposes the @hide api [targetUserId] in order to supply proper values for
+ * reflection based code that is inspecting this field.
+ *
+ * @property targetUserId
+ */
+ private class ReflectedResolveInfo(@JvmField val targetUserId: Int) : ResolveInfo() {
+
+ override fun isCrossProfileIntentForwarderActivity(): Boolean = true
+ }
+
private val PLATFORM_PROVIDED_PROFILE_LABEL = "Platform Label"
private val USER_HANDLE_PRIMARY: UserHandle
@@ -148,10 +159,10 @@ class UserMonitorTest {
whenever(mockUserManager.isManagedProfile(USER_ID_MANAGED)) { true }
whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { USER_HANDLE_PRIMARY }
- val mockResolveInfo = mock(ResolveInfo::class.java)
- whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true }
+ // Fake for a CrossProfileIntentForwarderActivity for the managed profile
+ val resolveInfoForManagedUser = ReflectedResolveInfo(USER_HANDLE_MANAGED.getIdentifier())
whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
- listOf(mockResolveInfo)
+ listOf(resolveInfoForManagedUser)
}
if (SdkLevel.isAtLeastV()) {
@@ -159,13 +170,13 @@ class UserMonitorTest {
resources.getDrawable(R.drawable.android, /* theme= */ null)
}
whenever(mockUserManager.getProfileLabel()) { PLATFORM_PROVIDED_PROFILE_LABEL }
- whenever(mockUserManager.getUserProperties(USER_HANDLE_PRIMARY))
- @JvmSerializableLambda {
- UserProperties.Builder().build()
- }
+ whenever(
+ mockUserManager.getUserProperties(USER_HANDLE_PRIMARY)
+ ) @JvmSerializableLambda { UserProperties.Builder().build() }
// By default, allow managed profile to be available
- whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED))
- @JvmSerializableLambda {
+ whenever(
+ mockUserManager.getUserProperties(USER_HANDLE_MANAGED)
+ ) @JvmSerializableLambda {
UserProperties.Builder()
.setCrossProfileContentSharingStrategy(
UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT
@@ -208,8 +219,12 @@ class UserMonitorTest {
fun testProfilesForCrossProfileIntentForwardingVPlus() {
assumeTrue(SdkLevel.isAtLeastV())
- whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED))
- @JvmSerializableLambda {
+
+ // Since the UserProperties here will return no delegation, this will
+ // have to rely on CrossProfileIntentForwarderActivity found for the managed
+ // user in order to enable cross profile for this profile.
+ // This is already setup in the base setup method.
+ whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED)) @JvmSerializableLambda {
UserProperties.Builder()
.setCrossProfileContentSharingStrategy(
UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION
@@ -217,12 +232,6 @@ class UserMonitorTest {
.build()
}
- val mockResolveInfo = mock(ResolveInfo::class.java)
- whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true }
- whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
- listOf(mockResolveInfo)
- }
-
runTest { // this: TestScope
userMonitor =
UserMonitor(
@@ -250,14 +259,115 @@ class UserMonitorTest {
/** Ensures profiles with a cross profile forwarding intent are active */
@Test
fun testProfilesForCrossProfileIntentForwardingUMinus() {
+ assumeFalse(SdkLevel.isAtLeastV())
+
+ runTest { // this: TestScope
+ userMonitor =
+ UserMonitor(
+ mockContext,
+ 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,
+ )
+
+ launch {
+ val reportedStatus = userMonitor.userStatus.first()
+ assertUserStatusIsEqualIgnoringFields(reportedStatus, initialExpectedStatus)
+ }
+ }
+ }
+
+ /** Ensures profiles without a cross profile forwarding intent are disabled */
+ @Test
+ fun testProfilesForCrossProfileIntentManagedDoesNotSupportVPlus() {
+
+ assumeTrue(SdkLevel.isAtLeastV())
+
+ // Since the UserProperties here will return no delegation, this will
+ // have to rely on CrossProfileIntentForwarderActivity found for the managed
+ // user in order to enable cross profile for this profile.
+ // This is already setup in the base setup method.
+ whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED)) @JvmSerializableLambda {
+ UserProperties.Builder()
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION
+ )
+ .build()
+ }
+ whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
+ emptyList<ResolveInfo>()
+ }
+
+ val expectedStatus =
+ UserStatus(
+ activeUserProfile = PRIMARY_PROFILE_BASE,
+ allProfiles =
+ listOf(
+ PRIMARY_PROFILE_BASE,
+ MANAGED_PROFILE_BASE.copy(
+ disabledReasons =
+ setOf(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED)
+ ),
+ ),
+ activeContentResolver = mockContentResolver,
+ )
+
+ runTest { // this: TestScope
+ userMonitor =
+ UserMonitor(
+ mockContext,
+ 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,
+ )
+
+ launch {
+ val reportedStatus = userMonitor.userStatus.first()
+ assertUserStatusIsEqualIgnoringFields(reportedStatus, expectedStatus)
+ }
+ }
+ }
+
+ /** Ensures profiles without a cross profile forwarding intent are disabled */
+ @Test
+ fun testProfilesForCrossProfileIntentManagedDoesNotSupportUMinus() {
assumeFalse(SdkLevel.isAtLeastV())
- val mockResolveInfo = mock(ResolveInfo::class.java)
- whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true }
+
whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
- listOf(mockResolveInfo)
+ emptyList<ResolveInfo>()
}
+ val expectedStatus =
+ UserStatus(
+ activeUserProfile = PRIMARY_PROFILE_BASE,
+ allProfiles =
+ listOf(
+ PRIMARY_PROFILE_BASE,
+ MANAGED_PROFILE_BASE.copy(
+ disabledReasons =
+ setOf(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED)
+ ),
+ ),
+ activeContentResolver = mockContentResolver,
+ )
+
runTest { // this: TestScope
userMonitor =
UserMonitor(
@@ -277,7 +387,362 @@ class UserMonitorTest {
launch {
val reportedStatus = userMonitor.userStatus.first()
- assertUserStatusIsEqualIgnoringFields(reportedStatus, initialExpectedStatus)
+ assertUserStatusIsEqualIgnoringFields(reportedStatus, expectedStatus)
+ }
+ }
+ }
+
+ @Test
+ fun testProfilesForCrossProfileMultipleManagedProfilesOneAllowedVPlus() {
+
+ assumeTrue(SdkLevel.isAtLeastV())
+
+ // Create a second managed profile, apparently that's a thing on some devices.
+ val userIdManagedUnknown = 11
+ val parcel1 = Parcel.obtain()
+ parcel1.writeInt(userIdManagedUnknown)
+ parcel1.setDataPosition(0)
+ val userHandleUnknownManaged = UserHandle(parcel1)
+ parcel1.recycle()
+
+ // Initial setup state: Three profiles (Personal/Work/Work???), all enabled
+ whenever(mockUserManager.userProfiles) {
+ listOf(USER_HANDLE_PRIMARY, USER_HANDLE_MANAGED, userHandleUnknownManaged)
+ }
+
+ // Default responses for relevant UserManager apis
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_PRIMARY)) { false }
+ whenever(mockUserManager.isManagedProfile(USER_ID_PRIMARY)) { false }
+
+ // Managed 1
+ whenever(mockUserManager.isManagedProfile(USER_ID_MANAGED)) { true }
+ whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { USER_HANDLE_PRIMARY }
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { false }
+ whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED)) @JvmSerializableLambda {
+ UserProperties.Builder()
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION
+ )
+ .build()
+ }
+
+ // Managed 2
+ whenever(mockUserManager.isManagedProfile(userIdManagedUnknown)) { true }
+ whenever(mockUserManager.getProfileParent(userHandleUnknownManaged)) { USER_HANDLE_PRIMARY }
+ whenever(mockUserManager.isQuietModeEnabled(userHandleUnknownManaged)) { false }
+
+ whenever(
+ mockUserManager.getUserProperties(userHandleUnknownManaged)
+ ) @JvmSerializableLambda {
+ UserProperties.Builder()
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION
+ )
+ .build()
+ }
+
+ whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
+ listOf(ReflectedResolveInfo(USER_HANDLE_MANAGED.getIdentifier()))
+ }
+
+ val unknownManagedProfileBase =
+ UserProfile(
+ handle = userHandleUnknownManaged,
+ profileType = UserProfile.ProfileType.MANAGED,
+ label = PLATFORM_PROVIDED_PROFILE_LABEL,
+ disabledReasons = setOf(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED),
+ )
+
+ val expectedStatus =
+ UserStatus(
+ activeUserProfile = PRIMARY_PROFILE_BASE,
+ allProfiles =
+ listOf(PRIMARY_PROFILE_BASE, MANAGED_PROFILE_BASE, unknownManagedProfileBase),
+ activeContentResolver = mockContentResolver,
+ )
+
+ runTest { // this: TestScope
+ userMonitor =
+ UserMonitor(
+ mockContext,
+ 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,
+ )
+
+ launch {
+ val reportedStatus = userMonitor.userStatus.first()
+ assertUserStatusIsEqualIgnoringFields(reportedStatus, expectedStatus)
+ }
+ }
+ }
+
+ @Test
+ fun testProfilesForCrossProfileMultipleManagedProfilesOneAllowedUMinus() {
+
+ assumeFalse(SdkLevel.isAtLeastV())
+
+ // Create a second managed profile, apparently that's a thing on some devices.
+ val userIdManagedUnknown = 11
+ val parcel1 = Parcel.obtain()
+ parcel1.writeInt(userIdManagedUnknown)
+ parcel1.setDataPosition(0)
+ val userHandleUnknownManaged = UserHandle(parcel1)
+ parcel1.recycle()
+
+ // Initial setup state: Three profiles (Personal/Work/Work???), all enabled
+ whenever(mockUserManager.userProfiles) {
+ listOf(USER_HANDLE_PRIMARY, USER_HANDLE_MANAGED, userHandleUnknownManaged)
+ }
+
+ // Default responses for relevant UserManager apis
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_PRIMARY)) { false }
+ whenever(mockUserManager.isManagedProfile(USER_ID_PRIMARY)) { false }
+
+ // Managed 1
+ whenever(mockUserManager.isManagedProfile(USER_ID_MANAGED)) { true }
+ whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { USER_HANDLE_PRIMARY }
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { false }
+
+ // Managed 2
+ whenever(mockUserManager.isManagedProfile(userIdManagedUnknown)) { true }
+ whenever(mockUserManager.getProfileParent(userHandleUnknownManaged)) { USER_HANDLE_PRIMARY }
+ whenever(mockUserManager.isQuietModeEnabled(userHandleUnknownManaged)) { false }
+
+ whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
+ listOf(ReflectedResolveInfo(USER_HANDLE_MANAGED.getIdentifier()))
+ }
+
+ val unknownManagedProfileBase =
+ UserProfile(
+ handle = userHandleUnknownManaged,
+ profileType = UserProfile.ProfileType.MANAGED,
+ label = PLATFORM_PROVIDED_PROFILE_LABEL,
+ disabledReasons = setOf(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED),
+ )
+
+ val expectedStatus =
+ UserStatus(
+ activeUserProfile = PRIMARY_PROFILE_BASE,
+ allProfiles =
+ listOf(PRIMARY_PROFILE_BASE, MANAGED_PROFILE_BASE, unknownManagedProfileBase),
+ activeContentResolver = mockContentResolver,
+ )
+
+ runTest { // this: TestScope
+ userMonitor =
+ UserMonitor(
+ mockContext,
+ 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,
+ )
+
+ launch {
+ val reportedStatus = userMonitor.userStatus.first()
+ assertUserStatusIsEqualIgnoringFields(reportedStatus, expectedStatus)
+ }
+ }
+ }
+
+ @Test
+ fun testProfilesForCrossProfileMultipleManagedProfilesAllManagedDisabledVPlus() {
+
+ assumeTrue(SdkLevel.isAtLeastV())
+
+ // Create a second managed profile, apparently that's a thing on some devices.
+ val userIdManagedUnknown = 11
+ val parcel1 = Parcel.obtain()
+ parcel1.writeInt(userIdManagedUnknown)
+ parcel1.setDataPosition(0)
+ val userHandleUnknownManaged = UserHandle(parcel1)
+ parcel1.recycle()
+
+ // Initial setup state: Three profiles (Personal/Work/Work???), all enabled
+ whenever(mockUserManager.userProfiles) {
+ listOf(USER_HANDLE_PRIMARY, USER_HANDLE_MANAGED, userHandleUnknownManaged)
+ }
+
+ // Default responses for relevant UserManager apis
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_PRIMARY)) { false }
+ whenever(mockUserManager.isManagedProfile(USER_ID_PRIMARY)) { false }
+
+ // Managed 1
+ whenever(mockUserManager.isManagedProfile(USER_ID_MANAGED)) { true }
+ whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { USER_HANDLE_PRIMARY }
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { false }
+
+ // Since the UserProperties here will return no delegation, this will
+ // have to rely on CrossProfileIntentForwarderActivity found for the managed
+ // user in order to enable cross profile for this profile.
+ whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED)) @JvmSerializableLambda {
+ UserProperties.Builder()
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION
+ )
+ .build()
+ }
+
+ // Managed 2
+ whenever(mockUserManager.isManagedProfile(userIdManagedUnknown)) { true }
+ whenever(mockUserManager.getProfileParent(userHandleUnknownManaged)) { USER_HANDLE_PRIMARY }
+ whenever(mockUserManager.isQuietModeEnabled(userHandleUnknownManaged)) { false }
+ whenever(
+ mockUserManager.getUserProperties(userHandleUnknownManaged)
+ ) @JvmSerializableLambda {
+ UserProperties.Builder()
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION
+ )
+ .build()
+ }
+
+ whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
+ emptyList<ResolveInfo>()
+ }
+
+ val unknownManagedProfileBase =
+ UserProfile(
+ handle = userHandleUnknownManaged,
+ profileType = UserProfile.ProfileType.MANAGED,
+ label = PLATFORM_PROVIDED_PROFILE_LABEL,
+ disabledReasons = setOf(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED),
+ )
+
+ val expectedStatus =
+ UserStatus(
+ activeUserProfile = PRIMARY_PROFILE_BASE,
+ allProfiles =
+ listOf(
+ PRIMARY_PROFILE_BASE,
+ MANAGED_PROFILE_BASE.copy(
+ disabledReasons =
+ setOf(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED)
+ ),
+ unknownManagedProfileBase,
+ ),
+ activeContentResolver = mockContentResolver,
+ )
+
+ runTest { // this: TestScope
+ userMonitor =
+ UserMonitor(
+ mockContext,
+ 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,
+ )
+
+ launch {
+ val reportedStatus = userMonitor.userStatus.first()
+ assertUserStatusIsEqualIgnoringFields(reportedStatus, expectedStatus)
+ }
+ }
+ }
+
+ @Test
+ fun testProfilesForCrossProfileMultipleManagedProfilesAllManagedDisabledUMinus() {
+
+ assumeFalse(SdkLevel.isAtLeastV())
+
+ // Create a second managed profile, apparently that's a thing on some devices.
+ val userIdManagedUnknown = 11
+ val parcel1 = Parcel.obtain()
+ parcel1.writeInt(userIdManagedUnknown)
+ parcel1.setDataPosition(0)
+ val userHandleUnknownManaged = UserHandle(parcel1)
+ parcel1.recycle()
+
+ // Initial setup state: Three profiles (Personal/Work/Work???), all enabled
+ whenever(mockUserManager.userProfiles) {
+ listOf(USER_HANDLE_PRIMARY, USER_HANDLE_MANAGED, userHandleUnknownManaged)
+ }
+
+ // Default responses for relevant UserManager apis
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_PRIMARY)) { false }
+ whenever(mockUserManager.isManagedProfile(USER_ID_PRIMARY)) { false }
+
+ // Managed 1
+ whenever(mockUserManager.isManagedProfile(USER_ID_MANAGED)) { true }
+ whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { USER_HANDLE_PRIMARY }
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { false }
+
+ // Managed 2
+ whenever(mockUserManager.isManagedProfile(userIdManagedUnknown)) { true }
+ whenever(mockUserManager.getProfileParent(userHandleUnknownManaged)) { USER_HANDLE_PRIMARY }
+ whenever(mockUserManager.isQuietModeEnabled(userHandleUnknownManaged)) { false }
+
+ whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
+ emptyList<ResolveInfo>()
+ }
+
+ val unknownManagedProfileBase =
+ UserProfile(
+ handle = userHandleUnknownManaged,
+ profileType = UserProfile.ProfileType.MANAGED,
+ label = PLATFORM_PROVIDED_PROFILE_LABEL,
+ disabledReasons = setOf(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED),
+ )
+
+ val expectedStatus =
+ UserStatus(
+ activeUserProfile = PRIMARY_PROFILE_BASE,
+ allProfiles =
+ listOf(
+ PRIMARY_PROFILE_BASE,
+ MANAGED_PROFILE_BASE.copy(
+ disabledReasons =
+ setOf(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED)
+ ),
+ unknownManagedProfileBase,
+ ),
+ activeContentResolver = mockContentResolver,
+ )
+
+ runTest { // this: TestScope
+ userMonitor =
+ UserMonitor(
+ mockContext,
+ 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,
+ )
+
+ launch {
+ val reportedStatus = userMonitor.userStatus.first()
+ assertUserStatusIsEqualIgnoringFields(reportedStatus, expectedStatus)
}
}
}
@@ -300,8 +765,7 @@ class UserMonitorTest {
whenever(mockUserManager.userProfiles) {
listOf(USER_HANDLE_PRIMARY, USER_HANDLE_MANAGED, disabledSharingProfile)
}
- whenever(mockUserManager.getUserProperties(disabledSharingProfile))
- @JvmSerializableLambda {
+ whenever(mockUserManager.getUserProperties(disabledSharingProfile)) @JvmSerializableLambda {
UserProperties.Builder()
.setShowInSharingSurfaces(UserProperties.SHOW_IN_SHARING_SURFACES_NO)
.build()
diff --git a/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt b/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt
index b79a569f5..7d776d813 100644
--- a/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt
@@ -62,7 +62,6 @@ import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
@SmallTest
@@ -70,6 +69,17 @@ import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
class ProfileSelectorViewModelTest {
+ /**
+ * Class that exposes the @hide api [targetUserId] in order to supply proper values for
+ * reflection based code that is inspecting this field.
+ *
+ * @property targetUserId
+ */
+ private class ReflectedResolveInfo(@JvmField val targetUserId: Int) : ResolveInfo() {
+
+ override fun isCrossProfileIntentForwarderActivity(): Boolean = true
+ }
+
@Mock lateinit var mockContext: Context
@Mock lateinit var mockUserManager: UserManager
@Mock lateinit var mockPackageManager: PackageManager
@@ -142,8 +152,9 @@ class ProfileSelectorViewModelTest {
}
if (SdkLevel.isAtLeastV()) {
- whenever(mockUserManager.getUserProperties(any(UserHandle::class.java)))
- @JvmSerializableLambda {
+ whenever(
+ mockUserManager.getUserProperties(any(UserHandle::class.java))
+ ) @JvmSerializableLambda {
UserProperties.Builder()
.setCrossProfileContentSharingStrategy(
UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT
@@ -158,8 +169,7 @@ class ProfileSelectorViewModelTest {
}
whenever(mockUserManager.getProfileLabel()) { "label" }
}
- val mockResolveInfo = mock(ResolveInfo::class.java)
- whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true }
+ val mockResolveInfo = ReflectedResolveInfo(USER_ID_MANAGED)
whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
listOf(mockResolveInfo)
}
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 766090a50..657704dd2 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -105,7 +105,7 @@
<string name="not_selected" msgid="2244008151669896758">"ei valittu"</string>
<string name="preloading_dialog_title" msgid="4974348221848532887">"Valitsemaasi mediaa valmistellaan"</string>
<string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> valmiina"</string>
- <string name="preloading_cancel_button" msgid="824053521307342209">"Peruuta"</string>
+ <string name="preloading_cancel_button" msgid="824053521307342209">"Peru"</string>
<string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Varmuuskopioidut kuvat löytyvät nyt täältä"</string>
<string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Voit valita sovelluksen <xliff:g id="APP_NAME">%1$s</xliff:g> kuvat tililtä <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
<string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Tili päivitetty: <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index 269ae622e..8074656bd 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -432,7 +432,7 @@ public class LocalCallingIdentity {
context, pid, uid, getPackageName(), attributionTag, forDataDelivery);
case PERMISSION_IS_SYSTEM_GALLERY:
return checkWriteImagesOrVideoAppOps(
- context, uid, getPackageName(), attributionTag);
+ context, uid, getPackageName(), attributionTag, forDataDelivery);
case PERMISSION_INSTALL_PACKAGES:
return checkPermissionInstallPackages(
context, pid, uid, getPackageName(), attributionTag);
@@ -480,8 +480,7 @@ public class LocalCallingIdentity {
// To address b/338519249, we will check for sdk version V+
boolean targetSdkIsAtLeastV =
getTargetSdkVersion() >= Build.VERSION_CODES.VANILLA_ICE_CREAM;
- return checkIsLegacyStorageGranted(context, uid, getPackageName(), attributionTag,
- targetSdkIsAtLeastV);
+ return checkIsLegacyStorageGranted(context, uid, getPackageName(), targetSdkIsAtLeastV);
}
private volatile boolean shouldBypass;
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index c338352f6..5336b089f 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -565,7 +565,7 @@ public class MediaProvider extends ContentProvider {
* Attempting to send more than 2000 uris will result in an IllegalArgumentException.
*/
@ChangeId
- @EnabledSince(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
static final long LIMIT_CREATE_REQUEST_URIS = 203408344L;
@GuardedBy("mPendingOpenInfo")
@@ -7849,7 +7849,8 @@ public class MediaProvider extends ContentProvider {
String packageName = arg;
int uid = extras.getInt(MediaStore.EXTRA_IS_SYSTEM_GALLERY_UID);
boolean isSystemGallery = PermissionUtils.checkWriteImagesOrVideoAppOps(
- getContext(), uid, packageName, getContext().getAttributionTag());
+ getContext(), uid, packageName, getContext().getAttributionTag(),
+ /*forDataDelivery*/ false);
Bundle res = new Bundle();
res.putBoolean(MediaStore.EXTRA_IS_SYSTEM_GALLERY_RESPONSE, isSystemGallery);
return res;
@@ -11969,14 +11970,15 @@ public class MediaProvider extends ContentProvider {
final ContentResolver resolver = getContext().getContentResolver();
final Uri uri = getBaseContentUri(volumeName);
// TODO(b/182396009) we probably also want to notify clone profile (and vice versa)
- resolver.notifyChange(getBaseContentUri(volumeName), null);
+ ForegroundThread.getExecutor().execute(() -> {
+ resolver.notifyChange(getBaseContentUri(volumeName), null);
+ });
if (LOGV) Log.v(TAG, "Attached volume: " + volume);
if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
- // Also notify on synthetic view of all devices
- resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null);
-
ForegroundThread.getExecutor().execute(() -> {
+ // Also notify on synthetic view of all devices
+ resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null);
mExternalDatabase.runWithTransaction((db) -> {
ensureNecessaryFolders(volume, db);
return null;
@@ -12037,11 +12039,15 @@ public class MediaProvider extends ContentProvider {
}
final ContentResolver resolver = getContext().getContentResolver();
- resolver.notifyChange(getBaseContentUri(volumeName), null);
+ ForegroundThread.getExecutor().execute(() -> {
+ resolver.notifyChange(getBaseContentUri(volumeName), null);
+ });
if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
- // Also notify on synthetic view of all devices
- resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null);
+ ForegroundThread.getExecutor().execute(() -> {
+ // Also notify on synthetic view of all devices
+ resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null);
+ });
}
if (LOGV) Log.v(TAG, "Detached volume: " + volumeName);
diff --git a/src/com/android/providers/media/photopicker/PickerSyncController.java b/src/com/android/providers/media/photopicker/PickerSyncController.java
index 34e02645a..b29d52896 100644
--- a/src/com/android/providers/media/photopicker/PickerSyncController.java
+++ b/src/com/android/providers/media/photopicker/PickerSyncController.java
@@ -1469,6 +1469,38 @@ public class PickerSyncController {
return bundle;
}
+ /**
+ * Checks if full sync is pending for the given CMP.
+ *
+ * @param authority Authority of the CMP that uniquely identifies it.
+ * @param isLocal true of the authority belongs to the local provider, else false.
+ * @return true if full sync is pending for the CMP, else false.
+ * @throws RequestObsoleteException if the input authority is different than the authority of
+ * the current cloud provider.
+ */
+ public boolean isFullSyncPending(@NonNull String authority, boolean isLocal)
+ throws RequestObsoleteException {
+ final ProviderCollectionInfo latestCollectionInfo = isLocal
+ ? getLocalProviderLatestCollectionInfo()
+ : getCloudProviderLatestCollectionInfo();
+
+ if (!authority.equals(latestCollectionInfo.getAuthority())) {
+ throw new RequestObsoleteException(
+ "Authority has changed to " + latestCollectionInfo.getAuthority());
+ }
+
+ final Bundle cachedPreviousCollectionInfo = getCachedMediaCollectionInfo(isLocal);
+ final String cachedPreviousCollectionId =
+ cachedPreviousCollectionInfo.getString(MEDIA_COLLECTION_ID);
+ final long cachedPreviousGeneration =
+ cachedPreviousCollectionInfo.getLong(LAST_MEDIA_SYNC_GENERATION);
+
+ return isFullSyncRequired(
+ latestCollectionInfo.getCollectionId(),
+ cachedPreviousCollectionId,
+ cachedPreviousGeneration);
+ }
+
@NonNull
private SyncRequestParams getSyncRequestParams(@Nullable String authority,
boolean isLocal) throws RequestObsoleteException, UnableToAcquireLockException {
@@ -1523,7 +1555,7 @@ public class PickerSyncController {
+ "ID/Gen=" + latestCollectionId + "/" + latestGeneration);
}
- if (!Objects.equals(latestCollectionId, cachedCollectionId)) {
+ if (isFullSyncWithResetRequired(latestCollectionId, cachedCollectionId)) {
result = SyncRequestParams.forFullMediaWithReset(latestMediaCollectionInfo);
// Update collection info cache.
@@ -1535,7 +1567,8 @@ public class PickerSyncController {
new ProviderCollectionInfo(authority, latestCollectionId, latestAccountName,
latestAccountConfigurationIntent);
updateLatestKnownCollectionInfoLocked(isLocal, latestCollectionInfo);
- } else if (cachedGeneration == DEFAULT_GENERATION) {
+ } else if (isFullSyncWithoutResetRequired(
+ latestCollectionId, cachedCollectionId, cachedGeneration)) {
result = SyncRequestParams.forFullMedia(latestMediaCollectionInfo);
} else if (cachedGeneration == latestGeneration) {
result = SyncRequestParams.forNone();
@@ -1548,6 +1581,49 @@ public class PickerSyncController {
return result;
}
+ /**
+ * @param latestCollectionId The latest collection id of the CMP library.
+ * @param cachedCollectionId The last collection id Picker DB was synced with, either fully
+ * or partially.
+ * @param cachedGenerationId The last generation id Picker DB was synced with.
+ * @return true if a full sync is pending, else false.
+ */
+ private boolean isFullSyncRequired(
+ @Nullable String latestCollectionId,
+ @Nullable String cachedCollectionId,
+ long cachedGenerationId) {
+ return isFullSyncWithResetRequired(latestCollectionId, cachedCollectionId)
+ || isFullSyncWithoutResetRequired(latestCollectionId, cachedCollectionId,
+ cachedGenerationId);
+ }
+
+ /**
+ * @param latestCollectionId The latest collection id of the CMP library.
+ * @param cachedCollectionId The last collection id Picker DB was synced with, either fully
+ * or partially.
+ * @return true if a full sync with reset is pending, else false.
+ */
+ private boolean isFullSyncWithResetRequired(
+ @Nullable String latestCollectionId,
+ @Nullable String cachedCollectionId) {
+ return !Objects.equals(latestCollectionId, cachedCollectionId);
+ }
+
+ /**
+ * @param latestCollectionId The latest collection id of the CMP library.
+ * @param cachedCollectionId The last collection id Picker DB was synced with, either fully
+ * or partially.
+ * @param cachedGenerationId The last generation id Picker DB was synced with.
+ * @return true if a resumable full sync is pending, else false.
+ */
+ private boolean isFullSyncWithoutResetRequired(
+ @Nullable String latestCollectionId,
+ @Nullable String cachedCollectionId,
+ long cachedGenerationId) {
+ return Objects.equals(latestCollectionId, cachedCollectionId)
+ && cachedGenerationId == DEFAULT_GENERATION;
+ }
+
private void updateLatestKnownCollectionInfoLocked(
boolean isLocal,
@Nullable ProviderCollectionInfo latestCollectionInfo) {
diff --git a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
index 85f430950..3145ab73b 100644
--- a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
+++ b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
@@ -35,6 +35,7 @@ import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
+import android.database.DatabaseUtils;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.database.sqlite.SQLiteConstraintException;
@@ -75,7 +76,7 @@ import java.util.Objects;
public class PickerDbFacade {
private static final String VIDEO_MIME_TYPES = "video/%";
private final Context mContext;
- private final SQLiteDatabase mDatabase;
+ private final PickerDatabaseHelper mPickerDatabaseHelper;
private final PickerSyncLockManager mPickerSyncLockManager;
private final String mLocalProvider;
// This is the cloud provider the database is synced with. It can be set as null to disable
@@ -83,6 +84,7 @@ public class PickerDbFacade {
@Nullable
private String mCloudProvider;
+
public PickerDbFacade(Context context, PickerSyncLockManager pickerSyncLockManager) {
this(context, pickerSyncLockManager, PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY);
}
@@ -98,7 +100,7 @@ public class PickerDbFacade {
String localProvider, PickerDatabaseHelper dbHelper) {
mContext = context;
mLocalProvider = localProvider;
- mDatabase = dbHelper.getWritableDatabase();
+ mPickerDatabaseHelper = dbHelper;
mPickerSyncLockManager = pickerSyncLockManager;
}
@@ -300,21 +302,21 @@ public class PickerDbFacade {
* db.
*/
public DbWriteOperation beginAddMediaOperation(String authority) {
- return new AddMediaOperation(mDatabase, isLocal(authority));
+ return new AddMediaOperation(getDatabase(), isLocal(authority));
}
/**
* Returns {@link DbWriteOperation} that can be used to insert grants into the database.
*/
public DbWriteOperation beginInsertGrantsOperation() {
- return new InsertGrantsOperation(mDatabase, /* isLocal */ true);
+ return new InsertGrantsOperation(getDatabase(), /* isLocal */ true);
}
/**
* Returns {@link DbWriteOperation} that can be used to clear all grants from the database.
*/
public DbWriteOperation beginClearGrantsOperation(String[] packageNames, int userId) {
- return new ClearGrantsOperation(mDatabase, /* isLocal */ true, packageNames, userId);
+ return new ClearGrantsOperation(getDatabase(), /* isLocal */ true, packageNames, userId);
}
/**
@@ -322,7 +324,7 @@ public class PickerDbFacade {
* into the picker db.
*/
public DbWriteOperation beginAddAlbumMediaOperation(String authority, String albumId) {
- return new AddAlbumMediaOperation(mDatabase, isLocal(authority), albumId);
+ return new AddAlbumMediaOperation(getDatabase(), isLocal(authority), albumId);
}
/**
@@ -330,7 +332,7 @@ public class PickerDbFacade {
* picker db.
*/
public DbWriteOperation beginRemoveMediaOperation(String authority) {
- return new RemoveMediaOperation(mDatabase, isLocal(authority));
+ return new RemoveMediaOperation(getDatabase(), isLocal(authority));
}
/**
@@ -340,7 +342,7 @@ public class PickerDbFacade {
* @param authority to determine whether local or cloud media should be cleared
*/
public DbWriteOperation beginResetMediaOperation(String authority) {
- return new ResetMediaOperation(mDatabase, isLocal(authority));
+ return new ResetMediaOperation(getDatabase(), isLocal(authority));
}
/**
@@ -354,7 +356,7 @@ public class PickerDbFacade {
* @param authority to determine whether local or cloud media should be cleared
*/
public DbWriteOperation beginResetAlbumMediaOperation(String authority, String albumId) {
- return new ResetAlbumOperation(mDatabase, isLocal(authority), albumId);
+ return new ResetAlbumOperation(getDatabase(), isLocal(authority), albumId);
}
/**
@@ -364,7 +366,7 @@ public class PickerDbFacade {
* @param authority to determine whether local or cloud media should be updated
*/
public UpdateMediaOperation beginUpdateMediaOperation(String authority) {
- return new UpdateMediaOperation(mDatabase, isLocal(authority));
+ return new UpdateMediaOperation(getDatabase(), isLocal(authority));
}
/**
@@ -1166,11 +1168,31 @@ public class PickerDbFacade {
}
private Cursor queryMediaIdForAppsLocked(@NonNull SQLiteQueryBuilder qb,
- @NonNull String[] projection, @NonNull String[] selectionArgs,
+ @NonNull String[] columns, @NonNull String[] selectionArgs,
String pickerSegmentType) {
- return qb.query(mDatabase, getMediaStoreProjectionLocked(projection, pickerSegmentType),
- /* selection */ null, selectionArgs, /* groupBy */ null, /* having */ null,
- /* orderBy */ null, /* limitStr */ null);
+ final Cursor cursor =
+ qb.query(getDatabase(), getMediaStoreProjectionLocked(columns, pickerSegmentType),
+ /* selection */ null, selectionArgs, /* groupBy */ null, /* having */ null,
+ /* orderBy */ null, /* limitStr */ null);
+
+ if (columns == null || columns.length == 0 || cursor.getColumnCount() == columns.length) {
+ return cursor;
+ } else {
+ // An unknown column was encountered. Populate it will null for backwards compatibility.
+ final MatrixCursor result = new MatrixCursor(columns);
+ if (cursor.moveToFirst()) {
+ do {
+ final ContentValues contentValues = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(cursor, contentValues);
+ final MatrixCursor.RowBuilder rowBuilder = result.newRow();
+ for (String column : columns) {
+ rowBuilder.add(column, contentValues.get(column));
+ }
+ } while (cursor.moveToNext());
+ }
+ cursor.close();
+ return result;
+ }
}
/**
@@ -1196,9 +1218,9 @@ public class PickerDbFacade {
}
addMimeTypesToQueryBuilderAndSelectionArgs(qb, selectionArgs, query.mMimeTypes);
- Cursor cursor = qb.query(mDatabase, getMergedAlbumProjection(), /* selection */ null,
- selectionArgs.toArray(new String[0]), /* groupBy */ null, /* having */ null,
- /* orderBy */ null, /* limit */ null);
+ Cursor cursor = qb.query(getDatabase(), getMergedAlbumProjection(),
+ /* selection */ null, selectionArgs.toArray(new String[0]), /* groupBy */ null,
+ /* having */ null, /* orderBy */ null, /* limit */ null);
if (cursor == null || !cursor.moveToFirst()) {
continue;
@@ -1296,7 +1318,7 @@ public class PickerDbFacade {
private Cursor queryMediaForUiLocked(SQLiteQueryBuilder qb, String[] selectionArgs,
String orderBy, String limitStr) {
- return qb.query(mDatabase, getCloudMediaProjectionLocked(), /* selection */ null,
+ return qb.query(getDatabase(), getCloudMediaProjectionLocked(), /* selection */ null,
selectionArgs, /* groupBy */ null, /* having */ null, orderBy, limitStr);
}
@@ -1324,54 +1346,51 @@ public class PickerDbFacade {
}
private String[] getMediaStoreProjectionLocked(String[] columns, String pickerSegmentType) {
- final String[] projection = new String[columns.length];
+ final List<String> projection = new ArrayList<>();
- for (int i = 0; i < projection.length; i++) {
+ for (int i = 0; i < columns.length; i++) {
switch (columns[i]) {
case PickerMediaColumns.DATA:
- projection[i] = getProjectionDataLocked(PickerMediaColumns.DATA,
- pickerSegmentType);
+ projection.add(getProjectionDataLocked(PickerMediaColumns.DATA,
+ pickerSegmentType));
break;
case PickerMediaColumns.DISPLAY_NAME:
- projection[i] =
- getProjectionSimple(
- getDisplayNameSql(), PickerMediaColumns.DISPLAY_NAME);
+ projection.add(getProjectionSimple(
+ getDisplayNameSql(), PickerMediaColumns.DISPLAY_NAME));
break;
case PickerMediaColumns.MIME_TYPE:
- projection[i] =
- getProjectionSimple(KEY_MIME_TYPE, PickerMediaColumns.MIME_TYPE);
+ projection.add(getProjectionSimple(
+ KEY_MIME_TYPE, PickerMediaColumns.MIME_TYPE));
break;
case PickerMediaColumns.DATE_TAKEN:
- projection[i] =
- getProjectionSimple(KEY_DATE_TAKEN_MS, PickerMediaColumns.DATE_TAKEN);
+ projection.add(getProjectionSimple(
+ KEY_DATE_TAKEN_MS, PickerMediaColumns.DATE_TAKEN));
break;
case PickerMediaColumns.SIZE:
- projection[i] = getProjectionSimple(KEY_SIZE_BYTES, PickerMediaColumns.SIZE);
+ projection.add(getProjectionSimple(KEY_SIZE_BYTES, PickerMediaColumns.SIZE));
break;
case PickerMediaColumns.DURATION_MILLIS:
- projection[i] =
- getProjectionSimple(
- KEY_DURATION_MS, PickerMediaColumns.DURATION_MILLIS);
+ projection.add(getProjectionSimple(
+ KEY_DURATION_MS, PickerMediaColumns.DURATION_MILLIS));
break;
case PickerMediaColumns.HEIGHT:
- projection[i] = getProjectionSimple(KEY_HEIGHT, PickerMediaColumns.HEIGHT);
+ projection.add(getProjectionSimple(KEY_HEIGHT, PickerMediaColumns.HEIGHT));
break;
case PickerMediaColumns.WIDTH:
- projection[i] = getProjectionSimple(KEY_WIDTH, PickerMediaColumns.WIDTH);
+ projection.add(getProjectionSimple(KEY_WIDTH, PickerMediaColumns.WIDTH));
break;
case PickerMediaColumns.ORIENTATION:
- projection[i] =
- getProjectionSimple(KEY_ORIENTATION, PickerMediaColumns.ORIENTATION);
+ projection.add(getProjectionSimple(
+ KEY_ORIENTATION, PickerMediaColumns.ORIENTATION));
break;
default:
- projection[i] = getProjectionSimple("NULL", columns[i]);
// Ignore unsupported columns; we do not throw error here to support
- // backward compatibility
+ // backward compatibility for ACTION_GET_CONTENT takeover.
Log.w(TAG, "Unexpected Picker column: " + columns[i]);
}
}
- return projection;
+ return projection.toArray(new String[0]);
}
private String getProjectionAuthorityLocked() {
@@ -1939,6 +1958,6 @@ public class PickerDbFacade {
* Returns the associated SQLiteDatabase instance.
*/
public SQLiteDatabase getDatabase() {
- return mDatabase;
+ return mPickerDatabaseHelper.getWritableDatabase();
}
}
diff --git a/src/com/android/providers/media/photopicker/data/UserManagerState.java b/src/com/android/providers/media/photopicker/data/UserManagerState.java
index 58b70e87e..74c06e872 100644
--- a/src/com/android/providers/media/photopicker/data/UserManagerState.java
+++ b/src/com/android/providers/media/photopicker/data/UserManagerState.java
@@ -21,12 +21,13 @@ import static androidx.core.util.Preconditions.checkNotNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresApi;
+import android.annotation.SuppressLint;
import android.app.ActivityManager;
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.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
@@ -44,6 +45,7 @@ import com.android.providers.media.photopicker.data.model.UserId;
import com.android.providers.media.photopicker.ui.TabFragment;
import com.android.providers.media.photopicker.util.CrossProfileUtils;
+import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -54,9 +56,7 @@ import java.util.Map;
* Interface to query user ids {@link UserId}
*/
public interface UserManagerState {
- /**
- * Whether there are more than 1 user profiles associated with the current user.
- */
+ /** Whether there are more than 1 user profiles associated with the current user. */
boolean isMultiUserProfiles();
/**
@@ -73,9 +73,9 @@ public interface UserManagerState {
/**
* A Map of all the profiles with their cross profile allowed status from current user.
- * key : userId of a profile
- * Value : cross profile allowed status of a user profile corresponding to user id with
- * current user .
+ *
+ * <p>key : userId of a profile Value : cross profile allowed status of a user profile
+ * corresponding to user id with current user .
*/
@NonNull
Map<UserId, Boolean> getCrossProfileAllowedStatusForAll();
@@ -86,16 +86,13 @@ public interface UserManagerState {
*/
int getProfileCount();
- /**
- *
- * A {@link MutableLiveData} to check if cross profile interaction allowed or not.
- */
+ /** A {@link MutableLiveData} to check if cross profile interaction allowed or not. */
@NonNull
MutableLiveData<Map<UserId, Boolean>> getCrossProfileAllowed();
/**
- * A list of all user profile ids including current user that need to be shown
- * separately in PhotoPicker
+ * A list of all user profile ids including current user that need to be shown separately in
+ * PhotoPicker
*/
@NonNull
List<UserId> getAllUserProfileIds();
@@ -105,38 +102,32 @@ public interface UserManagerState {
*/
void updateProfileOffValuesAndPostCrossProfileStatus();
- /**
- * Updates on/off values of all the user profiles
- */
+ /** Updates on/off values of all the user profiles */
void updateProfileOffValues();
- /**
- * Waits for Media Provider of the user profile corresponding to userId to be available.
- */
+ /** Waits for Media Provider of the user profile corresponding to userId to be available. */
void waitForMediaProviderToBeAvailable(UserId userId);
/**
- * Get if it is allowed to access the otherUser profile from current user ( current user :
- * the user profile that started the photo picker activity)
- **/
+ * Get if it is allowed to access the otherUser profile from current user ( current user : the
+ * user profile that started the photo picker activity)
+ */
@NonNull
boolean isCrossProfileAllowedToUser(UserId otherUser);
- /**
- * A {@link MutableLiveData} to check if there are multiple user profiles or not
- */
+ /** A {@link MutableLiveData} to check if there are multiple user profiles or not */
@NonNull
MutableLiveData<Boolean> getIsMultiUserProfiles();
/**
- * Resets the user ids. This is usually called as a result of receiving broadcast that
- * any profile has been added or removed.
+ * Resets the user ids. This is usually called as a result of receiving broadcast that any
+ * profile has been added or removed.
*/
void resetUserIds();
/**
- * Resets the user ids and set their cross profile values. This is usually called as a result
- * of receiving broadcast that any profile has been added or removed.
+ * Resets the user ids and set their cross profile values. This is usually called as a result of
+ * receiving broadcast that any profile has been added or removed.
*/
void resetUserIdsAndSetCrossProfileValues(Intent intent);
@@ -152,39 +143,31 @@ public interface UserManagerState {
void setIntentAndCheckRestrictions(Intent intent);
/**
- * Whether cross profile access corresponding to the userID is blocked
- * by admin for the current user.
+ * Whether cross profile access corresponding to the userID is blocked by admin for the current
+ * user.
*/
boolean isBlockedByAdmin(UserId userId);
- /**
- * Whether profile corresponding to the userID is on or off.
- */
+ /** Whether profile corresponding to the userID is on or off. */
boolean isProfileOff(UserId userId);
- /**
- * A map of all user profile labels corresponding to all profile userIds
- */
+ /** A map of all user profile labels corresponding to all profile userIds */
Map<UserId, String> getProfileLabelsForAll();
/**
* Returns whether a user should be shown in the PhotoPicker depending on its quite mode status.
*
- * @return One of {@link UserProperties.SHOW_IN_QUIET_MODE_PAUSED},
- * {@link UserProperties.SHOW_IN_QUIET_MODE_HIDDEN}, or
- * {@link UserProperties.SHOW_IN_QUIET_MODE_DEFAULT} depending on whether the profile
- * should be shown in quiet mode or not.
+ * @return One of {@link UserProperties.SHOW_IN_QUIET_MODE_PAUSED}, {@link
+ * UserProperties.SHOW_IN_QUIET_MODE_HIDDEN}, or {@link
+ * UserProperties.SHOW_IN_QUIET_MODE_DEFAULT} depending on whether the profile should be
+ * shown in quiet mode or not.
*/
int getShowInQuietMode(UserId userId);
- /**
- * A map of all user profile Icon ids corresponding to all profile userIds
- */
+ /** A map of all user profile Icon ids corresponding to all profile userIds */
Map<UserId, Drawable> getProfileBadgeForAll();
- /**
- * Set a user as a current user profile
- **/
+ /** Set a user as a current user profile */
void setUserAsCurrentUserProfile(UserId userId);
/**
@@ -193,8 +176,7 @@ public interface UserManagerState {
boolean isUserSelectedAsCurrentUserProfile(UserId userId);
/**
- * Creates an implementation of {@link UserManagerState}.
- * Todo(b/319067964): make this singleton
+ * Creates an implementation of {@link UserManagerState}. Todo(b/319067964): make this singleton
*/
static UserManagerState create(Context context) {
return new RuntimeUserManagerState(context);
@@ -212,37 +194,37 @@ public interface UserManagerState {
private static final int SHOW_IN_QUIET_MODE_DEFAULT = -1;
private final Context mContext;
- // This is the user profile that started the photo picker activity. That's why it cannot
- // change in a UserIdManager instance.
+ // This is the user profile that started the photo picker activity. That's why
+ // it cannot change in a UserIdManager instance.
private final UserId mCurrentUser;
private final Handler mHandler;
private Map<UserId, Runnable> mIsProviderAvailableRunnableMap = new HashMap<>();
- // This is the user profile selected in the photo picker. Photo picker will display media
- // for this user. It could be different from mCurrentUser.
+ // This is the user profile selected in the photo picker. Photo picker will
+ // display media for this user. It could be different from mCurrentUser.
private UserId mCurrentUserProfile = null;
- // A map of user profile ids (Except current user) with a Boolean value that represents
- // whether corresponding user profile is blocked by admin or not.
- private Map<UserId , Boolean> mIsProfileBlockedByAdminMap = new HashMap<>();
+ // A map of user profile ids (Except current user) with a Boolean value that
+ // represents whether corresponding user profile is blocked by admin or not.
+ private Map<UserId, Boolean> mIsProfileBlockedByAdminMap = new HashMap<>();
- // A map of user profile ids (Except current user) with a Boolean value that represents
- // whether corresponding user profile is on or off.
- private Map<UserId , Boolean> mProfileOffStatus = new HashMap<>();
+ // A map of user profile ids (Except current user) with a Boolean value that
+ // represents whether corresponding user profile is on or off.
+ private Map<UserId, Boolean> mProfileOffStatus = new HashMap<>();
private final MutableLiveData<Boolean> mIsMultiUserProfiles = new MutableLiveData<>();
- // A list of all user profile Ids present on the device that require a separate tab to show
- // in PhotoPicker. It also includes currentUser/contextUser.
+ // A list of all user profile Ids present on the device that require a separate
+ // tab to show in PhotoPicker. It also includes currentUser/contextUser.
private List<UserId> mUserProfileIds = new ArrayList<>();
private UserManager mUserManager;
/**
* This live data will be posted every time when a user profile change occurs in the
- * background such as turning on/off/adding/removing a user profile. The complete map
- * will be reinitiated again in {@link #getCrossProfileAllowedStatusForAll()} and will
- * be posted into the below mutable live data. This live data will be observed later in
- * {@link TabFragment}.
- **/
+ * background such as turning on/off/adding/removing a user profile. The complete map will
+ * be reinitiated again in {@link #getCrossProfileAllowedStatusForAll()} and will be posted
+ * into the below mutable live data. This live data will be observed later in {@link
+ * TabFragment}.
+ */
private final MutableLiveData<Map<UserId, Boolean>> mCrossProfileAllowedStatus =
new MutableLiveData<>();
@@ -277,8 +259,8 @@ public interface UserManagerState {
return;
}
- // Here there could be other profiles too , that we do not want to show anywhere in
- // photo picker at all.
+ // Here there could be other profiles too , that we do not want to show anywhere
+ // in photo picker at all.
final List<UserHandle> userProfiles = mUserManager.getUserProfiles();
if (SdkLevel.isAtLeastV()) {
for (UserHandle userHandle : userProfiles) {
@@ -289,13 +271,13 @@ public interface UserManagerState {
// an owner profile itself.
if (getSystemUser().getIdentifier() != userHandle.getIdentifier()
&& userProperties.getShowInSharingSurfaces()
- == userProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) {
+ == userProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) {
mUserProfileIds.add(userId);
}
}
} else {
- // if sdk version is less than V, then maximum two profiles with separate tab could
- // only be available
+ // if sdk version is less than V, then maximum two profiles with separate tab
+ // could only be available
for (UserHandle userHandle : userProfiles) {
if (mUserManager.isManagedProfile(userHandle.getIdentifier())) {
mUserProfileIds.add(UserId.of(userHandle));
@@ -311,7 +293,7 @@ public interface UserManagerState {
}
@Override
- public int getProfileCount() {
+ public int getProfileCount() {
return mUserProfileIds.size();
}
@@ -364,11 +346,11 @@ public interface UserManagerState {
@Override
public void setIntentAndCheckRestrictions(Intent intent) {
assertMainThread();
- // The below method should be called even if only one profile is present on the device
- // because we want to have current profile off value and blocked by admin values in the
- // corresponding maps
+ // The below method should be called even if only one profile is present on the
+ // device because we want to have current profile off value and blocked by admin
+ // values
+ // in the corresponding maps
updateCrossProfileValues(intent);
-
}
@Override
@@ -431,13 +413,14 @@ public interface UserManagerState {
}
}
}
+
@Override
public void waitForMediaProviderToBeAvailable(UserId userId) {
assertMainThread();
- // Remove callbacks if any pre-available callbacks are present in the message queue for
- // given user
+ // Remove callbacks if any pre-available callbacks are present in the message
+ // queue for given user
stopWaitingForProviderToBeAvailableForUser(userId);
- if (CrossProfileUtils.isMediaProviderAvailable(userId , mContext)) {
+ if (CrossProfileUtils.isMediaProviderAvailable(userId, mContext)) {
mProfileOffStatus.put(userId, false);
updateAndPostCrossProfileStatus();
return;
@@ -446,37 +429,49 @@ public interface UserManagerState {
}
private void waitForProviderToBeAvailable(UserId userId, int numOfTries) {
- // The runnable should make sure to post update on the live data if it is changed.
- Runnable runnable = () -> {
- try {
- // We stop the recursive check when
- // 1. the provider is available
- // 2. the profile is in quiet mode, i.e. provider will not be available
- // 3. after maximum retries
- if (CrossProfileUtils.isMediaProviderAvailable(userId, mContext)) {
- mProfileOffStatus.put(userId, false);
- updateAndPostCrossProfileStatus();
- return;
- }
-
- if (CrossProfileUtils.isQuietModeEnabled(userId, mContext)) {
- return;
- }
-
- if (numOfTries <= PROVIDER_AVAILABILITY_MAX_RETRIES) {
- Log.d(TAG, "MediaProvider is not available. Retry after "
- + PROVIDER_AVAILABILITY_CHECK_DELAY);
- waitForProviderToBeAvailable(userId, numOfTries + 1);
- return;
- }
-
- Log.w(TAG, "Failed waiting for MediaProvider for user:" + userId
- + " to be available");
- } catch (Exception e) {
- Log.e(TAG, "An error occurred in runnable while waiting for "
- + "MediaProvider for user:" + userId + " to be available", e);
- }
- };
+ // The runnable should make sure to post update on the live data if it is
+ // changed.
+ Runnable runnable =
+ () -> {
+ try {
+ // We stop the recursive check when
+ // 1. the provider is available
+ // 2. the profile is in quiet mode, i.e. provider will not be available
+ // 3. after maximum retries
+ if (CrossProfileUtils.isMediaProviderAvailable(userId, mContext)) {
+ mProfileOffStatus.put(userId, false);
+ updateAndPostCrossProfileStatus();
+ return;
+ }
+
+ if (CrossProfileUtils.isQuietModeEnabled(userId, mContext)) {
+ return;
+ }
+
+ if (numOfTries <= PROVIDER_AVAILABILITY_MAX_RETRIES) {
+ Log.d(
+ TAG,
+ "MediaProvider is not available. Retry after "
+ + PROVIDER_AVAILABILITY_CHECK_DELAY);
+ waitForProviderToBeAvailable(userId, numOfTries + 1);
+ return;
+ }
+
+ Log.w(
+ TAG,
+ "Failed waiting for MediaProvider for user:"
+ + userId
+ + " to be available");
+ } catch (Exception e) {
+ Log.e(
+ TAG,
+ "An error occurred in runnable while waiting for "
+ + "MediaProvider for user:"
+ + userId
+ + " to be available",
+ e);
+ }
+ };
mIsProviderAvailableRunnableMap.put(userId, runnable);
mHandler.postDelayed(runnable, PROVIDER_AVAILABILITY_CHECK_DELAY);
}
@@ -536,80 +531,134 @@ public interface UserManagerState {
return null;
}
return isCrossProfileStrategyDelegatedToParent(userHandle)
- ? mUserManager.getProfileParent(userHandle) : userHandle;
+ ? mUserManager.getProfileParent(userHandle)
+ : userHandle;
}
-
/**
- * {@link #setBlockedByAdminValue(Intent)} Based on assumption that the only profiles with
- * {@link UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION} could be systemUser
- * and managedUser(if available).
+ * Updates Cross Profile access for all UserProfiles in {@code mUserProfileIds}
*
- * Todo(b/319567023):Refactor the below {@link #setBlockedByAdminValue(Intent)} to
- * avoid assumptions mentioned above.
+ * <p>This method looks at a variety of situations for each Profile and decides if the
+ * profile's content is accessible by the current process owner user id.
+ *
+ * <p>- UserProperties attributes for CrossProfileDelegation are checked first -
+ * CrossProfileIntentForwardingActivitys are resolved via the process owner's
+ * PackageManager, and are considered when evaluating cross profile to the target profile.
+ *
+ * <p>- In the event none of the above checks succeeds, the profile is considered to be
+ * inaccessible to the current process user, and is thus marked as "BlockedByAdmin".
+ *
+ * @param intent The intent Photopicker is currently running under, for
+ * CrossProfileForwardActivity checking.
*/
+ @SuppressLint("DiscouragedPrivateApi")
private void setBlockedByAdminValue(Intent intent) {
if (intent == null) {
- Log.e(TAG, "No intent specified to check if cross profile forwarding is"
- + " allowed.");
+ Log.e(
+ TAG,
+ "No intent specified to check if cross profile forwarding is"
+ + " allowed.");
return;
}
- // List of all user profile ids that context user cannot access
- List<UserId> canNotForwardToUserProfiles = new ArrayList<>();
+ Map<UserId, Boolean> profileIsAccessibleToProcessOwner = new HashMap<>();
+ List<UserId> delegatedFromParent = new ArrayList<>();
- /*
- * List of all user profile ids that have cross profile access among themselves.
- * It contains parent user and child profiles with user property
- * {@link UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT}
- */
- List<UserId> parentOrDelegatedFromParent = new ArrayList<>();
+ final PackageManager pm = mContext.getPackageManager();
- // Userprofile to check cross profile intentForwarderActivity for
- UserHandle needToCheck = null;
+ // Resolve CrossProfile activities for all user profiles that Photopicker is
+ // aware of.
+ for (UserId userId : mUserProfileIds) {
- if (mUserManager == null) {
- Log.e(TAG, "Cannot obtain user manager");
- return;
- }
+ // If the UserId is the system user, exit early.
+ if (userId.getIdentifier() == mCurrentUser.getIdentifier()) {
+ profileIsAccessibleToProcessOwner.put(userId, true);
+ continue;
+ }
- for (UserId userId : mUserProfileIds) {
- /*
- * All user profiles with user property
- * {@link UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT}
- * can access each other including its parent.
- */
- if (userId.equals(getSystemUser())
- || isCrossProfileStrategyDelegatedToParent(userId.getUserHandle())) {
- parentOrDelegatedFromParent.add(userId);
- } else {
- needToCheck = userId.getUserHandle();
+ // This UserId delegates its strategy to the parent profile
+ if (isCrossProfileStrategyDelegatedToParent(userId.getUserHandle())) {
+ delegatedFromParent.add(userId);
+ continue;
}
- }
- // When context user is a managed user , then will replace needToCheck with its parent
- // to check cross profile intentForwarderActivity for.
- if (needToCheck != null && needToCheck.equals(mCurrentUser.getUserHandle())) {
- needToCheck = mUserManager.getProfileParent(mCurrentUser.getUserHandle());
+ // Clear out the component & package before attempting to match
+ Intent intentToCheck = (Intent) intent.clone();
+ intentToCheck.setComponent(null);
+ intentToCheck.setPackage(null);
+
+ for (ResolveInfo resolveInfo :
+ pm.queryIntentActivities(
+ intentToCheck, PackageManager.MATCH_DEFAULT_ONLY)) {
+
+ // If the activity is a CrossProfileIntentForwardingActivity, inspect its
+ // targetUserId to
+ // see if it targets the user we are currently checking for.
+ if (resolveInfo.isCrossProfileIntentForwarderActivity()) {
+
+ /*
+ * IMPORTANT: This is a reflection based hack to ensure the profile is
+ * actually the installer of the CrossProfileIntentForwardingActivity.
+ *
+ * ResolveInfo.targetUserId exists, but is a hidden API not available to
+ * mainline modules, and no such API exists, so it is accessed via
+ * reflection below. All exceptions are caught to protect against
+ * reflection related issues such as:
+ * NoSuchFieldException / IllegalAccessException / SecurityException.
+ *
+ * In the event of an exception, the code fails "closed" for the current
+ * profile to avoid showing content that should not be visible.
+ */
+ try {
+ Field targetUserIdField =
+ resolveInfo.getClass().getDeclaredField("targetUserId");
+ targetUserIdField.setAccessible(true);
+ int targetUserId = (int) targetUserIdField.get(resolveInfo);
+
+ if (targetUserId == userId.getIdentifier()) {
+ profileIsAccessibleToProcessOwner.put(userId, true);
+
+ // Don't need to look further, exit the loop.
+ break;
+ }
+
+ } catch (NoSuchFieldException
+ | IllegalAccessException
+ | SecurityException ex) {
+ // Couldn't check the targetUserId via reflection, so fail without
+ // further
+ // iterations.
+ Log.e(TAG, "Could not access targetUserId via reflection.", ex);
+ break;
+ } catch (Exception ex) {
+ Log.e(TAG, "Exception occurred during cross profile checks", ex);
+ }
+ }
+ }
+ // Fail case, was unable to find a suitable Activity for this user.
+ profileIsAccessibleToProcessOwner.putIfAbsent(userId, false);
}
- final PackageManager packageManager = mContext.getPackageManager();
- if (needToCheck != null && !CrossProfileUtils.isIntentAllowedCrossProfileAccessFromUser(
- intent, packageManager,
- getProfileToCheckCrossProfileAccess(mCurrentUser.getUserHandle()))) {
- if (parentOrDelegatedFromParent.contains(UserId.of(needToCheck))) {
- // if user profile cannot access its parent then all direct child profiles with
- // delegated from parent will also be inaccessible.
- canNotForwardToUserProfiles.addAll(parentOrDelegatedFromParent);
- } else {
- canNotForwardToUserProfiles.add(UserId.of(needToCheck));
- }
+ // For profiles that delegate their access to the parent, set the access for
+ // those profiles equal to the same as their parent.
+ for (UserId userId : delegatedFromParent) {
+ UserHandle parent =
+ mUserManager.getProfileParent(UserHandle.of(userId.getIdentifier()));
+ profileIsAccessibleToProcessOwner.put(
+ userId,
+ profileIsAccessibleToProcessOwner.getOrDefault(
+ UserId.of(parent), /* default= */ false));
}
mIsProfileBlockedByAdminMap.clear();
for (UserId userId : mUserProfileIds) {
- mIsProfileBlockedByAdminMap.put(userId,
- canNotForwardToUserProfiles.contains(userId));
+ mIsProfileBlockedByAdminMap.put(
+ // Boolean inversion seems strange, but this map is the opposite of what was
+ // calculated, (which are blocked, rather than which are accessible) so the
+ // boolean needs to be inverted.
+ userId,
+ !profileIsAccessibleToProcessOwner.getOrDefault(
+ userId, /* default= */ false));
}
}
@@ -617,68 +666,79 @@ public interface UserManagerState {
public Map<UserId, String> getProfileLabelsForAll() {
assertMainThread();
Map<UserId, String> profileLabels = new HashMap<>();
- String personalTabLabel = mContext.getString(R.string.picker_personal_profile_label);
- profileLabels.put(getSystemUser(), personalTabLabel);
- if (SdkLevel.isAtLeastV()) {
- for (UserId userId : mUserProfileIds) {
- UserHandle userHandle = userId.getUserHandle();
- if (userHandle.getIdentifier() != getSystemUser().getIdentifier()) {
- profileLabels.put(userId, getProfileLabel(userHandle));
- }
- }
+ for (UserId userId : mUserProfileIds) {
+ UserHandle userHandle = userId.getUserHandle();
+ profileLabels.put(userId, getProfileLabel(userHandle));
}
return profileLabels;
}
+
private String getProfileLabel(UserHandle userHandle) {
if (SdkLevel.isAtLeastV()) {
- Context userContext = mContext.createContextAsUser(userHandle, 0 /* flags */);
try {
+ Context userContext = mContext.createContextAsUser(userHandle, 0 /* flags */);
UserManager userManager = userContext.getSystemService(UserManager.class);
if (userManager == null) {
Log.e(TAG, "Cannot obtain user manager");
return null;
}
return userManager.getProfileLabel();
- } catch (Resources.NotFoundException e) {
- //Todo(b/318530691): Handle the label for the profile that is not defined
- // already
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "could not create user context for user.", e);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while fetching profile badge", e);
}
}
- return null;
+
+ // Fall back case if not V, or an error encountered above, return hard coded strings.
+ boolean isPrimaryProfile = mUserManager.getProfileParent(userHandle) == null;
+ boolean isManagedProfile = mUserManager.isManagedProfile(userHandle.getIdentifier());
+
+ int resId;
+ if (isPrimaryProfile) {
+ resId = R.string.photopicker_profile_primary_label;
+ } else if (isManagedProfile) {
+ resId = R.string.photopicker_profile_managed_label;
+ } else {
+ resId = R.string.photopicker_profile_unknown_label;
+ }
+
+ return mContext.getString(resId);
}
@Override
public Map<UserId, Drawable> getProfileBadgeForAll() {
assertMainThread();
Map<UserId, Drawable> profileBadges = new HashMap<>();
- profileBadges.put(getSystemUser(), mContext.getDrawable(R.drawable.ic_personal_mode));
- if (SdkLevel.isAtLeastV()) {
- for (UserId userId : mUserProfileIds) {
- UserHandle userHandle = userId.getUserHandle();
- if (userHandle.getIdentifier() != getSystemUser().getIdentifier()) {
- profileBadges.put(userId, getProfileBadge(userHandle));
- }
- }
+ for (UserId userId : mUserProfileIds) {
+ profileBadges.put(userId, getProfileBadge(userId.getUserHandle()));
}
return profileBadges;
}
private Drawable getProfileBadge(UserHandle userHandle) {
if (SdkLevel.isAtLeastV()) {
- Context userContext = mContext.createContextAsUser(userHandle, 0 /* flags */);
try {
+ Context userContext = mContext.createContextAsUser(userHandle, 0 /* flags */);
UserManager userManager = userContext.getSystemService(UserManager.class);
if (userManager == null) {
Log.e(TAG, "Cannot obtain user manager");
return null;
}
return userManager.getUserBadge();
- } catch (Resources.NotFoundException e) {
- //Todo(b/318530691): Handle the icon for the profile that is not defined already
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "could not create user context for user.", e);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while fetching profile badge", e);
}
}
- return null;
+
+ // Fall back case if not V, or an error encountered above, return hard coded icons.
+ boolean isManagedProfile = mUserManager.isManagedProfile(userHandle.getIdentifier());
+ int drawable =
+ isManagedProfile ? R.drawable.ic_work_outline : R.drawable.ic_personal_mode;
+ return mContext.getDrawable(drawable);
}
@Override
@@ -707,6 +767,7 @@ public interface UserManagerState {
assertMainThread();
return mIsProfileBlockedByAdminMap.get(userId);
}
+
@Override
public boolean isProfileOff(UserId userId) {
assertMainThread();
@@ -716,10 +777,15 @@ public interface UserManagerState {
private void assertMainThread() {
if (Looper.getMainLooper().isCurrentThread()) return;
- throw new IllegalStateException("UserManagerState methods are expected to be called"
- + "from main thread. " + (Looper.myLooper() == null ? "" : "Current thread "
- + Looper.myLooper().getThread() + ", Main thread "
- + Looper.getMainLooper().getThread()));
+ throw new IllegalStateException(
+ "UserManagerState methods are expected to be called"
+ + "from main thread. "
+ + (Looper.myLooper() == null
+ ? ""
+ : "Current thread "
+ + Looper.myLooper().getThread()
+ + ", Main thread "
+ + Looper.getMainLooper().getThread()));
}
}
}
diff --git a/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorker.java b/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorker.java
index bc65bcc27..cf34c5949 100644
--- a/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorker.java
+++ b/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorker.java
@@ -29,6 +29,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
import android.provider.CloudMediaProviderContract;
import android.util.Log;
import android.util.Pair;
@@ -40,8 +41,8 @@ import androidx.work.ListenableWorker;
import androidx.work.Worker;
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.PhotopickerSyncHelper;
import com.android.providers.media.photopicker.v2.PickerNotificationSender;
import com.android.providers.media.photopicker.v2.sqlite.MediaInMediaSetsDatabaseUtil;
import com.android.providers.media.photopicker.v2.sqlite.MediaSetsDatabaseUtil;
@@ -67,12 +68,14 @@ public class MediaInMediaSetsSyncWorker extends Worker {
private final CancellationSignal mCancellationSignal;
private final SQLiteDatabase mDatabase;
private boolean mMarkedSyncWorkAsComplete = false;
+ private final PhotopickerSyncHelper mPhotopickerSyncHelper;
public MediaInMediaSetsSyncWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
mContext = context;
mCancellationSignal = new CancellationSignal();
- mDatabase = getDatabase();
+ mPhotopickerSyncHelper = new PhotopickerSyncHelper();
+ mDatabase = mPhotopickerSyncHelper.getDatabase();
}
@NonNull
@@ -133,7 +136,7 @@ public class MediaInMediaSetsSyncWorker extends Worker {
int syncSource, @NonNull String mediaSetId,
@NonNull Long mediaSetPickerId, @NonNull String mediaSetAuthority,
@Nullable String[] mimeTypes)
- throws RequestObsoleteException, IllegalArgumentException {
+ throws RequestObsoleteException, IllegalArgumentException, OperationCanceledException {
final PickerSearchProviderClient searchClient =
PickerSearchProviderClient.create(mContext, mediaSetAuthority);
@@ -166,7 +169,7 @@ public class MediaInMediaSetsSyncWorker extends Worker {
MediaInMediaSetsDatabaseUtil.getMediaContentValuesFromCursor(
mediaInMediaSetsCursor,
mediaSetPickerId,
- isAuthorityLocal(mediaSetAuthority)
+ mPhotopickerSyncHelper.isAuthorityLocal(mediaSetAuthority)
);
checkIfWorkerHasStopped();
@@ -223,7 +226,7 @@ public class MediaInMediaSetsSyncWorker extends Worker {
private Cursor fetchMediaInMediaSetFromCmp(
@NonNull PickerSearchProviderClient pickerSearchProviderClient,
@NonNull String mediaSetId, @Nullable String resumePageToken,
- @Nullable String[] mimeTypes) {
+ @Nullable String[] mimeTypes) throws OperationCanceledException {
final Cursor cursor = pickerSearchProviderClient.fetchMediasInMediaSetFromCmp(
mediaSetId,
resumePageToken,
@@ -258,10 +261,11 @@ public class MediaInMediaSetsSyncWorker extends Worker {
private void checkIfCurrentCloudProviderAuthorityHasChanged(@NonNull String authority)
throws RequestObsoleteException {
- if (isAuthorityLocal(authority)) {
+ if (mPhotopickerSyncHelper.isAuthorityLocal(authority)) {
return;
}
- final String currentCloudAuthority = getCurrentCloudProviderAuthority();
+ final String currentCloudAuthority =
+ mPhotopickerSyncHelper.getCurrentCloudProviderAuthority();
if (!authority.equals(currentCloudAuthority)) {
throw new RequestObsoleteException("Cloud provider authority has changed."
+ " Sync will not be continued."
@@ -292,22 +296,4 @@ public class MediaInMediaSetsSyncWorker extends Worker {
throw new IllegalArgumentException("mediaSetPickerId was an empty string");
}
}
-
- private boolean isAuthorityLocal(@NonNull String authority) {
- return getLocalProviderAuthority().equals(authority);
- }
-
- @Nullable
- private String getLocalProviderAuthority() {
- return PickerSyncController.getInstanceOrThrow().getLocalProvider();
- }
-
- @Nullable
- private String getCurrentCloudProviderAuthority() {
- return PickerSyncController.getInstanceOrThrow().getCloudProvider();
- }
-
- private SQLiteDatabase getDatabase() {
- return PickerSyncController.getInstanceOrThrow().getDbFacade().getDatabase();
- }
}
diff --git a/src/com/android/providers/media/photopicker/sync/MediaSetsResetWorker.java b/src/com/android/providers/media/photopicker/sync/MediaSetsResetWorker.java
index 5991ae049..e847bd505 100644
--- a/src/com/android/providers/media/photopicker/sync/MediaSetsResetWorker.java
+++ b/src/com/android/providers/media/photopicker/sync/MediaSetsResetWorker.java
@@ -16,9 +16,13 @@
package com.android.providers.media.photopicker.sync;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_AUTHORITY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_CATEGORY_ID;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markMediaSetsSyncAsComplete;
+import static java.util.Objects.requireNonNull;
+
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
@@ -33,9 +37,11 @@ import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteEx
import com.android.providers.media.photopicker.v2.sqlite.MediaInMediaSetsDatabaseUtil;
import com.android.providers.media.photopicker.v2.sqlite.MediaSetsDatabaseUtil;
+import java.util.List;
+
/**
- * This worker is responsible for cleaning up the cached media sets or media sets content based
- * on the type input reset parameter received
+ * This worker is responsible for cleaning up the cached media sets or media sets content for the
+ * given categoryId
*/
public class MediaSetsResetWorker extends Worker {
private static final String TAG = "MediaSetsResetWorker";
@@ -50,6 +56,8 @@ public class MediaSetsResetWorker extends Worker {
final int syncSource = getInputData().getInt(SYNC_WORKER_INPUT_SYNC_SOURCE,
/* defaultValue */ -1);
+ final String categoryId = getInputData().getString(SYNC_WORKER_INPUT_CATEGORY_ID);
+ final String authority = getInputData().getString(SYNC_WORKER_INPUT_AUTHORITY);
// Do not allow endless re-runs of this worker, if this isn't the original run,
// just fail and wait until the next scheduled run.
@@ -58,18 +66,24 @@ public class MediaSetsResetWorker extends Worker {
return ListenableWorker.Result.failure();
}
- boolean isMediaSetsTableDeleted = clearMediaSetsCache(syncSource);
- boolean isMediaInMediaSetsTabledDeleted = clearMediaSetsContentCache();
+ boolean isMediaInMediaSetsCacheDeleted = clearMediaSetsContentCache(categoryId, authority);
+ boolean isMediaSetsCacheDeleted = clearMediaSetsCache(syncSource, categoryId, authority);
// Both the tables were cleared. Mark the worker's run as success
- if (isMediaSetsTableDeleted && isMediaInMediaSetsTabledDeleted) {
+ if (isMediaSetsCacheDeleted && isMediaInMediaSetsCacheDeleted) {
return ListenableWorker.Result.success();
}
return ListenableWorker.Result.failure();
}
- private boolean clearMediaSetsCache(int syncSource) {
+ private boolean clearMediaSetsCache(int syncSource,
+ @NonNull String categoryId,
+ @NonNull String authority) {
+
+ requireNonNull(categoryId);
+ requireNonNull(authority);
+
SQLiteDatabase database = getDatabase();
try {
@@ -77,7 +91,7 @@ public class MediaSetsResetWorker extends Worker {
database.beginTransaction();
- MediaSetsDatabaseUtil.clearMediaSetsCache(database);
+ MediaSetsDatabaseUtil.clearMediaSetsCache(database, categoryId, authority);
if (database.inTransaction()) {
database.setTransactionSuccessful();
@@ -97,7 +111,12 @@ public class MediaSetsResetWorker extends Worker {
}
}
- private boolean clearMediaSetsContentCache() {
+ private boolean clearMediaSetsContentCache(
+ @NonNull String categoryId,
+ @NonNull String authority) {
+
+ requireNonNull(categoryId);
+ requireNonNull(authority);
SQLiteDatabase database = getDatabase();
@@ -106,7 +125,10 @@ public class MediaSetsResetWorker extends Worker {
database.beginTransaction();
- MediaInMediaSetsDatabaseUtil.clearMediaInMediaSetsCache(database);
+ List<String> mediaSetPickerIds = MediaSetsDatabaseUtil
+ .getMediaSetPickerIdsForGivenCategoryId(database, categoryId, authority);
+ MediaInMediaSetsDatabaseUtil.clearMediaInMediaSetsCache(
+ database, mediaSetPickerIds);
if (database.inTransaction()) {
database.setTransactionSuccessful();
diff --git a/src/com/android/providers/media/photopicker/sync/MediaSetsSyncWorker.java b/src/com/android/providers/media/photopicker/sync/MediaSetsSyncWorker.java
index 8e151141a..f3a91d6d1 100644
--- a/src/com/android/providers/media/photopicker/sync/MediaSetsSyncWorker.java
+++ b/src/com/android/providers/media/photopicker/sync/MediaSetsSyncWorker.java
@@ -26,9 +26,9 @@ import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.m
import android.content.Context;
import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
import android.provider.CloudMediaProviderContract;
import android.util.Log;
@@ -38,8 +38,8 @@ import androidx.work.ListenableWorker;
import androidx.work.Worker;
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.PhotopickerSyncHelper;
import com.android.providers.media.photopicker.v2.PickerNotificationSender;
import com.android.providers.media.photopicker.v2.sqlite.MediaSetsDatabaseUtil;
@@ -59,6 +59,7 @@ public class MediaSetsSyncWorker extends Worker {
private final Context mContext;
private final CancellationSignal mCancellationSignal;
private boolean mMarkedSyncWorkAsComplete = false;
+ private final PhotopickerSyncHelper mPhotopickerSyncHelper;
public MediaSetsSyncWorker(@NonNull Context context, @NonNull WorkerParameters parameters) {
@@ -66,6 +67,7 @@ public class MediaSetsSyncWorker extends Worker {
mContext = context;
mCancellationSignal = new CancellationSignal();
+ mPhotopickerSyncHelper = new PhotopickerSyncHelper();
}
@NonNull
@@ -124,7 +126,7 @@ public class MediaSetsSyncWorker extends Worker {
private void syncMediaSets(
int syncSource, @NonNull String categoryId,
@NonNull String categoryAuthority, @Nullable String[] mimeTypes)
- throws RequestObsoleteException, IllegalArgumentException {
+ throws RequestObsoleteException, IllegalArgumentException, OperationCanceledException {
List<String> mimeTypesList = mimeTypes == null || mimeTypes.length == 0 ? null
: Arrays.asList(mimeTypes);
@@ -142,7 +144,7 @@ public class MediaSetsSyncWorker extends Worker {
searchClient, categoryId, nextPageToken, mimeTypes, mCancellationSignal)) {
// Cache the retrieved media sets
int numberOfRowsInserted = MediaSetsDatabaseUtil.cacheMediaSets(
- getDatabase(), mediaSetsCursor, categoryId,
+ mPhotopickerSyncHelper.getDatabase(), mediaSetsCursor, categoryId,
categoryAuthority, mimeTypesList);
Log.i(TAG, "Cached " + numberOfRowsInserted + " media sets");
@@ -183,7 +185,7 @@ public class MediaSetsSyncWorker extends Worker {
String categoryId,
String nextPageToken,
String[] mimeTypes,
- CancellationSignal cancellationSignal) {
+ CancellationSignal cancellationSignal) throws OperationCanceledException {
final Cursor cursor = client.fetchMediaSetsFromCmp(
categoryId, nextPageToken, PAGE_SIZE, mimeTypes, cancellationSignal);
@@ -210,10 +212,11 @@ public class MediaSetsSyncWorker extends Worker {
private void checkIfCurrentCloudProviderAuthorityHasChanged(@NonNull String authority)
throws RequestObsoleteException {
- if (isAuthorityLocal(authority)) {
+ if (mPhotopickerSyncHelper.isAuthorityLocal(authority)) {
return;
}
- final String currentCloudAuthority = getCurrentCloudProviderAuthority();
+ final String currentCloudAuthority =
+ mPhotopickerSyncHelper.getCurrentCloudProviderAuthority();
if (!authority.equals(currentCloudAuthority)) {
throw new RequestObsoleteException("Cloud provider authority has changed."
+ " Sync will not be continued."
@@ -221,22 +224,4 @@ public class MediaSetsSyncWorker extends Worker {
+ " Cloud provider authority to sync with: " + authority);
}
}
-
- private boolean isAuthorityLocal(@NonNull String authority) {
- return getLocalProviderAuthority().equals(authority);
- }
-
- @Nullable
- private String getLocalProviderAuthority() {
- return PickerSyncController.getInstanceOrThrow().getLocalProvider();
- }
-
- @Nullable
- private String getCurrentCloudProviderAuthority() {
- return PickerSyncController.getInstanceOrThrow().getCloudProvider();
- }
-
- private SQLiteDatabase getDatabase() {
- return PickerSyncController.getInstanceOrThrow().getDbFacade().getDatabase();
- }
}
diff --git a/src/com/android/providers/media/photopicker/sync/PickerSearchProviderClient.java b/src/com/android/providers/media/photopicker/sync/PickerSearchProviderClient.java
index 76ac99bfd..4b1e8963b 100644
--- a/src/com/android/providers/media/photopicker/sync/PickerSearchProviderClient.java
+++ b/src/com/android/providers/media/photopicker/sync/PickerSearchProviderClient.java
@@ -27,6 +27,7 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
import android.provider.CloudMediaProviderContract;
import android.provider.CloudMediaProviderContract.SortOrder;
import android.util.Log;
@@ -152,7 +153,7 @@ public class PickerSearchProviderClient {
final Cursor cursor = mContext.getContentResolver().query(
getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_MEDIA_CATEGORY),
- null, queryArgs, null);
+ null, queryArgs, cancellationSignal);
if (cursor == null) {
Log.d(TAG, "Categories response from the CMP is null.");
@@ -170,7 +171,8 @@ public class PickerSearchProviderClient {
@Nullable
public Cursor fetchMediaSetsFromCmp(
@NonNull String mediaCategoryId, @Nullable String nextPageToken, int pageSize,
- @Nullable String[] mimeTypes, @Nullable CancellationSignal cancellationSignal) {
+ @Nullable String[] mimeTypes, @Nullable CancellationSignal cancellationSignal)
+ throws OperationCanceledException {
final Bundle queryArgs = new Bundle();
queryArgs.putString(CloudMediaProviderContract.KEY_MEDIA_CATEGORY_ID,
requireNonNull(mediaCategoryId));
@@ -182,7 +184,7 @@ public class PickerSearchProviderClient {
final Cursor cursor = mContext.getContentResolver().query(
getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_MEDIA_SET),
- null, queryArgs, null);
+ null, queryArgs, cancellationSignal);
if (cursor == null) {
Log.d(TAG, "Media sets response from the CMP is null.");
@@ -204,7 +206,7 @@ public class PickerSearchProviderClient {
int pageSize,
int sortOrder,
@Nullable String[] mimeTypes,
- @Nullable CancellationSignal cancellationSignal) {
+ @Nullable CancellationSignal cancellationSignal) throws OperationCanceledException {
final Bundle queryArgs = new Bundle();
queryArgs.putString(CloudMediaProviderContract.KEY_MEDIA_SET_ID,
requireNonNull(mediaSetId));
@@ -217,7 +219,7 @@ public class PickerSearchProviderClient {
final Cursor cursor = mContext.getContentResolver().query(
getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_MEDIA_IN_MEDIA_SET),
- null, queryArgs, null);
+ null, queryArgs, cancellationSignal);
if (cursor == null) {
Log.d(TAG, "Media set contents response from the CMP is null.");
diff --git a/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java b/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
index 3c247a947..85cd059ac 100644
--- a/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
+++ b/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
@@ -650,9 +650,10 @@ public class PickerSyncManager {
/**
* Creates OneTimeWork request for syncing media sets with the given provider.
- * The existing media sets cache and the media sets content cache is cleared before a new media
- * sets sync is triggered to ensure accuracy of the media sets metadata stored in the database.
- * The reset cache and sync requests are chained to ensure correctness of the entire operation.
+ * The existing media sets cache and the media sets content cache for the given categoryId
+ * is cleared before a new media sets sync is triggered to ensure accuracy of the media sets
+ * metadata stored in the database. The reset cache and sync requests are chained to ensure
+ * correctness of the entire operation.
* @param requestParams The MediaSetsSyncRequestsParams object containing all input parameters
* for creating a sync request
* @param syncSource Indicates whether the sync is required with the local provider or
@@ -675,15 +676,18 @@ public class PickerSyncManager {
buildOneTimeWorkerRequest(MediaSetsSyncWorker.class, syncRequestInputData);
// Create media sets reset request. MediaSets sync are non-resumable.
- // It's fine to delete the entire cache before a new set is triggered.
- // The media sets content cache is also completely cleared before we start syncing
- // any particular media set for its content.
+ // It's fine to delete the entire cache before a new set is triggered for the given
+ // categoryId.
+ // The media sets content cache for media sets belonging to the given categoryId
+ // is also cleared before we start syncing any particular media set for its content.
// These tables are cleared once per picker session before the media sets sync for this
// session is triggered. This ensures that the data read from the cache in every session
// is always in sync with the cloud provider.
- final Data resetRequestInputData = new Data(Map.of(
- SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource
- ));
+ final Map<String, Object> resetRequestInputMap = new HashMap<>();
+ resetRequestInputMap.put(SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource);
+ resetRequestInputMap.put(SYNC_WORKER_INPUT_CATEGORY_ID, requestParams.getCategoryId());
+ resetRequestInputMap.put(SYNC_WORKER_INPUT_AUTHORITY, requestParams.getAuthority());
+ final Data resetRequestInputData = new Data(resetRequestInputMap);
final OneTimeWorkRequest resetRequest =
buildOneTimeWorkerRequest(MediaSetsResetWorker.class, resetRequestInputData);
diff --git a/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java b/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java
index 86aeb8203..76e0e6e06 100644
--- a/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java
+++ b/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java
@@ -44,8 +44,8 @@ import androidx.work.ListenableWorker;
import androidx.work.Worker;
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.PhotopickerSyncHelper;
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;
@@ -73,6 +73,8 @@ public class SearchResultsSyncWorker extends Worker {
private final Context mContext;
private final CancellationSignal mCancellationSignal;
private boolean mMarkedSyncWorkAsComplete = false;
+ private final PhotopickerSyncHelper mPhotopickerSyncHelper;
+ private final SQLiteDatabase mDatabase;
/**
* Creates an instance of the {@link Worker}.
@@ -87,6 +89,8 @@ public class SearchResultsSyncWorker extends Worker {
mContext = context;
mCancellationSignal = new CancellationSignal();
+ mPhotopickerSyncHelper = new PhotopickerSyncHelper();
+ mDatabase = mPhotopickerSyncHelper.getDatabase();
}
@NonNull
@@ -113,7 +117,7 @@ public class SearchResultsSyncWorker extends Worker {
syncSource, syncAuthority, searchRequestId));
final SearchRequest searchRequest = SearchRequestDatabaseUtil
- .getSearchRequestDetails(getDatabase(), searchRequestId);
+ .getSearchRequestDetails(mDatabase, searchRequestId);
validateWorkInput(syncSource, syncAuthority, searchRequestId, searchRequest);
syncWithSource(syncSource, syncAuthority, searchRequestId, searchRequest);
@@ -158,7 +162,7 @@ public class SearchResultsSyncWorker extends Worker {
maybeResetResumeKey(searchRequestId, searchRequest, authority, syncSource);
if (resetResumeKey) {
searchRequest = requireNonNull(SearchRequestDatabaseUtil
- .getSearchRequestDetails(getDatabase(), searchRequestId));
+ .getSearchRequestDetails(mDatabase, searchRequestId));
}
final Pair<String, String> resumeKey = getResumeKey(searchRequest, syncSource);
@@ -187,13 +191,14 @@ public class SearchResultsSyncWorker extends Worker {
List<ContentValues> contentValues =
SearchResultsDatabaseUtil.extractContentValuesList(
- searchRequestId, cursor, isLocal(authority));
+ searchRequestId, cursor,
+ mPhotopickerSyncHelper.isAuthorityLocal(authority));
throwIfWorkerStopped();
throwIfCloudProviderHasChanged(authority);
int numberOfRowsInserted = SearchResultsDatabaseUtil
- .cacheSearchResults(getDatabase(), authority, contentValues,
+ .cacheSearchResults(mDatabase, authority, contentValues,
mCancellationSignal);
nextPageToken = getResumePageToken(cursor.getExtras());
@@ -229,8 +234,8 @@ public class SearchResultsSyncWorker extends Worker {
throwIfWorkerStopped();
setResumeKey(searchRequest, nextPageToken, syncSource);
SearchRequestDatabaseUtil
- .updateResumeKey(getDatabase(), searchRequestId, SYNC_COMPLETE_RESUME_KEY,
- authority, isLocal(authority));
+ .updateResumeKey(mDatabase, searchRequestId, SYNC_COMPLETE_RESUME_KEY,
+ authority, mPhotopickerSyncHelper.isAuthorityLocal(authority));
}
}
}
@@ -251,27 +256,29 @@ public class SearchResultsSyncWorker extends Worker {
authority));
try {
- getDatabase().beginTransaction();
+ mDatabase.beginTransaction();
SearchRequestDatabaseUtil.clearSyncResumeInfo(
- getDatabase(), List.of(searchRequestId), isLocal(authority));
+ mDatabase, List.of(searchRequestId),
+ mPhotopickerSyncHelper.isAuthorityLocal(authority));
SearchResultsDatabaseUtil.clearObsoleteSearchResults(
- getDatabase(), List.of(searchRequestId), isLocal(authority));
+ mDatabase, List.of(searchRequestId),
+ mPhotopickerSyncHelper.isAuthorityLocal(authority));
// Check if this worker has stopped and the current sync request is obsolete before
// committing the change.
throwIfWorkerStopped();
throwIfCloudProviderHasChanged(authority);
- if (getDatabase().inTransaction()) {
- getDatabase().setTransactionSuccessful();
+ if (mDatabase.inTransaction()) {
+ mDatabase.setTransactionSuccessful();
}
return true;
} catch (RuntimeException e) {
Log.e(TAG, "Could not clear sync resume info", e);
} finally {
- if (getDatabase().inTransaction()) {
- getDatabase().endTransaction();
+ if (mDatabase.inTransaction()) {
+ mDatabase.endTransaction();
}
}
}
@@ -380,7 +387,7 @@ public class SearchResultsSyncWorker extends Worker {
// Check if the input authority matches the current provider.
if (syncSource == SYNC_LOCAL_ONLY) {
- final String localAuthority = getLocalProviderAuthority();
+ final String localAuthority = mPhotopickerSyncHelper.getLocalProviderAuthority();
if (!authority.equals(localAuthority)) {
throw new RequestObsoleteException(String.format(
Locale.ROOT,
@@ -391,7 +398,7 @@ public class SearchResultsSyncWorker extends Worker {
);
}
} else {
- final String cloudAuthority = getCurrentCloudProviderAuthority();
+ final String cloudAuthority = mPhotopickerSyncHelper.getCurrentCloudProviderAuthority();
if (!authority.equals(cloudAuthority)) {
throw new RequestObsoleteException(String.format(
Locale.ROOT,
@@ -421,7 +428,8 @@ public class SearchResultsSyncWorker extends Worker {
if (searchSuggestionRequest.getSearchSuggestion().getSearchSuggestionType()
== SEARCH_SUGGESTION_ALBUM) {
final boolean isLocal =
- isLocal(searchSuggestionRequest.getSearchSuggestion().getAuthority());
+ mPhotopickerSyncHelper.isAuthorityLocal(
+ searchSuggestionRequest.getSearchSuggestion().getAuthority());
if (isLocal && syncSource == SYNC_CLOUD_ONLY) {
throw new IllegalArgumentException(
@@ -439,11 +447,12 @@ public class SearchResultsSyncWorker extends Worker {
private void throwIfCloudProviderHasChanged(@NonNull String authority)
throws RequestObsoleteException {
// Local provider's authority cannot change.
- if (isLocal(authority)) {
+ if (mPhotopickerSyncHelper.isAuthorityLocal(authority)) {
return;
}
- final String currentCloudAuthority = getCurrentCloudProviderAuthority();
+ final String currentCloudAuthority =
+ mPhotopickerSyncHelper.getCurrentCloudProviderAuthority();
if (!authority.equals(currentCloudAuthority)) {
throw new RequestObsoleteException("Cloud provider authority has changed. "
+ " Current cloud provider authority: " + currentCloudAuthority
@@ -457,26 +466,6 @@ public class SearchResultsSyncWorker extends Worker {
}
}
- private boolean isLocal(@NonNull String authority) {
- final String localAuthority = getLocalProviderAuthority();
- return localAuthority != null && localAuthority.equals(authority);
- }
-
- @Nullable
- private String getLocalProviderAuthority() {
- return PickerSyncController.getInstanceOrThrow().getLocalProvider();
- }
-
- @Nullable
- private String getCurrentCloudProviderAuthority() {
- return PickerSyncController.getInstanceOrThrow()
- .getCloudProviderOrDefault(/* defaultValue */ null);
- }
-
- private SQLiteDatabase getDatabase() {
- return PickerSyncController.getInstanceOrThrow().getDbFacade().getDatabase();
- }
-
@Override
public void onStopped() {
// Mark the operation as cancelled so that the cancellation can be propagated to subtasks.
diff --git a/src/com/android/providers/media/photopicker/v2/PhotopickerSyncHelper.java b/src/com/android/providers/media/photopicker/v2/PhotopickerSyncHelper.java
new file mode 100644
index 000000000..37ec98190
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/v2/PhotopickerSyncHelper.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.photopicker.v2;
+
+import android.database.sqlite.SQLiteDatabase;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+
+import java.util.Objects;
+
+/**
+ * Helper methods required across the picker search sync workers are extracted out into this
+ * utility class.
+ */
+public class PhotopickerSyncHelper {
+
+ /**
+ * Checks if the authority in the parameter is that of the local provider
+ */
+ public boolean isAuthorityLocal(@NonNull String authority) {
+ Objects.requireNonNull(authority);
+ return getLocalProviderAuthority().equals(authority);
+ }
+
+ /**
+ * Returns local provider authority
+ */
+ @Nullable
+ public String getLocalProviderAuthority() {
+ return PickerSyncController.getInstanceOrThrow().getLocalProvider();
+ }
+
+ /**
+ * Returns the authority of the current cloud provider
+ */
+ @Nullable
+ public String getCurrentCloudProviderAuthority() {
+ return PickerSyncController.getInstanceOrThrow()
+ .getCloudProviderOrDefault(/* defaultValue */ null);
+ }
+
+ /**
+ * Returns a database object to be used for required database operations
+ */
+ public SQLiteDatabase getDatabase() {
+ return PickerSyncController.getInstanceOrThrow().getDbFacade().getDatabase();
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java b/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java
index 3f970463f..6471205f2 100644
--- a/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java
+++ b/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java
@@ -1132,6 +1132,18 @@ public class PickerDataLayerV2 {
Log.d(TAG, "Cannot fetch cloud categories when cloud authority is null.");
return null;
}
+
+ try {
+ if (syncController.isFullSyncPending(cloudAuthority, /* isLocal */ false)) {
+ Log.d(TAG, "Don't return cloud categories when full sync is pending.");
+ return null;
+ }
+ } catch (RequestObsoleteException | RuntimeException e) {
+ Log.e(TAG, "Could not check if full sync is pending. "
+ + "Not returning cloud categories", e);
+ return null;
+ }
+
final PickerSearchProviderClient searchClient = PickerSearchProviderClient.create(
appContext, cloudAuthority);
if (syncController.getCategoriesState().areCategoriesEnabled(
diff --git a/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsDatabaseUtil.java b/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsDatabaseUtil.java
index 0c43445fe..e6e729718 100644
--- a/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsDatabaseUtil.java
+++ b/src/com/android/providers/media/photopicker/v2/sqlite/MediaInMediaSetsDatabaseUtil.java
@@ -61,8 +61,7 @@ public class MediaInMediaSetsDatabaseUtil {
*/
public static int cacheMediaOfMediaSet(
@NonNull SQLiteDatabase database,
- @Nullable List<ContentValues> mediaListToInsert,
- @NonNull String authority) {
+ @Nullable List<ContentValues> mediaListToInsert, @NonNull String authority) {
requireNonNull(database);
requireNonNull(authority);
@@ -342,19 +341,41 @@ public class MediaInMediaSetsDatabaseUtil {
/**
* Deletes all the rows from the MediaInMediaSets table
*/
- public static void clearMediaInMediaSetsCache(@NonNull SQLiteDatabase database) {
+ public static void clearMediaInMediaSetsCache(
+ @NonNull SQLiteDatabase database, @NonNull List<String> mediaSetPickerIds) {
requireNonNull(database);
+ requireNonNull(mediaSetPickerIds);
+
+ if (mediaSetPickerIds.isEmpty()) {
+ return;
+ }
+
+ String whereClause =
+ PickerSQLConstants.MediaInMediaSetsTableColumns.MEDIA_SETS_PICKER_ID.getColumnName()
+ + " IN (" + generatePlaceholders(mediaSetPickerIds.size()) + ")";
+ String[] whereArgs = mediaSetPickerIds.toArray(new String[0]);
try {
int deletedRows = database.delete(
PickerSQLConstants.Table.MEDIA_IN_MEDIA_SETS.name(),
- /*whereClause*/ null,
- /*whereArgs*/ null);
+ whereClause,
+ whereArgs);
Log.d(TAG, "Deleted " + deletedRows + " rows from the media in media sets table");
} catch (Exception e) {
Log.d(TAG, "Couldn't clear the media in media sets table due to " + e);
}
}
+
+ private static String generatePlaceholders(int size) {
+ StringBuilder placeholders = new StringBuilder();
+ for (int i = 0; i < size; i++) {
+ placeholders.append("?");
+ if (i < size - 1) {
+ placeholders.append(",");
+ }
+ }
+ return placeholders.toString();
+ }
}
diff --git a/src/com/android/providers/media/photopicker/v2/sqlite/MediaSetsDatabaseUtil.java b/src/com/android/providers/media/photopicker/v2/sqlite/MediaSetsDatabaseUtil.java
index f71b9d4ac..6fc045561 100644
--- a/src/com/android/providers/media/photopicker/v2/sqlite/MediaSetsDatabaseUtil.java
+++ b/src/com/android/providers/media/photopicker/v2/sqlite/MediaSetsDatabaseUtil.java
@@ -289,15 +289,25 @@ public class MediaSetsDatabaseUtil {
/**
* Deletes all the rows from the MediaSets table
*/
- public static void clearMediaSetsCache(@NonNull SQLiteDatabase database) {
+ public static void clearMediaSetsCache(
+ @NonNull SQLiteDatabase database,
+ @NonNull String categoryId,
+ @NonNull String authority) {
requireNonNull(database);
+ requireNonNull(categoryId);
+ requireNonNull(authority);
+ String whereClause = PickerSQLConstants.MediaSetsTableColumns.CATEGORY_ID.getColumnName()
+ + " = ? AND "
+ + PickerSQLConstants.MediaSetsTableColumns.MEDIA_SET_AUTHORITY.getColumnName()
+ + " = ?";
+ String[] whereArgs = new String[] { categoryId, authority };
try {
int deletedRows = database.delete(
PickerSQLConstants.Table.MEDIA_SETS.name(),
- /*whereClause*/ null,
- /*whereClauseArgs*/ null);
+ whereClause,
+ whereArgs);
Log.d(TAG, "Deleted " + deletedRows + " rows from the media sets table.");
} catch (Exception exception) {
@@ -305,6 +315,52 @@ public class MediaSetsDatabaseUtil {
}
}
+ /**
+ * Fetches the generated database ids, also called media_set_picker_id for the given
+ * categoryId
+ */
+ public static List<String> getMediaSetPickerIdsForGivenCategoryId(
+ @NonNull SQLiteDatabase database,
+ @NonNull String categoryId,
+ @NonNull String authority) {
+
+ requireNonNull(database);
+ requireNonNull(categoryId);
+ requireNonNull(authority);
+
+ List<String> mediaSetPickerIds = new ArrayList<>();
+
+ final List<String> projection = List.of(
+ PickerSQLConstants.MediaSetsTableColumns.PICKER_ID
+ .getColumnName());
+ final SelectSQLiteQueryBuilder queryBuilder = new SelectSQLiteQueryBuilder(database)
+ .setTables(PickerSQLConstants.Table.MEDIA_SETS.name())
+ .setProjection(projection);
+ queryBuilder.appendWhereStandalone(
+ String.format(Locale.ROOT, " %s = '%s' ",
+ PickerSQLConstants.MediaSetsTableColumns.CATEGORY_ID.getColumnName(),
+ categoryId)
+ );
+ queryBuilder.appendWhereStandalone(
+ String.format(Locale.ROOT, " %s = '%s' ",
+ PickerSQLConstants.MediaSetsTableColumns.MEDIA_SET_AUTHORITY
+ .getColumnName(),
+ authority)
+ );
+
+ try (Cursor cursor = database.rawQuery(queryBuilder.buildQuery(), /*selectionArgs*/ null)) {
+ if (cursor.moveToFirst()) {
+ do {
+ int pickerIdIndex = cursor.getColumnIndex(
+ PickerSQLConstants.MediaSetsTableColumns.PICKER_ID.getColumnName());
+ String pickerId = cursor.getString(pickerIdIndex);
+ mediaSetPickerIds.add(pickerId);
+ } while (cursor.moveToNext());
+ }
+ }
+ return mediaSetPickerIds;
+ }
+
private static List<ContentValues> getMediaSetContentValues(
Cursor mediaSetCursor, String categoryId, String authority, String mimeTypes) {
diff --git a/src/com/android/providers/media/util/MimeTypeFixHandler.java b/src/com/android/providers/media/util/MimeTypeFixHandler.java
index bf0aa535e..8bdd88ef1 100644
--- a/src/com/android/providers/media/util/MimeTypeFixHandler.java
+++ b/src/com/android/providers/media/util/MimeTypeFixHandler.java
@@ -44,7 +44,10 @@ public final class MimeTypeFixHandler {
private static final String TAG = "MimeTypeFixHandler";
private static final Map<String, String> sExtToMimeType = new HashMap<>();
+ private static final Map<String, String> sMimeTypeToExt = new HashMap<>();
+
private static final Map<String, String> sCorruptedExtToMimeType = new HashMap<>();
+ private static final Map<String, String> sCorruptedMimeTypeToExt = new HashMap<>();
/**
* Loads MIME type mappings from the classpath resource if not already loaded.
@@ -58,13 +61,14 @@ public final class MimeTypeFixHandler {
}
if (sExtToMimeType.isEmpty()) {
- parseTypes(context, R.raw.mime_types, sExtToMimeType);
+ parseTypes(context, R.raw.mime_types, sExtToMimeType, sMimeTypeToExt);
// this will add or override the extension to mime type mapping
- parseTypes(context, R.raw.android_mime_types, sExtToMimeType);
+ parseTypes(context, R.raw.android_mime_types, sExtToMimeType, sMimeTypeToExt);
Log.v(TAG, "MIME types loaded");
}
if (sCorruptedExtToMimeType.isEmpty()) {
- parseTypes(context, R.raw.corrupted_mime_types, sCorruptedExtToMimeType);
+ parseTypes(context, R.raw.corrupted_mime_types, sCorruptedExtToMimeType,
+ sCorruptedMimeTypeToExt);
Log.v(TAG, "Corrupted MIME types loaded");
}
@@ -77,7 +81,8 @@ public final class MimeTypeFixHandler {
* @param resource the mime.type resource
* @param mapping the map to populate with file extension (key) to MIME type (value) mappings
*/
- private static void parseTypes(Context context, int resource, Map<String, String> mapping) {
+ private static void parseTypes(Context context, int resource, Map<String, String> extToMimeType,
+ Map<String, String> mimeTypeToExt) {
try (InputStream inputStream = context.getResources().openRawResource(resource)) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
@@ -92,10 +97,24 @@ public final class MimeTypeFixHandler {
if (tokens.length < 2) {
continue;
}
- String mimeType = tokens[0];
+ String mimeType = tokens[0].toLowerCase(Locale.ROOT);
+ String firstExt = tokens[1].toLowerCase(Locale.ROOT);
+ if (firstExt.startsWith("?")) {
+ firstExt = firstExt.substring(1);
+ if (firstExt.isEmpty()) {
+ continue;
+ }
+ }
+
// ?mime ext1 ?ext2 ext3
if (mimeType.toLowerCase(Locale.ROOT).startsWith("?")) {
mimeType = mimeType.substring(1); // Remove the "?"
+ if (mimeType.isEmpty()) {
+ continue;
+ }
+ mimeTypeToExt.putIfAbsent(mimeType, firstExt);
+ } else {
+ mimeTypeToExt.put(mimeType, firstExt);
}
for (int i = 1; i < tokens.length; i++) {
@@ -103,11 +122,9 @@ public final class MimeTypeFixHandler {
boolean putIfAbsent = extension.startsWith("?");
if (putIfAbsent) {
extension = extension.substring(1); // Remove the "?"
- if (!mapping.containsKey(extension)) {
- mapping.put(extension, mimeType);
- }
+ extToMimeType.putIfAbsent(extension, mimeType);
} else {
- mapping.put(extension, mimeType);
+ extToMimeType.put(extension, mimeType);
}
}
}
@@ -139,6 +156,35 @@ public final class MimeTypeFixHandler {
return Optional.empty();
}
+ /**
+ * Gets file extension from MIME type.
+ *
+ * @param mimeType The MIME type.
+ * @return Optional file extension, or empty.
+ */
+ static Optional<String> getExtFromMimeType(String mimeType) {
+ if (mimeType == null) {
+ return Optional.empty();
+ }
+
+ mimeType = mimeType.toLowerCase(Locale.ROOT);
+ return Optional.ofNullable(sMimeTypeToExt.get(mimeType));
+ }
+
+ /**
+ * Checks if a MIME type is corrupted.
+ *
+ * @param mimeType The MIME type.
+ * @return {@code true} if corrupted, {@code false} otherwise.
+ */
+ static boolean isCorruptedMimeType(String mimeType) {
+ if (sMimeTypeToExt.containsKey(mimeType)) {
+ return false;
+ }
+
+ return sCorruptedMimeTypeToExt.containsKey(mimeType);
+ }
+
/**
* Scans the database for files with unsupported or mismatched MIME types and updates them.
diff --git a/src/com/android/providers/media/util/MimeUtils.java b/src/com/android/providers/media/util/MimeUtils.java
index 354ceb193..5698a83aa 100644
--- a/src/com/android/providers/media/util/MimeUtils.java
+++ b/src/com/android/providers/media/util/MimeUtils.java
@@ -276,6 +276,11 @@ public class MimeUtils {
*/
@NonNull
public static String getExtensionFromMimeType(@Nullable String mimeType) {
+ Optional<String> android15Extension = getExtFromMimeTypeForAndroid15(mimeType);
+ if (android15Extension.isPresent()) {
+ return android15Extension.get();
+ }
+
final String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
if (extension != null) {
return "." + extension;
@@ -302,4 +307,28 @@ public class MimeUtils {
}
return Optional.empty();
}
+
+ /**
+ * Gets file extension from MIME type for Android 15.
+ * Handles Android 15 specific MIME type to extension mapping. If the mime-type is corrupted,
+ * then return the default one with respect to mime type.
+ *
+ * @param mimeType The MIME type.
+ * @return Optional file extension (with dot), or empty.
+ */
+ private static Optional<String> getExtFromMimeTypeForAndroid15(String mimeType) {
+ if (Flags.enableMimeTypeFixForAndroid15()
+ && Build.VERSION.SDK_INT == Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ Optional<String> value = MimeTypeFixHandler.getExtFromMimeType(mimeType);
+ if (value.isPresent()) {
+ return Optional.of("." + value.get());
+ } else if (MimeTypeFixHandler.isCorruptedMimeType(mimeType)) {
+ if (isImageMimeType(mimeType)) return Optional.of(DEFAULT_IMAGE_FILE_EXTENSION);
+ if (isVideoMimeType(mimeType)) return Optional.of(DEFAULT_VIDEO_FILE_EXTENSION);
+ return Optional.of("");
+ }
+ }
+ return Optional.empty();
+ }
+
}
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index 064095144..2458785e2 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -209,13 +209,13 @@ public class PermissionUtils {
}
public static boolean checkIsLegacyStorageGranted(@NonNull Context context, int uid,
- String packageName, @Nullable String attributionTag, boolean isTargetSdkAtLeastV) {
+ String packageName, boolean isTargetSdkAtLeastV) {
if (!isTargetSdkAtLeastV && context.getSystemService(AppOpsManager.class)
.unsafeCheckOp(OPSTR_LEGACY_STORAGE, uid, packageName) == MODE_ALLOWED) {
return true;
}
// Check OPSTR_NO_ISOLATED_STORAGE app op.
- return checkNoIsolatedStorageGranted(context, uid, packageName, attributionTag);
+ return checkNoIsolatedStorageGranted(context, uid, packageName);
}
public static boolean checkPermissionReadAudio(
@@ -395,13 +395,13 @@ public class PermissionUtils {
* indicates the package is a system gallery.
*/
public static boolean checkWriteImagesOrVideoAppOps(@NonNull Context context, int uid,
- @NonNull String packageName, @Nullable String attributionTag) {
+ @NonNull String packageName, @Nullable String attributionTag, boolean forDataDelivery) {
return checkAppOp(
context, OPSTR_WRITE_MEDIA_IMAGES, uid, packageName, attributionTag,
- generateAppOpMessage(packageName, sOpDescription.get()))
+ generateAppOpMessage(packageName, sOpDescription.get()), forDataDelivery)
|| checkAppOp(
context, OPSTR_WRITE_MEDIA_VIDEO, uid, packageName, attributionTag,
- generateAppOpMessage(packageName, sOpDescription.get()));
+ generateAppOpMessage(packageName, sOpDescription.get()), forDataDelivery);
}
/**
@@ -411,7 +411,8 @@ public class PermissionUtils {
int uid, @NonNull String[] sharedPackageNames, @Nullable String attributionTag) {
for (String packageName : sharedPackageNames) {
if (checkAppOp(context, OPSTR_REQUEST_INSTALL_PACKAGES, uid, packageName,
- attributionTag, generateAppOpMessage(packageName, sOpDescription.get()))) {
+ attributionTag, generateAppOpMessage(packageName, sOpDescription.get()),
+ /*forDataDelivery*/ false)) {
return true;
}
}
@@ -436,10 +437,9 @@ public class PermissionUtils {
@VisibleForTesting
static boolean checkNoIsolatedStorageGranted(@NonNull Context context, int uid,
- @NonNull String packageName, @Nullable String attributionTag) {
+ @NonNull String packageName) {
final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
- int ret = appOps.noteOpNoThrow(OPSTR_NO_ISOLATED_STORAGE, uid, packageName, attributionTag,
- generateAppOpMessage(packageName, "am instrument --no-isolated-storage"));
+ int ret = appOps.unsafeCheckOpNoThrow(OPSTR_NO_ISOLATED_STORAGE, uid, packageName);
return ret == AppOpsManager.MODE_ALLOWED;
}
@@ -476,9 +476,10 @@ public class PermissionUtils {
*/
private static boolean checkAppOp(@NonNull Context context,
@NonNull String op, int uid, @NonNull String packageName,
- @Nullable String attributionTag, @Nullable String opMessage) {
+ @Nullable String attributionTag, @Nullable String opMessage, boolean forDataDelivery) {
final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
- final int mode = appOps.noteOpNoThrow(op, uid, packageName, attributionTag, opMessage);
+ final int mode = forDataDelivery ? appOps.noteOpNoThrow(op, uid, packageName,
+ attributionTag, opMessage) : appOps.unsafeCheckOpNoThrow(op, uid, packageName);
switch (mode) {
case AppOpsManager.MODE_ALLOWED:
return true;
diff --git a/tests/Android.bp b/tests/Android.bp
index f3c98e530..cdb44b814 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -173,6 +173,7 @@ android_test {
resource_dirs: [
"main_res",
"res",
+ "photopicker_res",
],
srcs: [
diff --git a/tests/photopicker_res b/tests/photopicker_res
new file mode 120000
index 000000000..c1f73810f
--- /dev/null
+++ b/tests/photopicker_res
@@ -0,0 +1 @@
+../photopicker/res \ No newline at end of file
diff --git a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
index ba98432fc..9dc227cc0 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
@@ -25,6 +25,7 @@ import static com.android.providers.media.util.BackgroundThreadUtils.waitForIdle
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
@@ -59,6 +60,7 @@ import com.android.providers.media.photopicker.data.CloudProviderInfo;
import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
import com.android.providers.media.photopicker.data.PickerDbFacade;
import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
+import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
import com.android.providers.media.photopicker.v2.model.ProviderCollectionInfo;
@@ -2101,6 +2103,70 @@ public class PickerSyncControllerTest {
.that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
+ @Test
+ public void testIsFullSyncPending() throws RequestObsoleteException {
+ mController.setCloudProvider(/* authority */ CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Full sync should be pending after setting the CMP.")
+ .that(mController.isFullSyncPending(CLOUD_PRIMARY_PROVIDER_AUTHORITY, false))
+ .isTrue();
+
+ mController.syncAllMedia();
+ assertWithMessage("Full sync should be completed after syncing with the CMP.")
+ .that(mController.isFullSyncPending(CLOUD_PRIMARY_PROVIDER_AUTHORITY, false))
+ .isFalse();
+
+ // First Page of data
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_1);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_2);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_3);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_4);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_5);
+ // Second Page of data
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_6);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_7);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_8);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_9);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_10);
+ // Third Page of data
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_11);
+
+ mController.setCloudProvider(/* authority */ FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertWithMessage("Full sync should be pending after setting the CMP.")
+ .that(mController.isFullSyncPending(FLAKY_CLOUD_PROVIDER_AUTHORITY, false))
+ .isTrue();
+
+ // FlakyCloudMediaProvider will throw errors on 2 out of 3 requests, if we sync once, it
+ // should not be able to complete the sync.
+ mController.syncAllMedia();
+ assertWithMessage("Full sync should still be pending because it was stopped in between.")
+ .that(mController.isFullSyncPending(FLAKY_CLOUD_PROVIDER_AUTHORITY, false))
+ .isTrue();
+
+ mController.syncAllMedia();
+ mController.syncAllMedia();
+ mController.syncAllMedia();
+ assertWithMessage("Full sync should be complete now.")
+ .that(mController.isFullSyncPending(FLAKY_CLOUD_PROVIDER_AUTHORITY, false))
+ .isFalse();
+ }
+
+ @Test
+ public void testIsFullSyncPendingForStaleCMP() throws RequestObsoleteException {
+ mController.setCloudProvider(/* authority */ CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Full sync should be pending after setting the CMP.")
+ .that(mController.isFullSyncPending(CLOUD_PRIMARY_PROVIDER_AUTHORITY, false))
+ .isTrue();
+
+ mController.syncAllMedia();
+ assertWithMessage("Full sync should be completed after syncing with the CMP.")
+ .that(mController.isFullSyncPending(CLOUD_PRIMARY_PROVIDER_AUTHORITY, false))
+ .isFalse();
+
+ assertThrows(RequestObsoleteException.class,
+ () -> mController.isFullSyncPending(
+ CLOUD_SECONDARY_PROVIDER_AUTHORITY, false));
+ }
+
private static void addMedia(MediaGenerator generator, Pair<String, String> media) {
generator.addMedia(media.first, media.second);
}
diff --git a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
index 68438b176..45069df81 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
@@ -1459,7 +1459,7 @@ public class PickerDbFacadeTest {
}
// Assert invalid projection column
- final String invalidColumn = "testInvalidColumn";
+ final String invalidColumn = "test invalid column";
final String[] invalidProjection = new String[]{
PickerMediaColumns.DATE_TAKEN,
invalidColumn
@@ -1471,12 +1471,17 @@ public class PickerDbFacadeTest {
"Unexpected number of rows when asserting invalid projection column with "
+ "cloud provider.")
.that(cr.getCount()).isEqualTo(1);
+ assertWithMessage("Unexpected number of columns in cursor")
+ .that(cr.getColumnCount())
+ .isEqualTo(2);
cr.moveToFirst();
- assertWithMessage(
- "Unexpected value of the invalidColumn with cloud provider.")
+ assertWithMessage("Unexpected value of the invalidColumn with cloud provider.")
.that(cr.getLong(cr.getColumnIndexOrThrow(invalidColumn)))
.isEqualTo(0);
+ assertWithMessage("Unexpected value of the invalidColumn with cloud provider.")
+ .that(cr.getString(cr.getColumnIndexOrThrow(invalidColumn)))
+ .isEqualTo(null);
assertWithMessage(
"Unexpected value of PickerMediaColumns.DATE_TAKEN with cloud provider.")
.that(cr.getLong(cr.getColumnIndexOrThrow(PickerMediaColumns.DATE_TAKEN)))
diff --git a/tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java b/tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java
index f8ea7ca08..dda66c673 100644
--- a/tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java
@@ -19,14 +19,24 @@ package com.android.providers.media.photopicker.data;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import android.annotation.SuppressLint;
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.UserHandle;
import android.os.UserManager;
+import android.provider.MediaStore;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -44,29 +54,67 @@ import java.util.List;
public class UserManagerStateTest {
private final UserHandle mPersonalUser = UserHandle.of(UserHandle.myUserId());
private final UserHandle mManagedUser = UserHandle.of(100); // like a managed profile
+ private final UserHandle mManagedUser2 = UserHandle.of(150); // like a managed profile
private final UserHandle mOtherUser1 = UserHandle.of(101); // like a private profile
private final UserHandle mOtherUser2 = UserHandle.of(102); // like a clone profile
- private final UserHandle mOtherUser3 = UserHandle.of(103); // like a invalid user
+ private final UserHandle mOtherUser3 = UserHandle.of(103); // like a invalid user
private final Context mMockContext = mock(Context.class);
private final UserManager mMockUserManager = mock(UserManager.class);
private final PackageManager mMockPackageManager = mock(PackageManager.class);
private UserManagerState mUserManagerState;
+ private MockContentResolver mMockContentResolver;
+ private MockContentProvider mMockContentProvider;
+
+ /**
+ * Class that exposes the @hide api [targetUserId] in order to supply proper values for
+ * reflection based code that is inspecting this field.
+ */
+ private static class ReflectedResolveInfo extends ResolveInfo {
+
+ @SuppressLint({"HidingField", "UnusedVariable"})
+ public int targetUserId;
+
+ ReflectedResolveInfo(int targetUserId) {
+ this.targetUserId = targetUserId;
+ }
+
+ @Override
+ public boolean isCrossProfileIntentForwarderActivity() {
+ return true;
+ }
+ }
@Before
public void setUp() throws Exception {
+
+ // Setup mock context to always return itself for various transforms.
when(mMockContext.getApplicationContext()).thenReturn(mMockContext);
+ when(mMockContext.createPackageContextAsUser(any(), anyInt(), any(UserHandle.class)))
+ .thenReturn(mMockContext);
+ when(mMockContext.createContextAsUser(any(UserHandle.class), anyInt()))
+ .thenReturn(mMockContext);
+ when(mMockContext.getApplicationInfo())
+ .thenReturn(
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getApplicationInfo());
+
+ // Mock out MediaProvider since certain logic waits on a ContentProviderClient
+ // for mediaprovider to be available.
+ mMockContentProvider = new MockContentProvider(mMockContext);
+ mMockContentResolver = new MockContentResolver(mMockContext);
+ mMockContentResolver.addProvider(MediaStore.AUTHORITY, mMockContentProvider);
+
+ // Mock out other platform apis.
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ when(mMockContext.getContentResolver()).thenReturn(mMockContentResolver);
// set Managed Profile identification
- when(mMockUserManager.isManagedProfile(
- mManagedUser.getIdentifier())).thenReturn(true);
- when(mMockUserManager.isManagedProfile(
- mPersonalUser.getIdentifier())).thenReturn(false);
- when(mMockUserManager.isManagedProfile(
- mOtherUser1.getIdentifier())).thenReturn(false);
- when(mMockUserManager.isManagedProfile(
- mOtherUser2.getIdentifier())).thenReturn(false);
- when(mMockUserManager.isManagedProfile(
- mOtherUser3.getIdentifier())).thenReturn(false);
+ when(mMockUserManager.isManagedProfile(mManagedUser.getIdentifier())).thenReturn(true);
+ when(mMockUserManager.isManagedProfile(mPersonalUser.getIdentifier())).thenReturn(false);
+ when(mMockUserManager.isManagedProfile(mOtherUser1.getIdentifier())).thenReturn(false);
+ when(mMockUserManager.isManagedProfile(mOtherUser2.getIdentifier())).thenReturn(false);
+ when(mMockUserManager.isManagedProfile(mOtherUser3.getIdentifier())).thenReturn(false);
// set all user profile parents
when(mMockUserManager.getProfileParent(mPersonalUser)).thenReturn(null);
@@ -76,29 +124,37 @@ public class UserManagerStateTest {
when(mMockUserManager.getProfileParent(mOtherUser3)).thenReturn(mPersonalUser);
if (SdkLevel.isAtLeastV()) {
- //Personal user
+ // Personal user
UserProperties mPersonalUserProperties = new UserProperties.Builder().build();
- UserProperties mManagedUserProperties = new UserProperties.Builder() // managed user
- .setShowInSharingSurfaces(UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
- .setCrossProfileContentSharingStrategy(
- UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION)
- .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_PAUSED)
- .build();
-
- UserProperties mOtherUser1Properties = new UserProperties.Builder() // private user
- .setShowInSharingSurfaces(UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
- .setCrossProfileContentSharingStrategy(
- UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT)
- .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_HIDDEN)
- .build();
-
- UserProperties mOtherUser2Properties = new UserProperties.Builder() // clone user
- .setShowInSharingSurfaces(UserProperties.SHOW_IN_SHARING_SURFACES_WITH_PARENT)
- .setCrossProfileContentSharingStrategy(
- UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT)
- .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_DEFAULT)
- .build();
+ UserProperties mManagedUserProperties =
+ new UserProperties.Builder() // managed user
+ .setShowInSharingSurfaces(
+ UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION)
+ .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_PAUSED)
+ .build();
+
+ UserProperties mOtherUser1Properties =
+ new UserProperties.Builder() // private user
+ .setShowInSharingSurfaces(
+ UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
+ .setCrossProfileContentSharingStrategy(
+ UserProperties
+ .CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT)
+ .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_HIDDEN)
+ .build();
+
+ UserProperties mOtherUser2Properties =
+ new UserProperties.Builder() // clone user
+ .setShowInSharingSurfaces(
+ UserProperties.SHOW_IN_SHARING_SURFACES_WITH_PARENT)
+ .setCrossProfileContentSharingStrategy(
+ UserProperties
+ .CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT)
+ .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_DEFAULT)
+ .build();
// get user properties
when(mMockUserManager.getUserProperties(mPersonalUser))
@@ -109,86 +165,252 @@ public class UserManagerStateTest {
when(mMockUserManager.getUserProperties(mOtherUser2)).thenReturn(mOtherUser2Properties);
}
- when(mMockContext.getSystemServiceName(UserManager.class)).thenReturn(
- "mockUserManager");
+ when(mMockContext.getSystemServiceName(UserManager.class)).thenReturn("mockUserManager");
when(mMockContext.getSystemService(UserManager.class)).thenReturn(mMockUserManager);
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
}
@Test
+ public void testCrossProfileAccessWithMultipleManagedProfilesIsAllowedVPlus() {
+ assumeTrue(SdkLevel.isAtLeastV());
+
+ UserProperties mManagedUser2Properties =
+ new UserProperties.Builder() // managed user
+ .setShowInSharingSurfaces(UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION)
+ .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_PAUSED)
+ .build();
+ when(mMockUserManager.getProfileParent(mManagedUser2)).thenReturn(mPersonalUser);
+ when(mMockUserManager.isManagedProfile(mManagedUser2.getIdentifier())).thenReturn(true);
+ when(mMockUserManager.getUserProperties(mManagedUser2)).thenReturn(mManagedUser2Properties);
+
+ // Return a ResolveInfo for the correct managed profile.
+ when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt()))
+ .thenReturn(List.of(new ReflectedResolveInfo(mManagedUser2.getIdentifier())));
+
+ initializeUserManagerState(
+ UserId.of(mPersonalUser),
+ Arrays.asList(mPersonalUser, mManagedUser, mManagedUser2));
+
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mUserManagerState.setIntentAndCheckRestrictions(new Intent());
+ assertThat(
+ mUserManagerState.isCrossProfileAllowedToUser(
+ UserId.of(mManagedUser)))
+ .isFalse();
+ assertThat(
+ mUserManagerState.isCrossProfileAllowedToUser(
+ UserId.of(mManagedUser2)))
+ .isTrue();
+ });
+ }
+
+ @Test
+ public void testCrossProfileAccessWithMultipleManagedProfilesIsNotAllowedVPlus() {
+ assumeTrue(SdkLevel.isAtLeastV());
+
+ UserProperties mManagedUser2Properties =
+ new UserProperties.Builder() // managed user
+ .setShowInSharingSurfaces(UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION)
+ .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_PAUSED)
+ .build();
+ when(mMockUserManager.getProfileParent(mManagedUser2)).thenReturn(mPersonalUser);
+ when(mMockUserManager.isManagedProfile(mManagedUser2.getIdentifier())).thenReturn(true);
+ when(mMockUserManager.getUserProperties(mManagedUser2)).thenReturn(mManagedUser2Properties);
+
+ // Return a ResolveInfo for the OTHER managed profile.
+ when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt()))
+ .thenReturn(List.of(new ReflectedResolveInfo(mManagedUser.getIdentifier())));
+
+ initializeUserManagerState(
+ UserId.of(mPersonalUser),
+ Arrays.asList(mPersonalUser, mManagedUser, mManagedUser2));
+
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mUserManagerState.setIntentAndCheckRestrictions(new Intent());
+ assertThat(
+ mUserManagerState.isCrossProfileAllowedToUser(
+ UserId.of(mManagedUser)))
+ .isTrue();
+ assertThat(
+ mUserManagerState.isCrossProfileAllowedToUser(
+ UserId.of(mManagedUser2)))
+ .isFalse();
+ });
+ }
+
+ @Test
+ public void testCrossProfileAccessWithMultipleManagedProfilesIsAllowedUMinus() {
+ assumeFalse(SdkLevel.isAtLeastV());
+
+ when(mMockUserManager.getProfileParent(mManagedUser2)).thenReturn(mPersonalUser);
+ when(mMockUserManager.isManagedProfile(mManagedUser2.getIdentifier())).thenReturn(true);
+
+ // Return a ResolveInfo for the correct managed profile.
+ when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt()))
+ .thenReturn(List.of(new ReflectedResolveInfo(mManagedUser2.getIdentifier())));
+
+ initializeUserManagerState(
+ UserId.of(mPersonalUser),
+ Arrays.asList(mPersonalUser, mManagedUser, mManagedUser2));
+
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mUserManagerState.setIntentAndCheckRestrictions(new Intent());
+ assertThat(
+ mUserManagerState.isCrossProfileAllowedToUser(
+ UserId.of(mManagedUser2)))
+ .isTrue();
+ });
+ }
+
+ @Test
+ public void testCrossProfileAccessWithMultipleManagedProfilesIsNotAllowedUMinus() {
+ assumeFalse(SdkLevel.isAtLeastV());
+
+ when(mMockUserManager.getProfileParent(mManagedUser2)).thenReturn(mPersonalUser);
+ when(mMockUserManager.isManagedProfile(mManagedUser2.getIdentifier())).thenReturn(true);
+
+ // Return a ResolveInfo for the OTHER managed profile.
+ when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt()))
+ .thenReturn(List.of(new ReflectedResolveInfo(mManagedUser.getIdentifier())));
+
+ initializeUserManagerState(
+ UserId.of(mPersonalUser),
+ Arrays.asList(mPersonalUser, mManagedUser, mManagedUser2));
+
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mUserManagerState.setIntentAndCheckRestrictions(new Intent());
+ assertThat(
+ mUserManagerState.isCrossProfileAllowedToUser(
+ UserId.of(mManagedUser2)))
+ .isFalse();
+ });
+ }
+
+ @Test
public void testUserManagerStateThrowsErrorIfCalledFromNonMainThread() {
- initializeUserManagerState(UserId.of(mPersonalUser),
+ initializeUserManagerState(
+ UserId.of(mPersonalUser),
Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1, mOtherUser2));
assertThrows(IllegalStateException.class, () -> mUserManagerState.isMultiUserProfiles());
- assertThrows(IllegalStateException.class,
+ assertThrows(
+ IllegalStateException.class,
() -> mUserManagerState.isManagedUserProfile(UserId.of(mManagedUser)));
- assertThrows(IllegalStateException.class,
- () -> mUserManagerState.getCurrentUserProfileId());
- assertThrows(IllegalStateException.class,
+ assertThrows(
+ IllegalStateException.class, () -> mUserManagerState.getCurrentUserProfileId());
+ assertThrows(
+ IllegalStateException.class,
() -> mUserManagerState.getCrossProfileAllowedStatusForAll());
assertThrows(IllegalStateException.class, () -> mUserManagerState.getAllUserProfileIds());
- assertThrows(IllegalStateException.class,
+ assertThrows(
+ IllegalStateException.class,
() -> mUserManagerState.updateProfileOffValuesAndPostCrossProfileStatus());
- assertThrows(IllegalStateException.class,
- () -> mUserManagerState.waitForMediaProviderToBeAvailable(
- UserId.of(mPersonalUser)));
- assertThrows(IllegalStateException.class,
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ mUserManagerState.waitForMediaProviderToBeAvailable(
+ UserId.of(mPersonalUser)));
+ assertThrows(
+ IllegalStateException.class,
() -> mUserManagerState.isCrossProfileAllowedToUser(UserId.of(mManagedUser)));
assertThrows(IllegalStateException.class, () -> mUserManagerState.resetUserIds());
assertThrows(IllegalStateException.class, () -> mUserManagerState.isCurrentUserSelected());
- assertThrows(IllegalStateException.class,
+ assertThrows(
+ IllegalStateException.class,
() -> mUserManagerState.isBlockedByAdmin(UserId.of(mManagedUser)));
- assertThrows(IllegalStateException.class,
+ assertThrows(
+ IllegalStateException.class,
() -> mUserManagerState.isProfileOff(UserId.of(mManagedUser)));
- assertThrows(IllegalStateException.class,
+ assertThrows(
+ IllegalStateException.class,
() -> mUserManagerState.getShowInQuietMode(UserId.of(mOtherUser1)));
- assertThrows(IllegalStateException.class,
+ assertThrows(
+ IllegalStateException.class,
() -> mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mManagedUser)));
- assertThrows(IllegalStateException.class,
- () -> mUserManagerState.isUserSelectedAsCurrentUserProfile(
- UserId.of(mPersonalUser)));
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ mUserManagerState.isUserSelectedAsCurrentUserProfile(
+ UserId.of(mPersonalUser)));
}
@Test
public void testGetAllUserProfileIdsThatNeedToShowInPhotoPicker_currentUserIsPersonalUser() {
- initializeUserManagerState(UserId.of(mPersonalUser),
+ initializeUserManagerState(
+ UserId.of(mPersonalUser),
Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1, mOtherUser2));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-
- List<UserId> userIdList = SdkLevel.isAtLeastV() ? Arrays.asList(
- UserId.of(mPersonalUser), UserId.of(mManagedUser), UserId.of(mOtherUser1))
- : Arrays.asList(UserId.of(mPersonalUser), UserId.of(mManagedUser));
-
- assertThat(mUserManagerState.getAllUserProfileIds())
- .containsExactlyElementsIn(userIdList);
- });
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ List<UserId> userIdList =
+ SdkLevel.isAtLeastV()
+ ? Arrays.asList(
+ UserId.of(mPersonalUser),
+ UserId.of(mManagedUser),
+ UserId.of(mOtherUser1))
+ : Arrays.asList(
+ UserId.of(mPersonalUser),
+ UserId.of(mManagedUser));
+
+ assertThat(mUserManagerState.getAllUserProfileIds())
+ .containsExactlyElementsIn(userIdList);
+ });
}
@Test
public void testGetAllUserProfileIdsThatNeedToShowInPhotoPicker_currentUserIsManagedUser() {
- initializeUserManagerState(UserId.of(mManagedUser),
+ initializeUserManagerState(
+ UserId.of(mManagedUser),
Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1, mOtherUser2));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- List<UserId> userIdList = SdkLevel.isAtLeastV() ? Arrays.asList(
- UserId.of(mPersonalUser), UserId.of(mManagedUser), UserId.of(mOtherUser1))
- : Arrays.asList(UserId.of(mPersonalUser), UserId.of(mManagedUser));
-
- assertThat(mUserManagerState.getAllUserProfileIds())
- .containsExactlyElementsIn(userIdList);
- });
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ List<UserId> userIdList =
+ SdkLevel.isAtLeastV()
+ ? Arrays.asList(
+ UserId.of(mPersonalUser),
+ UserId.of(mManagedUser),
+ UserId.of(mOtherUser1))
+ : Arrays.asList(
+ UserId.of(mPersonalUser),
+ UserId.of(mManagedUser));
+
+ assertThat(mUserManagerState.getAllUserProfileIds())
+ .containsExactlyElementsIn(userIdList);
+ });
}
+
@Test
public void testGetAllUserProfileIdsThatNeedToShowInPhotoPicker_currentUserIsOtherUser1() {
- initializeUserManagerState(UserId.of(mOtherUser1),
+ initializeUserManagerState(
+ UserId.of(mOtherUser1),
Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1, mOtherUser2));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-
- List<UserId> userIdList = SdkLevel.isAtLeastV() ? Arrays.asList(
- UserId.of(mPersonalUser), UserId.of(mManagedUser), UserId.of(mOtherUser1))
- : Arrays.asList(UserId.of(mPersonalUser), UserId.of(mManagedUser));
- assertThat(mUserManagerState.getAllUserProfileIds())
- .containsExactlyElementsIn(userIdList);
- });
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ List<UserId> userIdList =
+ SdkLevel.isAtLeastV()
+ ? Arrays.asList(
+ UserId.of(mPersonalUser),
+ UserId.of(mManagedUser),
+ UserId.of(mOtherUser1))
+ : Arrays.asList(
+ UserId.of(mPersonalUser),
+ UserId.of(mManagedUser));
+ assertThat(mUserManagerState.getAllUserProfileIds())
+ .containsExactlyElementsIn(userIdList);
+ });
}
@Test
@@ -196,157 +418,204 @@ public class UserManagerStateTest {
// if current user is personal and no other profile is available
UserId currentUser = UserId.of(mPersonalUser);
initializeUserManagerState(currentUser, Arrays.asList(mPersonalUser));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- assertThat(mUserManagerState.isMultiUserProfiles()).isFalse();
-
- assertThat(mUserManagerState.getCurrentUserProfileId())
- .isEqualTo(UserId.of(mPersonalUser));
- assertThat(mUserManagerState.getAllUserProfileIds())
- .containsExactlyElementsIn(Arrays.asList(UserId.of(mPersonalUser)));
- });
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ assertThat(mUserManagerState.isMultiUserProfiles()).isFalse();
+
+ assertThat(mUserManagerState.getCurrentUserProfileId())
+ .isEqualTo(UserId.of(mPersonalUser));
+ assertThat(mUserManagerState.getAllUserProfileIds())
+ .containsExactlyElementsIn(
+ Arrays.asList(UserId.of(mPersonalUser)));
+ });
}
-
@Test
public void testUserIds_multiUserProfilesAvailable_currentUserIsPersonalUser() {
UserId currentUser = UserId.of(mPersonalUser);
// if available user profiles are {personal , managed, otherUser1 }
- initializeUserManagerState(currentUser,
- Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- assertThat(mUserManagerState.isMultiUserProfiles()).isTrue();
-
- assertThat(mUserManagerState.getCurrentUserProfileId()).isEqualTo(UserId.of(
- mPersonalUser));
-
- List<UserId> userIdList = SdkLevel.isAtLeastV() ? Arrays.asList(
- UserId.of(mPersonalUser), UserId.of(mManagedUser), UserId.of(mOtherUser1))
- : Arrays.asList(UserId.of(mPersonalUser), UserId.of(mManagedUser));
- assertThat(mUserManagerState.getAllUserProfileIds())
- .containsExactlyElementsIn(userIdList);
-
- assertThat(mUserManagerState.isManagedUserProfile(
- mUserManagerState.getCurrentUserProfileId())).isFalse();
- });
+ initializeUserManagerState(
+ currentUser, Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1));
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ assertThat(mUserManagerState.isMultiUserProfiles()).isTrue();
+
+ assertThat(mUserManagerState.getCurrentUserProfileId())
+ .isEqualTo(UserId.of(mPersonalUser));
+
+ List<UserId> userIdList =
+ SdkLevel.isAtLeastV()
+ ? Arrays.asList(
+ UserId.of(mPersonalUser),
+ UserId.of(mManagedUser),
+ UserId.of(mOtherUser1))
+ : Arrays.asList(
+ UserId.of(mPersonalUser),
+ UserId.of(mManagedUser));
+ assertThat(mUserManagerState.getAllUserProfileIds())
+ .containsExactlyElementsIn(userIdList);
+
+ assertThat(
+ mUserManagerState.isManagedUserProfile(
+ mUserManagerState.getCurrentUserProfileId()))
+ .isFalse();
+ });
// if available user profiles are {personal , otherUser1 }
initializeUserManagerState(currentUser, Arrays.asList(mPersonalUser, mOtherUser1));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-
- if (SdkLevel.isAtLeastV()) {
- assertThat(mUserManagerState.isMultiUserProfiles()).isTrue();
- } else {
- assertThat(mUserManagerState.isMultiUserProfiles()).isFalse();
- }
-
- List<UserId> userIdList = SdkLevel.isAtLeastV() ? Arrays.asList(
- UserId.of(mPersonalUser), UserId.of(mOtherUser1))
- : Arrays.asList(UserId.of(mPersonalUser));
- assertThat(mUserManagerState.getAllUserProfileIds())
- .containsExactlyElementsIn(userIdList);
- });
-
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ if (SdkLevel.isAtLeastV()) {
+ assertThat(mUserManagerState.isMultiUserProfiles()).isTrue();
+ } else {
+ assertThat(mUserManagerState.isMultiUserProfiles()).isFalse();
+ }
+
+ List<UserId> userIdList =
+ SdkLevel.isAtLeastV()
+ ? Arrays.asList(
+ UserId.of(mPersonalUser),
+ UserId.of(mOtherUser1))
+ : Arrays.asList(UserId.of(mPersonalUser));
+ assertThat(mUserManagerState.getAllUserProfileIds())
+ .containsExactlyElementsIn(userIdList);
+ });
// if available user profiles are {personal , otherUser2 }
initializeUserManagerState(currentUser, Arrays.asList(mPersonalUser, mOtherUser2));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- assertThat(mUserManagerState.isMultiUserProfiles()).isFalse();
-
- assertThat(mUserManagerState.getCurrentUserProfileId())
- .isEqualTo(UserId.of(mPersonalUser));
- assertThat(mUserManagerState.getAllUserProfileIds())
- .containsExactlyElementsIn(Arrays.asList(UserId.of(mPersonalUser)));
- });
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ assertThat(mUserManagerState.isMultiUserProfiles()).isFalse();
+
+ assertThat(mUserManagerState.getCurrentUserProfileId())
+ .isEqualTo(UserId.of(mPersonalUser));
+ assertThat(mUserManagerState.getAllUserProfileIds())
+ .containsExactlyElementsIn(
+ Arrays.asList(UserId.of(mPersonalUser)));
+ });
}
@Test
public void testUserIds_multiUserProfilesAvailable_currentUserIsOtherUser2() {
UserId currentUser = UserId.of(mOtherUser2);
- initializeUserManagerState(currentUser,
- Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1, mOtherUser2));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- assertThat(mUserManagerState.isMultiUserProfiles()).isTrue();
- assertThat(mUserManagerState.getCurrentUserProfileId())
- .isEqualTo(UserId.of(mOtherUser2));
-
- List<UserId> userIdList = SdkLevel.isAtLeastV() ? Arrays.asList(
- UserId.of(mPersonalUser), UserId.of(mManagedUser), UserId.of(mOtherUser1))
- : Arrays.asList(UserId.of(mPersonalUser), UserId.of(mManagedUser));
- assertThat(mUserManagerState.getAllUserProfileIds())
- .containsExactlyElementsIn(userIdList);
-
- });
+ initializeUserManagerState(
+ currentUser, Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1, mOtherUser2));
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ assertThat(mUserManagerState.isMultiUserProfiles()).isTrue();
+ assertThat(mUserManagerState.getCurrentUserProfileId())
+ .isEqualTo(UserId.of(mOtherUser2));
+
+ List<UserId> userIdList =
+ SdkLevel.isAtLeastV()
+ ? Arrays.asList(
+ UserId.of(mPersonalUser),
+ UserId.of(mManagedUser),
+ UserId.of(mOtherUser1))
+ : Arrays.asList(
+ UserId.of(mPersonalUser),
+ UserId.of(mManagedUser));
+ assertThat(mUserManagerState.getAllUserProfileIds())
+ .containsExactlyElementsIn(userIdList);
+ });
initializeUserManagerState(currentUser, Arrays.asList(mPersonalUser, mOtherUser2));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- assertThat(mUserManagerState.isMultiUserProfiles()).isFalse();
- assertThat(mUserManagerState.getAllUserProfileIds())
- .containsExactlyElementsIn(Arrays.asList(UserId.of(mPersonalUser)));
-
- });
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ assertThat(mUserManagerState.isMultiUserProfiles()).isFalse();
+ assertThat(mUserManagerState.getAllUserProfileIds())
+ .containsExactlyElementsIn(
+ Arrays.asList(UserId.of(mPersonalUser)));
+ });
}
@Test
public void testCurrentUser_AfterSettingASpecificUserAsCurrentUser() {
UserId currentUser = UserId.of(mPersonalUser);
- initializeUserManagerState(currentUser,
- Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1, mOtherUser2));
+ initializeUserManagerState(
+ currentUser, Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1, mOtherUser2));
// set current user as managed profile
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mManagedUser));
-
- assertThat(mUserManagerState.getCurrentUserProfileId())
- .isEqualTo(UserId.of(mManagedUser));
- assertThat(mUserManagerState.isManagedUserProfile(
- mUserManagerState.getCurrentUserProfileId())).isTrue();
- });
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mManagedUser));
+
+ assertThat(mUserManagerState.getCurrentUserProfileId())
+ .isEqualTo(UserId.of(mManagedUser));
+ assertThat(
+ mUserManagerState.isManagedUserProfile(
+ mUserManagerState.getCurrentUserProfileId()))
+ .isTrue();
+ });
// set current user as otherUser2
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mOtherUser2));
- assertThat(mUserManagerState.getCurrentUserProfileId())
- .isEqualTo(UserId.of(mManagedUser));
- });
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mOtherUser2));
+ assertThat(mUserManagerState.getCurrentUserProfileId())
+ .isEqualTo(UserId.of(mManagedUser));
+ });
// set current user as otherUser1
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mOtherUser1));
- UserHandle currentUserProfile = SdkLevel.isAtLeastV() ? mOtherUser1 : mManagedUser;
- assertThat(mUserManagerState.getCurrentUserProfileId())
- .isEqualTo(UserId.of(currentUserProfile));
- });
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mOtherUser1));
+ UserHandle currentUserProfile =
+ SdkLevel.isAtLeastV() ? mOtherUser1 : mManagedUser;
+ assertThat(mUserManagerState.getCurrentUserProfileId())
+ .isEqualTo(UserId.of(currentUserProfile));
+ });
// set current user as personalUser
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mPersonalUser));
- assertThat(mUserManagerState.getCurrentUserProfileId())
- .isEqualTo(UserId.of(mPersonalUser));
- });
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mPersonalUser));
+ assertThat(mUserManagerState.getCurrentUserProfileId())
+ .isEqualTo(UserId.of(mPersonalUser));
+ });
// set current user otherUser3
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mOtherUser3));
- assertThat(mUserManagerState.getCurrentUserProfileId())
- .isEqualTo(UserId.of(mPersonalUser));
-
- List<UserId> userIdList = SdkLevel.isAtLeastV() ? Arrays.asList(
- UserId.of(mPersonalUser), UserId.of(mManagedUser), UserId.of(mOtherUser1))
- : Arrays.asList(UserId.of(mPersonalUser), UserId.of(mManagedUser));
- assertThat(mUserManagerState.getAllUserProfileIds())
- .containsExactlyElementsIn(userIdList);
- });
-
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mOtherUser3));
+ assertThat(mUserManagerState.getCurrentUserProfileId())
+ .isEqualTo(UserId.of(mPersonalUser));
+
+ List<UserId> userIdList =
+ SdkLevel.isAtLeastV()
+ ? Arrays.asList(
+ UserId.of(mPersonalUser),
+ UserId.of(mManagedUser),
+ UserId.of(mOtherUser1))
+ : Arrays.asList(
+ UserId.of(mPersonalUser),
+ UserId.of(mManagedUser));
+ assertThat(mUserManagerState.getAllUserProfileIds())
+ .containsExactlyElementsIn(userIdList);
+ });
}
-
-
private void initializeUserManagerState(UserId current, List<UserHandle> usersOnDevice) {
when(mMockUserManager.getUserProfiles()).thenReturn(usersOnDevice);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- mUserManagerState = new UserManagerState.RuntimeUserManagerState(mMockContext, current);
- });
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mUserManagerState =
+ new UserManagerState.RuntimeUserManagerState(
+ mMockContext, current);
+ });
}
}
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 78ca6fc46..8ea42965e 100644
--- a/tests/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorkerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/sync/MediaInMediaSetsSyncWorkerTest.java
@@ -57,6 +57,7 @@ import com.android.providers.media.flags.Flags;
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
import com.android.providers.media.photopicker.v2.model.MediaSetsSyncRequestParams;
import com.android.providers.media.photopicker.v2.sqlite.MediaSetsDatabaseUtil;
import com.android.providers.media.photopicker.v2.sqlite.PickerSQLConstants;
@@ -182,7 +183,7 @@ public class MediaInMediaSetsSyncWorkerTest {
@Test
public void testMediaInMediaSetSyncWithCloudProvider() throws
- ExecutionException, InterruptedException {
+ ExecutionException, InterruptedException, RequestObsoleteException {
String categoryId = "categoryId";
String auth = String.valueOf(SYNC_CLOUD_ONLY);
@@ -301,7 +302,7 @@ public class MediaInMediaSetsSyncWorkerTest {
@Test
public void testMediaInMediaSetsSyncLocalProvider() throws
- ExecutionException, InterruptedException {
+ ExecutionException, InterruptedException, RequestObsoleteException {
doReturn(SearchProvider.AUTHORITY).when(mMockSyncController).getLocalProvider();
@@ -407,7 +408,7 @@ public class MediaInMediaSetsSyncWorkerTest {
@Test
@Ignore("Enable when b/391639613 is fixed")
public void testMediaSetContentsSyncLoop() throws
- ExecutionException, InterruptedException {
+ ExecutionException, InterruptedException, RequestObsoleteException {
String categoryId = "categoryId";
String auth = String.valueOf(SYNC_CLOUD_ONLY);
@@ -502,7 +503,7 @@ public class MediaInMediaSetsSyncWorkerTest {
@Test
public void testMediaInMediaSetSyncComplete() throws
- ExecutionException, InterruptedException {
+ ExecutionException, InterruptedException, RequestObsoleteException {
String categoryId = "categoryId";
String auth = String.valueOf(SYNC_CLOUD_ONLY);
diff --git a/tests/src/com/android/providers/media/photopicker/sync/MediaSetsResetWorkerTest.java b/tests/src/com/android/providers/media/photopicker/sync/MediaSetsResetWorkerTest.java
index 17678b08b..c318151d5 100644
--- a/tests/src/com/android/providers/media/photopicker/sync/MediaSetsResetWorkerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/sync/MediaSetsResetWorkerTest.java
@@ -17,6 +17,10 @@
package com.android.providers.media.photopicker.sync;
import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_AUTHORITY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_CATEGORY_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.initializeTestWorkManager;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_1;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_ID_2;
@@ -43,6 +47,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.provider.CloudMediaProviderContract;
import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
@@ -51,6 +56,7 @@ import com.android.providers.media.cloudproviders.SearchProvider;
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
import com.android.providers.media.photopicker.v2.sqlite.MediaInMediaSetsDatabaseUtil;
import com.android.providers.media.photopicker.v2.sqlite.MediaSetsDatabaseUtil;
import com.android.providers.media.photopicker.v2.sqlite.PickerSQLConstants;
@@ -64,6 +70,7 @@ import org.mockito.Mock;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.ExecutionException;
public class MediaSetsResetWorkerTest {
@@ -113,7 +120,7 @@ public class MediaSetsResetWorkerTest {
@Test
public void testMediaSetsAndMediaSetsContentCacheReset() throws
- ExecutionException, InterruptedException {
+ ExecutionException, InterruptedException, RequestObsoleteException {
Cursor c = getCursorForMediaSetInsertionTest();
List<String> mimeTypes = new ArrayList<>();
mimeTypes.add(mMimeType);
@@ -146,6 +153,10 @@ public class MediaSetsResetWorkerTest {
// Setup
final OneTimeWorkRequest request =
new OneTimeWorkRequest.Builder(MediaSetsResetWorker.class)
+ .setInputData(new Data(Map.of(
+ SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_CLOUD_ONLY,
+ SYNC_WORKER_INPUT_CATEGORY_ID, mCategoryId,
+ SYNC_WORKER_INPUT_AUTHORITY, mAuthority)))
.build();
final WorkManager workManager = WorkManager.getInstance(mContext);
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 60e43e934..59473d7e1 100644
--- a/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
@@ -800,6 +800,15 @@ public class PickerSyncManagerTest {
assertThat(resetRequest.getWorkSpec().isPeriodic()).isFalse();
assertThat(resetRequest.getWorkSpec().id).isNotNull();
assertThat(resetRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+ assertThat(resetRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_LOCAL_ONLY);
+ assertThat(resetRequest.getWorkSpec().input
+ .getString(SYNC_WORKER_INPUT_CATEGORY_ID))
+ .isEqualTo(categoryId);
+ assertThat(resetRequest.getWorkSpec().input
+ .getString(SYNC_WORKER_INPUT_AUTHORITY))
+ .isEqualTo(SearchProvider.AUTHORITY);
WorkRequest syncRequest = workRequestList.get(1).get(0);
assertThat(syncRequest.getWorkSpec().workerClassName)
@@ -860,6 +869,15 @@ public class PickerSyncManagerTest {
assertThat(resetRequest.getWorkSpec().isPeriodic()).isFalse();
assertThat(resetRequest.getWorkSpec().id).isNotNull();
assertThat(resetRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+ assertThat(resetRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_CLOUD_ONLY);
+ assertThat(resetRequest.getWorkSpec().input
+ .getString(SYNC_WORKER_INPUT_CATEGORY_ID))
+ .isEqualTo(categoryId);
+ assertThat(resetRequest.getWorkSpec().input
+ .getString(SYNC_WORKER_INPUT_AUTHORITY))
+ .isEqualTo(SearchProvider.AUTHORITY);
WorkRequest syncRequest = workRequestList.get(1).get(0);
assertThat(syncRequest.getWorkSpec().workerClassName)
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 83c5c1ada..d4d904648 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,7 @@ import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
import com.android.providers.media.photopicker.data.PickerDbFacade;
import com.android.providers.media.photopicker.data.model.UserId;
import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
+import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
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;
@@ -815,7 +816,7 @@ public class PickerDataLayerV2Test {
}
@Test
- public void testQueryMediaSets() {
+ public void testQueryMediaSets() throws RequestObsoleteException {
List<String> mimeTypes = new ArrayList<>();
mimeTypes.add("image/*");
String mediaSetId1 = "mediaSetId1";
@@ -1030,7 +1031,7 @@ public class PickerDataLayerV2Test {
}
@Test
- public void testQueryMediaInMediaSet() {
+ public void testQueryMediaInMediaSet() throws RequestObsoleteException {
final Cursor cursor1 = getLocalMediaCursor(LOCAL_ID_1, 0);
assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor1, 1);
final Cursor cursor2 = getLocalMediaCursor(LOCAL_ID_2, 0);
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 f132eb626..bdb7944d3 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
@@ -58,6 +58,7 @@ import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
import com.android.providers.media.photopicker.data.PickerDbFacade;
import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
+import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
import org.junit.After;
import org.junit.Before;
@@ -100,7 +101,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
}
@Test
- public void testQueryLocalMediaInMediaSet() {
+ public void testQueryLocalMediaInMediaSet() throws RequestObsoleteException {
final Cursor cursor1 = getLocalMediaCursor(LOCAL_ID_1, 0);
assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursor1, 1);
final Cursor cursor2 = getLocalMediaCursor(LOCAL_ID_2, 0);
@@ -158,7 +159,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
}
@Test
- public void testQueryCloudMediaInMediaSet() {
+ public void testQueryCloudMediaInMediaSet() throws RequestObsoleteException {
final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
@@ -212,7 +213,8 @@ public class MediaInMediaSetsDatabaseUtilTest {
}
@Test
- public void testQueryMediaInMediaSetForSpecificMediaSetPickerId() {
+ public void testQueryMediaInMediaSetForSpecificMediaSetPickerId()
+ throws RequestObsoleteException {
final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
@@ -261,7 +263,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
}
@Test
- public void testQueryMediaInMediaSetsSortOrder() {
+ public void testQueryMediaInMediaSetsSortOrder() throws RequestObsoleteException {
final long dateTaken = 0L;
final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, dateTaken + 1);
@@ -335,7 +337,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
}
@Test
- public void testQueryMediaInMediaSetsPagination() {
+ public void testQueryMediaInMediaSetsPagination() throws RequestObsoleteException {
final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
@@ -384,7 +386,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
}
@Test
- public void testQueryMediaInMediaSetsMimeTypeFilter() {
+ public void testQueryMediaInMediaSetsMimeTypeFilter() throws RequestObsoleteException {
final Cursor cursor1 = getMediaCursor(CLOUD_ID_1, DATE_TAKEN_MS, GENERATION_MODIFIED,
/* mediaStoreUri */ null, /* sizeBytes */ 1, MP4_VIDEO_MIME_TYPE,
STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
@@ -453,7 +455,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
}
@Test
- public void testQueryMediaInMediaSetsLocalProviderFilter() {
+ public void testQueryMediaInMediaSetsLocalProviderFilter() throws RequestObsoleteException {
final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
@@ -507,7 +509,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
}
@Test
- public void testQueryMediaInMediaSetsCloudProviderFilter() {
+ public void testQueryMediaInMediaSetsCloudProviderFilter() throws RequestObsoleteException {
final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_2, LOCAL_ID_2, 0);
@@ -574,7 +576,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
}
@Test
- public void testCacheMediaInMediaSet() {
+ public void testCacheMediaInMediaSet() throws RequestObsoleteException {
final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
final Cursor cursor2 = getLocalMediaCursor(LOCAL_ID_2, 0);
@@ -608,7 +610,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
}
@Test
- public void testClearMediaInMediaSetCache() {
+ public void testClearMediaInMediaSetCache() throws RequestObsoleteException {
// Insert data
final Cursor cursor1 = getCloudMediaCursor(CLOUD_ID_1, null, 0);
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor1, 1);
@@ -618,6 +620,7 @@ public class MediaInMediaSetsDatabaseUtilTest {
assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursor3, 1);
Long mediaSetPickerId = 1L;
+ Long secondMediaSetPickerId = 2L;
final long cloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
mDatabase, List.of(
@@ -630,10 +633,22 @@ public class MediaInMediaSetsDatabaseUtilTest {
.that(cloudRowsInsertedCount)
.isEqualTo(3);
+ final long secondCloudRowsInsertedCount = MediaInMediaSetsDatabaseUtil.cacheMediaOfMediaSet(
+ mDatabase, List.of(
+ getContentValues(null, CLOUD_ID_3, secondMediaSetPickerId),
+ getContentValues(LOCAL_ID_2, CLOUD_ID_2, secondMediaSetPickerId),
+ getContentValues(LOCAL_ID_1, CLOUD_ID_1, secondMediaSetPickerId)
+ ), CLOUD_PROVIDER);
+
+ assertWithMessage("Unexpected number of rows inserted in the search results table")
+ .that(secondCloudRowsInsertedCount)
+ .isEqualTo(3);
+
// Clear the data
- MediaInMediaSetsDatabaseUtil.clearMediaInMediaSetsCache(mDatabase);
+ MediaInMediaSetsDatabaseUtil.clearMediaInMediaSetsCache(
+ mDatabase, List.of(mediaSetPickerId.toString()));
- // Retrieved cursor should be empty
+ // Retrieved cursor for mediaSetPickerId should be empty
Bundle extras = new Bundle();
extras.putInt("page_size", 100);
extras.putStringArrayList("providers",
@@ -646,6 +661,19 @@ public class MediaInMediaSetsDatabaseUtilTest {
assertNotNull(mediaCursor);
assertEquals(/*expected*/0, /*actual*/ mediaCursor.getCount());
+ // Retrieved cursor for secondmediaSetPickerId should be non-empty
+ Bundle secondExtras = new Bundle();
+ secondExtras.putInt("page_size", 100);
+ secondExtras.putStringArrayList("providers",
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+ secondExtras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
+ MediaInMediaSetsQuery secondMediaInMediaSetQuery = new MediaInMediaSetsQuery(
+ secondExtras, secondMediaSetPickerId);
+ Cursor secondMediaCursor = MediaInMediaSetsDatabaseUtil.queryMediaInMediaSet(
+ mMockSyncController, secondMediaInMediaSetQuery, LOCAL_PROVIDER, CLOUD_PROVIDER);
+ assertNotNull(secondMediaCursor);
+ assertEquals(/*expected*/3, /*actual*/ secondMediaCursor.getCount());
+
}
private ContentValues getContentValues(
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 72ccb7795..432f76a32 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
@@ -33,6 +33,7 @@ import android.util.Pair;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
import com.android.providers.media.photopicker.v2.model.MediaSetsSyncRequestParams;
import org.junit.After;
@@ -71,7 +72,7 @@ public class MediaSetsDatabaseUtilsTest {
}
@Test
- public void testInsertMediaSetMetadataIntoMediaSetsTable() {
+ public void testInsertMediaSetMetadataIntoMediaSetsTable() throws RequestObsoleteException {
Cursor c = getCursorForMediaSetInsertionTest();
List<String> mimeTypes = new ArrayList<>();
mimeTypes.add(mMimeType);
@@ -83,14 +84,16 @@ public class MediaSetsDatabaseUtilsTest {
}
@Test
- public void testInsertMediaSetMetadataIntoMediaTableMimeTypeFilter() {
+ public void testInsertMediaSetMetadataIntoMediaTableMimeTypeFilter()
+ throws RequestObsoleteException {
Cursor c = getCursorForMediaSetInsertionTest();
List<String> firstMimeTypeFilter = new ArrayList<>();
firstMimeTypeFilter.add("image/*");
firstMimeTypeFilter.add("video/*");
int firstInsertionCount = MediaSetsDatabaseUtil.cacheMediaSets(
- mDatabase, c, mCategoryId, mAuthority, firstMimeTypeFilter);
+ mDatabase, c, mCategoryId, mAuthority, firstMimeTypeFilter
+ );
assertEquals("Count of inserted media sets should be equal to the cursor size",
/*expected*/ c.getCount(), /*actual*/ firstInsertionCount);
@@ -109,7 +112,7 @@ public class MediaSetsDatabaseUtilsTest {
}
@Test
- public void testInsertMediaSetMetadataWhenMediaSetIdIsNull() {
+ public void testInsertMediaSetMetadataWhenMediaSetIdIsNull() throws RequestObsoleteException {
List<String> mimeTypes = new ArrayList<>();
mimeTypes.add(mMimeType);
@@ -129,7 +132,7 @@ public class MediaSetsDatabaseUtilsTest {
}
@Test
- public void testGetMediaSetMetadataForCategory() {
+ public void testGetMediaSetMetadataForCategory() throws RequestObsoleteException {
Cursor c = getCursorForMediaSetInsertionTest();
List<String> mimeTypes = new ArrayList<>();
mimeTypes.add(mMimeType);
@@ -163,7 +166,7 @@ public class MediaSetsDatabaseUtilsTest {
}
@Test
- public void testUpdateAndGetMediaInMediaSetResumeKey() {
+ public void testUpdateAndGetMediaInMediaSetResumeKey() throws RequestObsoleteException {
Cursor c = getCursorForMediaSetInsertionTest();
List<String> mimeTypes = new ArrayList<>();
mimeTypes.add(mMimeType);
@@ -201,7 +204,8 @@ public class MediaSetsDatabaseUtilsTest {
}
@Test
- public void testGetMediaSetIdAndMimeTypesUsingMediaSetPickerId() {
+ public void testGetMediaSetIdAndMimeTypesUsingMediaSetPickerId()
+ throws RequestObsoleteException {
Cursor c = getCursorForMediaSetInsertionTest();
List<String> mimeTypes = new ArrayList<>();
mimeTypes.add(mMimeType);
@@ -234,6 +238,25 @@ public class MediaSetsDatabaseUtilsTest {
}
@Test
+ public void testGetMediaSetPickerIdsForCategoryId() {
+ Cursor c = getCursorForMediaSetInsertionTest();
+ List<String> mimeTypes = new ArrayList<>();
+ mimeTypes.add(mMimeType);
+
+ long mediaSetsInserted = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, mCategoryId, mAuthority, mimeTypes);
+ // Assert successful insertion
+ assertEquals("Count of inserted media sets should be equal to the cursor size",
+ /*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
+
+ List<String> mediaSetPickerIds = MediaSetsDatabaseUtil
+ .getMediaSetPickerIdsForGivenCategoryId(mDatabase, mCategoryId, mAuthority);
+ // Assert that the list has some sqlite generated ids
+ assertNotNull(mediaSetPickerIds);
+ assertTrue(!mediaSetPickerIds.isEmpty());
+ }
+
+ @Test
public void testClearMediaSetsCache() {
// Insert metadata into the table
Cursor c = getCursorForMediaSetInsertionTest();
@@ -245,10 +268,17 @@ public class MediaSetsDatabaseUtilsTest {
assertEquals("Count of inserted media sets should be equal to the cursor size",
/*expected*/ c.getCount(), /*actual*/ mediaSetsInserted);
+ String secondCategoryId = "secCategoryId";
+ int mediaSetsInserted2 = MediaSetsDatabaseUtil.cacheMediaSets(
+ mDatabase, c, secondCategoryId, mAuthority, mimeTypes);
+ assertEquals("Count of inserted media sets should be equal to the cursor size",
+ /*expected*/ c.getCount(), /*actual*/ mediaSetsInserted2);
+
+
// Delete the inserted items
- MediaSetsDatabaseUtil.clearMediaSetsCache(mDatabase);
+ MediaSetsDatabaseUtil.clearMediaSetsCache(mDatabase, mCategoryId, mAuthority);
- // Retrieved cursor should be empty
+ // Retrieved cursor should be empty for mCategoryId
Bundle extras = new Bundle();
extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, mAuthority);
extras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, mCategoryId);
@@ -261,6 +291,23 @@ public class MediaSetsDatabaseUtilsTest {
mDatabase, requestParams);
assertNotNull(mediaSetCursor);
assertEquals(/*expected*/ 0, /*actual*/ mediaSetCursor.getCount());
+
+ // Retrieved cursor should not be empty for secondCategoryId since only the media sets for
+ // mCategoryId have been deleted in the previous call
+ Bundle secondExtras = new Bundle();
+ secondExtras.putString(
+ MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_AUTHORITY, mAuthority);
+ secondExtras.putString(MediaSetsSyncRequestParams.KEY_PARENT_CATEGORY_ID, secondCategoryId);
+ secondExtras.putStringArrayList(
+ MediaSetsSyncRequestParams.KEY_MIME_TYPES,
+ new ArrayList<String>(mimeTypes));
+ MediaSetsSyncRequestParams secondRequestParams =
+ new MediaSetsSyncRequestParams(secondExtras);
+
+ Cursor secondMediaSetCursor = MediaSetsDatabaseUtil.getMediaSetsForCategory(
+ mDatabase, secondRequestParams);
+ assertNotNull(secondMediaSetCursor);
+ assertEquals(/*expected*/ 1, /*actual*/ secondMediaSetCursor.getCount());
}
private Cursor getCursorForMediaSetInsertionTest() {
diff --git a/tests/src/com/android/providers/media/photopickersearch/PickerSearchProviderClientTest.java b/tests/src/com/android/providers/media/photopickersearch/PickerSearchProviderClientTest.java
index 60da5d032..5e227385a 100644
--- a/tests/src/com/android/providers/media/photopickersearch/PickerSearchProviderClientTest.java
+++ b/tests/src/com/android/providers/media/photopickersearch/PickerSearchProviderClientTest.java
@@ -30,6 +30,7 @@ import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.database.Cursor;
+import android.os.OperationCanceledException;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -99,7 +100,7 @@ public class PickerSearchProviderClientTest {
}
@Test
- public void testFetchMediasInMediaSetFromCmp() {
+ public void testFetchMediasInMediaSetFromCmp() throws OperationCanceledException {
Cursor cursor = mPickerSearchProviderClient.fetchMediasInMediaSetFromCmp(TEST_MEDIA_SET_ID,
null, 100, CloudMediaProviderContract.SORT_ORDER_DESC_DATE_TAKEN,
null, null);
@@ -120,7 +121,7 @@ public class PickerSearchProviderClientTest {
}
@Test
- public void testFetchMediaSetsFromCmp() {
+ public void testFetchMediaSetsFromCmp() throws OperationCanceledException {
Cursor cursor = mPickerSearchProviderClient.fetchMediaSetsFromCmp(TEST_MEDIA_CATEGORY_ID,
null, 10, null, null);
cursor.moveToFirst();
diff --git a/tests/src/com/android/providers/media/util/MimeTypeFixHandlerTest.java b/tests/src/com/android/providers/media/util/MimeTypeFixHandlerTest.java
index de86f97d7..738d2008e 100644
--- a/tests/src/com/android/providers/media/util/MimeTypeFixHandlerTest.java
+++ b/tests/src/com/android/providers/media/util/MimeTypeFixHandlerTest.java
@@ -17,6 +17,7 @@
package com.android.providers.media.util;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -98,6 +99,35 @@ public class MimeTypeFixHandlerTest {
@Test
@EnableFlags(Flags.FLAG_ENABLE_MIME_TYPE_FIX_FOR_ANDROID_15)
+ public void testIsCorruptedMimeType() {
+ // jpeg present in mime.types mapping
+ assertFalse(MimeTypeFixHandler.isCorruptedMimeType("image/jpeg"));
+
+ // avif present in android.mime.types mapping
+ assertFalse(MimeTypeFixHandler.isCorruptedMimeType("image/avif"));
+
+ // dwg in corrupted mapping
+ assertTrue(MimeTypeFixHandler.isCorruptedMimeType("image/vnd.dwg"));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MIME_TYPE_FIX_FOR_ANDROID_15)
+ public void testGetExtFromMimeType() {
+ // jpeg present in mime.types mapping
+ Optional<String> jpegExtension = MimeTypeFixHandler.getExtFromMimeType("image/jpeg");
+ assertTrue(jpegExtension.isPresent());
+
+ // avif present in android.mime.types mapping
+ Optional<String> avifExtension = MimeTypeFixHandler.getExtFromMimeType("image/avif");
+ assertTrue(avifExtension.isPresent());
+
+ // dwg in corrupted mapping
+ Optional<String> dwgExtension = MimeTypeFixHandler.getExtFromMimeType("image/vnd.dwg");
+ assertFalse(dwgExtension.isPresent());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MIME_TYPE_FIX_FOR_ANDROID_15)
public void testUpdateUnsupportedMimeTypesForWrongEntries() {
createEntriesInFilesTable();
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
index eafc383a4..f58f1bbf6 100644
--- a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -162,7 +162,7 @@ public class PermissionUtilsTest {
final Context context = getContext();
final int uid = android.os.Process.myUid();
final String packageName = context.getPackageName();
- assertThat(checkNoIsolatedStorageGranted(context, uid, packageName, null)).isFalse();
+ assertThat(checkNoIsolatedStorageGranted(context, uid, packageName)).isFalse();
}
@Test
@@ -177,7 +177,7 @@ public class PermissionUtilsTest {
assertThat(checkPermissionShell(testAppUid)).isFalse();
assertThat(
checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
- null, /* isTargetSdkAtLeastS */ false)).isFalse();
+ /* isTargetSdkAtLeastS */ false)).isFalse();
assertThat(
checkPermissionInstallPackages(getContext(), TEST_APP_PID, testAppUid,
packageName, null)).isFalse();
@@ -212,7 +212,7 @@ public class PermissionUtilsTest {
assertThat(checkPermissionSelf(getContext(), TEST_APP_PID, testAppUid)).isFalse();
assertThat(checkPermissionShell(testAppUid)).isFalse();
assertThat(checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
- null, /* isTargetSdkAtLeastV */ false)).isFalse();
+ /* isTargetSdkAtLeastV */ false)).isFalse();
assertThat(checkPermissionInstallPackages(
getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
assertThat(checkPermissionAccessMtp(
@@ -242,7 +242,7 @@ public class PermissionUtilsTest {
assertThat(checkPermissionSelf(getContext(), TEST_APP_PID, testAppUid)).isFalse();
assertThat(checkPermissionShell(testAppUid)).isFalse();
assertThat(checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
- null, /* isTargetSdkAtLeastV */ true)).isFalse();
+ /* isTargetSdkAtLeastV */ true)).isFalse();
assertThat(checkPermissionInstallPackages(
getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
assertThat(checkPermissionAccessMtp(
@@ -279,7 +279,7 @@ public class PermissionUtilsTest {
assertThat(
checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
- null, /* isTargetSdkAtLeastS */ false)).isFalse();
+ /* isTargetSdkAtLeastS */ false)).isFalse();
assertThat(
checkPermissionInstallPackages(getContext(), TEST_APP_PID, testAppUid,
packageName, null)).isFalse();
@@ -328,7 +328,7 @@ public class PermissionUtilsTest {
assertThat(
checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
- null, /* isTargetSdkAtLeastS */ false)).isTrue();
+ /* isTargetSdkAtLeastS */ false)).isTrue();
assertThat(
checkPermissionInstallPackages(getContext(), TEST_APP_PID, testAppUid,
packageName, null)).isFalse();
@@ -429,18 +429,15 @@ public class PermissionUtilsTest {
try {
assertThat(
- checkNoIsolatedStorageGranted(getContext(), testAppUid, packageName,
- null)).isFalse();
+ checkNoIsolatedStorageGranted(getContext(), testAppUid, packageName)).isFalse();
modifyAppOp(testAppUid, OPSTR_NO_ISOLATED_STORAGE, AppOpsManager.MODE_ALLOWED);
assertThat(
- checkNoIsolatedStorageGranted(getContext(), testAppUid, packageName,
- null)).isTrue();
+ checkNoIsolatedStorageGranted(getContext(), testAppUid, packageName)).isTrue();
modifyAppOp(testAppUid, OPSTR_NO_ISOLATED_STORAGE, AppOpsManager.MODE_ERRORED);
assertThat(
- checkNoIsolatedStorageGranted(getContext(), testAppUid, packageName,
- null)).isFalse();
+ checkNoIsolatedStorageGranted(getContext(), testAppUid, packageName)).isFalse();
} finally {
dropShellPermission();
}
@@ -689,7 +686,8 @@ public class PermissionUtilsTest {
static private void checkPermissionsForGallery(int uid, int pid, String packageName,
boolean expected) {
assertEquals(expected,
- checkWriteImagesOrVideoAppOps(getContext(), uid, packageName, null));
+ checkWriteImagesOrVideoAppOps(getContext(), uid, packageName, null,
+ /* forDataDelivery */ true));
assertEquals(expected,
checkPermissionWriteImages(getContext(), pid, uid, packageName, null,
/* forDataDelivery */ true));