Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 298526e8 authored by Pierre Barbier de Reuille's avatar Pierre Barbier de Reuille
Browse files

Enforce rounded corners on App Widgets.

The feature is controlled by the ENABLE_ENFORCED_ROUNDED_CORNERS flag
(currently set to false).

If does not yet handle P/H flags to control its behavior.

Bug: 183097166
Test: Manual tests with top 1P App Widgets (See bug for results)
Change-Id: I56fca1b717f37ad518588115409f2144a71d4b98
parent b4002575
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -277,4 +277,7 @@
<!-- Taskbar related (placeholders to compile in Launcher3 without Quickstep) -->
    <dimen name="taskbar_size">0dp</dimen>

    <!-- Size of the maximum radius for the enforced rounded rectangles. -->
    <dimen name="enforced_rounded_corner_max_radius">16dp</dimen>

</resources>
+3 −0
Original line number Diff line number Diff line
@@ -215,6 +215,9 @@ public final class FeatureFlags {
    public static final BooleanFlag ENABLE_SPLIT_SELECT = getDebugFlag(
            "ENABLE_SPLIT_SELECT", false, "Uses new split screen selection overview UI");

    public static final BooleanFlag ENABLE_ENFORCED_ROUNDED_CORNERS = new DeviceFlag(
            "ENABLE_ENFORCED_ROUNDED_CORNERS", true, "Enforce rounded corners on all App Widgets");

    public static void initialize(Context context) {
        synchronized (sDebugFlags) {
            for (DebugFlag flag : sDebugFlags) {
+20 −0
Original line number Diff line number Diff line
@@ -17,8 +17,12 @@ package com.android.launcher3.dragndrop;

import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;

import com.android.launcher3.widget.LauncherAppWidgetHostView;
@@ -28,14 +32,30 @@ public final class AppWidgetHostViewDrawable extends Drawable {

    private final LauncherAppWidgetHostView mAppWidgetHostView;
    private Paint mPaint = new Paint();
    private final Path mClipPath;

    public AppWidgetHostViewDrawable(LauncherAppWidgetHostView appWidgetHostView) {
        mAppWidgetHostView = appWidgetHostView;
        Path clipPath = null;
        if (appWidgetHostView.getClipToOutline()) {
            Outline outline = new Outline();
            mAppWidgetHostView.getOutlineProvider().getOutline(mAppWidgetHostView, outline);
            Rect rect = new Rect();
            if (outline.getRect(rect)) {
                float radius = outline.getRadius();
                clipPath = new Path();
                clipPath.addRoundRect(new RectF(rect), radius, radius, Path.Direction.CCW);
            }
        }
        mClipPath = clipPath;
    }

    @Override
    public void draw(Canvas canvas) {
        int saveCount = canvas.saveLayer(0, 0, getIntrinsicWidth(), getIntrinsicHeight(), mPaint);
        if (mClipPath != null) {
            canvas.clipPath(mClipPath);
        }
        mAppWidgetHostView.draw(canvas);
        canvas.restoreToCount(saveCount);
    }
+54 −1
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
@@ -32,12 +33,14 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.AdapterView;
import android.widget.Advanceable;
import android.widget.RemoteViews;

import androidx.annotation.Nullable;
import androidx.annotation.UiThread;

import com.android.launcher3.CheckLongPressHelper;
import com.android.launcher3.Launcher;
@@ -95,6 +98,18 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
    private final Rect mWidgetSizeAtDrag = new Rect();
    private final RectF mTempRectF = new RectF();
    private final boolean mIsRtl;
    private final Rect mEnforcedRectangle = new Rect();
    private final float mEnforcedCornerRadius;
    private final ViewOutlineProvider mCornerRadiusEnforcementOutline = new ViewOutlineProvider() {
        @Override
        public void getOutline(View view, Outline outline) {
            if (mEnforcedRectangle.isEmpty() || mEnforcedCornerRadius <= 0) {
                outline.setEmpty();
            } else {
                outline.setRoundRect(mEnforcedRectangle, mEnforcedCornerRadius);
            }
        }
    };

    public LauncherAppWidgetHostView(Context context) {
        super(context);
@@ -112,6 +127,8 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
        mIsRtl = Utilities.isRtl(context.getResources());
        mColorExtractor = LocalColorExtractor.newInstance(getContext());
        mColorExtractor.setListener(this);

        mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext());
    }

    @Override
@@ -272,6 +289,8 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
            int pageId = mWorkspace.getPageIndexForScreenId(info.screenId);
            updateColorExtraction(mCurrentWidgetSize, pageId);
        }

        enforceRoundedCorners();
    }

    /** Starts the drag mode. */
@@ -469,4 +488,38 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
        }
        return false;
    }

    @UiThread
    private void resetRoundedCorners() {
        setOutlineProvider(ViewOutlineProvider.BACKGROUND);
        setClipToOutline(false);
    }

    @UiThread
    private void enforceRoundedCorners() {
        if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled(this)) {
            resetRoundedCorners();
            return;
        }
        View background = RoundedCornerEnforcement.findBackground(this);
        if (RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
            resetRoundedCorners();
            return;
        }
        RoundedCornerEnforcement.computeRoundedRectangle(this,
                background,
                mEnforcedRectangle);
        setOutlineProvider(mCornerRadiusEnforcementOutline);
        setClipToOutline(true);
    }

    /** Returns the corner radius currently enforced, in pixels. */
    public float getEnforcedCornerRadius() {
        return mEnforcedCornerRadius;
    }

    /** Returns true if the corner radius are enforced for this App Widget. */
    public boolean hasEnforcedCornerRadius() {
        return getClipToOutline();
    }
}
+170 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.launcher3.widget;

