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