Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
14792 manas 1
/**
2
 * Copyright 2010-present Facebook.
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.facebook.widget;
18
 
19
import android.content.Context;
20
import android.content.res.TypedArray;
21
import android.graphics.Bitmap;
22
import android.graphics.BitmapFactory;
23
import android.os.Bundle;
24
import android.os.Parcelable;
25
import android.util.AttributeSet;
26
import android.util.Log;
27
import android.view.ViewGroup;
28
import android.widget.FrameLayout;
29
import android.widget.ImageView;
30
import com.facebook.FacebookException;
31
import com.facebook.LoggingBehavior;
32
import com.facebook.android.R;
33
import com.facebook.internal.*;
34
 
35
import java.net.URISyntaxException;
36
 
37
/**
38
 * View that displays the profile photo of a supplied profile ID, while conforming
39
 * to user specified dimensions.
40
 */
41
public class ProfilePictureView extends FrameLayout {
42
 
43
    /**
44
     * Callback interface that will be called when a network or other error is encountered
45
     * while retrieving profile pictures.
46
     */
47
    public interface OnErrorListener {
48
        /**
49
         * Called when a network or other error is encountered.
50
         * @param error     a FacebookException representing the error that was encountered.
51
         */
52
        void onError(FacebookException error);
53
    }
54
 
55
    /**
56
     * Tag used when logging calls are made by ProfilePictureView
57
     */
58
    public static final String TAG = ProfilePictureView.class.getSimpleName();
59
 
60
    /**
61
     * Indicates that the specific size of the View will be set via layout params.
62
     * ProfilePictureView will default to NORMAL X NORMAL, if the layout params set on
63
     * this instance do not have a fixed size.
64
     * Used in calls to setPresetSize() and getPresetSize().
65
     * Corresponds with the preset_size Xml attribute that can be set on ProfilePictureView.
66
     */
67
    public static final int CUSTOM = -1;
68
 
69
    /**
70
     * Indicates that the profile image should fit in a SMALL X SMALL space, regardless
71
     * of whether the cropped or un-cropped version is chosen.
72
     * Used in calls to setPresetSize() and getPresetSize().
73
     * Corresponds with the preset_size Xml attribute that can be set on ProfilePictureView.
74
     */
75
    public static final int SMALL = -2;
76
 
77
    /**
78
     * Indicates that the profile image should fit in a NORMAL X NORMAL space, regardless
79
     * of whether the cropped or un-cropped version is chosen.
80
     * Used in calls to setPresetSize() and getPresetSize().
81
     * Corresponds with the preset_size Xml attribute that can be set on ProfilePictureView.
82
     */
83
    public static final int NORMAL = -3;
84
 
85
    /**
86
     * Indicates that the profile image should fit in a LARGE X LARGE space, regardless
87
     * of whether the cropped or un-cropped version is chosen.
88
     * Used in calls to setPresetSize() and getPresetSize().
89
     * Corresponds with the preset_size Xml attribute that can be set on ProfilePictureView.
90
     */
91
    public static final int LARGE = -4;
92
 
93
    private static final int MIN_SIZE = 1;
94
    private static final boolean IS_CROPPED_DEFAULT_VALUE = true;
95
    private static final String SUPER_STATE_KEY = "ProfilePictureView_superState";
96
    private static final String PROFILE_ID_KEY = "ProfilePictureView_profileId";
97
    private static final String PRESET_SIZE_KEY = "ProfilePictureView_presetSize";
98
    private static final String IS_CROPPED_KEY = "ProfilePictureView_isCropped";
99
    private static final String BITMAP_KEY = "ProfilePictureView_bitmap";
100
    private static final String BITMAP_WIDTH_KEY = "ProfilePictureView_width";
101
    private static final String BITMAP_HEIGHT_KEY = "ProfilePictureView_height";
102
    private static final String PENDING_REFRESH_KEY = "ProfilePictureView_refresh";
103
 
104
    private String profileId;
105
    private int queryHeight = ImageRequest.UNSPECIFIED_DIMENSION;
106
    private int queryWidth = ImageRequest.UNSPECIFIED_DIMENSION;
107
    private boolean isCropped = IS_CROPPED_DEFAULT_VALUE;
108
    private Bitmap imageContents;
109
    private ImageView image;
110
    private int presetSizeType = CUSTOM;
111
    private ImageRequest lastRequest;
112
    private OnErrorListener onErrorListener;
113
    private Bitmap customizedDefaultProfilePicture = null;
114
 
115
    /**
116
     * Constructor
117
     *
118
     * @param context Context for this View
119
     */
120
    public ProfilePictureView(Context context) {
121
        super(context);
122
        initialize(context);
123
    }
124
 
125
    /**
126
     * Constructor
127
     *
128
     * @param context Context for this View
129
     * @param attrs   AttributeSet for this View.
130
     *                The attribute 'preset_size' is processed here
131
     */
132
    public ProfilePictureView(Context context, AttributeSet attrs) {
133
        super(context, attrs);
134
        initialize(context);
135
        parseAttributes(attrs);
136
    }
137
 
138
    /**
139
     * Constructor
140
     *
141
     * @param context  Context for this View
142
     * @param attrs    AttributeSet for this View.
143
     *                 The attribute 'preset_size' is processed here
144
     * @param defStyle Default style for this View
145
     */
146
    public ProfilePictureView(Context context, AttributeSet attrs, int defStyle) {
147
        super(context, attrs, defStyle);
148
        initialize(context);
149
        parseAttributes(attrs);
150
    }
151
 
152
    /**
153
     * Gets the current preset size type
154
     *
155
     * @return The current preset size type, if set; CUSTOM if not
156
     */
157
    public final int getPresetSize() {
158
        return presetSizeType;
159
    }
160
 
161
    /**
162
     * Apply a preset size to this profile photo
163
     *
164
     * @param sizeType The size type to apply: SMALL, NORMAL or LARGE
165
     */
166
    public final void setPresetSize(int sizeType) {
167
        switch (sizeType) {
168
            case SMALL:
169
            case NORMAL:
170
            case LARGE:
171
            case CUSTOM:
172
                this.presetSizeType = sizeType;
173
                break;
174
 
175
            default:
176
                throw new IllegalArgumentException("Must use a predefined preset size");
177
        }
178
 
179
        requestLayout();
180
    }
181
 
182
    /**
183
     * Indicates whether the cropped version of the profile photo has been chosen
184
     *
185
     * @return True if the cropped version is chosen, false if not.
186
     */
187
    public final boolean isCropped() {
188
        return isCropped;
189
    }
190
 
191
    /**
192
     * Sets the profile photo to be the cropped version, or the original version
193
     *
194
     * @param showCroppedVersion True to select the cropped version
195
     *                           False to select the standard version
196
     */
197
    public final void setCropped(boolean showCroppedVersion) {
198
        isCropped = showCroppedVersion;
199
        // No need to force the refresh since we will catch the change in required dimensions
200
        refreshImage(false);
201
    }
202
 
203
    /**
204
     * Returns the profile Id for the current profile photo
205
     *
206
     * @return The profile Id
207
     */
208
    public final String getProfileId() {
209
        return profileId;
210
    }
211
 
212
    /**
213
     * Sets the profile Id for this profile photo
214
     *
215
     * @param profileId The profileId
216
     *               NULL/Empty String will show the blank profile photo
217
     */
218
    public final void setProfileId(String profileId) {
219
        boolean force = false;
220
        if (Utility.isNullOrEmpty(this.profileId) || !this.profileId.equalsIgnoreCase(profileId)) {
221
            // Clear out the old profilePicture before requesting for the new one.
222
            setBlankProfilePicture();
223
            force = true;
224
        }
225
 
226
        this.profileId = profileId;
227
        refreshImage(force);
228
    }
229
 
230
    /**
231
     * Returns the current OnErrorListener for this instance of ProfilePictureView
232
     *
233
     * @return The OnErrorListener
234
     */
235
    public final OnErrorListener getOnErrorListener() {
236
        return onErrorListener;
237
    }
238
 
239
    /**
240
     * Sets an OnErrorListener for this instance of ProfilePictureView to call into when
241
     * certain exceptions occur.
242
     *
243
     * @param onErrorListener The Listener object to set
244
     */
245
    public final void setOnErrorListener(OnErrorListener onErrorListener) {
246
      this.onErrorListener = onErrorListener;
247
    }
248
 
249
    /**
250
     * The ProfilePictureView will display the provided image while the specified
251
     * profile is being loaded, or if the specified profile is not available.
252
     *
253
     * @param inputBitmap The bitmap to render until the actual profile is loaded.
254
     */
255
    public final void setDefaultProfilePicture(Bitmap inputBitmap) {
256
        customizedDefaultProfilePicture = inputBitmap;
257
    }
258
 
259
    /**
260
     * Overriding onMeasure to handle the case where WRAP_CONTENT might be
261
     * specified in the layout. Since we don't know the dimensions of the profile
262
     * photo, we need to handle this case specifically.
263
     * <p/>
264
     * The approach is to default to a NORMAL sized amount of space in the case that
265
     * a preset size is not specified. This logic is applied to both width and height
266
     */
267
    @Override
268
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
269
        ViewGroup.LayoutParams params = getLayoutParams();
270
        boolean customMeasure = false;
271
        int newHeight = MeasureSpec.getSize(heightMeasureSpec);
272
        int newWidth = MeasureSpec.getSize(widthMeasureSpec);
273
        if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY &&
274
                params.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
275
            newHeight = getPresetSizeInPixels(true); // Default to a preset size
276
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY);
277
            customMeasure = true;
