/**
* 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.annotation.SuppressLint;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.net.http.SslError;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Pair;
import android.view.*;
import android.webkit.SslErrorHandler;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import com.facebook.*;
import com.facebook.android.*;
import com.facebook.internal.Logger;
import com.facebook.internal.ServerProtocol;
import com.facebook.internal.Utility;
import com.facebook.internal.Validate;
/**
* This class provides a mechanism for displaying Facebook Web dialogs inside a Dialog. Helper
* methods are provided to construct commonly-used dialogs, or a caller can specify arbitrary
* parameters to call other dialogs.
*/
public class WebDialog extends Dialog {
private static final String LOG_TAG = Logger.LOG_TAG_BASE + "WebDialog";
private static final String DISPLAY_TOUCH = "touch";
private static final String USER_AGENT = "user_agent";
static final String REDIRECT_URI = "fbconnect://success";
static final String CANCEL_URI = "fbconnect://cancel";
static final boolean DISABLE_SSL_CHECK_FOR_TESTING = false;
// width below which there are no extra margins
private static final int NO_BUFFER_SCREEN_WIDTH = 512;
// width beyond which we're always using the MIN_SCALE_FACTOR
private static final int MAX_BUFFER_SCREEN_WIDTH = 1024;
// the minimum scaling factor for the web dialog (60% of screen size)
private static final double MIN_SCALE_FACTOR = 0.6;
// translucent border around the webview
private static final int BACKGROUND_GRAY = 0xCC000000;
public static final int DEFAULT_THEME = android.R.style.Theme_Translucent_NoTitleBar;
private String url;
private OnCompleteListener onCompleteListener;
private WebView webView;
private ProgressDialog spinner;
private ImageView crossImageView;
private FrameLayout contentFrameLayout;
private boolean listenerCalled = false;
private boolean isDetached = false;
/**
* Interface that implements a listener to be called when the user's interaction with the
* dialog completes, whether because the dialog finished successfully, or it was cancelled,
* or an error was encountered.
*/
public interface OnCompleteListener {
/**
* Called when the dialog completes.
*
* @param values on success, contains the values returned by the dialog
* @param error on an error, contains an exception describing the error
*/
void onComplete(Bundle values, FacebookException error);
}
/**
* Constructor which can be used to display a dialog with an already-constructed URL.
*
* @param context the context to use to display the dialog
* @param url the URL of the Web Dialog to display; no validation is done on this URL, but it should
* be a valid URL pointing to a Facebook Web Dialog
*/
public WebDialog(Context context, String url) {
this(context, url, DEFAULT_THEME);
}
/**
* Constructor which can be used to display a dialog with an already-constructed URL and a custom theme.
*
* @param context the context to use to display the dialog
* @param url the URL of the Web Dialog to display; no validation is done on this URL, but it should
* be a valid URL pointing to a Facebook Web Dialog
* @param theme identifier of a theme to pass to the Dialog class
*/
public WebDialog(Context context, String url, int theme) {
super(context, theme);
this.url = url;
}
/**
* Constructor which will construct the URL of the Web dialog based on the specified parameters.
*
* @param context the context to use to display the dialog
* @param action the portion of the dialog URL following "dialog/"
* @param parameters parameters which will be included as part of the URL
* @param theme identifier of a theme to pass to the Dialog class
* @param listener the listener to notify, or null if no notification is desired
*/
public WebDialog(Context context, String action, Bundle parameters, int theme, OnCompleteListener listener) {
super(context, theme);
if (parameters == null) {
parameters = new Bundle();
}
parameters.putString(ServerProtocol.DIALOG_PARAM_DISPLAY, DISPLAY_TOUCH);
parameters.putString(ServerProtocol.DIALOG_PARAM_TYPE, USER_AGENT);
Uri uri = Utility.buildUri(ServerProtocol.getDialogAuthority(), ServerProtocol.DIALOG_PATH + action,
parameters);
this.url = uri.toString();
onCompleteListener = listener;
}
/**
* Sets the listener which will be notified when the dialog finishes.
*
* @param listener the listener to notify, or null if no notification is desired
*/
public void setOnCompleteListener(OnCompleteListener listener) {
onCompleteListener = listener;
}
/**
* Gets the listener which will be notified when the dialog finishes.
*
* @return the listener, or null if none has been specified
*/
public OnCompleteListener getOnCompleteListener() {
return onCompleteListener;
}
@Override
public void dismiss() {
if (webView != null) {
webView.stopLoading();
}
if (!isDetached) {
if (spinner.isShowing()) {
spinner.dismiss();
}
super.dismiss();
}
}
@Override
public void onDetachedFromWindow() {
isDetached = true;
super.onDetachedFromWindow();
}
@Override
public void onAttachedToWindow() {
isDetached = false;
super.onAttachedToWindow();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
sendCancelToListener();
}
});
spinner = new ProgressDialog(getContext());
spinner.requestWindowFeature(Window.FEATURE_NO_TITLE);
spinner.setMessage(getContext().getString(R.string.com_facebook_loading));
spinner.setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
sendCancelToListener();
WebDialog.this.dismiss();
}
});
requestWindowFeature(Window.FEATURE_NO_TITLE);
contentFrameLayout = new FrameLayout(getContext());
// First calculate the margins around the frame layout
Pair<Integer, Integer> margins = getMargins();
contentFrameLayout.setPadding(margins.first, margins.second, margins.first, margins.second);
/* Create the 'x' image, but don't add to the contentFrameLayout layout yet
* at this point, we only need to know its drawable width and height
* to place the webview
*/
createCrossImage();
/* Now we know 'x' drawable width and height,
* layout the webview and add it the contentFrameLayout layout
*/
int crossWidth = crossImageView.getDrawable().getIntrinsicWidth();
setUpWebView(crossWidth / 2 + 1);
/* Finally add the 'x' image to the contentFrameLayout layout and
* add contentFrameLayout to the Dialog view
*/
contentFrameLayout.addView(crossImageView, new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
addContentView(contentFrameLayout,
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
private Pair<Integer, Integer> getMargins() {
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
int width = metrics.widthPixels;
int height = metrics.heightPixels;
double scaleFactor;
int scaledWidth = (int) ((float) width / metrics.density);
if (scaledWidth <= NO_BUFFER_SCREEN_WIDTH) {
scaleFactor = 1.0;
} else if (scaledWidth >= MAX_BUFFER_SCREEN_WIDTH) {
scaleFactor = MIN_SCALE_FACTOR;
} else {
// between the NO_BUFFER and MAX_BUFFER widths, we take a linear reduction to go from 100%
// of screen size down to MIN_SCALE_FACTOR
scaleFactor = MIN_SCALE_FACTOR +
((double) (MAX_BUFFER_SCREEN_WIDTH - scaledWidth))
/ ((double) (MAX_BUFFER_SCREEN_WIDTH - NO_BUFFER_SCREEN_WIDTH))
* (1.0 - MIN_SCALE_FACTOR);
}
int leftRightMargin = (int) (width * (1.0 - scaleFactor) / 2);
int topBottomMargin = (int) (height * (1.0 - scaleFactor) / 2);
return new Pair<Integer, Integer>(leftRightMargin, topBottomMargin);
}
private void sendSuccessToListener(Bundle values) {
if (onCompleteListener != null && !listenerCalled) {
listenerCalled = true;
onCompleteListener.onComplete(values, null);
}
}
private void sendErrorToListener(Throwable error) {
if (onCompleteListener != null && !listenerCalled) {
listenerCalled = true;
FacebookException facebookException = null;
if (error instanceof FacebookException) {
facebookException = (FacebookException) error;
} else {
facebookException = new FacebookException(error);
}
onCompleteListener.onComplete(null, facebookException);
}
}
private void sendCancelToListener() {
sendErrorToListener(new FacebookOperationCanceledException());
}
private void createCrossImage() {
crossImageView = new ImageView(getContext());
// Dismiss the dialog when user click on the 'x'
crossImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendCancelToListener();
WebDialog.this.dismiss();
}
});
Drawable crossDrawable = getContext().getResources().getDrawable(R.drawable.com_facebook_close);
crossImageView.setImageDrawable(crossDrawable);
/* 'x' should not be visible while webview is loading
* make it visible only after webview has fully loaded
*/
crossImageView.setVisibility(View.INVISIBLE);
}
@SuppressLint("SetJavaScriptEnabled")
private void setUpWebView(int margin) {
LinearLayout webViewContainer = new LinearLayout(getContext());
webView = new WebView(getContext());
webView.setVerticalScrollBarEnabled(false);
webView.setHorizontalScrollBarEnabled(false);
webView.setWebViewClient(new DialogWebViewClient());
webView.getSettings().setJavaScriptEnabled(true);
webView.loadUrl(url);
webView.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
webView.setVisibility(View.INVISIBLE);
webView.getSettings().setSavePassword(false);
webViewContainer.setPadding(margin, margin, margin, margin);
webViewContainer.addView(webView);
webViewContainer.setBackgroundColor(BACKGROUND_GRAY);
contentFrameLayout.addView(webViewContainer);
}
private class DialogWebViewClient extends WebViewClient {
@Override
@SuppressWarnings("deprecation")
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Utility.logd(LOG_TAG, "Redirect URL: " + url);
if (url.startsWith(WebDialog.REDIRECT_URI)) {
Bundle values = Util.parseUrl(url);
String error = values.getString("error");
if (error == null) {
error = values.getString("error_type");
}
String errorMessage = values.getString("error_msg");
if (errorMessage == null) {
errorMessage = values.getString("error_description");
}
String errorCodeString = values.getString("error_code");
int errorCode = FacebookRequestError.INVALID_ERROR_CODE;
if (!Utility.isNullOrEmpty(errorCodeString)) {
try {
errorCode = Integer.parseInt(errorCodeString);
} catch (NumberFormatException ex) {
errorCode = FacebookRequestError.INVALID_ERROR_CODE;
}
}
if (Utility.isNullOrEmpty(error) && Utility
.isNullOrEmpty(errorMessage) && errorCode == FacebookRequestError.INVALID_ERROR_CODE) {
sendSuccessToListener(values);
} else if (error != null && (error.equals("access_denied") ||
error.equals("OAuthAccessDeniedException"))) {
sendCancelToListener();
} else {
FacebookRequestError requestError = new FacebookRequestError(errorCode, error, errorMessage);
sendErrorToListener(new FacebookServiceException(requestError, errorMessage));
}
WebDialog.this.dismiss();
return true;
} else if (url.startsWith(WebDialog.CANCEL_URI)) {
sendCancelToListener();
WebDialog.this.dismiss();
return true;
} else if (url.contains(DISPLAY_TOUCH)) {
return false;
}
// launch non-dialog URLs in a full browser
getContext().startActivity(
new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
}
@Override
public void onReceivedError(WebView view, int errorCode,
String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
sendErrorToListener(new FacebookDialogException(description, errorCode, failingUrl));
WebDialog.this.dismiss();
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
if (DISABLE_SSL_CHECK_FOR_TESTING) {
handler.proceed();
} else {
super.onReceivedSslError(view, handler, error);
sendErrorToListener(new FacebookDialogException(null, ERROR_FAILED_SSL_HANDSHAKE, null));
handler.cancel();
WebDialog.this.dismiss();
}
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
Utility.logd(LOG_TAG, "Webview loading URL: " + url);
super.onPageStarted(view, url, favicon);
if (!isDetached) {
spinner.show();
}
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (!isDetached) {
spinner.dismiss();
}
/*
* Once web view is fully loaded, set the contentFrameLayout background to be transparent
* and make visible the 'x' image.
*/
contentFrameLayout.setBackgroundColor(Color.TRANSPARENT);
webView.setVisibility(View.VISIBLE);
crossImageView.setVisibility(View.VISIBLE);
}
}
private static class BuilderBase<CONCRETE extends BuilderBase<?>> {
private Context context;
private Session session;
private String applicationId;
private String action;
private int theme = DEFAULT_THEME;
private OnCompleteListener listener;
private Bundle parameters;
protected BuilderBase(Context context, Session session, String action, Bundle parameters) {
Validate.notNull(session, "session");
if (!session.isOpened()) {
throw new FacebookException("Attempted to use a Session that was not open.");
}
this.session = session;
finishInit(context, action, parameters);
}
protected BuilderBase(Context context, String applicationId, String action, Bundle parameters) {
Validate.notNullOrEmpty(applicationId, "applicationId");
this.applicationId = applicationId;
finishInit(context, action, parameters);
}
/**
* Sets a theme identifier which will be passed to the underlying Dialog.
*
* @param theme a theme identifier which will be passed to the Dialog class
* @return the builder
*/
public CONCRETE setTheme(int theme) {
this.theme = theme;
@SuppressWarnings("unchecked")
CONCRETE result = (CONCRETE) this;
return result;
}
/**
* Sets the listener which will be notified when the dialog finishes.
*
* @param listener the listener to notify, or null if no notification is desired
* @return the builder
*/
public CONCRETE setOnCompleteListener(OnCompleteListener listener) {
this.listener = listener;
@SuppressWarnings("unchecked")
CONCRETE result = (CONCRETE) this;
return result;
}
/**
* Constructs a WebDialog using the parameters provided. The dialog is not shown,
* but is ready to be shown by calling Dialog.show().
*
* @return the WebDialog
*/
public WebDialog build() {
if (session != null && session.isOpened()) {
parameters.putString(ServerProtocol.DIALOG_PARAM_APP_ID, session.getApplicationId());
parameters.putString(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, session.getAccessToken());
} else {
parameters.putString(ServerProtocol.DIALOG_PARAM_APP_ID, applicationId);
}
if (!parameters.containsKey(ServerProtocol.DIALOG_PARAM_REDIRECT_URI)) {
parameters.putString(ServerProtocol.DIALOG_PARAM_REDIRECT_URI, REDIRECT_URI);
}
return new WebDialog(context, action, parameters, theme, listener);
}
protected String getApplicationId() {
return applicationId;
}
protected Context getContext() {
return context;
}
protected int getTheme() {
return theme;
}
protected Bundle getParameters() {
return parameters;
}
protected WebDialog.OnCompleteListener getListener() {
return listener;
}
private void finishInit(Context context, String action, Bundle parameters) {
this.context = context;
this.action = action;
if (parameters != null) {
this.parameters = parameters;
} else {
this.parameters = new Bundle();
}
}
}
/**
* Provides a builder that allows construction of an arbitary Facebook web dialog.
*/
public static class Builder extends BuilderBase<Builder> {
/**
* Constructor that builds a dialog for an authenticated user.
*
* @param context the Context within which the dialog will be shown.
* @param session the Session representing an authenticating user to use for
* showing the dialog; must not be null, and must be opened.
* @param action the portion of the dialog URL following www.facebook.com/dialog/.
* See https://developers.facebook.com/docs/reference/dialogs/ for details.
* @param parameters a Bundle containing parameters to pass as part of the URL.
*/
public Builder(Context context, Session session, String action, Bundle parameters) {
super(context, session, action, parameters);
}
/**
* Constructor that builds a dialog without an authenticated user.
*
* @param context the Context within which the dialog will be shown.
* @param applicationId the application ID to be included in the dialog URL.
* @param action the portion of the dialog URL following www.facebook.com/dialog/.
* See https://developers.facebook.com/docs/reference/dialogs/ for details.
* @param parameters a Bundle containing parameters to pass as part of the URL.
*/
public Builder(Context context, String applicationId, String action, Bundle parameters) {
super(context, applicationId, action, parameters);
}
}
/**
* Provides a builder that allows construction of the parameters for showing
* the <a href="https://developers.facebook.com/docs/reference/dialogs/feed">Feed Dialog</a>.
*/
public static class FeedDialogBuilder extends BuilderBase<FeedDialogBuilder> {
private static final String FEED_DIALOG = "feed";
private static final String FROM_PARAM = "from";
private static final String TO_PARAM = "to";
private static final String LINK_PARAM = "link";
private static final String PICTURE_PARAM = "picture";
private static final String SOURCE_PARAM = "source";
private static final String NAME_PARAM = "name";
private static final String CAPTION_PARAM = "caption";
private static final String DESCRIPTION_PARAM = "description";
/**
* Constructor.
*
* @param context the Context within which the dialog will be shown.
* @param session the Session representing an authenticating user to use for
* showing the dialog; must not be null, and must be opened.
*/
public FeedDialogBuilder(Context context, Session session) {
super(context, session, FEED_DIALOG, null);
}
/**
* Constructor.
*
* @param context the Context within which the dialog will be shown.
* @param parameters a Bundle containing parameters to pass as part of the
* dialog URL. No validation is done on these parameters; it is
* the caller's responsibility to ensure they are valid. For more information,
* see <a href="https://developers.facebook.com/docs/reference/dialogs/feed/">
* https://developers.facebook.com/docs/reference/dialogs/feed/</a>.
* @param session the Session representing an authenticating user to use for
* showing the dialog; must not be null, and must be opened.
*/
public FeedDialogBuilder(Context context, Session session, Bundle parameters) {
super(context, session, FEED_DIALOG, parameters);
}
/**
* Sets the ID of the profile that is posting to Facebook. If none is specified,
* the default is "me". This profile must be either the authenticated user or a
* Page that the user is an administrator of.
*
* @param id Facebook ID of the profile to post from
* @return the builder
*/
public FeedDialogBuilder setFrom(String id) {
getParameters().putString(FROM_PARAM, id);
return this;
}
/**
* Sets the ID of the profile that the story will be published to. If not specified, it
* will default to the same profile that the story is being published from.
*
* @param id Facebook ID of the profile to post to
* @return the builder
*/
public FeedDialogBuilder setTo(String id) {
getParameters().putString(TO_PARAM, id);
return this;
}
/**
* Sets the URL of a link to be shared.
*
* @param link the URL
* @return the builder
*/
public FeedDialogBuilder setLink(String link) {
getParameters().putString(LINK_PARAM, link);
return this;
}
/**
* Sets the URL of a picture to be shared.
*
* @param picture the URL of the picture
* @return the builder
*/
public FeedDialogBuilder setPicture(String picture) {
getParameters().putString(PICTURE_PARAM, picture);
return this;
}
/**
* Sets the URL of a media file attached to this post. If this is set, any picture
* set via setPicture will be ignored.
*
* @param source the URL of the media file
* @return the builder
*/
public FeedDialogBuilder setSource(String source) {
getParameters().putString(SOURCE_PARAM, source);
return this;
}
/**
* Sets the name of the item being shared.
*
* @param name the name
* @return the builder
*/
public FeedDialogBuilder setName(String name) {
getParameters().putString(NAME_PARAM, name);
return this;
}
/**
* Sets the caption to be displayed.
*
* @param caption the caption
* @return the builder
*/
public FeedDialogBuilder setCaption(String caption) {
getParameters().putString(CAPTION_PARAM, caption);
return this;
}
/**
* Sets the description to be displayed.
*
* @param description the description
* @return the builder
*/
public FeedDialogBuilder setDescription(String description) {
getParameters().putString(DESCRIPTION_PARAM, description);
return this;
}
}
/**
* Provides a builder that allows construction of the parameters for showing
* the <a href="https://developers.facebook.com/docs/reference/dialogs/requests">Requests Dialog</a>.
*/
public static class RequestsDialogBuilder extends BuilderBase<RequestsDialogBuilder> {
private static final String APPREQUESTS_DIALOG = "apprequests";
private static final String MESSAGE_PARAM = "message";
private static final String TO_PARAM = "to";
private static final String DATA_PARAM = "data";
private static final String TITLE_PARAM = "title";
/**
* Constructor.
*
* @param context the Context within which the dialog will be shown.
* @param session the Session representing an authenticating user to use for
* showing the dialog; must not be null, and must be opened.
*/
public RequestsDialogBuilder(Context context, Session session) {
super(context, session, APPREQUESTS_DIALOG, null);
}
/**
* Constructor.
*
* @param context the Context within which the dialog will be shown.
* @param parameters a Bundle containing parameters to pass as part of the
* dialog URL. No validation is done on these parameters; it is
* the caller's responsibility to ensure they are valid. For more information,
* see <a href="https://developers.facebook.com/docs/reference/dialogs/requests/">
* https://developers.facebook.com/docs/reference/dialogs/requests/</a>.
* @param session the Session representing an authenticating user to use for
* showing the dialog; must not be null, and must be opened.
*/
public RequestsDialogBuilder(Context context, Session session, Bundle parameters) {
super(context, session, APPREQUESTS_DIALOG, parameters);
}
/**
* Sets the string users receiving the request will see. The maximum length
* is 60 characters.
*
* @param message the message
* @return the builder
*/
public RequestsDialogBuilder setMessage(String message) {
getParameters().putString(MESSAGE_PARAM, message);
return this;
}
/**
* Sets the user ID or user name the request will be sent to. If this is not
* specified, a friend selector will be displayed and the user can select up
* to 50 friends.
*
* @param id the id or user name to send the request to
* @return the builder
*/
public RequestsDialogBuilder setTo(String id) {
getParameters().putString(TO_PARAM, id);
return this;
}
/**
* Sets optional data which can be used for tracking; maximum length is 255
* characters.
*
* @param data the data
* @return the builder
*/
public RequestsDialogBuilder setData(String data) {
getParameters().putString(DATA_PARAM, data);
return this;
}
/**
* Sets an optional title for the dialog; maximum length is 50 characters.
*
* @param title the title
* @return the builder
*/
public RequestsDialogBuilder setTitle(String title) {
getParameters().putString(TITLE_PARAM, title);
return this;
}
}
}