Merge "Block and flag unsupported media art, flag non vector buttons." into pi-car-dev
diff --git a/car-apps-common/res/values-port/values.xml b/car-apps-common/res/values-port/values.xml
index 7476cb9..5ce54da 100644
--- a/car-apps-common/res/values-port/values.xml
+++ b/car-apps-common/res/values-port/values.xml
@@ -17,7 +17,4 @@
 
 <resources>
     <bool name="car_tab_flexible_layout">true</bool>
-
-    <!-- Scale factor for the background image -->
-    <dimen name="background_image_scale" format="float">1.5</dimen>
 </resources>
\ No newline at end of file
diff --git a/car-apps-common/res/values/dimens.xml b/car-apps-common/res/values/dimens.xml
index 1b616bf..b2993e3 100644
--- a/car-apps-common/res/values/dimens.xml
+++ b/car-apps-common/res/values/dimens.xml
@@ -70,11 +70,10 @@
     <item name="scroller_deceleration_time_divisor" format="float" type="dimen">0.45</item>
     <item name="scroller_interpolator_factor" format="float" type="dimen">1.8</item>
 
-    <!-- Scale factor for the background image -->
-    <dimen name="background_image_scale" format="float">1</dimen>
-    <!-- Blurring radius used to blur background images -->
-    <item name="background_image_blur_radius" format="float" type="dimen">25</item>
-    <!-- Scale to apply to images before blurring them to create the playback background -->
-    <item name="background_image_blur_scale" format="float" type="dimen">.5</item>
+    <!-- Target size for the background bitmap to blur (in pixels) -->
+    <integer name="background_bitmap_target_size_px">64</integer>
+    <!-- Value used to blur background images, the blur radius is P * (W + H)/2.
+        The computed blur radius is capped at 25 pixels. -->
+    <item name="background_bitmap_blur_percent" format="float" type="dimen">0.05</item>
 
 </resources>
diff --git a/car-apps-common/src/com/android/car/apps/common/BackgroundImageView.java b/car-apps-common/src/com/android/car/apps/common/BackgroundImageView.java
index 7cac306..935034c 100644
--- a/car-apps-common/src/com/android/car/apps/common/BackgroundImageView.java
+++ b/car-apps-common/src/com/android/car/apps/common/BackgroundImageView.java
@@ -32,13 +32,9 @@
     private CrossfadeImageView mImageView;
 
     /** Configuration (controlled from resources) */
-    private float mBackgroundBlurRadius;
-    private float mBackgroundBlurScale;
+    private int mBitmapTargetSize;
+    private float mBitmapBlurPercent;
 
-    private float mBackgroundScale = 0;
-    private int mBackgroundImageSize = 0;
-
-    private Bitmap mBitmap;
     private View mDarkeningScrim;
 
     public BackgroundImageView(Context context) {
@@ -57,20 +53,8 @@
         mImageView = findViewById(R.id.background_image_image);
         mDarkeningScrim = findViewById(R.id.background_image_darkening_scrim);
 
-        mBackgroundScale = getResources().getFloat(R.dimen.background_image_scale);
-
-        mBackgroundBlurRadius = getResources().getFloat(R.dimen.background_image_blur_radius);
-        mBackgroundBlurScale = getResources().getFloat(R.dimen.background_image_blur_scale);
-
-        addOnLayoutChangeListener(
-                (view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
-                    int newBackgroundImageSize = Math.round(getHeight() * mBackgroundScale);
-
-                    if (newBackgroundImageSize != mBackgroundImageSize) {
-                        mBackgroundImageSize = newBackgroundImageSize;
-                        setBackgroundImage(mBitmap, false);
-                    }
-                });
+        mBitmapTargetSize = getResources().getInteger(R.integer.background_bitmap_target_size_px);
+        mBitmapBlurPercent = getResources().getFloat(R.dimen.background_bitmap_blur_percent);
     }
 
     /**
@@ -79,61 +63,29 @@
      * @param showAnimation Whether or not to cross fade to the new image
      */
     public void setBackgroundImage(@Nullable Bitmap bitmap, boolean showAnimation) {
-        // Save the bitmap so that we can reset it when we resize
-        mBitmap = bitmap;
-
         if (bitmap == null) {
             mImageView.setImageBitmap(null, false);
             return;
         }
 
-        if (mBackgroundImageSize == 0) {
-            // We're not set up yet, wait for the OnLayoutChangeListener to set the correct size
-            return;
-        }
-
-        // STOPSHIP(b130576879) Rework this to not be so wasteful
-        // We need to scale it to a reasonable size, because if the image was small
-        // our blur radius would be way to large, comparably.
-        bitmap = Bitmap.createScaledBitmap(bitmap,
-                mBackgroundImageSize,
-                mBackgroundImageSize,
-                false);
-
-        if (bitmap != null) {
-            bitmap = ImageUtils.blur(getContext(), bitmap, mBackgroundBlurScale,
-                    mBackgroundBlurRadius);
-        }
-
-        if (bitmap == null) {
-            showAnimation = false;
-        }
-
+        bitmap = ImageUtils.blur(getContext(), bitmap, mBitmapTargetSize, mBitmapBlurPercent);
         mImageView.setImageBitmap(bitmap, showAnimation);
 
         invalidate();
         requestLayout();
     }
 