278
        }
279
 
280
        if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY &&
281
                params.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
282
            newWidth = getPresetSizeInPixels(true); // Default to a preset size
283
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY);
284
            customMeasure = true;
285
        }
286
 
287
        if (customMeasure) {
288
            // Since we are providing custom dimensions, we need to handle the measure
289
            // phase from here
290
            setMeasuredDimension(newWidth, newHeight);
291
            measureChildren(widthMeasureSpec, heightMeasureSpec);
292
        } else {
293
            // Rely on FrameLayout to do the right thing
294
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
295
        }
296
    }
297
 
298
    /**
299
     * In addition to calling super.Layout(), we also attempt to get a new image that
300
     * is properly size for the layout dimensions
301
     */
302
    @Override
303
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
304
        super.onLayout(changed, left, top, right, bottom);
305
 
306
        // See if the image needs redrawing
307
        refreshImage(false);
308
    }
309
 
310
    /**
311
     * Some of the current state is returned as a Bundle to allow quick restoration
312
     * of the ProfilePictureView object in scenarios like orientation changes.
313
     * @return a Parcelable containing the current state
314
     */
315
    @Override
316
    protected Parcelable onSaveInstanceState() {
317
        Parcelable superState = super.onSaveInstanceState();
318
        Bundle instanceState = new Bundle();
319
        instanceState.putParcelable(SUPER_STATE_KEY, superState);
320
        instanceState.putString(PROFILE_ID_KEY, profileId);
321
        instanceState.putInt(PRESET_SIZE_KEY, presetSizeType);
322
        instanceState.putBoolean(IS_CROPPED_KEY, isCropped);
323
        instanceState.putParcelable(BITMAP_KEY, imageContents);
324
        instanceState.putInt(BITMAP_WIDTH_KEY, queryWidth);
325
        instanceState.putInt(BITMAP_HEIGHT_KEY, queryHeight);
326
        instanceState.putBoolean(PENDING_REFRESH_KEY, lastRequest != null);
327
 
328
        return instanceState;
329
    }
