text: /**
   * Copyright 2010-present Facebook.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *    http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.facebook.widget;
  
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.graphics.Bitmap;
  import android.graphics.BitmapFactory;
  import android.os.Bundle;
  import android.os.Parcelable;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.view.ViewGroup;
  import android.widget.FrameLayout;
  import android.widget.ImageView;
  import com.facebook.FacebookException;
  import com.facebook.LoggingBehavior;
  import com.facebook.android.R;
  import com.facebook.internal.Logger;
  import com.facebook.internal.Utility;
  
  import java.net.MalformedURLException;
  
  /**
   * View that displays the profile photo of a supplied profile ID, while conforming
   * to user specified dimensions.
   */
  public class ProfilePictureView extends FrameLayout {
  
      /**
       * Callback interface that will be called when a network or other error is encountered
       * while retrieving profile pictures.
       */
      public interface OnErrorListener {
          /**
           * Called when a network or other error is encountered.
           * @param error     a FacebookException representing the error that was encountered.
           */
          void onError(FacebookException error);
      }
  
      /**
       * Tag used when logging calls are made by ProfilePictureView
       */
      public static final String TAG = ProfilePictureView.class.getSimpleName();
  
      /**
       * Indicates that the specific size of the View will be set via layout params.
       * ProfilePictureView will default to NORMAL X NORMAL, if the layout params set on
       * this instance do not have a fixed size.
       * Used in calls to setPresetSize() and getPresetSize().
       * Corresponds with the preset_size Xml attribute that can be set on ProfilePictureView.
       */
      public static final int CUSTOM = -1;
  
      /**
       * Indicates that the profile image should fit in a SMALL X SMALL space, regardless
       * of whether the cropped or un-cropped version is chosen.
       * Used in calls to setPresetSize() and getPresetSize().
       * Corresponds with the preset_size Xml attribute that can be set on ProfilePictureView.
       */
      public static final int SMALL = -2;
  
      /**
       * Indicates that the profile image should fit in a NORMAL X NORMAL space, regardless
       * of whether the cropped or un-cropped version is chosen.
       * Used in calls to setPresetSize() and getPresetSize().
       * Corresponds with the preset_size Xml attribute that can be set on ProfilePictureView.
       */
      public static final int NORMAL = -3;
  
      /**
       * Indicates that the profile image should fit in a LARGE X LARGE space, regardless
       * of whether the cropped or un-cropped version is chosen.
       * Used in calls to setPresetSize() and getPresetSize().
       * Corresponds with the preset_size Xml attribute that can be set on ProfilePictureView.
       */
      public static final int LARGE = -4;
  
      private static final int MIN_SIZE = 1;
      private static final boolean IS_CROPPED_DEFAULT_VALUE = true;
      private static final String SUPER_STATE_KEY = "ProfilePictureView_superState";
      private static final String PROFILE_ID_KEY = "ProfilePictureView_profileId";
      private static final String PRESET_SIZE_KEY = "ProfilePictureView_presetSize";
      private static final String IS_CROPPED_KEY = "ProfilePictureView_isCropped";
      private static final String BITMAP_KEY = "ProfilePictureView_bitmap";
      private static final String BITMAP_WIDTH_KEY = "ProfilePictureView_width";
      private static final String BITMAP_HEIGHT_KEY = "ProfilePictureView_height";
      private static final String PENDING_REFRESH_KEY = "ProfilePictureView_refresh";
  
      private String profileId;
      private int queryHeight = ImageRequest.UNSPECIFIED_DIMENSION;
      private int queryWidth = ImageRequest.UNSPECIFIED_DIMENSION;
      private boolean isCropped = IS_CROPPED_DEFAULT_VALUE;
      private Bitmap imageContents;
      private ImageView image;
      private int presetSizeType = CUSTOM;
      private ImageRequest lastRequest;
      private OnErrorListener onErrorListener;
  
      /**
       * Constructor
       *
       * @param context Context for this View
       */
      public ProfilePictureView(Context context) {
          super(context);
          initialize(context);
      }
  
      /**
       * Constructor
       *
       * @param context Context for this View
       * @param attrs   AttributeSet for this View.
       *                The attribute 'preset_size' is processed here
       */
      public ProfilePictureView(Context context, AttributeSet attrs) {
          super(context, attrs);
          initialize(context);
          parseAttributes(attrs);
      }
  
      /**
       * Constructor
       *
       * @param context  Context for this View
       * @param attrs    AttributeSet for this View.
       *                 The attribute 'preset_size' is processed here
       * @param defStyle Default style for this View
       */
      public ProfilePictureView(Context context, AttributeSet attrs, int defStyle) {
          super(context, attrs, defStyle);
          initialize(context);
          parseAttributes(attrs);
      }
  
      /**
       * Gets the current preset size type
       *
       * @return The current preset size type, if set; CUSTOM if not
       */
      public final int getPresetSize() {
          return presetSizeType;
      }
  
      /**
       * Apply a preset size to this profile photo
       *
       * @param sizeType The size type to apply: SMALL, NORMAL or LARGE
       */
      public final void setPresetSize(int sizeType) {
          switch (sizeType) {
              case SMALL:
              case NORMAL:
              case LARGE:
              case CUSTOM:
                  this.presetSizeType = sizeType;
                  break;
  
              default:
                  throw new IllegalArgumentException("Must use a predefined preset size");
          }
  
          requestLayout();
      }
  
      /**
       * Indicates whether the cropped version of the profile photo has been chosen
       *
       * @return True if the cropped version is chosen, false if not.
       */
      public final boolean isCropped() {
          return isCropped;
      }
  
      /**
       * Sets the profile photo to be the cropped version, or the original version
       *
       * @param showCroppedVersion True to select the cropped version
       *                           False to select the standard version
       */
      public final void setCropped(boolean showCroppedVersion) {
          isCropped = showCroppedVersion;
          // No need to force the refresh since we will catch the change in required dimensions
          refreshImage(false);
      }
  
      /**
       * Returns the profile Id for the current profile photo
       *
       * @return The profile Id
       */
      public final String getProfileId() {
          return profileId;
      }
  
      /**
       * Sets the profile Id for this profile photo
       *
       * @param profileId The profileId
       *               NULL/Empty String will show the blank profile photo
       */
      public final void setProfileId(String profileId) {
          boolean force = false;
          if (Utility.isNullOrEmpty(this.profileId) || !this.profileId.equalsIgnoreCase(profileId)) {
              // Clear out the old profilePicture before requesting for the new one.
              setBlankProfilePicture();
              force = true;
          }
  
          this.profileId = profileId;
          refreshImage(force);
      }
  
      /**
       * Returns the current OnErrorListener for this instance of ProfilePictureView
       *
       * @return The OnErrorListener
       */
      public final OnErrorListener getOnErrorListener() {
          return onErrorListener;
      }
  
      /**
       * Sets an OnErrorListener for this instance of ProfilePictureView to call into when
       * certain exceptions occur.
       *
       * @param onErrorListener The listener object to set
       */
      public final void setOnErrorListener(OnErrorListener onErrorListener) {
          this.onErrorListener = onErrorListener;
      }
  
      /**
       * Overriding onMeasure to handle the case where WRAP_CONTENT might be
       * specified in the layout. Since we don't know the dimensions of the profile
       * photo, we need to handle this case specifically.
       * <p/>
       * The approach is to default to a NORMAL sized amount of space in the case that
       * a preset size is not specified. This logic is applied to both width and height
       */
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          ViewGroup.LayoutParams params = getLayoutParams();
          boolean customMeasure = false;
          int newHeight = MeasureSpec.getSize(heightMeasureSpec);
          int newWidth = MeasureSpec.getSize(widthMeasureSpec);
          if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY &&
                  params.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
              newHeight = getPresetSizeInPixels(true); // Default to a preset size
              heightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY);
              customMeasure = true;
          }
  
          if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY &&
                  params.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
              newWidth = getPresetSizeInPixels(true); // Default to a preset size
              widthMeasureSpec = MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY);
              customMeasure = true;
          }
  
          if (customMeasure) {
              // Since we are providing custom dimensions, we need to handle the measure
              // phase from here
              setMeasuredDimension(newWidth, newHeight);
              measureChildren(widthMeasureSpec, heightMeasureSpec);
          } else {
              // Rely on FrameLayout to do the right thing
              super.onMeasure(widthMeasureSpec, heightMeasureSpec);
          }
      }
  
      /**
       * In addition to calling super.Layout(), we also attempt to get a new image that
       * is properly size for the layout dimensions
       */
      @Override
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
          super.onLayout(changed, left, top, right, bottom);
  
          // See if the image needs redrawing
          refreshImage(false);
      }
  
      /**
       * Some of the current state is returned as a Bundle to allow quick restoration
       * of the ProfilePictureView object in scenarios like orientation changes.
       * @return a Parcelable containing the current state
       */
      @Override
      protected Parcelable onSaveInstanceState() {
          Parcelable superState = super.onSaveInstanceState();
          Bundle instanceState = new Bundle();
          instanceState.putParcelable(SUPER_STATE_KEY, superState);
          instanceState.putString(PROFILE_ID_KEY, profileId);
          instanceState.putInt(PRESET_SIZE_KEY, presetSizeType);
          instanceState.putBoolean(IS_CROPPED_KEY, isCropped);
          instanceState.putParcelable(BITMAP_KEY, imageContents);
          instanceState.putInt(BITMAP_WIDTH_KEY, queryWidth);
          instanceState.putInt(BITMAP_HEIGHT_KEY, queryHeight);
          instanceState.putBoolean(PENDING_REFRESH_KEY, lastRequest != null);
  
          return instanceState;
      }
  
      /**
       * If the passed in state is a Bundle, an attempt is made to restore from it.
       * @param state a Parcelable containing the current state
       */
      @Override
      protected void onRestoreInstanceState(Parcelable state) {
          if (state.getClass() != Bundle.class) {
              super.onRestoreInstanceState(state);
          } else {
              Bundle instanceState = (Bundle)state;
              super.onRestoreInstanceState(instanceState.getParcelable(SUPER_STATE_KEY));
  
              profileId = instanceState.getString(PROFILE_ID_KEY);
              presetSizeType = instanceState.getInt(PRESET_SIZE_KEY);
              isCropped = instanceState.getBoolean(IS_CROPPED_KEY);
              queryWidth = instanceState.getInt(BITMAP_WIDTH_KEY);
              queryHeight = instanceState.getInt(BITMAP_HEIGHT_KEY);
  
              setImageBitmap((Bitmap)instanceState.getParcelable(BITMAP_KEY));
  
              if (instanceState.getBoolean(PENDING_REFRESH_KEY)) {
                  refreshImage(true);
              }
          }
      }
  
      @Override
      protected void onDetachedFromWindow() {
          super.onDetachedFromWindow();
  
          // Null out lastRequest. This way, when the response is returned, we can ascertain
          // that the view is detached and hence should not attempt to update its contents.
          lastRequest = null;
      }
  
      private void initialize(Context context) {
          // We only want our ImageView in here. Nothing else is permitted
          removeAllViews();
  
          image = new ImageView(context);
  
          LayoutParams imageLayout = new LayoutParams(
                  LayoutParams.MATCH_PARENT,
                  LayoutParams.MATCH_PARENT);
  
          image.setLayoutParams(imageLayout);
  
          // We want to prevent up-scaling the image, but still have it fit within
          // the layout bounds as best as possible.
          image.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
          addView(image);
      }
  
      private void parseAttributes(AttributeSet attrs) {
          TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.com_facebook_profile_picture_view);
          setPresetSize(a.getInt(R.styleable.com_facebook_profile_picture_view_preset_size, CUSTOM));
          isCropped = a.getBoolean(R.styleable.com_facebook_profile_picture_view_is_cropped, IS_CROPPED_DEFAULT_VALUE);
          a.recycle();
      }
  
      private void refreshImage(boolean force) {
          boolean changed = updateImageQueryParameters();
          // Note: do not use Utility.isNullOrEmpty here as this will cause the Eclipse
          // Graphical Layout editor to fail in some cases
          if (profileId == null || profileId.length() == 0 ||
                  ((queryWidth == ImageRequest.UNSPECIFIED_DIMENSION) &&
                          (queryHeight == ImageRequest.UNSPECIFIED_DIMENSION))) {
              setBlankProfilePicture();
          } else if (changed || force) {
              sendImageRequest(true);
          }
      }
  
      private void setBlankProfilePicture() {
          int blankImageResource = isCropped() ?
                  R.drawable.com_facebook_profile_picture_blank_square :
                  R.drawable.com_facebook_profile_picture_blank_portrait;
          setImageBitmap( BitmapFactory.decodeResource(getResources(), blankImageResource));
      }
  
      private void setImageBitmap(Bitmap imageBitmap) {
          if (image != null && imageBitmap != null) {
              imageContents = imageBitmap; // Hold for save-restore cycles
              image.setImageBitmap(imageBitmap);
          }
      }
  
      private void sendImageRequest(boolean allowCachedResponse) {
          try {
              ImageRequest.Builder requestBuilder = new ImageRequest.Builder(
                      getContext(),
                      ImageRequest.getProfilePictureUrl(profileId, queryWidth,  queryHeight));
  
              ImageRequest request = requestBuilder.setAllowCachedRedirects(allowCachedResponse)
                      .setCallerTag(this)
                      .setCallback(
                      new ImageRequest.Callback() {
                          @Override
                          public void onCompleted(ImageResponse response) {
                              processResponse(response);
                          }
                      })
                      .build();
  
              // Make sure to cancel the old request before sending the new one to prevent
              // accidental cancellation of the new request. This could happen if the URL and
              // caller tag stayed the same.
              if (lastRequest != null) {
                  ImageDownloader.cancelRequest(lastRequest);
              }
              lastRequest = request;
  
              ImageDownloader.downloadAsync(request);
          } catch (MalformedURLException e) {
              Logger.log(LoggingBehavior.REQUESTS, Log.ERROR, TAG, e.toString());
          }
      }
  
      private void processResponse(ImageResponse response) {
          // First check if the response is for the right request. We may have:
          // 1. Sent a new request, thus super-ceding this one.
          // 2. Detached this view, in which case the response should be discarded.
          if (response.getRequest() == lastRequest) {
              lastRequest = null;
              Bitmap responseImage = response.getBitmap();
              Exception error = response.getError();
              if (error != null) {
                  OnErrorListener listener = onErrorListener;
                  if (listener != null) {
                      listener.onError(new FacebookException(
                              "Error in downloading profile picture for profileId: " + getProfileId(), error));
                  } else {
                      Logger.log(LoggingBehavior.REQUESTS, Log.ERROR, TAG, error.toString());
                  }
              } else if (responseImage != null) {
                  setImageBitmap(responseImage);
  
                  if (response.isCachedRedirect()) {
                      sendImageRequest(false);
                  }
              }
          }
      }
  
      private boolean updateImageQueryParameters() {
          int newHeightPx = getHeight();
          int newWidthPx = getWidth();
          if (newWidthPx < MIN_SIZE || newHeightPx < MIN_SIZE) {
              // Not enough space laid out for this View yet. Or something else is awry.
              return false;
          }
  
          int presetSize = getPresetSizeInPixels(false);
          if (presetSize != ImageRequest.UNSPECIFIED_DIMENSION) {
              newWidthPx = presetSize;
              newHeightPx = presetSize;
          }
  
          // The cropped version is square
          // If full version is desired, then only one dimension is required.
          if (newWidthPx <= newHeightPx) {
              newHeightPx = isCropped() ? newWidthPx : ImageRequest.UNSPECIFIED_DIMENSION;
          } else {
              newWidthPx = isCropped() ? newHeightPx : ImageRequest.UNSPECIFIED_DIMENSION;
          }
  
          boolean changed = (newWidthPx != queryWidth) || (newHeightPx != queryHeight);
  
          queryWidth = newWidthPx;
          queryHeight = newHeightPx;
  
          return changed;
      }
  
      private int getPresetSizeInPixels(boolean forcePreset) {
          int dimensionId;
          switch (presetSizeType) {
              case SMALL:
                  dimensionId = R.dimen.com_facebook_profilepictureview_preset_size_small;
                  break;
              case NORMAL:
                  dimensionId = R.dimen.com_facebook_profilepictureview_preset_size_normal;
                  break;
              case LARGE:
                  dimensionId = R.dimen.com_facebook_profilepictureview_preset_size_large;
                  break;
              case CUSTOM:
                  if (!forcePreset) {
                      return ImageRequest.UNSPECIFIED_DIMENSION;
                  } else {
                      dimensionId = R.dimen.com_facebook_profilepictureview_preset_size_normal;
                      break;
                  }
              default:
                  return ImageRequest.UNSPECIFIED_DIMENSION;
          }
  
          return getResources().getDimensionPixelSize(dimensionId);
      }
  }
  