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
17package com.android.contacts.editor;
18
19import com.android.contacts.R;
20import com.android.contacts.common.ContactPhotoManager;
21import com.android.contacts.common.ContactPhotoManager.DefaultImageProvider;
22import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
23import com.android.contacts.common.ContactsUtils;
24import com.android.contacts.common.model.RawContactDelta;
25import com.android.contacts.common.model.ValuesDelta;
26import com.android.contacts.common.model.dataitem.DataKind;
27import com.android.contacts.common.util.MaterialColorMapUtils;
28import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
29import com.android.contacts.editor.CompactContactEditorFragment.PhotoHandler;
30import com.android.contacts.util.ContactPhotoUtils;
31import com.android.contacts.util.SchedulingUtils;
32import com.android.contacts.widget.QuickContactImageView;
33
34import android.app.Activity;
35import android.content.Context;
36import android.content.res.TypedArray;
37import android.graphics.Bitmap;
38import android.graphics.BitmapFactory;
39import android.graphics.drawable.GradientDrawable;
40import android.net.Uri;
41import android.provider.ContactsContract;
42import android.provider.ContactsContract.CommonDataKinds.Photo;
43import android.provider.ContactsContract.DisplayPhoto;
44import android.util.AttributeSet;
45import android.util.DisplayMetrics;
46import android.util.Log;
47import android.util.TypedValue;
48import android.view.View;
49import android.view.ViewGroup;
50import android.widget.ImageView;
51import android.widget.RelativeLayout;
52
53/**
54 * Displays the primary photo.
55 */
56public 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
80    public CompactPhotoEditorView(Context context) {
81        this(context, null);
82    }
83
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
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
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
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     */
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
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     */
218    public void setPhotoHandler(PhotoHandler photoHandler) {
219        mPhotoHandler = photoHandler;
220    }
221
222    /**
223     * Whether a writable {@link Photo} has been set.
224     */
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     */
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     */
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
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     */
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
307    public void onClick(View view) {
308        if (mPhotoHandler != null) {
309            mPhotoHandler.onClick(view);
310        }
311    }
312}
313