/** * 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.login; import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.DialogFragment; import android.text.Html; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import com.facebook.AccessToken; import com.facebook.AccessTokenSource; import com.facebook.FacebookActivity; import com.facebook.FacebookException; import com.facebook.FacebookRequestError; import com.facebook.FacebookSdk; import com.facebook.GraphRequest; import com.facebook.GraphRequestAsyncTask; import com.facebook.GraphResponse; import com.facebook.HttpMethod; import com.facebook.R; import com.facebook.appevents.AppEventsLogger; import com.facebook.devicerequests.internal.DeviceRequestsHelper; import com.facebook.internal.AnalyticsEvents; import com.facebook.internal.FetchedAppSettings; import com.facebook.internal.FetchedAppSettingsManager; import com.facebook.internal.ImageDownloader; import com.facebook.internal.ImageRequest; import com.facebook.internal.ImageResponse; import com.facebook.internal.SmartLoginOption; import com.facebook.internal.Utility; import com.facebook.internal.Validate; import org.json.JSONException; import org.json.JSONObject; import java.util.Date; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; public class DeviceAuthDialog extends DialogFragment { private static final String DEVICE_LOGIN_ENDPOINT = "device/login"; private static final String DEVICE_LOGIN_STATUS_ENDPOINT = "device/login_status"; private static final String REQUEST_STATE_KEY = "request_state"; private static final int LOGIN_ERROR_SUBCODE_EXCESSIVE_POLLING = 1349172; private static final int LOGIN_ERROR_SUBCODE_AUTHORIZATION_DECLINED = 1349173; private static final int LOGIN_ERROR_SUBCODE_AUTHORIZATION_PENDING = 1349174; private static final int LOGIN_ERROR_SUBCODE_CODE_EXPIRED = 1349152; private ProgressBar progressBar; private TextView confirmationCode; private DeviceAuthMethodHandler deviceAuthMethodHandler; private AtomicBoolean completed = new AtomicBoolean(); private volatile GraphRequestAsyncTask currentGraphRequestPoll; private volatile ScheduledFuture scheduledPoll; private volatile RequestState currentRequestState; private Dialog dialog; // Used to tell if we are destroying the fragment because it was dismissed or dismissing the // fragment because it is being destroyed. private boolean isBeingDestroyed = false; private boolean isRetry = false; private LoginClient.Request mRequest = null; @Nullable @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); FacebookActivity facebookActivity = (FacebookActivity) getActivity(); LoginFragment fragment = (LoginFragment)facebookActivity.getCurrentFragment(); deviceAuthMethodHandler = (DeviceAuthMethodHandler)fragment .getLoginClient() .getCurrentHandler(); if (savedInstanceState != null) { RequestState requestState = savedInstanceState.getParcelable(REQUEST_STATE_KEY); if (requestState != null) { setCurrentRequestState(requestState); } } return view; } @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { dialog = new Dialog(getActivity(), R.style.com_facebook_auth_dialog); LayoutInflater inflater = getActivity().getLayoutInflater(); View view = initializeContentView(DeviceRequestsHelper.isAvailable() && !this.isRetry); dialog.setContentView(view); return dialog; } @Override public void onDismiss(final DialogInterface dialog) { super.onDismiss(dialog); if (!isBeingDestroyed) { onCancel(); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (currentRequestState != null) { outState.putParcelable(REQUEST_STATE_KEY, currentRequestState); } } @Override public void onDestroy() { // Set this to true so we know if we are being destroyed and then dismissing the dialog // Or if we are dismissing the dialog and then destroying the fragment. In latter we want // to do a cancel callback. isBeingDestroyed = true; completed.set(true); super.onDestroy(); if (currentGraphRequestPoll != null) { currentGraphRequestPoll.cancel(true); } if (scheduledPoll != null) { scheduledPoll.cancel(true); } } public void startLogin(final LoginClient.Request request) { this.mRequest = request; final Bundle parameters = new Bundle(); parameters.putString("scope", TextUtils.join(",", request.getPermissions())); String redirectUriString = request.getDeviceRedirectUriString(); if (redirectUriString != null) { parameters.putString("redirect_uri", redirectUriString); } String accessToken = Validate.hasAppID()+ "|" + Validate.hasClientToken(); parameters.putString(GraphRequest.ACCESS_TOKEN_PARAM, accessToken); parameters.putString(DeviceRequestsHelper.DEVICE_INFO_PARAM, DeviceRequestsHelper.getDeviceInfo()); GraphRequest graphRequest = new GraphRequest( null, DEVICE_LOGIN_ENDPOINT, parameters, HttpMethod.POST, new GraphRequest.Callback() { @Override public void onCompleted(GraphResponse response) { if (isBeingDestroyed) { return; } if (response.getError() != null) { onError(response.getError().getException()); return; } JSONObject jsonObject = response.getJSONObject(); RequestState requestState = new RequestState(); try { requestState.setUserCode(jsonObject.getString("user_code")); requestState.setRequestCode(jsonObject.getString("code")); requestState.setInterval(jsonObject.getLong("interval")); } catch (JSONException ex) { onError(new FacebookException(ex)); return; } setCurrentRequestState(requestState); } }); graphRequest.executeAsync(); } private void setCurrentRequestState(RequestState currentRequestState) { this.currentRequestState = currentRequestState; confirmationCode.setText(currentRequestState.getUserCode()); confirmationCode.setVisibility(View.VISIBLE); progressBar.setVisibility(View.GONE); if (!isRetry) { if (DeviceRequestsHelper.startAdvertisementService(currentRequestState.getUserCode())) { final AppEventsLogger logger = AppEventsLogger.newLogger(getContext()); logger.logSdkEvent(AnalyticsEvents.EVENT_SMART_LOGIN_SERVICE, null, null); } } // If we polled within the last interval schedule a poll else start a poll. if (currentRequestState.withinLastRefreshWindow()) { schedulePoll(); } else { poll(); } } private View initializeContentView(boolean isSmartLogin) { View view; LayoutInflater inflater = this.getActivity().getLayoutInflater(); if (isSmartLogin) { view = inflater.inflate(R.layout.com_facebook_smart_device_dialog_fragment, null); } else { view = inflater.inflate(R.layout.com_facebook_device_auth_dialog_fragment, null); } progressBar = (ProgressBar)view.findViewById(R.id.progress_bar); confirmationCode = (TextView)view.findViewById(R.id.confirmation_code); Button cancelButton = (Button) view.findViewById(R.id.cancel_button); cancelButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onCancel(); } }); TextView instructions = (TextView)view.findViewById( R.id.com_facebook_device_auth_instructions); instructions.setText( Html.fromHtml(getString(R.string.com_facebook_device_auth_instructions))); return view; } private void poll() { currentRequestState.setLastPoll(new Date().getTime()); currentGraphRequestPoll = getPollRequest().executeAsync(); } private void schedulePoll() { scheduledPoll = DeviceAuthMethodHandler.getBackgroundExecutor().schedule( new Runnable() { @Override public void run() { poll(); } }, currentRequestState.getInterval(), TimeUnit.SECONDS); } private GraphRequest getPollRequest() { Bundle parameters = new Bundle(); parameters.putString("code", currentRequestState.getRequestCode()); return new GraphRequest( null, DEVICE_LOGIN_STATUS_ENDPOINT, parameters, HttpMethod.POST, new GraphRequest.Callback() { @Override public void onCompleted(GraphResponse response) { // Check if the request was already cancelled if (completed.get()) { return; } FacebookRequestError error = response.getError(); if (error != null) { // We need to decide if this is a fatal error by checking the error // message text switch (error.getSubErrorCode()) { case LOGIN_ERROR_SUBCODE_AUTHORIZATION_PENDING: case LOGIN_ERROR_SUBCODE_EXCESSIVE_POLLING: { // Keep polling. If we got the slow down message just ignore schedulePoll(); } break; case LOGIN_ERROR_SUBCODE_CODE_EXPIRED: case LOGIN_ERROR_SUBCODE_AUTHORIZATION_DECLINED: { onCancel(); } break; default: { onError(response.getError().getException()); } break; } return; } try { JSONObject resultObject = response.getJSONObject(); onSuccess(resultObject.getString("access_token")); } catch (JSONException ex) { onError(new FacebookException(ex)); } } }); } private void presentConfirmation(final String userId, final Utility.PermissionsPair permissions, final String accessToken, final String name) { final String message = getResources().getString( R.string.com_facebook_smart_login_confirmation_title); final String continueFormat = getResources().getString( R.string.com_facebook_smart_login_confirmation_continue_as); final String cancel = getResources().getString( R.string.com_facebook_smart_login_confirmation_cancel); final String continueText = String.format(continueFormat, name); AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setMessage(message) .setCancelable(true) .setNegativeButton(continueText, new DialogInterface.OnClickListener() { public void onClick(DialogInterface alertDialog, int which) { completeLogin(userId, permissions, accessToken); } }) .setPositiveButton(cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface alertDialog, int which) { View view = initializeContentView(false); dialog.setContentView(view); startLogin(mRequest); } }); builder.create().show(); } private void onSuccess(final String accessToken) { Bundle parameters = new Bundle(); parameters.putString("fields", "id,permissions,name"); AccessToken temporaryToken = new AccessToken( accessToken, FacebookSdk.getApplicationId(), "0", null, null, null, null, null); GraphRequest request = new GraphRequest( temporaryToken, "me", parameters, HttpMethod.GET, new GraphRequest.Callback() { @Override public void onCompleted(GraphResponse response) { if (completed.get()) { return; } if (response.getError() != null) { onError(response.getError().getException()); return; } String userId; Utility.PermissionsPair permissions; String name; try { JSONObject jsonObject = response.getJSONObject(); userId = jsonObject.getString("id"); permissions = Utility.handlePermissionResponse(jsonObject); name = jsonObject.getString("name"); } catch (JSONException ex) { onError(new FacebookException(ex)); return; } DeviceRequestsHelper.cleanUpAdvertisementService( currentRequestState.getUserCode()); boolean requireConfirm = FetchedAppSettingsManager. getAppSettingsWithoutQuery(FacebookSdk.getApplicationId()). getSmartLoginOptions().contains(SmartLoginOption.RequireConfirm); if (requireConfirm && !isRetry) { isRetry = true; presentConfirmation(userId, permissions, accessToken, name); return; } completeLogin(userId, permissions, accessToken); } }); request.executeAsync(); } private void completeLogin(String userId, Utility.PermissionsPair permissions, String accessToken) { deviceAuthMethodHandler.onSuccess( accessToken, FacebookSdk.getApplicationId(), userId, permissions.getGrantedPermissions(), permissions.getDeclinedPermissions(), AccessTokenSource.DEVICE_AUTH, null, null); dialog.dismiss(); } private void onError(FacebookException ex) { if (!completed.compareAndSet(false, true)) { return; } if (currentRequestState != null) { DeviceRequestsHelper.cleanUpAdvertisementService(currentRequestState.getUserCode()); } deviceAuthMethodHandler.onError(ex); dialog.dismiss(); } private void onCancel() { if (!completed.compareAndSet(false, true)) { // Should not have happened but we called cancel twice return; } if (currentRequestState != null) { DeviceRequestsHelper.cleanUpAdvertisementService(currentRequestState.getUserCode()); } if (deviceAuthMethodHandler != null) { // We are detached and cannot send a cancel message back deviceAuthMethodHandler.onCancel(); } dialog.dismiss(); } private static class RequestState implements Parcelable{ private String userCode; private String requestCode; private long interval; private long lastPoll; RequestState() {} public String getUserCode() { return userCode; } public void setUserCode(String userCode) { this.userCode = userCode; } public String getRequestCode() { return requestCode; } public void setRequestCode(String requestCode) { this.requestCode = requestCode; } public long getInterval() { return interval; } public void setInterval(long interval) { this.interval = interval; } public void setLastPoll(long lastPoll) { this.lastPoll = lastPoll; } protected RequestState(Parcel in) { userCode = in.readString(); requestCode = in.readString(); interval = in.readLong(); lastPoll = in.readLong(); } /** * * @return True if the current time is less than last poll time + polling interval. */ public boolean withinLastRefreshWindow() { if (lastPoll == 0) { return false; } long diff = new Date().getTime() - lastPoll - interval * 1000L; return diff < 0; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(userCode); dest.writeString(requestCode); dest.writeLong(interval); dest.writeLong(lastPoll); } @SuppressWarnings("unused") public static final Parcelable.Creator<RequestState> CREATOR = new Parcelable.Creator<RequestState>() { @Override public RequestState createFromParcel(Parcel in) { return new RequestState(in); } @Override public RequestState[] newArray(int size) { return new RequestState[size]; } }; } }