import android.appwidget.AppWidgetHostView;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;

import java.util.ArrayList;
import java.util.List;

/**
 * Utilities to compute the enforced the use of rounded corners on App Widgets.
 */
public class RoundedCornerEnforcement {
    // This class is only a namespace and not meant to be instantiated.
    private RoundedCornerEnforcement() {
    }

    /**
     * Find the background view for a widget.
     *
     * @param appWidget the view containing the App Widget (typically the instance of
     * {@link AppWidgetHostView}).
     */
    @Nullable
    public static View findBackground(@NonNull View appWidget) {
        List<View> backgrounds = findViewsWithId(appWidget, android.R.id.background);
        if (backgrounds.size() == 1) {
            return backgrounds.get(0);
        }
        // Really, the argument should contain the widget, so it cannot be the background.
        if (appWidget instanceof ViewGroup) {
            ViewGroup vg = (ViewGroup) appWidget;
            if (vg.getChildCount() > 0) {
                return findUndefinedBackground(vg.getChildAt(0));
            }
        }
        return appWidget;
    }

    /**
     * Check whether the app widget has opted out of the enforcement.
     */
    public static boolean hasAppWidgetOptedOut(@NonNull View appWidget, @NonNull View background) {
        return background.getId() == android.R.id.background && background.getClipToOutline();
    }

    /** Check if the app widget is in the deny list. */
    public static boolean isRoundedCornerEnabled(@NonNull View view) {
        if (!Utilities.ATLEAST_S || !FeatureFlags.ENABLE_ENFORCED_ROUNDED_CORNERS.get()) {
            return false;
        }
        // Here we need to test if the view's component is in the (to be created) deny list.
        return true;
    }

    /**
     * Computes the rounded rectangle needed for this app widget.
     *
     * @param appWidget View onto which the rounded rectangle will be applied.
     * @param background Background view. This must be either {@code appWidget} or a descendant
     *                  of {@code appWidget}.
     * @param outRect Rectangle set to the rounded rectangle coordinates, in the reference frame
     *                of {@code appWidget}.
     */
    public static void computeRoundedRectangle(@NonNull View appWidget, @NonNull View background,
            @NonNull Rect outRect) {
        outRect.left = 0;
        outRect.right = background.getWidth();
        outRect.top = 0;
        outRect.bottom = background.getHeight();
        while (background != appWidget) {
            outRect.offset(background.getLeft(), background.getTop());
            background = (View) background.getParent();
        }
    }

    /**
     * Computes the radius of the rounded rectangle that should be applied to a widget expanded
     * in the given context.
     */
    public static float computeEnforcedRadius(@NonNull Context context) {
        if (!Utilities.ATLEAST_S) {
            return 0;
        }
        Resources res = context.getResources();
        float systemRadius = res.getDimension(android.R.dimen.system_app_widget_background_radius);
        float defaultRadius = res.getDimension(R.dimen.enforced_rounded_corner_max_radius);
        return Math.min(defaultRadius, systemRadius);
    }

    private static List<View> findViewsWithId(View view, @IdRes int viewId) {
        List<View> output = new ArrayList<>();
        accumulateViewsWithId(view, viewId, output);
        return output;
    }

    // Traverse views. If the predicate returns true, continue on the children, otherwise, don't.
    private static void accumulateViewsWithId(View view, @IdRes int viewId, List<View> output) {
        if (view.getId() == viewId) {
            output.add(view);
            return;
        }
        if (view instanceof ViewGroup) {
            ViewGroup vg = (ViewGroup) view;
            for (int i = 0; i < vg.getChildCount(); i++) {
                accumulateViewsWithId(vg.getChildAt(i), viewId, output);
            }
        }
    }

    private static boolean isViewVisible(View view) {
        if (view.getVisibility() != View.VISIBLE) {
            return false;
        }
        return !view.willNotDraw() || view.getForeground() != null || view.getBackground() != null;
    }

    @Nullable
    private static View findUndefinedBackground(View current) {
        if (current.getVisibility() != View.VISIBLE) {
            return null;
        }
        if (isViewVisible(current)) {
            return current;
        }
        View lastVisibleView = null;
        // Find the first view that is either not a ViewGroup, or a ViewGroup which will draw
        // something, or a ViewGroup that contains more than one view.
        if (current instanceof ViewGroup) {
            ViewGroup vg = (ViewGroup) current;
            for (int i = 0; i < vg.getChildCount(); i++) {
                View visibleView = findUndefinedBackground(vg.getChildAt(i));
                if (visibleView != null) {
                    if (lastVisibleView != null) {
                        return current; // At least two visible children
                    }
                    lastVisibleView = visibleView;
                }
            }
        }
        return lastVisibleView;
    }
}