-    /**
-     * Sets the image to display to a bitmap
-     * @param bitmap The image to show. It will be scaled to the correct size and blurred.
-     */
-    public void setBackgroundImage(@Nullable Bitmap bitmap) {
-        setBackgroundImage(bitmap, true);
-    }
-
     /** Sets the background to a color */
     public void setBackgroundColor(int color) {
         mImageView.setBackgroundColor(color);
     }
 
     /**
-     * Gets the desired size for an image to pass to {@link #setBackgroundImage(Bitmap, boolean)}
-     * If the image doesn't match this size, it will be scaled to it.
+     * Gets the desired size for an image to pass to {@link #setBackgroundImage}. That size is
+     * defined by R.integer.background_bitmap_target_size_px.
      */
     public int getDesiredBackgroundSize() {
-        return mBackgroundImageSize;
+        return mBitmapTargetSize;
     }
 
     /** Dims/undims the background image by 30% */
diff --git a/car-apps-common/src/com/android/car/apps/common/ImageUtils.java b/car-apps-common/src/com/android/car/apps/common/ImageUtils.java
index deaedfa..0fae806 100644
--- a/car-apps-common/src/com/android/car/apps/common/ImageUtils.java
+++ b/car-apps-common/src/com/android/car/apps/common/ImageUtils.java
@@ -28,31 +28,54 @@
  * Utility methods to manipulate images.
  */
 public class ImageUtils {
+
+    private static final float MIN_BLUR = 0.1f;
+    private static final float MAX_BLUR = 25f;
+
     /**
      * Blurs the given image by scaling it down by the given factor and applying the given
      * blurring radius.
      */
     @NonNull
-    public static Bitmap blur(Context context, @NonNull Bitmap image, float scale, float radius) {
-        int width = Math.round(image.getWidth() * scale);
-        int height = Math.round(image.getHeight() * scale);
+    public static Bitmap blur(Context context, @NonNull Bitmap image, int bitmapTargetSize,
+            float bitmapBlurPercent) {
+        image = maybeResize(image, bitmapTargetSize);
+        float blurRadius = bitmapBlurPercent * getBitmapDimension(image);
+
+        if (blurRadius <= MIN_BLUR) return image;
+        if (blurRadius > MAX_BLUR) blurRadius = MAX_BLUR;
 
         if (image.getConfig() != Bitmap.Config.ARGB_8888) {
             image = image.copy(Bitmap.Config.ARGB_8888, true);
         }
 
-        Bitmap inputBitmap = Bitmap.createScaledBitmap(image, width, height, false);
-        Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap);
+        Bitmap outputBitmap = Bitmap.createBitmap(image);
 
         RenderScript rs = RenderScript.create(context);
         ScriptIntrinsicBlur theIntrinsic = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
-        Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap);
+        Allocation tmpIn = Allocation.createFromBitmap(rs, image);
         Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap);
-        theIntrinsic.setRadius(radius);
+        theIntrinsic.setRadius(blurRadius);
         theIntrinsic.setInput(tmpIn);
         theIntrinsic.forEach(tmpOut);
         tmpOut.copyTo(outputBitmap);
 
         return outputBitmap;
     }
+
+    private static Bitmap maybeResize(@NonNull Bitmap image, int bitmapTargetSize) {
+        int imageDim = getBitmapDimension(image);
+        if (imageDim > bitmapTargetSize) {
+            float scale = bitmapTargetSize / (float) imageDim;
+            int width = Math.round(scale * image.getWidth());
+            int height = Math.round(scale * image.getHeight());
+            return Bitmap.createScaledBitmap(image, width, height, false);
+        } else {
+            return image;
+        }
+    }
+
+    private static int getBitmapDimension(@NonNull Bitmap image) {
+        return (image.getWidth() + image.getHeight()) / 2;
+    }
 }
