1 /*
2  * Copyright (C) 2018 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.internal.widget;
18 
19 import android.annotation.AttrRes;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.StyleRes;
23 import android.app.Notification;
24 import android.content.Context;
25 import android.graphics.Canvas;
26 import android.graphics.Path;
27 import android.graphics.drawable.Drawable;
28 import android.net.Uri;
29 import android.util.AttributeSet;
30 import android.util.Log;
31 import android.view.LayoutInflater;
32 import android.view.ViewGroup;
33 import android.widget.ImageView;
34 import android.widget.RemoteViews;
35 
36 import com.android.internal.R;
37 
38 import java.io.IOException;
39 
40 /**
41  * A message of a {@link MessagingLayout} that is an image.
42  */
43 @RemoteViews.RemoteView
44 public class MessagingImageMessage extends ImageView implements MessagingMessage {
45     private static final String TAG = "MessagingImageMessage";
46     private static final MessagingPool<MessagingImageMessage> sInstancePool =
47             new MessagingPool<>(10);
48     private final MessagingMessageState mState = new MessagingMessageState(this);
49     private final int mMinImageHeight;
50     private final Path mPath = new Path();
51     private final int mImageRounding;
52     private final int mMaxImageHeight;
53     private final int mIsolatedSize;
54     private final int mExtraSpacing;
55     private Drawable mDrawable;
56     private float mAspectRatio;
57     private int mActualWidth;
58     private int mActualHeight;
59     private boolean mIsIsolated;
60     private ImageResolver mImageResolver;
61 
MessagingImageMessage(@onNull Context context)62     public MessagingImageMessage(@NonNull Context context) {
63         this(context, null);
64     }
65 
MessagingImageMessage(@onNull Context context, @Nullable AttributeSet attrs)66     public MessagingImageMessage(@NonNull Context context, @Nullable AttributeSet attrs) {
67         this(context, attrs, 0);
68     }
69 
MessagingImageMessage(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)70     public MessagingImageMessage(@NonNull Context context, @Nullable AttributeSet attrs,
71             @AttrRes int defStyleAttr) {
72         this(context, attrs, defStyleAttr, 0);
73     }
74 
MessagingImageMessage(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)75     public MessagingImageMessage(@NonNull Context context, @Nullable AttributeSet attrs,
76             @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
77         super(context, attrs, defStyleAttr, defStyleRes);
78         mMinImageHeight = context.getResources().getDimensionPixelSize(
79                 com.android.internal.R.dimen.messaging_image_min_size);
80         mMaxImageHeight = context.getResources().getDimensionPixelSize(
81                 com.android.internal.R.dimen.messaging_image_max_height);
82         mImageRounding = context.getResources().getDimensionPixelSize(
83                 com.android.internal.R.dimen.messaging_image_rounding);
84         mExtraSpacing = context.getResources().getDimensionPixelSize(
85                 com.android.internal.R.dimen.messaging_image_extra_spacing);
86         setMaxHeight(mMaxImageHeight);
87         mIsolatedSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size);
88     }
89 
90     @Override
getState()91     public MessagingMessageState getState() {
92         return mState;
93     }
94 
95     @Override
setMessage(Notification.MessagingStyle.Message message, boolean usePrecomputedText)96     public boolean setMessage(Notification.MessagingStyle.Message message,
97             boolean usePrecomputedText) {
98         MessagingMessage.super.setMessage(message, usePrecomputedText);
99         Drawable drawable;
100         try {
101             Uri uri = message.getDataUri();
102             drawable = mImageResolver != null ? mImageResolver.loadImage(uri) :
103                     LocalImageResolver.resolveImage(uri, getContext());
104         } catch (IOException | SecurityException e) {
105             e.printStackTrace();
106             return false;
107         }
108         if (drawable == null) {
109             return false;
110         }
111         int intrinsicHeight = drawable.getIntrinsicHeight();
112         if (intrinsicHeight == 0) {
113             Log.w(TAG, "Drawable with 0 intrinsic height was returned");
114             return false;
115         }
116         mDrawable = drawable;
117         mAspectRatio = ((float) mDrawable.getIntrinsicWidth()) / intrinsicHeight;
118         if (!usePrecomputedText) {
119             finalizeInflate();
120         }
121         return true;
122     }
123 
createMessage(IMessagingLayout layout, Notification.MessagingStyle.Message m, ImageResolver resolver, boolean usePrecomputedText)124     static MessagingMessage createMessage(IMessagingLayout layout,
125             Notification.MessagingStyle.Message m, ImageResolver resolver,
126             boolean usePrecomputedText) {
127         MessagingLinearLayout messagingLinearLayout = layout.getMessagingLinearLayout();
128         MessagingImageMessage createdMessage = sInstancePool.acquire();
129         if (createdMessage == null) {
130             createdMessage = (MessagingImageMessage) LayoutInflater.from(
131                     layout.getContext()).inflate(
132                     R.layout.notification_template_messaging_image_message,
133                     messagingLinearLayout,
134                     false);
135             createdMessage.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR);
136         }
137         createdMessage.setImageResolver(resolver);
138         // MessagingImageMessage does not use usePrecomputedText.
139         boolean populated = createdMessage.setMessage(m, /* usePrecomputedText= */false);
140         if (!populated) {
141             createdMessage.recycle();
142             return MessagingTextMessage.createMessage(layout, m, usePrecomputedText);
143         }
144         return createdMessage;
145     }
146 
147 
148     @Override
finalizeInflate()149     public void finalizeInflate() {
150         setImageDrawable(mDrawable);
151         setContentDescription(getMessage().getText());
152     }
153 
setImageResolver(ImageResolver resolver)154     private void setImageResolver(ImageResolver resolver) {
155         mImageResolver = resolver;
156     }
157 
158     @Override
onDraw(Canvas canvas)159     protected void onDraw(Canvas canvas) {
160         canvas.save();
161         canvas.clipPath(getRoundedRectPath());
162         // Calculate the right sizing ensuring that the image is nicely centered in the layout
163         // during transitions
164         int width = (int) Math.max((Math.min(getHeight(), getActualHeight()) * mAspectRatio),
165                 getActualWidth());
166         int height = (int) Math.max((Math.min(getWidth(), getActualWidth()) / mAspectRatio),
167                 getActualHeight());
168         height = (int) Math.max(height, width / mAspectRatio);
169         int left = (int) ((getActualWidth() - width) / 2.0f);
170         int top = (int) ((getActualHeight() - height) / 2.0f);
171         mDrawable.setBounds(left, top, left + width, top + height);
172         mDrawable.draw(canvas);
173         canvas.restore();
174     }
175 
getRoundedRectPath()176     public Path getRoundedRectPath() {
177         int left = 0;
178         int right = getActualWidth();
179         int top = 0;
180         int bottom = getActualHeight();
181         mPath.reset();
182         int width = right - left;
183         float roundnessX = mImageRounding;
184         float roundnessY = mImageRounding;
185         roundnessX = Math.min(width / 2, roundnessX);
186         roundnessY = Math.min((bottom - top) / 2, roundnessY);
187         mPath.moveTo(left, top + roundnessY);
188         mPath.quadTo(left, top, left + roundnessX, top);
189         mPath.lineTo(right - roundnessX, top);
190         mPath.quadTo(right, top, right, top + roundnessY);
191         mPath.lineTo(right, bottom - roundnessY);
192         mPath.quadTo(right, bottom, right - roundnessX, bottom);
193         mPath.lineTo(left + roundnessX, bottom);
194         mPath.quadTo(left, bottom, left, bottom - roundnessY);
195         mPath.close();
196         return mPath;
197     }
198 
recycle()199     public void recycle() {
200         MessagingMessage.super.recycle();
201         setImageBitmap(null);
202         mDrawable = null;
203         sInstancePool.release(this);
204     }
205 
dropCache()206     public static void dropCache() {
207         sInstancePool.clear();
208     }
209 
210     @Override
getMeasuredType()211     public int getMeasuredType() {
212         if (mDrawable == null) {
213             Log.e(TAG, "getMeasuredType() after recycle()!");
214             return MEASURED_NORMAL;
215         }
216 
217         int measuredHeight = getMeasuredHeight();
218         int minImageHeight;
219         if (mIsIsolated) {
220             minImageHeight = mIsolatedSize;
221         } else {
222             minImageHeight = mMinImageHeight;
223         }
224         boolean measuredTooSmall = measuredHeight < minImageHeight
225                 && measuredHeight != mDrawable.getIntrinsicHeight();
226         if (measuredTooSmall) {
227             return MEASURED_TOO_SMALL;
228         } else {
229             if (!mIsIsolated && measuredHeight != mDrawable.getIntrinsicHeight()) {
230                 return MEASURED_SHORTENED;
231             } else {
232                 return MEASURED_NORMAL;
233             }
234         }
235     }
236 
237     @Override
238     public void setMaxDisplayedLines(int lines) {
239         // Nothing to do, this should be handled automatically.
240     }
241 
242     @Override
243     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
244         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
245 
246         if (mDrawable == null) {
247             Log.e(TAG, "onMeasure() after recycle()!");
248             setMeasuredDimension(0, 0);
249             return;
250         }
251 
252         if (mIsIsolated) {
253             // When isolated we have a fixed size, let's use that sizing.
254             setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
255                     MeasureSpec.getSize(heightMeasureSpec));
256         } else {
257             // If we are displaying inline, we never want to go wider than actual size of the
258             // image, otherwise it will look quite blurry.
259             int width = Math.min(MeasureSpec.getSize(widthMeasureSpec),
260                     mDrawable.getIntrinsicWidth());
261             int height = (int) Math.min(MeasureSpec.getSize(heightMeasureSpec), width
262                     / mAspectRatio);
263             setMeasuredDimension(width, height);
264         }
265     }
266 
267     @Override
268     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
269         super.onLayout(changed, left, top, right, bottom);
270         // TODO: ensure that this isn't called when transforming
271         setActualWidth(getWidth());
272         setActualHeight(getHeight());
273     }
274 
275     @Override
276     public int getConsumedLines() {
277         return 3;
278     }
279 
280     public void setActualWidth(int actualWidth) {
281         mActualWidth = actualWidth;
282         invalidate();
283     }
284 
285     public int getActualWidth() {
286         return mActualWidth;
287     }
288 
289     public void setActualHeight(int actualHeight) {
290         mActualHeight = actualHeight;
291         invalidate();
292     }
293 
294     public int getActualHeight() {
295         return mActualHeight;
296     }
297 
298     public void setIsolated(boolean isolated) {
299         if (mIsIsolated != isolated) {
300             mIsIsolated = isolated;
301             // update the layout params not to have margins
302             ViewGroup.MarginLayoutParams layoutParams =
303                     (ViewGroup.MarginLayoutParams) getLayoutParams();
304             layoutParams.topMargin = isolated ? 0 : mExtraSpacing;
305             setLayoutParams(layoutParams);
306         }
307     }
308 
309     @Override
310     public int getExtraSpacing() {
311         return mExtraSpacing;
312     }
313 }
314