• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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