330
 
331
    /**
332
     * If the passed in state is a Bundle, an attempt is made to restore from it.
333
     * @param state a Parcelable containing the current state
334
     */
335
    @Override
336
    protected void onRestoreInstanceState(Parcelable state) {
337
        if (state.getClass() != Bundle.class) {
338
            super.onRestoreInstanceState(state);
339
        } else {
340
            Bundle instanceState = (Bundle)state;
341
            super.onRestoreInstanceState(instanceState.getParcelable(SUPER_STATE_KEY));
342
 
343
            profileId = instanceState.getString(PROFILE_ID_KEY);
344
            presetSizeType = instanceState.getInt(PRESET_SIZE_KEY);
345
            isCropped = instanceState.getBoolean(IS_CROPPED_KEY);
346
            queryWidth = instanceState.getInt(BITMAP_WIDTH_KEY);
347
            queryHeight = instanceState.getInt(BITMAP_HEIGHT_KEY);
348
 
349
            setImageBitmap((Bitmap)instanceState.getParcelable(BITMAP_KEY));
350
 
351
            if (instanceState.getBoolean(PENDING_REFRESH_KEY)) {
352
                refreshImage(true);
353
            }
354
        }
355
    }
356
 
357
    @Override
358
    protected void onDetachedFromWindow() {
359
        super.onDetachedFromWindow();
360
 
361
        // Null out lastRequest. This way, when the response is returned, we can ascertain
362
        // that the view is detached and hence should not attempt to update its contents.
363
        lastRequest = null;
364
    }
