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)96 public boolean setMessage(Notification.MessagingStyle.Message message) { 97 MessagingMessage.super.setMessage(message); 98 Drawable drawable; 99 try { 100 Uri uri = message.getDataUri(); 101 drawable = mImageResolver != null ? mImageResolver.loadImage(uri) : 102 LocalImageResolver.resolveImage(uri, getContext()); 103 } catch (IOException | SecurityException e) { 104 e.printStackTrace(); 105 return false; 106 } 107 if (drawable == null) { 108 return false; 109 } 110 int intrinsicHeight = drawable.getIntrinsicHeight(); 111 if (intrinsicHeight == 0) { 112 Log.w(TAG, "Drawable with 0 intrinsic height was returned"); 113 return false; 114 } 115 mDrawable = drawable; 116 mAspectRatio = ((float) mDrawable.getIntrinsicWidth()) / intrinsicHeight; 117 setImageDrawable(drawable); 118 setContentDescription(message.getText()); 119 return true; 120 } 121 createMessage(IMessagingLayout layout, Notification.MessagingStyle.Message m, ImageResolver resolver)122 static MessagingMessage createMessage(IMessagingLayout layout, 123 Notification.MessagingStyle.Message m, ImageResolver resolver) { 124 MessagingLinearLayout messagingLinearLayout = layout.getMessagingLinearLayout(); 125 MessagingImageMessage createdMessage = sInstancePool.acquire(); 126 if (createdMessage == null) { 127 createdMessage = (MessagingImageMessage) LayoutInflater.from( 128 layout.getContext()).inflate( 129 R.layout.notification_template_messaging_image_message, 130 messagingLinearLayout, 131 false); 132 createdMessage.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR); 133 } 134 createdMessage.setImageResolver(resolver); 135 boolean created = createdMessage.setMessage(m); 136 if (!created) { 137 createdMessage.recycle(); 138 return MessagingTextMessage.createMessage(layout, m); 139 } 140 return createdMessage; 141 } 142 setImageResolver(ImageResolver resolver)143 private void setImageResolver(ImageResolver resolver) { 144 mImageResolver = resolver; 145 } 146 147 @Override onDraw(Canvas canvas)148 protected void onDraw(Canvas canvas) { 149 canvas.save(); 150 canvas.clipPath(getRoundedRectPath()); 151 // Calculate the right sizing ensuring that the image is nicely centered in the layout 152 // during transitions 153 int width = (int) Math.max((Math.min(getHeight(), getActualHeight()) * mAspectRatio), 154 getActualWidth()); 155 int height = (int) Math.max((Math.min(getWidth(), getActualWidth()) / mAspectRatio), 156 getActualHeight()); 157 height = (int) Math.max(height, width / mAspectRatio); 158 int left = (int) ((getActualWidth() - width) / 2.0f); 159 int top = (int) ((getActualHeight() - height) / 2.0f); 160 mDrawable.setBounds(left, top, left + width, top + height); 161 mDrawable.draw(canvas); 162 canvas.restore(); 163 } 164 getRoundedRectPath()165 public Path getRoundedRectPath() { 166 int left = 0; 167 int right = getActualWidth(); 168 int top = 0; 169 int bottom = getActualHeight(); 170 mPath.reset(); 171 int width = right - left; 172 float roundnessX = mImageRounding; 173 float roundnessY = mImageRounding; 174 roundnessX = Math.min(width / 2, roundnessX); 175 roundnessY = Math.min((bottom - top) / 2, roundnessY); 176 mPath.moveTo(left, top + roundnessY); 177 mPath.quadTo(left, top, left + roundnessX, top); 178 mPath.lineTo(right - roundnessX, top); 179 mPath.quadTo(right, top, right, top + roundnessY); 180 mPath.lineTo(right, bottom - roundnessY); 181 mPath.quadTo(right, bottom, right - roundnessX, bottom); 182 mPath.lineTo(left + roundnessX, bottom); 183 mPath.quadTo(left, bottom, left, bottom - roundnessY); 184 mPath.close(); 185 return mPath; 186 } 187 recycle()188 public void recycle() { 189 MessagingMessage.super.recycle(); 190 setImageBitmap(null); 191 mDrawable = null; 192 sInstancePool.release(this); 193 } 194 dropCache()195 public static void dropCache() { 196 sInstancePool.clear(); 197 } 198 199 @Override getMeasuredType()200 public int getMeasuredType() { 201 int measuredHeight = getMeasuredHeight(); 202 int minImageHeight; 203 if (mIsIsolated) { 204 minImageHeight = mIsolatedSize; 205 } else { 206 minImageHeight = mMinImageHeight; 207 } 208 boolean measuredTooSmall = measuredHeight < minImageHeight 209 && measuredHeight != mDrawable.getIntrinsicHeight(); 210 if (measuredTooSmall) { 211 return MEASURED_TOO_SMALL; 212 } else { 213 if (!mIsIsolated && measuredHeight != mDrawable.getIntrinsicHeight()) { 214 return MEASURED_SHORTENED; 215 } else { 216 return MEASURED_NORMAL; 217 } 218 } 219 } 220 221 @Override 222 public void setMaxDisplayedLines(int lines) { 223 // Nothing to do, this should be handled automatically. 224 } 225 226 @Override 227 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 228 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 229 230 if (mDrawable == null) { 231 Log.e(TAG, "onMeasure() after recycle()!"); 232 setMeasuredDimension(0, 0); 233 return; 234 } 235 236 if (mIsIsolated) { 237 // When isolated we have a fixed size, let's use that sizing. 238 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 239 MeasureSpec.getSize(heightMeasureSpec)); 240 } else { 241 // If we are displaying inline, we never want to go wider than actual size of the 242 // image, otherwise it will look quite blurry. 243 int width = Math.min(MeasureSpec.getSize(widthMeasureSpec), 244 mDrawable.getIntrinsicWidth()); 245 int height = (int) Math.min(MeasureSpec.getSize(heightMeasureSpec), width 246 / mAspectRatio); 247 setMeasuredDimension(width, height); 248 } 249 } 250 251 @Override 252 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 253 super.onLayout(changed, left, top, right, bottom); 254 // TODO: ensure that this isn't called when transforming 255 setActualWidth(getWidth()); 256 setActualHeight(getHeight()); 257 } 258 259 @Override 260 public int getConsumedLines() { 261 return 3; 262 } 263 264 public void setActualWidth(int actualWidth) { 265 mActualWidth = actualWidth; 266 invalidate(); 267 } 268 269 public int getActualWidth() { 270 return mActualWidth; 271 } 272 273 public void setActualHeight(int actualHeight) { 274 mActualHeight = actualHeight; 275 invalidate(); 276 } 277 278 public int getActualHeight() { 279 return mActualHeight; 280 } 281 282 public void setIsolated(boolean isolated) { 283 if (mIsIsolated != isolated) { 284 mIsIsolated = isolated; 285 // update the layout params not to have margins 286 ViewGroup.MarginLayoutParams layoutParams = 287 (ViewGroup.MarginLayoutParams) getLayoutParams(); 288 layoutParams.topMargin = isolated ? 0 : mExtraSpacing; 289 setLayoutParams(layoutParams); 290 } 291 } 292 293 @Override 294 public int getExtraSpacing() { 295 return mExtraSpacing; 296 } 297 } 298