diff --git a/car-apps-common/src/com/android/car/apps/common/util/ScrollBarUI.java b/car-apps-common/src/com/android/car/apps/common/util/ScrollBarUI.java
index b4a5fa1..7098a82 100644
--- a/car-apps-common/src/com/android/car/apps/common/util/ScrollBarUI.java
+++ b/car-apps-common/src/com/android/car/apps/common/util/ScrollBarUI.java
@@ -47,4 +47,9 @@
      * affect the size of the scrollbar view.
      */
     public abstract void requestLayout();
+
+    /**
+     * Sets the padding of the scrollbar, relative to the padding of the RecyclerView.
+     */
+    public abstract void setPadding(int padddingStart, int paddingEnd);
 }
diff --git a/car-apps-common/src/com/android/car/apps/common/widget/CarScrollBar.java b/car-apps-common/src/com/android/car/apps/common/widget/CarScrollBar.java
index 6dfb62f..2de23a3 100644
--- a/car-apps-common/src/com/android/car/apps/common/widget/CarScrollBar.java
+++ b/car-apps-common/src/com/android/car/apps/common/widget/CarScrollBar.java
@@ -62,6 +62,9 @@
     private int mSeparatingMargin;
     private int mScrollBarThumbWidth;
 
+    private int mPaddingStart;
+    private int mPaddingEnd;
+
     /** The amount of space that the scroll thumb is allowed to roam over. */
     private int mScrollThumbTrackHeight;
 
@@ -138,10 +141,9 @@
 
             OrientationHelper orientationHelper =
                     getOrientationHelper(getRecyclerView().getLayoutManager());
-            int height = orientationHelper.getTotalSpace();
 
             // This value will keep track of the top of the current view being laid out.
-            int layoutTop = orientationHelper.getStartAfterPadding();
+            int layoutTop = orientationHelper.getStartAfterPadding() + mPaddingStart;
 
             // Lay out the up button at the top of the view.
             layoutViewCenteredFromTop(mUpButton, layoutTop, width);
@@ -152,7 +154,7 @@
             layoutViewCenteredFromTop(mScrollThumb, layoutTop, width);
 
             // Lay out the bottom button at the bottom of the view.
-            int downBottom = height + orientationHelper.getStartAfterPadding();
+            int downBottom = orientationHelper.getEndAfterPadding() - mPaddingEnd;
             layoutViewCenteredFromBottom(mDownButton, downBottom, width);
 
             mHandler.post(this::calculateScrollThumbTrackHeight);
@@ -165,6 +167,13 @@
         mScrollView.requestLayout();
     }
 
+    @Override
+    public void setPadding(int paddingStart, int paddingEnd) {
+        mPaddingStart = paddingStart;
+        mPaddingEnd = paddingEnd;
+        requestLayout();
+    }
+
     /**
      * Sets the listener that will be notified when the up and down buttons have been pressed.
      *
diff --git a/car-apps-common/src/com/android/car/apps/common/widget/PagedRecyclerView.java b/car-apps-common/src/com/android/car/apps/common/widget/PagedRecyclerView.java
index 70826a6..a8ebdba 100644
--- a/car-apps-common/src/com/android/car/apps/common/widget/PagedRecyclerView.java
+++ b/car-apps-common/src/com/android/car/apps/common/widget/PagedRecyclerView.java
@@ -23,11 +23,13 @@
 import android.content.res.TypedArray;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.View;
 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
@@ -49,7 +51,7 @@
 
     private Context mContext;
 
-    private final CarUxRestrictionsUtil mCarUxRestrictionsUtil;
+    private CarUxRestrictionsUtil mCarUxRestrictionsUtil;
     private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mListener;
 
     private boolean mScrollBarEnabled;
@@ -57,6 +59,8 @@
     private @ScrollBarPosition int mScrollBarPosition;
     private boolean mScrollBarAboveRecyclerView;
     private String mScrollBarClass;
+    private int mScrollBarPaddingStart;
+    private int mScrollBarPaddingEnd;
 
     @Gutter
     private int mGutter;
@@ -191,7 +195,11 @@
     public PagedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
 
-        mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(context);
+        try {
+            mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(context);
+        } catch (NullPointerException e) {
+            // Do nothing, mCarUxRestrictionsUtil will be null
+        }
         mListener = this::updateCarUxRestrictions;
 
         init(context, attrs, defStyle);
@@ -236,7 +244,7 @@
                 R.styleable.PagedRecyclerView_scrollBarContainerWidth, carMargin);
 
         mScrollBarPosition = a.getInt(R.styleable.PagedRecyclerView_scrollBarPosition,
-                    ScrollBarPosition.START);
+                ScrollBarPosition.START);
 
         mScrollBarAboveRecyclerView = a.getBoolean(
                 R.styleable.PagedRecyclerView_scrollBarAboveRecyclerView, /* defValue= */true);
