/** * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. * * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, * copy, modify, and distribute this software in source code or binary form for use * in connection with the web services and APIs provided by Facebook. * * As with any software that integrates with the Facebook platform, your use of * this software is subject to the Facebook Developer Principles and Policies * [http://developers.facebook.com/policy/]. This copyright notice shall be * included in all copies or substantial portions of the software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.facebook.share.widget; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.ContextWrapper; 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.app.Fragment; 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.FacebookException; import com.facebook.R; import com.facebook.internal.*; import com.facebook.share.internal.LikeActionController; import com.facebook.share.internal.LikeBoxCountView; import com.facebook.share.internal.LikeButton; /** * 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:object_type attribute for a LikeView */ public enum ObjectType { /** * This is the default value */ UNKNOWN("unknown", 0), /** * Indicates that the object id set on this LikeView is an Open Graph object */ OPEN_GRAPH("open_graph", 1), /** * Indicates that the object id set on this LikeView is a Page. */ PAGE("page", 2); public static ObjectType DEFAULT = UNKNOWN; public static ObjectType fromInt(int enumValue) { for (ObjectType objectType : values()) { if (objectType.getValue() == enumValue) { return objectType; } } return null; } private String stringValue; private int intValue; private ObjectType(String stringValue, int value) { this.stringValue = stringValue; this.intValue = value; } @Override public String toString() { return stringValue; } public int getValue() { return intValue; } } /** * 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. * <p/> * 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. * <p/> * 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. * <p/> * 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 ObjectType objectType; 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; private Fragment parentFragment; private boolean explicitlyDisabled; /** * 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 ID for this LikeView. Can be changed during runtime. * @param objectId The object ID, this can be a URL or a Facebook ID. */ public void setObjectIdAndType(String objectId, ObjectType objectType) { objectId = Utility.coerceValueIfNullOrEmpty(objectId, null); objectType = objectType != null ? objectType : ObjectType.DEFAULT; if (!Utility.areObjectsEqual(objectId, this.objectId) || (objectType != this.objectType)) { setObjectIdAndTypeForced(objectId, objectType); 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; } /** * Sets the parent Fragment which is hosting this LikeView. This allows the LikeView to be * embedded inside a Fragment, and will allow the fragment to receive the * {@link Fragment#onActivityResult(int, int, android.content.Intent) onActivityResult} * call rather than the Activity, upon completion of Likes from this view. * * @param fragment Fragment that is hosting the LikeView. */ public void setFragment(Fragment fragment) { this.parentFragment = fragment; } @Override public void setEnabled(boolean enabled) { explicitlyDisabled = !enabled; updateLikeStateAndLayout(); } @Override protected void onDetachedFromWindow() { // Disassociate from the object setObjectIdAndType(null, ObjectType.UNKNOWN); 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); objectType = ObjectType.fromInt( a.getInt(R.styleable.com_facebook_like_view_object_type, ObjectType.DEFAULT.getValue())); 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); setObjectIdAndTypeForced(this.objectId, this.objectType); updateLikeStateAndLayout(); } private void initializeLikeButton(Context context) { likeButton = new LikeButton( context, likeActionController != null && likeActionController.isObjectLiked()); 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 = null; if (parentFragment == null) { Context context = getContext(); if (context instanceof Activity) { activity = (Activity) context; } else if (context instanceof ContextWrapper) { Context baseContext = ((ContextWrapper) context).getBaseContext(); if (baseContext instanceof Activity) { activity = (Activity) baseContext; } } } likeActionController.toggleLike( activity, parentFragment, 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, "")); params.putString( AnalyticsEvents.PARAMETER_LIKE_VIEW_OBJECT_TYPE, objectType.toString()); return params; } private void setObjectIdAndTypeForced(String newObjectId, ObjectType newObjectType) { tearDownObjectAssociations(); objectId = newObjectId; objectType = newObjectType; if (Utility.isNullOrEmpty(newObjectId)) { return; } creationCallback = new LikeActionControllerCreationCallback(); LikeActionController.getControllerForObjectId( newObjectId, newObjectType, 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() { boolean enabled = !explicitlyDisabled; if (likeActionController == null) { likeButton.setSelected(false); socialSentenceView.setText(null); likeBoxCountView.setText(null); } else { likeButton.setSelected(likeActionController.isObjectLiked()); socialSentenceView.setText(likeActionController.getSocialSentence()); likeBoxCountView.setText(likeActionController.getLikeCountString()); enabled &= likeActionController.shouldEnableView(); } super.setEnabled(enabled); likeButton.setEnabled(enabled); 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 the share action encounters an error. * @param error The error that occurred */ public void onError(FacebookException error); } 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(NativeProtocol.getExceptionFromErrorData(extras)); } } else if (LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_DID_RESET.equals( intentAction)) { // This will recreate the controller and associated objects setObjectIdAndTypeForced(objectId, objectType); updateLikeStateAndLayout(); } } } private class LikeActionControllerCreationCallback implements LikeActionController.CreationCallback { private boolean isCancelled; public void cancel() { isCancelled = true; } @Override public void onComplete( LikeActionController likeActionController, FacebookException error) { if (isCancelled) { return; } if (likeActionController != null) { if (!likeActionController.shouldEnableView()) { error = new FacebookException( "Cannot use LikeView. The device may not be supported."); } // Always associate with the controller, so it can get updates if the view gets // enabled again. associateWithLikeActionController(likeActionController); updateLikeStateAndLayout(); } if (error != null) { if (onErrorListener != null) { onErrorListener.onError(error); } } LikeView.this.creationCallback = null; } } }