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