Loading core/java/android/app/Notification.java +75 −2 Original line number Diff line number Diff line Loading @@ -5704,6 +5704,7 @@ public class Notification implements Parcelable p.headerless(resId == getBaseLayoutResource() || resId == getHeadsUpBaseLayoutResource() || resId == getCompactHeadsUpBaseLayoutResource() || resId == getMessagingCompactHeadsUpLayoutResource() || resId == getMessagingLayoutResource() || resId == R.layout.notification_template_material_media); RemoteViews contentView = new BuilderRemoteViews(mContext.getApplicationInfo(), resId); Loading Loading @@ -7294,6 +7295,10 @@ public class Notification implements Parcelable return R.layout.notification_template_material_compact_heads_up_base; } private int getMessagingCompactHeadsUpLayoutResource() { return R.layout.notification_template_material_messaging_compact_heads_up; } private int getBigBaseLayoutResource() { return R.layout.notification_template_material_big_base; } Loading Loading @@ -9166,10 +9171,78 @@ public class Notification implements Parcelable @Nullable @Override public RemoteViews makeCompactHeadsUpContentView() { // TODO(b/336229954): Apply minimal HUN treatment to Messaging Notifications. return makeHeadsUpContentView(false); final boolean isConversationLayout = mConversationType != CONVERSATION_TYPE_LEGACY; Icon conversationIcon = null; Notification.Action remoteInputAction = null; if (isConversationLayout) { conversationIcon = mShortcutIcon; // conversation icon is m // Extract the conversation icon for one to one conversations from // the latest incoming message since // fixTitleAndTextExtras also uses it as data source for title and text if (conversationIcon == null && !mIsGroupConversation) { final Message message = findLatestIncomingMessage(); if (message != null) { final Person sender = message.mSender; if (sender != null) { conversationIcon = sender.getIcon(); } } } if (Flags.compactHeadsUpNotificationReply()) { // Get the first non-contextual inline reply action. final List<Notification.Action> nonContextualActions = mBuilder.getNonContextualActions(); for (int i = 0; i < nonContextualActions.size(); i++) { final Notification.Action action = nonContextualActions.get(i); if (mBuilder.hasValidRemoteInput(action)) { remoteInputAction = action; break; } } } } // This method fills title and text fixTitleAndTextExtras(mBuilder.mN.extras); final StandardTemplateParams p = mBuilder.mParams.reset() .viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP) .highlightExpander(isConversationLayout) .fillTextsFrom(mBuilder) .hideTime(true) .summaryText(""); p.headerTextSecondary(p.mText); TemplateBindResult bindResult = new TemplateBindResult(); RemoteViews contentView = mBuilder.applyStandardTemplate( mBuilder.getMessagingCompactHeadsUpLayoutResource(), p, bindResult); if (conversationIcon != null) { contentView.setViewVisibility(R.id.icon, View.GONE); contentView.setViewVisibility(R.id.conversation_icon, View.VISIBLE); contentView.setBoolean(R.id.conversation_icon, "setApplyCircularCrop", true); contentView.setImageViewIcon(R.id.conversation_icon, conversationIcon); } if (remoteInputAction != null) { contentView.setViewVisibility(R.id.reply_action_container, View.VISIBLE); final RemoteViews inlineReplyButton = mBuilder.generateActionButton(remoteInputAction, false, p); // Clear the drawable inlineReplyButton.setInt(R.id.action0, "setBackgroundResource", 0); inlineReplyButton.setTextViewText(R.id.action0, mBuilder.mContext.getString(R.string.notification_compact_heads_up_reply)); contentView.addView(R.id.reply_action_container, inlineReplyButton); } else { contentView.setViewVisibility(R.id.reply_action_container, View.GONE); } return contentView; } /** * @hide */ Loading core/java/android/app/notification.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -160,3 +160,10 @@ flag { description: "[Minimal HUN] Enables the compact heads up notification feature" bug: "270709257" } flag { name: "compact_heads_up_notification_reply" namespace: "systemui" description: "[Minimal HUN] Enables the compact heads up notification reply capability for Conversation Notifications" bug: "336229954" } No newline at end of file core/java/com/android/internal/widget/NotificationRowIconView.java +89 −2 Original line number Diff line number Diff line Loading @@ -16,18 +16,27 @@ package com.android.internal.widget; import android.annotation.Nullable; import android.app.Flags; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.util.AttributeSet; import android.view.RemotableViewMethod; import android.widget.RemoteViews; import androidx.annotation.Nullable; /** * An image view that holds the icon displayed on the left side of a notification row. */ @RemoteViews.RemoteView public class NotificationRowIconView extends CachingIconView { private boolean mApplyCircularCrop = false; public NotificationRowIconView(Context context) { super(context); } Loading Loading @@ -57,4 +66,82 @@ public class NotificationRowIconView extends CachingIconView { super.onFinishInflate(); } @Nullable @Override Drawable loadSizeRestrictedIcon(@Nullable Icon icon) { final Drawable original = super.loadSizeRestrictedIcon(icon); final Drawable result; if (mApplyCircularCrop) { result = makeCircularDrawable(original); } else { result = original; } return result; } /** * Enables circle crop that makes given image circular */ @RemotableViewMethod(asyncImpl = "setApplyCircularCropAsync") public void setApplyCircularCrop(boolean applyCircularCrop) { mApplyCircularCrop = applyCircularCrop; } /** * Async version of {@link NotificationRowIconView#setApplyCircularCrop} */ public Runnable setApplyCircularCropAsync(boolean applyCircularCrop) { mApplyCircularCrop = applyCircularCrop; return () -> { }; } @Nullable private Drawable makeCircularDrawable(@Nullable Drawable original) { if (original == null) { return original; } final Bitmap source = drawableToBitmap(original); int size = Math.min(source.getWidth(), source.getHeight()); Bitmap squared = Bitmap.createScaledBitmap(source, size, size, /* filter= */ false); Bitmap result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(result); final Paint paint = new Paint(); paint.setShader( new BitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP)); paint.setAntiAlias(true); float radius = size / 2f; canvas.drawCircle(radius, radius, radius, paint); return new BitmapDrawable(getResources(), result); } private static Bitmap drawableToBitmap(Drawable drawable) { if (drawable instanceof BitmapDrawable bitmapDrawable) { final Bitmap bitmap = bitmapDrawable.getBitmap(); if (bitmap.getConfig() == Bitmap.Config.HARDWARE) { return bitmap.copy(Bitmap.Config.ARGB_8888, false); } else { return bitmap; } } int width = drawable.getIntrinsicWidth(); width = width > 0 ? width : 1; int height = drawable.getIntrinsicHeight(); height = height > 0 ? height : 1; Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } } core/res/res/layout/notification_template_material_messaging_compact_heads_up.xml 0 → 100644 +104 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?><!-- ~ 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 --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/status_bar_latest_event_content" android:layout_width="match_parent" android:layout_height="@dimen/notification_header_height" android:clipChildren="false" android:tag="compactMessagingHUN" android:gravity="center_vertical" android:theme="@style/Theme.DeviceDefault.Notification" android:importantForAccessibility="no"> <com.android.internal.widget.NotificationRowIconView android:id="@+id/icon" android:layout_width="@dimen/notification_icon_circle_size" android:layout_height="@dimen/notification_icon_circle_size" android:layout_gravity="center_vertical|start" android:layout_marginStart="@dimen/notification_icon_circle_start" android:background="@drawable/notification_icon_circle" android:padding="@dimen/notification_icon_circle_padding" android:maxDrawableWidth="@dimen/notification_icon_circle_size" android:maxDrawableHeight="@dimen/notification_icon_circle_size" /> <com.android.internal.widget.NotificationRowIconView android:id="@+id/conversation_icon" android:layout_width="@dimen/notification_icon_circle_size" android:layout_height="@dimen/notification_icon_circle_size" android:layout_gravity="center_vertical|start" android:layout_marginStart="@dimen/notification_icon_circle_start" android:maxDrawableWidth="@dimen/notification_icon_circle_size" android:maxDrawableHeight="@dimen/notification_icon_circle_size" android:scaleType="centerCrop" android:importantForAccessibility="no" /> <FrameLayout android:id="@+id/alternate_expand_target" android:layout_width="@dimen/notification_content_margin_start" android:layout_height="match_parent" android:layout_gravity="start" android:importantForAccessibility="no" android:focusable="false" /> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginStart="@dimen/notification_content_margin_start" android:orientation="horizontal" > <NotificationTopLineView android:id="@+id/notification_top_line" android:layout_width="0dp" android:layout_height="match_parent" android:layout_centerVertical="true" android:layout_weight="1" android:clipChildren="false" android:gravity="center_vertical" android:theme="@style/Theme.DeviceDefault.Notification" > <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/notification_header_separating_margin" android:ellipsize="end" android:fadingEdge="horizontal" android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" /> <include layout="@layout/notification_top_line_views" /> </NotificationTopLineView> <FrameLayout android:id="@+id/reply_action_container" android:layout_width="wrap_content" android:layout_height="@dimen/notification_action_list_height" android:gravity="center_vertical" android:orientation="horizontal" /> <FrameLayout android:id="@+id/expand_button_touch_container" android:layout_width="wrap_content" android:layout_height="match_parent" android:minWidth="@dimen/notification_content_margin_end" > <include layout="@layout/notification_expand_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" /> </FrameLayout> </LinearLayout> </FrameLayout> core/res/res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -794,6 +794,9 @@ <!-- The divider symbol between different parts of the notification header including spaces. not translatable [CHAR LIMIT=3] --> <string name="notification_header_divider_symbol_with_spaces" translatable="false">" • "</string> <!-- Text for inline reply button for compact conversation heads ups --> <string name="notification_compact_heads_up_reply">Reply</string> <!-- Text shown in place of notification contents when the notification is hidden on a secure lockscreen --> <string name="notification_hidden_text">New notification</string> Loading Loading
core/java/android/app/Notification.java +75 −2 Original line number Diff line number Diff line Loading @@ -5704,6 +5704,7 @@ public class Notification implements Parcelable p.headerless(resId == getBaseLayoutResource() || resId == getHeadsUpBaseLayoutResource() || resId == getCompactHeadsUpBaseLayoutResource() || resId == getMessagingCompactHeadsUpLayoutResource() || resId == getMessagingLayoutResource() || resId == R.layout.notification_template_material_media); RemoteViews contentView = new BuilderRemoteViews(mContext.getApplicationInfo(), resId); Loading Loading @@ -7294,6 +7295,10 @@ public class Notification implements Parcelable return R.layout.notification_template_material_compact_heads_up_base; } private int getMessagingCompactHeadsUpLayoutResource() { return R.layout.notification_template_material_messaging_compact_heads_up; } private int getBigBaseLayoutResource() { return R.layout.notification_template_material_big_base; } Loading Loading @@ -9166,10 +9171,78 @@ public class Notification implements Parcelable @Nullable @Override public RemoteViews makeCompactHeadsUpContentView() { // TODO(b/336229954): Apply minimal HUN treatment to Messaging Notifications. return makeHeadsUpContentView(false); final boolean isConversationLayout = mConversationType != CONVERSATION_TYPE_LEGACY; Icon conversationIcon = null; Notification.Action remoteInputAction = null; if (isConversationLayout) { conversationIcon = mShortcutIcon; // conversation icon is m // Extract the conversation icon for one to one conversations from // the latest incoming message since // fixTitleAndTextExtras also uses it as data source for title and text if (conversationIcon == null && !mIsGroupConversation) { final Message message = findLatestIncomingMessage(); if (message != null) { final Person sender = message.mSender; if (sender != null) { conversationIcon = sender.getIcon(); } } } if (Flags.compactHeadsUpNotificationReply()) { // Get the first non-contextual inline reply action. final List<Notification.Action> nonContextualActions = mBuilder.getNonContextualActions(); for (int i = 0; i < nonContextualActions.size(); i++) { final Notification.Action action = nonContextualActions.get(i); if (mBuilder.hasValidRemoteInput(action)) { remoteInputAction = action; break; } } } } // This method fills title and text fixTitleAndTextExtras(mBuilder.mN.extras); final StandardTemplateParams p = mBuilder.mParams.reset() .viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP) .highlightExpander(isConversationLayout) .fillTextsFrom(mBuilder) .hideTime(true) .summaryText(""); p.headerTextSecondary(p.mText); TemplateBindResult bindResult = new TemplateBindResult(); RemoteViews contentView = mBuilder.applyStandardTemplate( mBuilder.getMessagingCompactHeadsUpLayoutResource(), p, bindResult); if (conversationIcon != null) { contentView.setViewVisibility(R.id.icon, View.GONE); contentView.setViewVisibility(R.id.conversation_icon, View.VISIBLE); contentView.setBoolean(R.id.conversation_icon, "setApplyCircularCrop", true); contentView.setImageViewIcon(R.id.conversation_icon, conversationIcon); } if (remoteInputAction != null) { contentView.setViewVisibility(R.id.reply_action_container, View.VISIBLE); final RemoteViews inlineReplyButton = mBuilder.generateActionButton(remoteInputAction, false, p); // Clear the drawable inlineReplyButton.setInt(R.id.action0, "setBackgroundResource", 0); inlineReplyButton.setTextViewText(R.id.action0, mBuilder.mContext.getString(R.string.notification_compact_heads_up_reply)); contentView.addView(R.id.reply_action_container, inlineReplyButton); } else { contentView.setViewVisibility(R.id.reply_action_container, View.GONE); } return contentView; } /** * @hide */ Loading
core/java/android/app/notification.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -160,3 +160,10 @@ flag { description: "[Minimal HUN] Enables the compact heads up notification feature" bug: "270709257" } flag { name: "compact_heads_up_notification_reply" namespace: "systemui" description: "[Minimal HUN] Enables the compact heads up notification reply capability for Conversation Notifications" bug: "336229954" } No newline at end of file
core/java/com/android/internal/widget/NotificationRowIconView.java +89 −2 Original line number Diff line number Diff line Loading @@ -16,18 +16,27 @@ package com.android.internal.widget; import android.annotation.Nullable; import android.app.Flags; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.util.AttributeSet; import android.view.RemotableViewMethod; import android.widget.RemoteViews; import androidx.annotation.Nullable; /** * An image view that holds the icon displayed on the left side of a notification row. */ @RemoteViews.RemoteView public class NotificationRowIconView extends CachingIconView { private boolean mApplyCircularCrop = false; public NotificationRowIconView(Context context) { super(context); } Loading Loading @@ -57,4 +66,82 @@ public class NotificationRowIconView extends CachingIconView { super.onFinishInflate(); } @Nullable @Override Drawable loadSizeRestrictedIcon(@Nullable Icon icon) { final Drawable original = super.loadSizeRestrictedIcon(icon); final Drawable result; if (mApplyCircularCrop) { result = makeCircularDrawable(original); } else { result = original; } return result; } /** * Enables circle crop that makes given image circular */ @RemotableViewMethod(asyncImpl = "setApplyCircularCropAsync") public void setApplyCircularCrop(boolean applyCircularCrop) { mApplyCircularCrop = applyCircularCrop; } /** * Async version of {@link NotificationRowIconView#setApplyCircularCrop} */ public Runnable setApplyCircularCropAsync(boolean applyCircularCrop) { mApplyCircularCrop = applyCircularCrop; return () -> { }; } @Nullable private Drawable makeCircularDrawable(@Nullable Drawable original) { if (original == null) { return original; } final Bitmap source = drawableToBitmap(original); int size = Math.min(source.getWidth(), source.getHeight()); Bitmap squared = Bitmap.createScaledBitmap(source, size, size, /* filter= */ false); Bitmap result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(result); final Paint paint = new Paint(); paint.setShader( new BitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP)); paint.setAntiAlias(true); float radius = size / 2f; canvas.drawCircle(radius, radius, radius, paint); return new BitmapDrawable(getResources(), result); } private static Bitmap drawableToBitmap(Drawable drawable) { if (drawable instanceof BitmapDrawable bitmapDrawable) { final Bitmap bitmap = bitmapDrawable.getBitmap(); if (bitmap.getConfig() == Bitmap.Config.HARDWARE) { return bitmap.copy(Bitmap.Config.ARGB_8888, false); } else { return bitmap; } } int width = drawable.getIntrinsicWidth(); width = width > 0 ? width : 1; int height = drawable.getIntrinsicHeight(); height = height > 0 ? height : 1; Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } }
core/res/res/layout/notification_template_material_messaging_compact_heads_up.xml 0 → 100644 +104 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?><!-- ~ 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 --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/status_bar_latest_event_content" android:layout_width="match_parent" android:layout_height="@dimen/notification_header_height" android:clipChildren="false" android:tag="compactMessagingHUN" android:gravity="center_vertical" android:theme="@style/Theme.DeviceDefault.Notification" android:importantForAccessibility="no"> <com.android.internal.widget.NotificationRowIconView android:id="@+id/icon" android:layout_width="@dimen/notification_icon_circle_size" android:layout_height="@dimen/notification_icon_circle_size" android:layout_gravity="center_vertical|start" android:layout_marginStart="@dimen/notification_icon_circle_start" android:background="@drawable/notification_icon_circle" android:padding="@dimen/notification_icon_circle_padding" android:maxDrawableWidth="@dimen/notification_icon_circle_size" android:maxDrawableHeight="@dimen/notification_icon_circle_size" /> <com.android.internal.widget.NotificationRowIconView android:id="@+id/conversation_icon" android:layout_width="@dimen/notification_icon_circle_size" android:layout_height="@dimen/notification_icon_circle_size" android:layout_gravity="center_vertical|start" android:layout_marginStart="@dimen/notification_icon_circle_start" android:maxDrawableWidth="@dimen/notification_icon_circle_size" android:maxDrawableHeight="@dimen/notification_icon_circle_size" android:scaleType="centerCrop" android:importantForAccessibility="no" /> <FrameLayout android:id="@+id/alternate_expand_target" android:layout_width="@dimen/notification_content_margin_start" android:layout_height="match_parent" android:layout_gravity="start" android:importantForAccessibility="no" android:focusable="false" /> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginStart="@dimen/notification_content_margin_start" android:orientation="horizontal" > <NotificationTopLineView android:id="@+id/notification_top_line" android:layout_width="0dp" android:layout_height="match_parent" android:layout_centerVertical="true" android:layout_weight="1" android:clipChildren="false" android:gravity="center_vertical" android:theme="@style/Theme.DeviceDefault.Notification" > <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/notification_header_separating_margin" android:ellipsize="end" android:fadingEdge="horizontal" android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" /> <include layout="@layout/notification_top_line_views" /> </NotificationTopLineView> <FrameLayout android:id="@+id/reply_action_container" android:layout_width="wrap_content" android:layout_height="@dimen/notification_action_list_height" android:gravity="center_vertical" android:orientation="horizontal" /> <FrameLayout android:id="@+id/expand_button_touch_container" android:layout_width="wrap_content" android:layout_height="match_parent" android:minWidth="@dimen/notification_content_margin_end" > <include layout="@layout/notification_expand_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" /> </FrameLayout> </LinearLayout> </FrameLayout>
core/res/res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -794,6 +794,9 @@ <!-- The divider symbol between different parts of the notification header including spaces. not translatable [CHAR LIMIT=3] --> <string name="notification_header_divider_symbol_with_spaces" translatable="false">" • "</string> <!-- Text for inline reply button for compact conversation heads ups --> <string name="notification_compact_heads_up_reply">Reply</string> <!-- Text shown in place of notification contents when the notification is hidden on a secure lockscreen --> <string name="notification_hidden_text">New notification</string> Loading