@@ -263,13 +271,17 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        mCarUxRestrictionsUtil.register(mListener);
+        if (mCarUxRestrictionsUtil != null) {
+            mCarUxRestrictionsUtil.register(mListener);
+        }
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-        mCarUxRestrictionsUtil.unregister(mListener);
+        if (mCarUxRestrictionsUtil != null) {
+            mCarUxRestrictionsUtil.unregister(mListener);
+        }
     }
 
     private void updateCarUxRestrictions(CarUxRestrictions carUxRestrictions) {
@@ -395,6 +407,43 @@
     }
 
     @Override
+    public ViewHolder findViewHolderForLayoutPosition(int position) {
+        if (mScrollBarEnabled) {
+            return mNestedRecyclerView.findViewHolderForLayoutPosition(position);
+        } else {
+            return super.findViewHolderForLayoutPosition(position);
+        }
+    }
+
+    @Override
+    public ViewHolder findContainingViewHolder(View view) {
+        if (mScrollBarEnabled) {
+            return mNestedRecyclerView.findContainingViewHolder(view);
+        } else {
+            return super.findContainingViewHolder(view);
+        }
+    }
+
+    @Override
+    @Nullable
+    public View findChildViewUnder(float x, float y) {
+        if (mScrollBarEnabled) {
+            return mNestedRecyclerView.findChildViewUnder(x, y);
+        } else {
+            return super.findChildViewUnder(x, y);
+        }
+    }
+
+    /**
+     * Calls {@link #layout(int, int, int, int)} for both this RecyclerView and the nested one.
+     */
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    public void layoutBothForTesting(int l, int t, int r, int b) {
+        super.layout(l, t, r, b);
+        mNestedRecyclerView.layout(l, t, r, b);
+    }
+
+    @Override
     public int getPaddingStart() {
         return mScrollBarEnabled ? mNestedRecyclerView.getPaddingStart() : super.getPaddingStart();
     }
@@ -457,10 +506,27 @@
         mScrollBarUI.initialize(mContext, mNestedRecyclerView, mScrollBarContainerWidth,
                 mScrollBarPosition, mScrollBarAboveRecyclerView);
 
+        mScrollBarUI.setPadding(mScrollBarPaddingStart, mScrollBarPaddingEnd);
+
         if (DEBUG) Log.d(TAG, "started " + mScrollBarUI.getClass().getSimpleName());
     }
 
     /**
+     * Sets the scrollbar's padding start (top) and end (bottom).
+     * This padding is applied in addition to the padding of the inner RecyclerView.
+     */
+    public void setScrollBarPadding(int paddingStart, int paddingEnd) {
+        if (mScrollBarEnabled) {
+            mScrollBarPaddingStart = paddingStart;
+            mScrollBarPaddingEnd = paddingEnd;
+
+            if (mScrollBarUI != null) {
+                mScrollBarUI.setPadding(paddingStart, paddingEnd);
+            }
+        }
+    }
+
+    /**
      * Set the nested view's layout to the specified value.
      *
      * <p>The gutter is the space to the start/end of the list view items and will be equal in size
diff --git a/car-media-common/res/layout/fragment_app_selection.xml b/car-media-common/res/layout/fragment_app_selection.xml
index dc5087c..05f1dcd 100644
--- a/car-media-common/res/layout/fragment_app_selection.xml
+++ b/car-media-common/res/layout/fragment_app_selection.xml
@@ -49,7 +49,7 @@
         android:background="@drawable/appbar_view_icon_background"
         android:gravity="center" />
 
-    <androidx.recyclerview.widget.RecyclerView
+    <com.android.car.apps.common.widget.PagedRecyclerView
         android:id="@+id/apps_grid"
         android:layout_width="match_parent"
         android:layout_height="match_parent"