/** * 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.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.TypedArray; import android.graphics.Color; import android.os.Bundle; import android.support.v4.content.LocalBroadcastManager; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; import com.facebook.android.R; import com.facebook.internal.*; /** * This class provides the UI for displaying the Facebook Like button and its associated components. */ public class LikeView extends FrameLayout { // *** // Keep all the enum values in sync with attrs.xml // *** /** * Encapsulates the valid values for the facebook:style attribute for a LikeView */ public enum Style { /** * Setting the attribute to this value will display the button and a sentence near it that describes the * social sentence for the associated object. * * This is the default value */ STANDARD("standard", 0), /** * Setting the attribute to this value will display the button by itself, with no other components */ BUTTON("button", 1), /** * Setting the attribute to this value will display the button and a box near it with the number of likes * for the associated object */ BOX_COUNT("box_count", 2); static Style DEFAULT = STANDARD; static Style fromInt(int enumValue) { for (Style style : values()) { if (style.getValue() == enumValue) { return style; } } return null; } private String stringValue; private int intValue; private Style(String stringValue, int value) { this.stringValue = stringValue; this.intValue = value; } @Override public String toString() { return stringValue; } private int getValue() { return intValue; } } /** * Encapsulates the valid values for the facebook:horizontal_alignment attribute for a LikeView. */ public enum HorizontalAlignment { /** * Setting the attribute to this value will center the button and auxiliary view in the parent view. * * This is the default value */ CENTER("center", 0), /** * Setting the attribute to this value will left-justify the button and auxiliary view in the parent view. */ LEFT("left", 1), /** * Setting the attribute to this value will right-justify the button and auxiliary view in the parent view. * If the facebook:auxiliary_view_position is set to INLINE, then the auxiliary view will be on the * left of the button */ RIGHT("right", 2); static HorizontalAlignment DEFAULT = CENTER; static HorizontalAlignment fromInt(int enumValue) { for (HorizontalAlignment horizontalAlignment : values()) { if (horizontalAlignment.getValue() == enumValue) { return horizontalAlignment; } } return null; } private String stringValue; private int intValue; private HorizontalAlignment(String stringValue, int value) { this.stringValue = stringValue; this.intValue = value; } @Override public String toString() { return stringValue; } private int getValue() { return intValue; } } /** * Encapsulates the valid values for the facebook:auxiliary_view_position attribute for a LikeView. */ public enum AuxiliaryViewPosition { /** * Setting the attribute to this value will put the social-sentence or box-count below the like button. * If the facebook:style is set to BUTTON, then this has no effect. * * This is the default value */ BOTTOM("bottom", 0), /** * Setting the attribute to this value will put the social-sentence or box-count inline with the like button. * The auxiliary view will be to the left of the button if the facebook:horizontal_alignment is set to RIGHT. * In all other cases, it will be to the right of the button. * If the facebook:style is set to BUTTON, then this has no effect. */ INLINE("inline", 1), /** * Setting the attribute to this value will put the social-sentence or box-count above the like button. * If the facebook:style is set to BUTTON, then this has no effect. */ TOP("top", 2); static AuxiliaryViewPosition DEFAULT = BOTTOM; static AuxiliaryViewPosition fromInt(int enumValue) { for (AuxiliaryViewPosition auxViewPosition : values()) { if (auxViewPosition.getValue() == enumValue) { return auxViewPosition; } } return null; } private String stringValue; private int intValue; private AuxiliaryViewPosition(String stringValue, int value) { this.stringValue = stringValue; this.intValue = value; } @Override public String toString() { return stringValue; } private int getValue() { return intValue; } } private static final int NO_FOREGROUND_COLOR = -1; private String objectId; private LinearLayout containerView; private LikeButton likeButton; private LikeBoxCountView likeBoxCountView; private TextView socialSentenceView; private LikeActionController likeActionController; private OnErrorListener onErrorListener; private BroadcastReceiver broadcastReceiver; private LikeActionControllerCreationCallback creationCallback; private Style likeViewStyle = Style.DEFAULT; private HorizontalAlignment horizontalAlignment = HorizontalAlignment.DEFAULT; private AuxiliaryViewPosition auxiliaryViewPosition = AuxiliaryViewPosition.DEFAULT; private int foregroundColor = NO_FOREGROUND_COLOR; private int edgePadding; private int internalPadding; /** * If your app does not use UiLifeCycleHelper, then you must call this method in the calling activity's * onActivityResult method, to process any pending like actions, where tapping the button had resulted in * the Like dialog being shown in the Facebook application. * * @param context Hosting context * @param requestCode From the originating call to onActivityResult * @param resultCode From the originating call to onActivityResult * @param data From the originating call to onActivityResult * @return Indication of whether the Intent was handled */ public static boolean handleOnActivityResult(Context context, int requestCode, int resultCode, Intent data) { return LikeActionController.handleOnActivityResult(context, requestCode, resultCode, data); } /** * Constructor * * @param context Context for this View */ public LikeView(Context context) { super(context); initialize(context); } /** * Constructor * * @param context Context for this View * @param attrs AttributeSet for this View. */ public LikeView(Context context, AttributeSet attrs) { super(context, attrs); parseAttributes(attrs); initialize(context); } /** * Sets the associated object for this LikeView. Can be changed during runtime. * @param objectId Object Id */ public void setObjectId(String objectId) { objectId = Utility.coerceValueIfNullOrEmpty(objectId, null); if (!Utility.areObjectsEqual(objectId, this.objectId)) { setObjectIdForced(objectId); updateLikeStateAndLayout(); } } /** * Sets the facebook:style for this LikeView. Can be changed during runtime. * @param likeViewStyle Should be either LikeView.STANDARD, LikeView.BUTTON or LikeView.BOX_COUNT */ public void setLikeViewStyle(Style likeViewStyle) { likeViewStyle = likeViewStyle != null ? likeViewStyle : Style.DEFAULT; if (this.likeViewStyle != likeViewStyle) { this.likeViewStyle = likeViewStyle; updateLayout(); } } /** * Sets the facebook:auxiliary_view_position for this LikeView. Can be changed during runtime. * @param auxiliaryViewPosition Should be either LikeView.TOP, LikeView.INLINE or LikeView.BOTTOM */ public void setAuxiliaryViewPosition(AuxiliaryViewPosition auxiliaryViewPosition) { auxiliaryViewPosition = auxiliaryViewPosition != null ? auxiliaryViewPosition : AuxiliaryViewPosition.DEFAULT; if (this.auxiliaryViewPosition != auxiliaryViewPosition) { this.auxiliaryViewPosition = auxiliaryViewPosition; updateLayout(); } } /** * Sets the facebook:horizontal_alignment for this LikeView. Can be changed during runtime. * @param horizontalAlignment Should be either LikeView.LEFT, LikeView.CENTER or LikeView.RIGHT */ public void setHorizontalAlignment(HorizontalAlignment horizontalAlignment) { horizontalAlignment = horizontalAlignment != null ? horizontalAlignment : HorizontalAlignment.DEFAULT; if (this.horizontalAlignment != horizontalAlignment) { this.horizontalAlignment = horizontalAlignment; updateLayout(); } } /** * Sets the facebook:foreground_color for this LikeView. Can be changed during runtime. * The color is only used for the social sentence text. * @param foregroundColor And valid android.graphics.Color value. */ public void setForegroundColor(int foregroundColor) { if (this.foregroundColor != foregroundColor) { socialSentenceView.setTextColor(foregroundColor); } } /** * Sets an OnErrorListener for this instance of LikeView to call into when * certain exceptions occur. * * @param onErrorListener The listener object to set */ public void setOnErrorListener(OnErrorListener onErrorListener) { this.onErrorListener = onErrorListener; } /** * Returns the current OnErrorListener for this instance of LikeView. * * @return The OnErrorListener */ public OnErrorListener getOnErrorListener() { return onErrorListener; } @Override protected void onDetachedFromWindow() { // Disassociate from the object setObjectId(null); super.onDetachedFromWindow(); } private void parseAttributes(AttributeSet attrs) { if (attrs == null || getContext() == null) { return; } TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.com_facebook_like_view); if (a == null) { return; } objectId = Utility.coerceValueIfNullOrEmpty(a.getString(R.styleable.com_facebook_like_view_object_id), null); likeViewStyle = Style.fromInt( a.getInt(R.styleable.com_facebook_like_view_style, Style.DEFAULT.getValue())); if (likeViewStyle == null) { throw new IllegalArgumentException("Unsupported value for LikeView 'style'"); } auxiliaryViewPosition = AuxiliaryViewPosition.fromInt( a.getInt(R.styleable.com_facebook_like_view_auxiliary_view_position, AuxiliaryViewPosition.DEFAULT.getValue())); if (auxiliaryViewPosition == null) { throw new IllegalArgumentException("Unsupported value for LikeView 'auxiliary_view_position'"); } horizontalAlignment = HorizontalAlignment.fromInt( a.getInt(R.styleable.com_facebook_like_view_horizontal_alignment, HorizontalAlignment.DEFAULT.getValue())); if (horizontalAlignment == null) { throw new IllegalArgumentException("Unsupported value for LikeView 'horizontal_alignment'"); } foregroundColor = a.getColor(R.styleable.com_facebook_like_view_foreground_color, NO_FOREGROUND_COLOR); a.recycle(); } // If attributes were present, parseAttributes MUST be called before initialize() to ensure proper behavior private void initialize(Context context) { edgePadding = getResources().getDimensionPixelSize(R.dimen.com_facebook_likeview_edge_padding); internalPadding = getResources().getDimensionPixelSize(R.dimen.com_facebook_likeview_internal_padding); if (foregroundColor == NO_FOREGROUND_COLOR) { foregroundColor = getResources().getColor(R.color.com_facebook_likeview_text_color); } setBackgroundColor(Color.TRANSPARENT); containerView = new LinearLayout(context); LayoutParams containerViewLayoutParams = new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); containerView.setLayoutParams(containerViewLayoutParams); initializeLikeButton(context); initializeSocialSentenceView(context); initializeLikeCountView(context); containerView.addView(likeButton); containerView.addView(socialSentenceView); containerView.addView(likeBoxCountView); addView(containerView); setObjectIdForced(this.objectId); updateLikeStateAndLayout(); } private void initializeLikeButton(Context context) { likeButton = new LikeButton( context, likeActionController != null ? likeActionController.isObjectLiked() : false); likeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { toggleLike(); } }); LinearLayout.LayoutParams buttonLayout = new LinearLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); likeButton.setLayoutParams(buttonLayout); } private void initializeSocialSentenceView(Context context) { socialSentenceView = new TextView(context); socialSentenceView.setTextSize( TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.com_facebook_likeview_text_size)); socialSentenceView.setMaxLines(2); socialSentenceView.setTextColor(foregroundColor); socialSentenceView.setGravity(Gravity.CENTER); LinearLayout.LayoutParams socialSentenceViewLayout = new LinearLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); socialSentenceView.setLayoutParams(socialSentenceViewLayout); } private void initializeLikeCountView(Context context) { likeBoxCountView = new LikeBoxCountView(context); LinearLayout.LayoutParams likeCountViewLayout = new LinearLayout.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); likeBoxCountView.setLayoutParams(likeCountViewLayout); } private void toggleLike() { if (likeActionController != null) { Activity activity = (Activity)getContext(); likeActionController.toggleLike(activity, getAnalyticsParameters()); } } private Bundle getAnalyticsParameters() { Bundle params = new Bundle(); params.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_STYLE, likeViewStyle.toString()); params.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_AUXILIARY_POSITION, auxiliaryViewPosition.toString()); params.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_HORIZONTAL_ALIGNMENT, horizontalAlignment.toString()); params.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_OBJECT_ID, Utility.coerceValueIfNullOrEmpty(objectId, "")); return params; } private void setObjectIdForced(String newObjectId) { tearDownObjectAssociations(); objectId = newObjectId; if (Utility.isNullOrEmpty(newObjectId)) { return; } creationCallback = new LikeActionControllerCreationCallback(); LikeActionController.getControllerForObjectId( getContext(), newObjectId, creationCallback); } private void associateWithLikeActionController(LikeActionController likeActionController) { this.likeActionController = likeActionController; this.broadcastReceiver = new LikeControllerBroadcastReceiver(); LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(getContext()); // add the broadcast receiver IntentFilter filter = new IntentFilter(); filter.addAction(LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_UPDATED); filter.addAction(LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR); filter.addAction(LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_DID_RESET); localBroadcastManager.registerReceiver(broadcastReceiver, filter); } private void tearDownObjectAssociations() { if (broadcastReceiver != null) { LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(getContext()); localBroadcastManager.unregisterReceiver(broadcastReceiver); broadcastReceiver = null; } // If we were already waiting on a controller to be given back, make sure we aren't waiting anymore. // Otherwise when that controller is given back to the callback, it will go and register a broadcast receiver // for it. if (creationCallback != null) { creationCallback.cancel(); creationCallback = null; } likeActionController = null; } private void updateLikeStateAndLayout() { if (likeActionController == null) { likeButton.setLikeState(false); socialSentenceView.setText(null); likeBoxCountView.setText(null); } else { likeButton.setLikeState(likeActionController.isObjectLiked()); socialSentenceView.setText(likeActionController.getSocialSentence()); likeBoxCountView.setText(likeActionController.getLikeCountString()); } updateLayout(); } private void updateLayout() { // Make sure the container is horizontally aligned according to specifications. LayoutParams containerViewLayoutParams = (LayoutParams)containerView.getLayoutParams(); LinearLayout.LayoutParams buttonLayoutParams = (LinearLayout.LayoutParams)likeButton.getLayoutParams(); int viewGravity = horizontalAlignment == HorizontalAlignment.LEFT ? Gravity.LEFT : horizontalAlignment == HorizontalAlignment.CENTER ? Gravity.CENTER_HORIZONTAL : Gravity.RIGHT; containerViewLayoutParams.gravity = viewGravity | Gravity.TOP; buttonLayoutParams.gravity = viewGravity; // Choose the right auxiliary view to make visible. socialSentenceView.setVisibility(GONE); likeBoxCountView.setVisibility(GONE); View auxView; if (likeViewStyle == Style.STANDARD && likeActionController != null && !Utility.isNullOrEmpty(likeActionController.getSocialSentence())) { auxView = socialSentenceView; } else if (likeViewStyle == Style.BOX_COUNT && likeActionController != null && !Utility.isNullOrEmpty(likeActionController.getLikeCountString())) { updateBoxCountCaretPosition(); auxView = likeBoxCountView; } else { // No more work to be done. return; } auxView.setVisibility(VISIBLE); // Now position the auxiliary view properly LinearLayout.LayoutParams auxViewLayoutParams = (LinearLayout.LayoutParams)auxView.getLayoutParams(); auxViewLayoutParams.gravity = viewGravity; containerView.setOrientation( auxiliaryViewPosition == AuxiliaryViewPosition.INLINE ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); if (auxiliaryViewPosition == AuxiliaryViewPosition.TOP || (auxiliaryViewPosition == AuxiliaryViewPosition.INLINE && horizontalAlignment == HorizontalAlignment.RIGHT)) { // Button comes after the auxiliary view. Make sure it is at the end containerView.removeView(likeButton); containerView.addView(likeButton); } else { // In all other cases, the button comes first containerView.removeView(auxView); containerView.addView(auxView); } switch (auxiliaryViewPosition) { case TOP: auxView.setPadding(edgePadding, edgePadding, edgePadding, internalPadding); break; case BOTTOM: auxView.setPadding(edgePadding, internalPadding, edgePadding, edgePadding); break; case INLINE: if (horizontalAlignment == HorizontalAlignment.RIGHT) { auxView.setPadding(edgePadding, edgePadding, internalPadding, edgePadding); } else { auxView.setPadding(internalPadding, edgePadding, edgePadding, edgePadding); } break; } } private void updateBoxCountCaretPosition() { switch (auxiliaryViewPosition) { case TOP: likeBoxCountView.setCaretPosition(LikeBoxCountView.LikeBoxCountViewCaretPosition.BOTTOM); break; case BOTTOM: likeBoxCountView.setCaretPosition(LikeBoxCountView.LikeBoxCountViewCaretPosition.TOP); break; case INLINE: likeBoxCountView.setCaretPosition( horizontalAlignment == HorizontalAlignment.RIGHT ? LikeBoxCountView.LikeBoxCountViewCaretPosition.RIGHT : LikeBoxCountView.LikeBoxCountViewCaretPosition.LEFT); break; } } /** * Callback interface that will be called when a network or other error is encountered * while logging in. */ public interface OnErrorListener { /** * Called when a network or other error is encountered. * @param errorBundle a FacebookException representing the error that was encountered. */ void onError(Bundle errorBundle); } private class LikeControllerBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String intentAction = intent.getAction(); Bundle extras = intent.getExtras(); boolean shouldRespond = true; if (extras != null) { // See if an Id was set in the broadcast Intent. If it was, treat it as a filter. String broadcastObjectId = extras.getString(LikeActionController.ACTION_OBJECT_ID_KEY); shouldRespond = Utility.isNullOrEmpty(broadcastObjectId) || Utility.areObjectsEqual(objectId, broadcastObjectId); } if (!shouldRespond) { return; } if (LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_UPDATED.equals(intentAction)) { updateLikeStateAndLayout(); } else if (LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR.equals(intentAction)) { if (onErrorListener != null) { onErrorListener.onError(extras); } } else if (LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_DID_RESET.equals(intentAction)) { // This will recreate the controller and associated objects setObjectIdForced(objectId); updateLikeStateAndLayout(); } } } private class LikeActionControllerCreationCallback implements LikeActionController.CreationCallback { private boolean isCancelled; public void cancel() { isCancelled = true; } @Override public void onComplete(LikeActionController likeActionController) { if (isCancelled) { return; } associateWithLikeActionController(likeActionController); updateLikeStateAndLayout(); LikeView.this.creationCallback = null; } } }