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