1 /* 2 * Copyright (C) 2015 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.contacts.editor; 18 19 import com.android.contacts.R; 20 import com.android.contacts.common.ContactPhotoManager; 21 import com.android.contacts.common.ContactPhotoManager.DefaultImageProvider; 22 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; 23 import com.android.contacts.common.ContactsUtils; 24 import com.android.contacts.common.model.RawContactDelta; 25 import com.android.contacts.common.model.ValuesDelta; 26 import com.android.contacts.common.model.dataitem.DataKind; 27 import com.android.contacts.common.util.MaterialColorMapUtils; 28 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; 29 import com.android.contacts.editor.CompactContactEditorFragment.PhotoHandler; 30 import com.android.contacts.util.ContactPhotoUtils; 31 import com.android.contacts.util.SchedulingUtils; 32 import com.android.contacts.widget.QuickContactImageView; 33 34 import android.app.Activity; 35 import android.content.Context; 36 import android.content.res.TypedArray; 37 import android.graphics.Bitmap; 38 import android.graphics.BitmapFactory; 39 import android.graphics.drawable.GradientDrawable; 40 import android.net.Uri; 41 import android.provider.ContactsContract; 42 import android.provider.ContactsContract.CommonDataKinds.Photo; 43 import android.provider.ContactsContract.DisplayPhoto; 44 import android.util.AttributeSet; 45 import android.util.DisplayMetrics; 46 import android.util.Log; 47 import android.util.TypedValue; 48 import android.view.View; 49 import android.view.ViewGroup; 50 import android.widget.ImageView; 51 import android.widget.RelativeLayout; 52 53 /** 54 * Displays the primary photo. 55 */ 56 public class CompactPhotoEditorView extends RelativeLayout implements View.OnClickListener { 57 58 private static final String TAG = CompactContactEditorFragment.TAG; 59 60 private ContactPhotoManager mContactPhotoManager; 61 private PhotoHandler mPhotoHandler; 62 63 private final float mLandscapePhotoRatio; 64 private final float mPortraitPhotoRatio; 65 private final boolean mIsTwoPanel; 66 67 private final int mActionBarHeight; 68 private final int mStatusBarHeight; 69 70 private ValuesDelta mValuesDelta; 71 private boolean mReadOnly; 72 private boolean mIsPhotoSet; 73 private MaterialPalette mMaterialPalette; 74 75 private QuickContactImageView mPhotoImageView; 76 private View mPhotoIcon; 77 private View mPhotoIconOverlay; 78 private View mPhotoTouchInterceptOverlay; 79 CompactPhotoEditorView(Context context)80 public CompactPhotoEditorView(Context context) { 81 this(context, null); 82 } 83 CompactPhotoEditorView(Context context, AttributeSet attrs)84 public CompactPhotoEditorView(Context context, AttributeSet attrs) { 85 super(context, attrs); 86 mLandscapePhotoRatio = getTypedFloat(R.dimen.quickcontact_landscape_photo_ratio); 87 mPortraitPhotoRatio = getTypedFloat(R.dimen.editor_portrait_photo_ratio); 88 mIsTwoPanel = getResources().getBoolean(R.bool.quickcontact_two_panel); 89 90 final TypedArray styledAttributes = getContext().getTheme().obtainStyledAttributes( 91 new int[] { android.R.attr.actionBarSize }); 92 mActionBarHeight = (int) styledAttributes.getDimension(0, 0); 93 styledAttributes.recycle(); 94 95 final int resourceId = getResources().getIdentifier( 96 "status_bar_height", "dimen", "android"); 97 mStatusBarHeight = resourceId > 0 ? getResources().getDimensionPixelSize(resourceId) : 0; 98 } 99 getTypedFloat(int resourceId)100 private float getTypedFloat(int resourceId) { 101 final TypedValue typedValue = new TypedValue(); 102 getResources().getValue(resourceId, typedValue, /* resolveRefs =*/ true); 103 return typedValue.getFloat(); 104 } 105 106 @Override onFinishInflate()107 protected void onFinishInflate() { 108 super.onFinishInflate(); 109 mContactPhotoManager = ContactPhotoManager.getInstance(getContext()); 110 111 mPhotoImageView = (QuickContactImageView) findViewById(R.id.photo); 112 mPhotoIcon = findViewById(R.id.photo_icon); 113 mPhotoIconOverlay = findViewById(R.id.photo_icon_overlay); 114 mPhotoTouchInterceptOverlay = findViewById(R.id.photo_touch_intercept_overlay); 115 } 116 setValues(DataKind dataKind, ValuesDelta valuesDelta, RawContactDelta rawContactDelta, boolean readOnly, MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator)117 public void setValues(DataKind dataKind, ValuesDelta valuesDelta, 118 RawContactDelta rawContactDelta, boolean readOnly, MaterialPalette materialPalette, 119 ViewIdGenerator viewIdGenerator) { 120 mValuesDelta = valuesDelta; 121 mReadOnly = readOnly; 122 mMaterialPalette = materialPalette; 123 124 if (mReadOnly) { 125 mPhotoIcon.setVisibility(View.GONE); 126 mPhotoIconOverlay.setVisibility(View.GONE); 127 } else { 128 mPhotoTouchInterceptOverlay.setOnClickListener(this); 129 } 130 131 setId(viewIdGenerator.getId(rawContactDelta, dataKind, valuesDelta, /* viewIndex =*/ 0)); 132 133 setPhoto(valuesDelta); 134 } 135 136 /** 137 * Sets the photo bitmap on this view from the given ValuesDelta. Note that the 138 * RawContactDelta underlying this view is not modified in any way. Using this method allows 139 * you to show one photo (from a read-only contact, for example) and yet have a different 140 * raw contact updated when a new photo is set (from the new raw contact created and attached 141 * to the read-only contact). See go/editing-read-only-contacts 142 */ setPhoto(ValuesDelta valuesDelta)143 public void setPhoto(ValuesDelta valuesDelta) { 144 if (valuesDelta == null) { 145 setDefaultPhoto(); 146 } else { 147 final byte[] bytes = valuesDelta.getAsByteArray(Photo.PHOTO); 148 if (bytes == null) { 149 setDefaultPhoto(); 150 } else { 151 final Bitmap bitmap = BitmapFactory.decodeByteArray( 152 bytes, /* offset =*/ 0, bytes.length); 153 mPhotoImageView.setImageBitmap(bitmap); 154 mIsPhotoSet = true; 155 mValuesDelta.setFromTemplate(false); 156 157 // Check if we can update to the full size photo immediately 158 if (valuesDelta.getAfter() == null 159 || valuesDelta.getAfter().get(Photo.PHOTO) == null) { 160 // If the user hasn't updated the PHOTO value, then PHOTO_FILE_ID may contain 161 // a reference to a larger version of PHOTO that we can bind to the UI. 162 // Otherwise, we need to wait for a call to #setFullSizedPhoto() to update 163 // our full sized image. 164 final Long fileId = valuesDelta.getAsLong(Photo.PHOTO_FILE_ID); 165 if (fileId != null) { 166 final Uri photoUri = DisplayPhoto.CONTENT_URI.buildUpon() 167 .appendPath(fileId.toString()).build(); 168 setFullSizedPhoto(photoUri); 169 } 170 } 171 } 172 } 173 174 if (mIsPhotoSet) { 175 // Add background color behind the white photo icon so that it's visible even 176 // if the contact photo is white. 177 mPhotoIconOverlay.setBackground(new GradientDrawable( 178 GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0, 0x88000000})); 179 } else { 180 setDefaultPhotoTint(); 181 } 182 183 // Adjust the photo dimensions following the same logic as MultiShrinkScroll.initialize 184 SchedulingUtils.doOnPreDraw(this, /* drawNextFrame =*/ false, new Runnable() { 185 @Override 186 public void run() { 187 final int photoHeight, photoWidth; 188 if (mIsTwoPanel) { 189 photoHeight = getContentViewHeight(); 190 photoWidth = (int) (photoHeight * mLandscapePhotoRatio); 191 } else { 192 // Make the photo slightly shorter that it is wide 193 photoWidth = getWidth(); 194 photoHeight = (int) (photoWidth / mPortraitPhotoRatio); 195 } 196 final ViewGroup.LayoutParams layoutParams = getLayoutParams(); 197 layoutParams.height = photoHeight; 198 layoutParams.width = photoWidth; 199 setLayoutParams(layoutParams); 200 } 201 }); 202 } 203 204 // We're calculating the height the hard way because using the height of the content view 205 // (found using android.view.Window.ID_ANDROID_CONTENT) with the soft keyboard up when 206 // going from portrait to landscape mode results in a very small height value. 207 // See b/20526470 getContentViewHeight()208 private int getContentViewHeight() { 209 final Activity activity = (Activity) getContext(); 210 final DisplayMetrics displayMetrics = new DisplayMetrics(); 211 activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); 212 return displayMetrics.heightPixels - mActionBarHeight - mStatusBarHeight; 213 } 214 215 /** 216 * Set the {@link PhotoHandler} to forward clicks (i.e. requests to edit the photo) to. 217 */ setPhotoHandler(PhotoHandler photoHandler)218 public void setPhotoHandler(PhotoHandler photoHandler) { 219 mPhotoHandler = photoHandler; 220 } 221 222 /** 223 * Whether a writable {@link Photo} has been set. 224 */ isWritablePhotoSet()225 public boolean isWritablePhotoSet() { 226 return mIsPhotoSet && !mReadOnly; 227 } 228 229 /** 230 * Set the given {@link Bitmap} as the photo in the underlying {@link ValuesDelta} 231 * and bind a thumbnail to the UI. 232 */ setPhoto(Bitmap bitmap)233 public void setPhoto(Bitmap bitmap) { 234 if (mReadOnly) { 235 Log.w(TAG, "Attempted to set read only photo. Aborting"); 236 return; 237 } 238 if (bitmap == null) { 239 mValuesDelta.put(ContactsContract.CommonDataKinds.Photo.PHOTO, (byte[]) null); 240 setDefaultPhoto(); 241 return; 242 } 243 244 final int thumbnailSize = ContactsUtils.getThumbnailSize(getContext()); 245 final Bitmap scaledBitmap = Bitmap.createScaledBitmap( 246 bitmap, thumbnailSize, thumbnailSize, /* filter =*/ false); 247 248 mPhotoImageView.setImageBitmap(scaledBitmap); 249 mIsPhotoSet = true; 250 mValuesDelta.setFromTemplate(false); 251 252 // When the user chooses a new photo mark it as super primary 253 mValuesDelta.setSuperPrimary(true); 254 255 // Even though high-res photos cannot be saved by passing them via 256 // an EntityDeltaList (since they cause the Bundle size limit to be 257 // exceeded), we still pass a low-res thumbnail. This simplifies 258 // code all over the place, because we don't have to test whether 259 // there is a change in EITHER the delta-list OR a changed photo... 260 // this way, there is always a change in the delta-list. 261 final byte[] compressed = ContactPhotoUtils.compressBitmap(scaledBitmap); 262 if (compressed != null) { 263 mValuesDelta.setPhoto(compressed); 264 } 265 } 266 267 /** 268 * Show the default "add photo" place holder. 269 */ setDefaultPhoto()270 private void setDefaultPhoto() { 271 mPhotoImageView.setImageDrawable(ContactPhotoManager.getDefaultAvatarDrawableForContact( 272 getResources(), /* hires =*/ false, /* defaultImageRequest =*/ null)); 273 setDefaultPhotoTint(); 274 mIsPhotoSet = false; 275 mValuesDelta.setFromTemplate(true); 276 } 277 setDefaultPhotoTint()278 private void setDefaultPhotoTint() { 279 final int color = mMaterialPalette == null 280 ? MaterialColorMapUtils.getDefaultPrimaryAndSecondaryColors( 281 getResources()).mPrimaryColor 282 : mMaterialPalette.mPrimaryColor; 283 mPhotoImageView.setTint(color); 284 } 285 286 /** 287 * Bind the photo at the given Uri to the UI but do not set the photo on the underlying 288 * {@link ValuesDelta}. 289 */ setFullSizedPhoto(Uri photoUri)290 public void setFullSizedPhoto(Uri photoUri) { 291 if (photoUri != null) { 292 final DefaultImageProvider fallbackToPreviousImage = new DefaultImageProvider() { 293 @Override 294 public void applyDefaultImage(ImageView view, int extent, boolean darkTheme, 295 DefaultImageRequest defaultImageRequest) { 296 // Before we finish setting the full sized image, don't change the current 297 // image that is set in any way. 298 } 299 }; 300 mContactPhotoManager.loadPhoto(mPhotoImageView, photoUri, 301 mPhotoImageView.getWidth(), /* darkTheme =*/ false, /* isCircular =*/ false, 302 /* defaultImageRequest =*/ null, fallbackToPreviousImage); 303 } 304 } 305 306 @Override onClick(View view)307 public void onClick(View view) { 308 if (mPhotoHandler != null) { 309 mPhotoHandler.onClick(view); 310 } 311 } 312 } 313