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.model.ValuesDelta;
22 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
23 import com.android.contacts.util.SchedulingUtils;
24 import com.android.contacts.widget.QuickContactImageView;
25 
26 import android.app.Activity;
27 import android.content.Context;
28 import android.content.res.TypedArray;
29 import android.graphics.Bitmap;
30 import android.net.Uri;
31 import android.provider.ContactsContract;
32 import android.util.AttributeSet;
33 import android.util.DisplayMetrics;
34 import android.util.TypedValue;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.widget.RelativeLayout;
38 
39 /**
40  * Displays a photo and calls the host back when the user clicks it.
41  */
42 public class CompactPhotoEditorView extends RelativeLayout implements View.OnClickListener {
43 
44     /**
45      * Callbacks for the host of this view.
46      */
47     public interface Listener {
48 
49         /**
50          * Invoked when the user wants to change their photo.
51          */
onPhotoEditorViewClicked()52         void onPhotoEditorViewClicked();
53     }
54 
55     private Listener mListener;
56 
57     private final float mLandscapePhotoRatio;
58     private final float mPortraitPhotoRatio;
59     private final boolean mIsTwoPanel;
60 
61     private final int mActionBarHeight;
62     private final int mStatusBarHeight;
63 
64     private QuickContactImageView mPhotoImageView;
65     private View mPhotoIcon;
66     private View mPhotoIconOverlay;
67     private View mPhotoTouchInterceptOverlay;
68 
69     private boolean mReadOnly;
70     private boolean mIsNonDefaultPhotoBound;
71 
CompactPhotoEditorView(Context context)72     public CompactPhotoEditorView(Context context) {
73         this(context, null);
74     }
75 
CompactPhotoEditorView(Context context, AttributeSet attrs)76     public CompactPhotoEditorView(Context context, AttributeSet attrs) {
77         super(context, attrs);
78 
79         mLandscapePhotoRatio = getTypedFloat(R.dimen.quickcontact_landscape_photo_ratio);
80         mPortraitPhotoRatio = getTypedFloat(R.dimen.editor_portrait_photo_ratio);
81         mIsTwoPanel = getResources().getBoolean(R.bool.quickcontact_two_panel);
82 
83         final TypedArray styledAttributes = getContext().getTheme().obtainStyledAttributes(
84                 new int[] { android.R.attr.actionBarSize });
85         mActionBarHeight = (int) styledAttributes.getDimension(0, 0);
86         styledAttributes.recycle();
87 
88         final int resourceId = getResources().getIdentifier(
89                 "status_bar_height", "dimen", "android");
90         mStatusBarHeight = resourceId > 0 ? getResources().getDimensionPixelSize(resourceId) : 0;
91     }
92 
getTypedFloat(int resourceId)93     private float getTypedFloat(int resourceId) {
94         final TypedValue typedValue = new TypedValue();
95         getResources().getValue(resourceId, typedValue, /* resolveRefs =*/ true);
96         return typedValue.getFloat();
97     }
98 
99     @Override
onFinishInflate()100     protected void onFinishInflate() {
101         super.onFinishInflate();
102         mPhotoImageView = (QuickContactImageView) findViewById(R.id.photo);
103         mPhotoIcon = findViewById(R.id.photo_icon);
104         mPhotoIconOverlay = findViewById(R.id.photo_icon_overlay);
105         mPhotoTouchInterceptOverlay = findViewById(R.id.photo_touch_intercept_overlay);
106     }
107 
setListener(Listener listener)108     public void setListener(Listener listener) {
109         mListener = listener;
110     }
111 
setReadOnly(boolean readOnly)112     public void setReadOnly(boolean readOnly) {
113         mReadOnly = readOnly;
114         if (mReadOnly) {
115             mPhotoIcon.setVisibility(View.GONE);
116             mPhotoIconOverlay.setVisibility(View.GONE);
117         } else {
118             mPhotoTouchInterceptOverlay.setOnClickListener(this);
119         }
120     }
121 
122     /**
123      * Tries to bind a full size photo or a bitmap loaded from the given ValuesDelta,
124      * and falls back to the default avatar, tinted using the given MaterialPalette (if it's not
125      * null);
126      */
setPhoto(ValuesDelta valuesDelta, MaterialPalette materialPalette)127     public void setPhoto(ValuesDelta valuesDelta, MaterialPalette materialPalette) {
128         // Check if we can update to the full size photo immediately
129         final Long photoFileId = EditorUiUtils.getPhotoFileId(valuesDelta);
130         if (photoFileId != null) {
131             final Uri photoUri = ContactsContract.DisplayPhoto.CONTENT_URI.buildUpon()
132                     .appendPath(photoFileId.toString()).build();
133             setFullSizedPhoto(photoUri);
134             adjustDimensions();
135             return;
136         }
137 
138         // Use the bitmap image from the values delta
139         final Bitmap bitmap = EditorUiUtils.getPhotoBitmap(valuesDelta);
140         if (bitmap != null) {
141             setPhoto(bitmap);
142             adjustDimensions();
143             return;
144         }
145 
146         setDefaultPhoto(materialPalette);
147         adjustDimensions();
148     }
149 
adjustDimensions()150     private void adjustDimensions() {
151         // Follow the same logic as MultiShrinkScroll.initialize
152         SchedulingUtils.doOnPreDraw(this, /* drawNextFrame =*/ false, new Runnable() {
153             @Override
154             public void run() {
155                 final int photoHeight, photoWidth;
156                 if (mIsTwoPanel) {
157                     photoHeight = getContentViewHeight();
158                     photoWidth = (int) (photoHeight * mLandscapePhotoRatio);
159                 } else {
160                     // Make the photo slightly shorter that it is wide
161                     photoWidth = getContentViewWidth();
162                     photoHeight = (int) (photoWidth / mPortraitPhotoRatio);
163                 }
164                 final ViewGroup.LayoutParams layoutParams = getLayoutParams();
165                 layoutParams.height = photoHeight;
166                 layoutParams.width = photoWidth;
167                 setLayoutParams(layoutParams);
168             }
169         });
170     }
171 
getContentViewWidth()172     private int getContentViewWidth() {
173         final Activity activity = (Activity) getContext();
174         final DisplayMetrics displayMetrics = new DisplayMetrics();
175         activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
176         return displayMetrics.widthPixels;
177     }
178 
179     // We're calculating the height the hard way because using the height of the content view
180     // (found using android.view.Window.ID_ANDROID_CONTENT) with the soft keyboard up when
181     // going from portrait to landscape mode results in a very small height value.
182     // See b/20526470
getContentViewHeight()183     private int getContentViewHeight() {
184         final Activity activity = (Activity) getContext();
185         final DisplayMetrics displayMetrics = new DisplayMetrics();
186         activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
187         return displayMetrics.heightPixels - mActionBarHeight - mStatusBarHeight;
188     }
189 
190     /**
191      * Whether a removable, non-default photo is bound to this view.
192      */
isWritablePhotoSet()193     public boolean isWritablePhotoSet() {
194         return !mReadOnly && mIsNonDefaultPhotoBound;
195     }
196 
197     /**
198      * Binds the given bitmap.
199      */
setPhoto(Bitmap bitmap)200     private void setPhoto(Bitmap bitmap) {
201         mPhotoImageView.setImageBitmap(bitmap);
202         mIsNonDefaultPhotoBound = true;
203     }
204 
setDefaultPhoto(MaterialPalette materialPalette)205     private void setDefaultPhoto(MaterialPalette materialPalette) {
206         EditorUiUtils.setDefaultPhoto(mPhotoImageView, getResources(), materialPalette);
207     }
208 
209     /**
210      * Binds a full size photo loaded from the given Uri.
211      */
setFullSizedPhoto(Uri photoUri)212     public void setFullSizedPhoto(Uri photoUri) {
213         EditorUiUtils.loadPhoto(ContactPhotoManager.getInstance(getContext()),
214                 mPhotoImageView, photoUri);
215         mIsNonDefaultPhotoBound = true;
216     }
217 
218     /**
219      * Removes the current bound photo bitmap.
220      */
removePhoto()221     public void removePhoto() {
222         mPhotoImageView.setImageBitmap(/* bitmap =*/ null);
223         mIsNonDefaultPhotoBound = false;
224         setDefaultPhoto(/* materialPalette =*/ null);
225     }
226 
227     @Override
onClick(View view)228     public void onClick(View view) {
229         if (mListener != null) {
230             mListener.onPhotoEditorViewClicked();
231         }
232     }
233 }
234