365
 
366
    private void initialize(Context context) {
367
        // We only want our ImageView in here. Nothing else is permitted
368
        removeAllViews();
369
 
370
        image = new ImageView(context);
371
 
372
        LayoutParams imageLayout = new LayoutParams(
373
                LayoutParams.MATCH_PARENT,
374
                LayoutParams.MATCH_PARENT);
375
 
376
        image.setLayoutParams(imageLayout);
377
 
378
        // We want to prevent up-scaling the image, but still have it fit within
379
        // the layout bounds as best as possible.
380
        image.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
381
        addView(image);
382
    }
383
 
384
    private void parseAttributes(AttributeSet attrs) {
385
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.com_facebook_profile_picture_view);
386
        setPresetSize(a.getInt(R.styleable.com_facebook_profile_picture_view_preset_size, CUSTOM));
387
        isCropped = a.getBoolean(R.styleable.com_facebook_profile_picture_view_is_cropped, IS_CROPPED_DEFAULT_VALUE);
388
        a.recycle();
389
    }
390
 
391
    private void refreshImage(boolean force) {
392
        boolean changed = updateImageQueryParameters();
393
        // Note: do not use Utility.isNullOrEmpty here as this will cause the Eclipse
394
        // Graphical Layout editor to fail in some cases
395
        if (profileId == null || profileId.length() == 0 ||
396
                ((queryWidth == ImageRequest.UNSPECIFIED_DIMENSION) &&
397
                        (queryHeight == ImageRequest.UNSPECIFIED_DIMENSION))) {
398
            setBlankProfilePicture();
399
        } else if (changed || force) {
400
            sendImageRequest(true);
401
        }
402
    }
403
 
404
    private void setBlankProfilePicture() {
405
        if (customizedDefaultProfilePicture == null) {
406
          int blankImageResource = isCropped() ?
407
                  R.drawable.com_facebook_profile_picture_blank_square :
408
                  R.drawable.com_facebook_profile_picture_blank_portrait;
409
          setImageBitmap( BitmapFactory.decodeResource(getResources(), blankImageResource));
410
	} else {
411
          // Update profile image dimensions.
412
          updateImageQueryParameters();
413
          // Resize inputBitmap to new dimensions of queryWidth and queryHeight.
414
          Bitmap scaledBitmap = Bitmap.createScaledBitmap(customizedDefaultProfilePicture, queryWidth, queryHeight, false);
415
          setImageBitmap(scaledBitmap);
416
	}
417
    }
418
 
419
    private void setImageBitmap(Bitmap imageBitmap) {
420
        if (image != null && imageBitmap != null) {
421
            imageContents = imageBitmap; // Hold for save-restore cycles
422
            image.setImageBitmap(imageBitmap);
423
        }
424
    }
425
 
