1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.statusbar.notification.row; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.content.res.ColorStateList; 23 import android.graphics.drawable.Icon; 24 import android.text.TextUtils; 25 import android.util.AttributeSet; 26 import android.view.View; 27 import android.view.ViewStub; 28 import android.widget.FrameLayout; 29 import android.widget.ImageView; 30 import android.widget.TextView; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.internal.widget.ConversationAvatarData; 34 import com.android.internal.widget.ConversationAvatarData.GroupConversationAvatarData; 35 import com.android.internal.widget.ConversationAvatarData.OneToOneConversationAvatarData; 36 import com.android.internal.widget.ConversationHeaderData; 37 import com.android.internal.widget.ConversationLayout; 38 import com.android.systemui.res.R; 39 import com.android.systemui.statusbar.notification.NotificationFadeAware; 40 import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; 41 import com.android.systemui.statusbar.notification.row.shared.ConversationStyleSetAvatarAsync; 42 import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar; 43 import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile; 44 import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon; 45 46 import java.util.Objects; 47 48 /** 49 * A hybrid view which may contain information about one ore more conversations. 50 */ 51 public class HybridConversationNotificationView extends HybridNotificationView { 52 53 private ImageView mConversationIconView; 54 private TextView mConversationSenderName; 55 private ViewStub mConversationFacePileStub; 56 private View mConversationFacePile; 57 private int mSingleAvatarSize; 58 private int mFacePileSize; 59 private int mFacePileAvatarSize; 60 private int mFacePileProtectionWidth; 61 HybridConversationNotificationView(Context context)62 public HybridConversationNotificationView(Context context) { 63 this(context, null); 64 } 65 HybridConversationNotificationView(Context context, @Nullable AttributeSet attrs)66 public HybridConversationNotificationView(Context context, @Nullable AttributeSet attrs) { 67 this(context, attrs, 0); 68 } 69 HybridConversationNotificationView( Context context, @Nullable AttributeSet attrs, int defStyleAttr)70 public HybridConversationNotificationView( 71 Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 72 this(context, attrs, defStyleAttr, 0); 73 } 74 HybridConversationNotificationView( Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)75 public HybridConversationNotificationView( 76 Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { 77 super(context, attrs, defStyleAttr, defStyleRes); 78 } 79 80 @Override onFinishInflate()81 protected void onFinishInflate() { 82 super.onFinishInflate(); 83 mConversationIconView = requireViewById(com.android.internal.R.id.conversation_icon); 84 if (AsyncHybridViewInflation.isEnabled()) { 85 mConversationFacePileStub = 86 requireViewById(com.android.internal.R.id.conversation_face_pile); 87 } else { 88 // TODO(b/217799515): This usage is vague because mConversationFacePile represents both 89 // View and ViewStub at different stages of View inflation, should be removed when 90 // AsyncHybridViewInflation flag is removed 91 mConversationFacePile = 92 requireViewById(com.android.internal.R.id.conversation_face_pile); 93 } 94 mConversationSenderName = requireViewById(R.id.conversation_notification_sender); 95 applyTextColor(mConversationSenderName, mSecondaryTextColor); 96 mFacePileSize = getResources() 97 .getDimensionPixelSize(R.dimen.conversation_single_line_face_pile_size); 98 mFacePileAvatarSize = getResources() 99 .getDimensionPixelSize(R.dimen.conversation_single_line_face_pile_avatar_size); 100 mSingleAvatarSize = getResources() 101 .getDimensionPixelSize(R.dimen.conversation_single_line_avatar_size); 102 mFacePileProtectionWidth = getResources().getDimensionPixelSize( 103 R.dimen.conversation_single_line_face_pile_protection_width); 104 mTransformationHelper.setCustomTransformation( 105 new FadeOutAndDownWithTitleTransformation(mConversationSenderName), 106 mConversationSenderName.getId()); 107 mTransformationHelper.addViewTransformingToSimilar(mConversationIconView); 108 mTransformationHelper.addTransformedView(mConversationSenderName); 109 } 110 111 @Override bind(@ullable CharSequence title, @Nullable CharSequence text, @Nullable View contentView)112 public void bind(@Nullable CharSequence title, @Nullable CharSequence text, 113 @Nullable View contentView) { 114 AsyncHybridViewInflation.assertInLegacyMode(); 115 if (!(contentView instanceof ConversationLayout)) { 116 super.bind(title, text, contentView); 117 return; 118 } 119 120 ConversationLayout conversationLayout = (ConversationLayout) contentView; 121 loadConversationAvatar(conversationLayout); 122 CharSequence conversationTitle = conversationLayout.getConversationTitle(); 123 if (TextUtils.isEmpty(conversationTitle)) { 124 conversationTitle = title; 125 } 126 if (conversationLayout.isOneToOne()) { 127 mConversationSenderName.setVisibility(GONE); 128 } else { 129 mConversationSenderName.setVisibility(VISIBLE); 130 mConversationSenderName.setText(conversationLayout.getConversationSenderName()); 131 } 132 CharSequence conversationText = conversationLayout.getConversationText(); 133 if (TextUtils.isEmpty(conversationText)) { 134 conversationText = text; 135 } 136 super.bind(conversationTitle, conversationText, conversationLayout); 137 } 138 loadConversationAvatar(ConversationLayout conversationLayout)139 private void loadConversationAvatar(ConversationLayout conversationLayout) { 140 AsyncHybridViewInflation.assertInLegacyMode(); 141 if (ConversationStyleSetAvatarAsync.isEnabled()) { 142 loadConversationAvatarWithDrawable(conversationLayout); 143 } else { 144 loadConversationAvatarWithIcon(conversationLayout); 145 } 146 } 147 148 @Deprecated loadConversationAvatarWithIcon(ConversationLayout conversationLayout)149 private void loadConversationAvatarWithIcon(ConversationLayout conversationLayout) { 150 ConversationStyleSetAvatarAsync.assertInLegacyMode(); 151 AsyncHybridViewInflation.assertInLegacyMode(); 152 final Icon conversationIcon = conversationLayout.getConversationIcon(); 153 if (conversationIcon != null) { 154 mConversationFacePile.setVisibility(GONE); 155 mConversationIconView.setVisibility(VISIBLE); 156 mConversationIconView.setImageIcon(conversationIcon); 157 setSize(mConversationIconView, mSingleAvatarSize); 158 } else { 159 // If there isn't an icon, generate a "face pile" based on the sender avatars 160 mConversationIconView.setVisibility(GONE); 161 mConversationFacePile.setVisibility(VISIBLE); 162 163 mConversationFacePile = 164 requireViewById(com.android.internal.R.id.conversation_face_pile); 165 final ImageView facePileBottomBg = mConversationFacePile.requireViewById( 166 com.android.internal.R.id.conversation_face_pile_bottom_background); 167 final ImageView facePileBottom = mConversationFacePile.requireViewById( 168 com.android.internal.R.id.conversation_face_pile_bottom); 169 final ImageView facePileTop = mConversationFacePile.requireViewById( 170 com.android.internal.R.id.conversation_face_pile_top); 171 conversationLayout.bindFacePile(facePileBottomBg, facePileBottom, facePileTop); 172 setSize(mConversationFacePile, mFacePileSize); 173 setSize(facePileBottom, mFacePileAvatarSize); 174 setSize(facePileTop, mFacePileAvatarSize); 175 setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth); 176 mTransformationHelper.addViewTransformingToSimilar(facePileTop); 177 mTransformationHelper.addViewTransformingToSimilar(facePileBottom); 178 mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg); 179 } 180 } 181 loadConversationAvatarWithDrawable(ConversationLayout conversationLayout)182 private void loadConversationAvatarWithDrawable(ConversationLayout conversationLayout) { 183 AsyncHybridViewInflation.assertInLegacyMode(); 184 final ConversationHeaderData conversationHeaderData = Objects.requireNonNull( 185 conversationLayout.getConversationHeaderData(), 186 /* message = */ "conversationHeaderData should not be null"); 187 final ConversationAvatarData conversationAvatar = 188 Objects.requireNonNull(conversationHeaderData.getConversationAvatar(), 189 /* message = */"conversationAvatar should not be null"); 190 191 if (conversationAvatar instanceof OneToOneConversationAvatarData oneToOneAvatar) { 192 mConversationFacePile.setVisibility(GONE); 193 mConversationIconView.setVisibility(VISIBLE); 194 mConversationIconView.setImageDrawable(oneToOneAvatar.mDrawable); 195 setSize(mConversationIconView, mSingleAvatarSize); 196 } else { 197 // If there isn't an icon, generate a "face pile" based on the sender avatars 198 mConversationIconView.setVisibility(GONE); 199 mConversationFacePile.setVisibility(VISIBLE); 200 201 final GroupConversationAvatarData groupAvatar = 202 (GroupConversationAvatarData) conversationAvatar; 203 mConversationFacePile = 204 requireViewById(com.android.internal.R.id.conversation_face_pile); 205 final ImageView facePileBottomBg = mConversationFacePile.requireViewById( 206 com.android.internal.R.id.conversation_face_pile_bottom_background); 207 final ImageView facePileBottom = mConversationFacePile.requireViewById( 208 com.android.internal.R.id.conversation_face_pile_bottom); 209 final ImageView facePileTop = mConversationFacePile.requireViewById( 210 com.android.internal.R.id.conversation_face_pile_top); 211 conversationLayout.bindFacePileWithDrawable(facePileBottomBg, facePileBottom, 212 facePileTop, groupAvatar); 213 setSize(mConversationFacePile, mFacePileSize); 214 setSize(facePileBottom, mFacePileAvatarSize); 215 setSize(facePileTop, mFacePileAvatarSize); 216 setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth); 217 mTransformationHelper.addViewTransformingToSimilar(facePileTop); 218 mTransformationHelper.addViewTransformingToSimilar(facePileBottom); 219 mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg); 220 } 221 } 222 223 /** 224 * Set the avatar using ConversationAvatar from SingleLineViewModel 225 * 226 * @param conversationAvatar the icon needed for a single-line conversation view, it should be 227 * either an instance of SingleIcon or FacePile 228 */ setAvatar(@onNull ConversationAvatar conversationAvatar)229 public void setAvatar(@NonNull ConversationAvatar conversationAvatar) { 230 if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return; 231 if (conversationAvatar instanceof SingleIcon) { 232 SingleIcon avatar = (SingleIcon) conversationAvatar; 233 if (mConversationFacePile != null) mConversationFacePile.setVisibility(GONE); 234 mConversationIconView.setVisibility(VISIBLE); 235 mConversationIconView.setImageDrawable(avatar.getIconDrawable()); 236 setSize(mConversationIconView, mSingleAvatarSize); 237 return; 238 } 239 240 // If conversationAvatar is not a SingleIcon, it should be a FacePile. 241 // Bind the face pile with it. 242 FacePile facePileModel = (FacePile) conversationAvatar; 243 mConversationIconView.setVisibility(GONE); 244 // Inflate mConversationFacePile from ViewStub 245 if (mConversationFacePile == null) { 246 mConversationFacePile = mConversationFacePileStub.inflate(); 247 } 248 mConversationFacePile.setVisibility(VISIBLE); 249 250 ImageView facePileBottomBg = mConversationFacePile.requireViewById( 251 com.android.internal.R.id.conversation_face_pile_bottom_background); 252 ImageView facePileBottom = mConversationFacePile.requireViewById( 253 com.android.internal.R.id.conversation_face_pile_bottom); 254 ImageView facePileTop = mConversationFacePile.requireViewById( 255 com.android.internal.R.id.conversation_face_pile_top); 256 257 int bottomBackgroundColor = facePileModel.getBottomBackgroundColor(); 258 facePileBottomBg.setImageTintList(ColorStateList.valueOf(bottomBackgroundColor)); 259 260 facePileBottom.setImageDrawable(facePileModel.getBottomIconDrawable()); 261 facePileTop.setImageDrawable(facePileModel.getTopIconDrawable()); 262 263 setSize(mConversationFacePile, mFacePileSize); 264 setSize(facePileBottom, mFacePileAvatarSize); 265 setSize(facePileTop, mFacePileAvatarSize); 266 setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth); 267 268 mTransformationHelper.addViewTransformingToSimilar(facePileTop); 269 mTransformationHelper.addViewTransformingToSimilar(facePileBottom); 270 mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg); 271 272 } 273 274 /** 275 * bind the text views 276 */ setText( CharSequence titleText, CharSequence contentText, CharSequence conversationSenderName )277 public void setText( 278 CharSequence titleText, 279 CharSequence contentText, 280 CharSequence conversationSenderName 281 ) { 282 if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return; 283 if (conversationSenderName == null) { 284 mConversationSenderName.setVisibility(GONE); 285 } else { 286 mConversationSenderName.setVisibility(VISIBLE); 287 mConversationSenderName.setText(conversationSenderName); 288 } 289 // TODO (b/217799515): super.bind() doesn't use contentView, remove the contentView 290 // argument when the flag is removed 291 super.bind(/* title = */ titleText, /* text = */ contentText, /* contentView = */ null); 292 } 293 setSize(View view, int size)294 private static void setSize(View view, int size) { 295 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) view.getLayoutParams(); 296 lp.width = size; 297 lp.height = size; 298 view.setLayoutParams(lp); 299 } 300 301 /** 302 * Apply the faded state as a layer type change to the face pile view which needs to have 303 * overlapping contents render precisely. 304 */ 305 @Override setNotificationFaded(boolean faded)306 public void setNotificationFaded(boolean faded) { 307 super.setNotificationFaded(faded); 308 NotificationFadeAware.setLayerTypeForFaded(mConversationFacePile, faded); 309 } 310 311 @VisibleForTesting getConversationSenderNameView()312 TextView getConversationSenderNameView() { 313 return mConversationSenderName; 314 } 315 } 316