• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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