426
    private void sendImageRequest(boolean allowCachedResponse) {
427
        try {
428
            ImageRequest.Builder requestBuilder = new ImageRequest.Builder(
429
                    getContext(),
430
                    ImageRequest.getProfilePictureUrl(profileId, queryWidth,  queryHeight));
431
 
432
            ImageRequest request = requestBuilder.setAllowCachedRedirects(allowCachedResponse)
433
                    .setCallerTag(this)
434
                    .setCallback(
435
                    new ImageRequest.Callback() {
436
                        @Override
437
                        public void onCompleted(ImageResponse response) {
438
                            processResponse(response);
439
                        }
440
                    })
441
                    .build();
442
 
443
            // Make sure to cancel the old request before sending the new one to prevent
444
            // accidental cancellation of the new request. This could happen if the URL and
445
            // caller tag stayed the same.
446
            if (lastRequest != null) {
447
                ImageDownloader.cancelRequest(lastRequest);
448
            }
449
            lastRequest = request;
450
 
451
            ImageDownloader.downloadAsync(request);
452
        } catch (URISyntaxException e) {
453
            Logger.log(LoggingBehavior.REQUESTS, Log.ERROR, TAG, e.toString());
454
        }
455
    }
456
 
457
    private void processResponse(ImageResponse response) {
458
        // First check if the response is for the right request. We may have:
459
        // 1. Sent a new request, thus super-ceding this one.
460
        // 2. Detached this view, in which case the response should be discarded.
461
        if (response.getRequest() == lastRequest) {
462
            lastRequest = null;
463
            Bitmap responseImage = response.getBitmap();
464
            Exception error = response.getError();
465
            if (error != null) {
466
                OnErrorListener listener = onErrorListener;
467
                if (listener != null) {
468
                    listener.onError(new FacebookException(
469
                            "Error in downloading profile picture for profileId: " + getProfileId(), error));
470
                } else {
471
                    Logger.log(LoggingBehavior.REQUESTS, Log.ERROR, TAG, error.toString());
472
                }
473
            } else if (responseImage != null) {
474
                setImageBitmap(responseImage);
475
 
476
                if (response.isCachedRedirect()) {
477
                    sendImageRequest(false);
478
                }
479
            }
480
        }
481
    }
482
 
483
    private boolean updateImageQueryParameters() {
484
        int newHeightPx = getHeight();
485
        int newWidthPx = getWidth();
486
        if (newWidthPx < MIN_SIZE || newHeightPx < MIN_SIZE) {
487
            // Not enough space laid out for this View yet. Or something else is awry.
488
            return false;
489
        }
490
 
491
        int presetSize = getPresetSizeInPixels(false);
492
        if (presetSize != ImageRequest.UNSPECIFIED_DIMENSION) {
493
            newWidthPx = presetSize;
494
            newHeightPx = presetSize;
495
        }
496
 
497
        // The cropped version is square
498
        // If full version is desired, then only one dimension is required.
499
        if (newWidthPx <= newHeightPx) {
500
            newHeightPx = isCropped() ? newWidthPx : ImageRequest.UNSPECIFIED_DIMENSION;
501
        } else {
502
            newWidthPx = isCropped() ? newHeightPx : ImageRequest.UNSPECIFIED_DIMENSION;
503
        }
504
 
505
        boolean changed = (newWidthPx != queryWidth) || (newHeightPx != queryHeight);
506
 
507
        queryWidth = newWidthPx;
508
        queryHeight = newHeightPx;
509
 
510
        return changed;
511
    }
512
 
513
    private int getPresetSizeInPixels(boolean forcePreset) {
514
        int dimensionId;
515
        switch (presetSizeType) {
516
            case SMALL:
517
                dimensionId = R.dimen.com_facebook_profilepictureview_preset_size_small;
518
                break;
519
            case NORMAL:
520
                dimensionId = R.dimen.com_facebook_profilepictureview_preset_size_normal;
521
                break;
522
            case LARGE:
523
                dimensionId = R.dimen.com_facebook_profilepictureview_preset_size_large;
524
                break;
525
            case CUSTOM:
526
                if (!forcePreset) {
527
                    return ImageRequest.UNSPECIFIED_DIMENSION;
528
                } else {
529
                    dimensionId = R.dimen.com_facebook_profilepictureview_preset_size_normal;
530
                    break;
531
                }
532
            default:
533
                return ImageRequest.UNSPECIFIED_DIMENSION;
534
        }
535
 
536
        return getResources().getDimensionPixelSize(dimensionId);
537
    }
538
}