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

Commit 71a1d657 authored by Ibrahim Yilmaz's avatar Ibrahim Yilmaz Committed by Android (Google) Code Review
Browse files

Merge "[RONs] Bind ProgressStyle values to NotificationProgressBar" into main

parents dccb2e3d 79778bce
Loading
Loading
Loading
Loading
+112 −8
Original line number Diff line number Diff line
@@ -115,6 +115,7 @@ import com.android.internal.graphics.ColorUtils;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.ContrastColorUtil;
import com.android.internal.util.NotificationBigTextNormalizer;
import com.android.internal.widget.NotificationProgressModel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -7318,12 +7319,16 @@ public class Notification implements Parcelable
         */
        @VisibleForTesting
        public static int ensureButtonFillContrast(int color, int bg) {
            return isColorDark(bg)
                    ? ContrastColorUtil.findContrastColorAgainstDark(color, bg, true, 1.3)
                    : ContrastColorUtil.findContrastColor(color, bg, true, 1.3);
            return ensureColorContrast(color, bg, 1.3);
        }
        private static int ensureColorContrast(int color, int bg, double contrastRatio) {
            return isColorDark(bg)
                    ? ContrastColorUtil.findContrastColorAgainstDark(color, bg, true, contrastRatio)
                    : ContrastColorUtil.findContrastColor(color, bg, true, contrastRatio);
        }
        /**
         * @return Whether we are currently building a notification from a legacy (an app that
         *         doesn't create material notifications by itself) app.
@@ -11657,6 +11662,7 @@ public class Notification implements Parcelable
            StandardTemplateParams p = mBuilder.mParams.reset()
                    .viewType(StandardTemplateParams.VIEW_TYPE_BIG)
                    .allowTextWithProgress(true)
                    .hideProgress(true)
                    .fillTextsFrom(mBuilder);
            // Replace the text with the big text, but only if the big text is not empty.
@@ -11678,10 +11684,28 @@ public class Notification implements Parcelable
                contentView.setViewVisibility(R.id.notification_progress_end_icon, View.GONE);
            }
            contentView.setViewVisibility(R.id.progress, View.VISIBLE);
            final int backgroundColor = mBuilder.getColors(p).getBackgroundColor();
            final int defaultProgressColor = mBuilder.getPrimaryAccentColor(p);
            final NotificationProgressModel model = createProgressModel(
                    defaultProgressColor, backgroundColor);
            contentView.setBundle(R.id.progress,
                    "setProgressModel", model.toBundle());
            if (mTrackerIcon != null) {
                contentView.setIcon(R.id.progress,
                        "setProgressTrackerIcon",
                        mTrackerIcon);
            }
            return contentView;
        }
        private static @NonNull ArrayList<Bundle> getProgressSegmentsAsBundleList(
        /**
         * @hide
         */
        public static @NonNull ArrayList<Bundle> getProgressSegmentsAsBundleList(
                @Nullable List<Segment> progressSegments) {
            final ArrayList<Bundle> segments = new ArrayList<>();
            if (progressSegments != null && !progressSegments.isEmpty()) {
@@ -11703,7 +11727,10 @@ public class Notification implements Parcelable
            return segments;
        }
        private static @NonNull List<Segment> getProgressSegmentsFromBundleList(
        /**
         * @hide
         */
        public  static @NonNull List<Segment> getProgressSegmentsFromBundleList(
                @Nullable List<Bundle> segmentBundleList) {
            final ArrayList<Segment> segments = new ArrayList<>();
            if (segmentBundleList != null && !segmentBundleList.isEmpty()) {
@@ -11726,8 +11753,10 @@ public class Notification implements Parcelable
            return segments;
        }
        private static @NonNull ArrayList<Bundle> getProgressPointsAsBundleList(
        /**
         * @hide
         */
        public static @NonNull ArrayList<Bundle> getProgressPointsAsBundleList(
                @Nullable List<Point> progressPoints) {
            final ArrayList<Bundle> points = new ArrayList<>();
            if (progressPoints != null && !progressPoints.isEmpty()) {
@@ -11749,7 +11778,10 @@ public class Notification implements Parcelable
            return points;
        }
        private static @NonNull List<Point> getProgressPointsFromBundleList(
        /**
         * @hide
         */
        public static @NonNull List<Point> getProgressPointsFromBundleList(
                @Nullable List<Bundle> pointBundleList) {
            final ArrayList<Point> points = new ArrayList<>();
@@ -11771,6 +11803,78 @@ public class Notification implements Parcelable
            return points;
        }
        @NonNull
        private NotificationProgressModel createProgressModel(int defaultProgressColor,
                int backgroundColor) {
            final NotificationProgressModel model;
            if (mIndeterminate) {
                final int indeterminateColor;
                if (!mProgressSegments.isEmpty()) {
                    indeterminateColor = mProgressSegments.get(0).mColor;
                } else {
                    indeterminateColor = defaultProgressColor;
                }
                model = new NotificationProgressModel(
                        sanitizeProgressColor(indeterminateColor,
                                backgroundColor, defaultProgressColor));
            } else {
                // Ensure segment color contrasts.
                final List<Segment> segments = new ArrayList<>();
                for (Segment segment : mProgressSegments) {
                    segments.add(sanitizeSegment(segment, backgroundColor,
                            defaultProgressColor));
                }
                // Create default segment when no segments are provided.
                if (segments.isEmpty()) {
                    segments.add(sanitizeSegment(new Segment(100), backgroundColor,
                            defaultProgressColor));
                }
                // Ensure point color contrasts.
                final List<Point> points = new ArrayList<>();
                for (Point point : mProgressPoints) {
                    points.add(sanitizePoint(point, backgroundColor, defaultProgressColor));
                }
                model = new NotificationProgressModel(segments, points,
                        mProgress, mIsStyledByProgress);
            }
            return model;
        }
        private Segment sanitizeSegment(@NonNull Segment segment,
                @ColorInt int bg,
                @ColorInt int defaultColor) {
            return new Segment(segment.getLength())
                    .setId(segment.getId())
                    .setColor(sanitizeProgressColor(segment.getColor(), bg, defaultColor));
        }
        private Point sanitizePoint(@NonNull Point point,
                @ColorInt int bg,
                @ColorInt int defaultColor) {
            return new Point(point.getPosition()).setId(point.getId())
                    .setColor(sanitizeProgressColor(point.getColor(), bg, defaultColor));
        }
        /**
         * Finds steps and points fill color with sufficient contrast over bg (1.3:1) that
         * has the same hue as the original color, but is lightened or darkened depending on
         * whether the background is dark or light.
         *
         */
        private int sanitizeProgressColor(@ColorInt int color,
                @ColorInt int bg,
                @ColorInt int defaultColor) {
            return Builder.ensureColorContrast(
                    Color.alpha(color) == 0 ? defaultColor : color,
                    bg,
                    1.3);
        }
        /**
         * A segment of the progress bar, which defines its length and color.
         * Segments allow for creating progress bars with multiple colors or sections
+58 −0
Original line number Diff line number Diff line
@@ -16,15 +16,22 @@

package com.android.internal.widget;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification.ProgressStyle;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.RemotableViewMethod;
import android.widget.ProgressBar;
import android.widget.RemoteViews;

import androidx.annotation.ColorInt;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.internal.widget.NotificationProgressDrawable.Part;
import com.android.internal.widget.NotificationProgressDrawable.Point;
import com.android.internal.widget.NotificationProgressDrawable.Segment;
@@ -42,6 +49,10 @@ import java.util.TreeSet;
 */
@RemoteViews.RemoteView
public class NotificationProgressBar extends ProgressBar {
    private NotificationProgressModel mProgressModel;
    @Nullable
    private Drawable mProgressTrackerDrawable = null;

    public NotificationProgressBar(Context context) {
        this(context, null);
    }
@@ -59,6 +70,53 @@ public class NotificationProgressBar extends ProgressBar {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    /**
     * Setter for the notification progress model.
     *
     * @see NotificationProgressModel#fromBundle
     * @see #setProgressModelAsync
     */
    @RemotableViewMethod(asyncImpl = "setProgressModelAsync")
    public void setProgressModel(@Nullable Bundle bundle) {
        Preconditions.checkArgument(bundle != null,
                "Bundle shouldn't be null");

        mProgressModel = NotificationProgressModel.fromBundle(bundle);
    }

    private void setProgressModel(@NonNull NotificationProgressModel model) {
        mProgressModel = model;
    }

    /**
     * Setter for the progress tracker icon.
     *
     * @see #setProgressTrackerIconAsync
     */
    @RemotableViewMethod(asyncImpl = "setProgressTrackerIconAsync")
    public void setProgressTrackerIcon(@Nullable Icon icon) {
    }


    /**
     * Async version of {@link #setProgressTrackerIcon}
     */
    public Runnable setProgressTrackerIconAsync(@Nullable Icon icon) {
        final Drawable progressTrackerDrawable;
        if (icon != null) {
            progressTrackerDrawable = icon.loadDrawable(getContext());
        } else {
            progressTrackerDrawable = null;
        }
        return () -> {
            setProgressTrackerDrawable(progressTrackerDrawable);
        };
    }

    private void setProgressTrackerDrawable(@Nullable  Drawable drawable) {
        mProgressTrackerDrawable = drawable;
    }

    /**
     * Processes the ProgressStyle data and convert to list of {@code
     * NotificationProgressDrawable.Part}.
+183 −0
Original line number Diff line number Diff line
/*
 * 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.internal.widget;


import android.annotation.ColorInt;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.app.Flags;
import android.app.Notification;
import android.app.Notification.ProgressStyle.Point;
import android.app.Notification.ProgressStyle.Segment;
import android.graphics.Color;
import android.os.Bundle;

import com.android.internal.util.Preconditions;

import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
 * Data model for {@link NotificationProgressBar}.
 *
 * This class holds the necessary data to render the notification progressbar.
 * It is used to bind the progress style progress data to {@link NotificationProgressBar}.
 *
 * @hide
 * @see NotificationProgressModel#toBundle
 * @see NotificationProgressModel#fromBundle
 */
@FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
public final class NotificationProgressModel {
    private static final int INVALID_INDETERMINATE_COLOR = Color.TRANSPARENT;
    private static final String KEY_SEGMENTS = "segments";
    private static final String KEY_POINTS = "points";
    private static final String KEY_PROGRESS = "progress";
    private static final String KEY_IS_STYLED_BY_PROGRESS = "isStyledByProgress";
    private static final String KEY_INDETERMINATE_COLOR = "indeterminateColor";
    private final List<Segment> mSegments;
    private final List<Point> mPoints;
    private final int mProgress;
    private final boolean mIsStyledByProgress;
    @ColorInt
    private final int mIndeterminateColor;

    public NotificationProgressModel(
            @NonNull List<Segment> segments,
            @NonNull List<Point> points,
            int progress,
            boolean isStyledByProgress
    ) {
        Preconditions.checkArgument(progress >= 0);
        Preconditions.checkArgument(!segments.isEmpty());
        mSegments = segments;
        mPoints = points;
        mProgress = progress;
        mIsStyledByProgress = isStyledByProgress;
        mIndeterminateColor = INVALID_INDETERMINATE_COLOR;
    }

    public NotificationProgressModel(
            @ColorInt int indeterminateColor
    ) {
        Preconditions.checkArgument(indeterminateColor != INVALID_INDETERMINATE_COLOR);
        mSegments = Collections.emptyList();
        mPoints = Collections.emptyList();
        mProgress = 0;
        mIsStyledByProgress = false;
        mIndeterminateColor = indeterminateColor;
    }

    public List<Segment> getSegments() {
        return mSegments;
    }

    public List<Point> getPoints() {
        return mPoints;
    }

    public int getProgress() {
        return mProgress;
    }

    public boolean isStyledByProgress() {
        return mIsStyledByProgress;
    }

    @ColorInt
    public int getIndeterminateColor() {
        return mIndeterminateColor;
    }

    public boolean isIndeterminate() {
        return mIndeterminateColor != INVALID_INDETERMINATE_COLOR;
    }

    /**
     * Returns a {@link Bundle} representation of this {@link NotificationProgressModel}.
     */
    @NonNull
    public Bundle toBundle() {
        final Bundle bundle = new Bundle();
        if (mIndeterminateColor != INVALID_INDETERMINATE_COLOR) {
            bundle.putInt(KEY_INDETERMINATE_COLOR, mIndeterminateColor);
        } else {
            bundle.putParcelableList(KEY_SEGMENTS,
                    Notification.ProgressStyle.getProgressSegmentsAsBundleList(mSegments));
            bundle.putParcelableList(KEY_POINTS,
                    Notification.ProgressStyle.getProgressPointsAsBundleList(mPoints));
            bundle.putInt(KEY_PROGRESS, mProgress);
            bundle.putBoolean(KEY_IS_STYLED_BY_PROGRESS, mIsStyledByProgress);
        }
        return bundle;
    }

    /**
     * Creates a {@link NotificationProgressModel} from a {@link Bundle}.
     */
    @NonNull
    public static NotificationProgressModel fromBundle(@NonNull Bundle bundle) {
        final int indeterminateColor = bundle.getInt(KEY_INDETERMINATE_COLOR,
                INVALID_INDETERMINATE_COLOR);
        if (indeterminateColor != INVALID_INDETERMINATE_COLOR) {
            return new NotificationProgressModel(indeterminateColor);
        } else {
            final List<Segment> segments =
                    Notification.ProgressStyle.getProgressSegmentsFromBundleList(
                            bundle.getParcelableArrayList(KEY_SEGMENTS, Bundle.class));
            final List<Point> points =
                    Notification.ProgressStyle.getProgressPointsFromBundleList(
                            bundle.getParcelableArrayList(KEY_POINTS, Bundle.class));
            final int progress = bundle.getInt(KEY_PROGRESS);
            final boolean isStyledByProgress = bundle.getBoolean(KEY_IS_STYLED_BY_PROGRESS);
            return new NotificationProgressModel(segments, points, progress, isStyledByProgress);
        }
    }

    @Override
    public String toString() {
        return "NotificationProgressModel{"
                + "mSegments=" + mSegments
                + ", mPoints=" + mPoints
                + ", mProgress=" + mProgress
                + ", mIsStyledByProgress=" + mIsStyledByProgress
                + ", mIndeterminateColor=" + mIndeterminateColor + "}";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        final NotificationProgressModel that = (NotificationProgressModel) o;
        return mProgress == that.mProgress
                && mIsStyledByProgress == that.mIsStyledByProgress
                && mIndeterminateColor == that.mIndeterminateColor
                && Objects.equals(mSegments, that.mSegments)
                && Objects.equals(mPoints, that.mPoints);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mSegments,
                mPoints,
                mProgress,
                mIsStyledByProgress,
                mIndeterminateColor);
    }
}
+3 −2
Original line number Diff line number Diff line
@@ -75,10 +75,11 @@
                        />


                    <include
                    <com.android.internal.widget.NotificationProgressBar
                        android:id="@+id/progress"
                        android:layout_width="0dp"
                        android:layout_height="@dimen/notification_progress_bar_height"
                        layout="@layout/notification_template_progress"
                        style="@style/Widget.Material.Light.ProgressBar.Horizontal"
                        android:layout_weight="1"
                        />

+111 −0
Original line number Diff line number Diff line
/*
 * 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.internal.widget;

import static com.google.common.truth.Truth.assertThat;

import android.app.Flags;
import android.app.Notification;
import android.graphics.Color;
import android.os.Bundle;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;

import androidx.test.filters.SmallTest;

import org.junit.Rule;
import org.junit.Test;

import java.util.List;

@SmallTest
@EnableFlags(Flags.FLAG_API_RICH_ONGOING)
public class NotificationProgressModelTest {

    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    @Test(expected = IllegalArgumentException.class)
    public void throw_exception_on_transparent_indeterminate_color() {
        new NotificationProgressModel(Color.TRANSPARENT);
    }

    @Test(expected = IllegalArgumentException.class)
    public void throw_exception_on_empty_segments() {
        new NotificationProgressModel(List.of(),
                List.of(),
                10,
                false);
    }

    @Test(expected = IllegalArgumentException.class)
    public void throw_exception_on_negative_progress() {
        new NotificationProgressModel(
                List.of(new Notification.ProgressStyle.Segment(50).setColor(Color.YELLOW)),
                List.of(),
                -1,
                false);
    }

    @Test
    public void save_and_restore_indeterminate_progress_model() {
        // GIVEN
        final NotificationProgressModel savedModel = new NotificationProgressModel(Color.RED);
        final Bundle bundle = savedModel.toBundle();

        // WHEN
        final NotificationProgressModel restoredModel =
                NotificationProgressModel.fromBundle(bundle);

        // THEN
        assertThat(restoredModel.getIndeterminateColor()).isEqualTo(Color.RED);
        assertThat(restoredModel.isIndeterminate()).isTrue();
        assertThat(restoredModel.getProgress()).isEqualTo(-1);
        assertThat(restoredModel.getSegments()).isEmpty();
        assertThat(restoredModel.getPoints()).isEmpty();
        assertThat(restoredModel.isStyledByProgress()).isFalse();
    }

    @Test
    public void save_and_restore_non_indeterminate_progress_model() {
        // GIVEN
        final List<Notification.ProgressStyle.Segment> segments = List.of(
                new Notification.ProgressStyle.Segment(50).setColor(Color.YELLOW),
                new Notification.ProgressStyle.Segment(50).setColor(Color.LTGRAY));
        final List<Notification.ProgressStyle.Point> points = List.of(
                new Notification.ProgressStyle.Point(0).setColor(Color.RED),
                new Notification.ProgressStyle.Point(20).setColor(Color.BLUE));
        final NotificationProgressModel savedModel = new NotificationProgressModel(segments,
                points,
                100,
                true);

        final Bundle bundle = savedModel.toBundle();

        // WHEN
        final NotificationProgressModel restoredModel =
                NotificationProgressModel.fromBundle(bundle);

        // THEN
        assertThat(restoredModel.isIndeterminate()).isFalse();
        assertThat(restoredModel.getSegments()).isEqualTo(segments);
        assertThat(restoredModel.getPoints()).isEqualTo(points);
        assertThat(restoredModel.getProgress()).isEqualTo(100);
        assertThat(restoredModel.isStyledByProgress()).isTrue();
        assertThat(restoredModel.getIndeterminateColor()).isEqualTo(-1);
    }
}