diff options
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 Binary files differindex 81215acb2..46c6ac08f 100644 --- a/pdf/framework/libs/pdfClient/testdata/page_object.pdf +++ b/pdf/framework/libs/pdfClient/testdata/page_object.pdf 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)); |