(targetsCount);
@@ -153,7 +157,7 @@ public void onCompleted(Response response) {
}
}
- Uri webFallbackUrl = getWebFallbackUriFromJson(uri, urlData);
+ Uri webFallbackUrl = getWebFallbackUriFromJson(uri, appLinkData);
AppLink appLink = new AppLink(uri, targets, webFallbackUrl);
appLinkResults.put(uri, appLink);
diff --git a/platforms/android/FacebookLib/src/com/facebook/FacebookBroadcastReceiver.java b/platforms/android/FacebookLib/src/com/facebook/FacebookBroadcastReceiver.java
index ec2556c9b..1704cdaf8 100644
--- a/platforms/android/FacebookLib/src/com/facebook/FacebookBroadcastReceiver.java
+++ b/platforms/android/FacebookLib/src/com/facebook/FacebookBroadcastReceiver.java
@@ -4,7 +4,6 @@
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
-import android.util.Log;
import com.facebook.internal.NativeProtocol;
/**
diff --git a/platforms/android/FacebookLib/src/com/facebook/FacebookRequestError.java b/platforms/android/FacebookLib/src/com/facebook/FacebookRequestError.java
index 937e2b580..954d0ecac 100644
--- a/platforms/android/FacebookLib/src/com/facebook/FacebookRequestError.java
+++ b/platforms/android/FacebookLib/src/com/facebook/FacebookRequestError.java
@@ -58,6 +58,9 @@ public final class FacebookRequestError {
private static final String ERROR_SUB_CODE_KEY = "error_subcode";
private static final String ERROR_MSG_KEY = "error_msg";
private static final String ERROR_REASON_KEY = "error_reason";
+ private static final String ERROR_USER_TITLE_KEY = "error_user_title";
+ private static final String ERROR_USER_MSG_KEY = "error_user_msg";
+ private static final String ERROR_IS_TRANSIENT_KEY = "is_transient";
private static class Range {
private final int start, end;
@@ -98,6 +101,9 @@ boolean contains(int value) {
private final int subErrorCode;
private final String errorType;
private final String errorMessage;
+ private final String errorUserTitle;
+ private final String errorUserMessage;
+ private final boolean errorIsTransient;
private final JSONObject requestResult;
private final JSONObject requestResultBody;
private final Object batchRequestResult;
@@ -105,9 +111,9 @@ boolean contains(int value) {
private final FacebookException exception;
private FacebookRequestError(int requestStatusCode, int errorCode,
- int subErrorCode, String errorType, String errorMessage, JSONObject requestResultBody,
- JSONObject requestResult, Object batchRequestResult, HttpURLConnection connection,
- FacebookException exception) {
+ int subErrorCode, String errorType, String errorMessage, String errorUserTitle, String errorUserMessage,
+ boolean errorIsTransient, JSONObject requestResultBody, JSONObject requestResult, Object batchRequestResult,
+ HttpURLConnection connection, FacebookException exception) {
this.requestStatusCode = requestStatusCode;
this.errorCode = errorCode;
this.subErrorCode = subErrorCode;
@@ -117,6 +123,9 @@ private FacebookRequestError(int requestStatusCode, int errorCode,
this.requestResult = requestResult;
this.batchRequestResult = batchRequestResult;
this.connection = connection;
+ this.errorUserTitle = errorUserTitle;
+ this.errorUserMessage = errorUserMessage;
+ this.errorIsTransient = errorIsTransient;
boolean isLocalException = false;
if (exception != null) {
@@ -172,28 +181,32 @@ private FacebookRequestError(int requestStatusCode, int errorCode,
}
}
+ // Notify user when error_user_msg is present
+ shouldNotify = errorUserMessage!= null && errorUserMessage.length() > 0;
+
this.category = errorCategory;
this.userActionMessageId = messageId;
this.shouldNotifyUser = shouldNotify;
}
private FacebookRequestError(int requestStatusCode, int errorCode,
- int subErrorCode, String errorType, String errorMessage, JSONObject requestResultBody,
- JSONObject requestResult, Object batchRequestResult, HttpURLConnection connection) {
- this(requestStatusCode, errorCode, subErrorCode, errorType, errorMessage,
- requestResultBody, requestResult, batchRequestResult, connection, null);
+ int subErrorCode, String errorType, String errorMessage, String errorUserTitle, String errorUserMessage,
+ boolean errorIsTransient, JSONObject requestResultBody, JSONObject requestResult, Object batchRequestResult,
+ HttpURLConnection connection) {
+ this(requestStatusCode, errorCode, subErrorCode, errorType, errorMessage, errorUserTitle, errorUserMessage,
+ errorIsTransient, requestResultBody, requestResult, batchRequestResult, connection, null);
}
FacebookRequestError(HttpURLConnection connection, Exception exception) {
this(INVALID_HTTP_STATUS_CODE, INVALID_ERROR_CODE, INVALID_ERROR_CODE,
- null, null, null, null, null, connection,
+ null, null, null, null, false, null, null, null, connection,
(exception instanceof FacebookException) ?
(FacebookException) exception : new FacebookException(exception));
}
public FacebookRequestError(int errorCode, String errorType, String errorMessage) {
this(INVALID_HTTP_STATUS_CODE, errorCode, INVALID_ERROR_CODE, errorType, errorMessage,
- null, null, null, null, null);
+ null, null, false, null, null, null, null, null);
}
/**
@@ -279,6 +292,36 @@ public String getErrorMessage() {
}
}
+ /**
+ * A message suitable for display to the user, describing a user action necessary to enable Facebook functionality.
+ * Not all Facebook errors yield a message suitable for user display; however in all cases where
+ * shouldNotifyUser() returns true, this method returns a non-null message suitable for display.
+ *
+ * @return the error message returned from Facebook
+ */
+ public String getErrorUserMessage() {
+ return errorUserMessage;
+ }
+
+ /**
+ * A short summary of the error suitable for display to the user.
+ * Not all Facebook errors yield a title/message suitable for user display; however in all cases where
+ * getErrorUserTitle() returns valid String - user should be notified.
+ *
+ * @return the error message returned from Facebook
+ */
+ public String getErrorUserTitle() {
+ return errorUserTitle;
+ }
+
+ /**
+ * @return true if given error is transient and may succeed if the initial action is retried as-is.
+ * Application may use this information to display a "Retry" button, if user should be notified about this error.
+ */
+ public boolean getErrorIsTransient() {
+ return errorIsTransient;
+ }
+
/**
* Returns the body portion of the response corresponding to the request from Facebook.
*
@@ -359,6 +402,9 @@ static FacebookRequestError checkResponseAndCreateError(JSONObject singleResult,
// with several sub-properties, or else one or more top-level fields containing error info.
String errorType = null;
String errorMessage = null;
+ String errorUserMessage = null;
+ String errorUserTitle = null;
+ boolean errorIsTransient = false;
int errorCode = INVALID_ERROR_CODE;
int errorSubCode = INVALID_ERROR_CODE;
@@ -371,6 +417,9 @@ static FacebookRequestError checkResponseAndCreateError(JSONObject singleResult,
errorMessage = error.optString(ERROR_MESSAGE_FIELD_KEY, null);
errorCode = error.optInt(ERROR_CODE_FIELD_KEY, INVALID_ERROR_CODE);
errorSubCode = error.optInt(ERROR_SUB_CODE_KEY, INVALID_ERROR_CODE);
+ errorUserMessage = error.optString(ERROR_USER_MSG_KEY, null);
+ errorUserTitle = error.optString(ERROR_USER_TITLE_KEY, null);
+ errorIsTransient = error.optBoolean(ERROR_IS_TRANSIENT_KEY, false);
hasError = true;
} else if (jsonBody.has(ERROR_CODE_KEY) || jsonBody.has(ERROR_MSG_KEY)
|| jsonBody.has(ERROR_REASON_KEY)) {
@@ -383,14 +432,15 @@ static FacebookRequestError checkResponseAndCreateError(JSONObject singleResult,
if (hasError) {
return new FacebookRequestError(responseCode, errorCode, errorSubCode,
- errorType, errorMessage, jsonBody, singleResult, batchResult, connection);
+ errorType, errorMessage, errorUserTitle, errorUserMessage, errorIsTransient, jsonBody,
+ singleResult, batchResult, connection);
}
}
// If we didn't get error details, but we did get a failure response code, report it.
if (!HTTP_RANGE_SUCCESS.contains(responseCode)) {
return new FacebookRequestError(responseCode, INVALID_ERROR_CODE,
- INVALID_ERROR_CODE, null, null,
+ INVALID_ERROR_CODE, null, null, null, null, false,
singleResult.has(BODY_KEY) ?
(JSONObject) Utility.getStringPropertyAsJSON(
singleResult, BODY_KEY, Response.NON_JSON_RESPONSE_PROPERTY) : null,
diff --git a/platforms/android/FacebookLib/src/com/facebook/FacebookSdkVersion.java b/platforms/android/FacebookLib/src/com/facebook/FacebookSdkVersion.java
index 4e0e5ea63..47be1df96 100644
--- a/platforms/android/FacebookLib/src/com/facebook/FacebookSdkVersion.java
+++ b/platforms/android/FacebookLib/src/com/facebook/FacebookSdkVersion.java
@@ -17,5 +17,5 @@
package com.facebook;
final class FacebookSdkVersion {
- public static final String BUILD = "3.16.0";
+ public static final String BUILD = "3.18.1";
}
diff --git a/platforms/android/FacebookLib/src/com/facebook/FacebookTimeSpentData.java b/platforms/android/FacebookLib/src/com/facebook/FacebookTimeSpentData.java
new file mode 100644
index 000000000..2a5e2155c
--- /dev/null
+++ b/platforms/android/FacebookLib/src/com/facebook/FacebookTimeSpentData.java
@@ -0,0 +1,301 @@
+package com.facebook;
+
+import android.os.Bundle;
+import android.text.format.DateUtils;
+
+import com.facebook.internal.Logger;
+
+import java.io.Serializable;
+
+class FacebookTimeSpentData implements Serializable {
+ // Constants
+ private static final long serialVersionUID = 1L;
+ private static final String TAG = AppEventsLogger.class.getCanonicalName();
+ private static final long FIRST_TIME_LOAD_RESUME_TIME = -1;
+ private static final long INTERRUPTION_THRESHOLD_MILLISECONDS = 1000;
+ private static final long NUM_MILLISECONDS_IDLE_TO_BE_NEW_SESSION =
+ 60 * DateUtils.SECOND_IN_MILLIS;
+ private static final long APP_ACTIVATE_SUPPRESSION_PERIOD_IN_MILLISECONDS =
+ 5 * DateUtils.MINUTE_IN_MILLIS;
+
+ // Should be kept in sync with the iOS sdk
+ private static final long[] INACTIVE_SECONDS_QUANTA =
+ new long[] {
+ 5 * DateUtils.MINUTE_IN_MILLIS,
+ 15 * DateUtils.MINUTE_IN_MILLIS,
+ 30 * DateUtils.MINUTE_IN_MILLIS,
+ 1 * DateUtils.HOUR_IN_MILLIS,
+ 6 * DateUtils.HOUR_IN_MILLIS,
+ 12 * DateUtils.HOUR_IN_MILLIS,
+ 1 * DateUtils.DAY_IN_MILLIS,
+ 2 * DateUtils.DAY_IN_MILLIS,
+ 3 * DateUtils.DAY_IN_MILLIS,
+ 7 * DateUtils.DAY_IN_MILLIS,
+ 14 * DateUtils.DAY_IN_MILLIS,
+ 21 * DateUtils.DAY_IN_MILLIS,
+ 28 * DateUtils.DAY_IN_MILLIS,
+ 60 * DateUtils.DAY_IN_MILLIS,
+ 90 * DateUtils.DAY_IN_MILLIS,
+ 120 * DateUtils.DAY_IN_MILLIS,
+ 150 * DateUtils.DAY_IN_MILLIS,
+ 180 * DateUtils.DAY_IN_MILLIS,
+ 365 * DateUtils.DAY_IN_MILLIS,
+ };
+
+ private boolean isWarmLaunch;
+ private boolean isAppActive;
+ private long lastActivateEventLoggedTime;
+
+ // Member data that's persisted to disk
+ private long lastResumeTime;
+ private long lastSuspendTime;
+ private long millisecondsSpentInSession;
+ private int interruptionCount;
+ private String firstOpenSourceApplication;
+
+ /**
+ * Serialization proxy for the FacebookTimeSpentData class. This is version 1 of
+ * serialization. Future serializations may differ in format. This
+ * class should not be modified. If serializations formats change,
+ * create a new class SerializationProxyVx.
+ */
+ private static class SerializationProxyV1 implements Serializable {
+ private static final long serialVersionUID = 6L;
+
+ private final long lastResumeTime;
+ private final long lastSuspendTime;
+ private final long millisecondsSpentInSession;
+ private final int interruptionCount;
+
+ SerializationProxyV1(
+ long lastResumeTime,
+ long lastSuspendTime,
+ long millisecondsSpentInSession,
+ int interruptionCount
+ ) {
+ this.lastResumeTime = lastResumeTime;
+ this.lastSuspendTime = lastSuspendTime;
+ this.millisecondsSpentInSession = millisecondsSpentInSession;
+ this.interruptionCount = interruptionCount;
+ }
+
+ private Object readResolve() {
+ return new FacebookTimeSpentData(
+ lastResumeTime,
+ lastSuspendTime,
+ millisecondsSpentInSession,
+ interruptionCount);
+ }
+ }
+
+
+ /**
+ * Constructor to be used for V1 serialization only, DO NOT CHANGE.
+ */
+ private FacebookTimeSpentData(
+ long lastResumeTime,
+ long lastSuspendTime,
+ long millisecondsSpentInSession,
+ int interruptionCount
+
+ ) {
+ resetSession();
+ this.lastResumeTime = lastResumeTime;
+ this.lastSuspendTime = lastSuspendTime;
+ this.millisecondsSpentInSession = millisecondsSpentInSession;
+ this.interruptionCount = interruptionCount;
+ }
+
+ /**
+ * Serialization proxy for the FacebookTimeSpentData class. This is version 2 of
+ * serialization. Future serializations may differ in format. This
+ * class should not be modified. If serializations formats change,
+ * create a new class SerializationProxyVx.
+ */
+ private static class SerializationProxyV2 implements Serializable {
+ private static final long serialVersionUID = 6L;
+
+ private final long lastResumeTime;
+ private final long lastSuspendTime;
+ private final long millisecondsSpentInSession;
+ private final int interruptionCount;
+ private final String firstOpenSourceApplication;
+
+ SerializationProxyV2(
+ long lastResumeTime,
+ long lastSuspendTime,
+ long millisecondsSpentInSession,
+ int interruptionCount,
+ String firstOpenSourceApplication
+
+ ) {
+ this.lastResumeTime = lastResumeTime;
+ this.lastSuspendTime = lastSuspendTime;
+ this.millisecondsSpentInSession = millisecondsSpentInSession;
+ this.interruptionCount = interruptionCount;
+ this.firstOpenSourceApplication = firstOpenSourceApplication;
+ }
+
+ private Object readResolve() {
+ return new FacebookTimeSpentData(
+ lastResumeTime,
+ lastSuspendTime,
+ millisecondsSpentInSession,
+ interruptionCount,
+ firstOpenSourceApplication);
+ }
+ }
+
+ FacebookTimeSpentData() {
+ resetSession();
+ }
+
+ /**
+ * Constructor to be used for V2 serialization only, DO NOT CHANGE.
+ */
+ private FacebookTimeSpentData(
+ long lastResumeTime,
+ long lastSuspendTime,
+ long millisecondsSpentInSession,
+ int interruptionCount,
+ String firstOpenSourceApplication
+ ) {
+ resetSession();
+ this.lastResumeTime = lastResumeTime;
+ this.lastSuspendTime = lastSuspendTime;
+ this.millisecondsSpentInSession = millisecondsSpentInSession;
+ this.interruptionCount = interruptionCount;
+ this.firstOpenSourceApplication = firstOpenSourceApplication;
+ }
+
+ private Object writeReplace() {
+ return new SerializationProxyV2(
+ lastResumeTime,
+ lastSuspendTime,
+ millisecondsSpentInSession,
+ interruptionCount,
+ firstOpenSourceApplication
+ );
+ }
+
+ void onSuspend(AppEventsLogger logger, long eventTime) {
+ if (!isAppActive) {
+ Logger.log(LoggingBehavior.APP_EVENTS, TAG, "Suspend for inactive app");
+ return;
+ }
+
+ long now = eventTime;
+ long delta = (now - lastResumeTime);
+ if (delta < 0) {
+ Logger.log(LoggingBehavior.APP_EVENTS, TAG, "Clock skew detected");
+ delta = 0;
+ }
+ millisecondsSpentInSession += delta;
+ lastSuspendTime = now;
+ isAppActive = false;
+ }
+
+ void onResume(AppEventsLogger logger, long eventTime, String sourceApplicationInfo) {
+ long now = eventTime;
+
+ // Retain old behavior for activated app event - log the event if the event hasn't
+ // been logged in the previous suppression interval or this is a cold launch.
+ // If this is a cold launch, always log the event. Otherwise, use the last
+ // event log time to determine if the app activate should be suppressed or not.
+ if (isColdLaunch() ||
+ ((now - lastActivateEventLoggedTime) > APP_ACTIVATE_SUPPRESSION_PERIOD_IN_MILLISECONDS)) {
+ Bundle eventParams = new Bundle();
+ eventParams.putString(
+ AppEventsConstants.EVENT_PARAM_SOURCE_APPLICATION,
+ sourceApplicationInfo);
+ logger.logEvent(AppEventsConstants.EVENT_NAME_ACTIVATED_APP, eventParams);
+ lastActivateEventLoggedTime = now;
+ }
+
+ // If this is an application that's not calling onSuspend yet, log and return. We can't
+ // track time spent for this application as there are no calls to onSuspend.
+ if (isAppActive) {
+ Logger.log(LoggingBehavior.APP_EVENTS, TAG, "Resume for active app");
+ return;
+ }
+
+ long interruptionDurationMillis = wasSuspendedEver() ? now - lastSuspendTime : 0;
+ if (interruptionDurationMillis < 0) {
+ Logger.log(LoggingBehavior.APP_EVENTS, TAG, "Clock skew detected");
+ interruptionDurationMillis = 0;
+ }
+
+ // If interruption duration is > new session threshold, then log old session
+ // event and start a new session.
+ if (interruptionDurationMillis > NUM_MILLISECONDS_IDLE_TO_BE_NEW_SESSION) {
+ logAppDeactivatedEvent(logger, interruptionDurationMillis);
+ } else {
+ // We're not logging this resume event - check to see if this should count
+ // as an interruption
+ if (interruptionDurationMillis > INTERRUPTION_THRESHOLD_MILLISECONDS) {
+ interruptionCount++;
+ }
+ }
+
+ // Set source application only for the first resume of the timespent session.
+ if (interruptionCount == 0) {
+ firstOpenSourceApplication = sourceApplicationInfo;
+ }
+
+ lastResumeTime = now;
+ isAppActive = true;
+ }
+
+ private void logAppDeactivatedEvent(AppEventsLogger logger,
+ long interruptionDurationMillis) {
+ // Log the old session information and clear the data
+ Bundle eventParams = new Bundle();
+ eventParams.putInt(
+ AppEventsConstants.EVENT_NAME_SESSION_INTERRUPTIONS,
+ interruptionCount);
+ eventParams.putInt(
+ AppEventsConstants.EVENT_NAME_TIME_BETWEEN_SESSIONS,
+ getQuantaIndex(interruptionDurationMillis));
+ eventParams.putString(
+ AppEventsConstants.EVENT_PARAM_SOURCE_APPLICATION,
+ firstOpenSourceApplication);
+ logger.logEvent(
+ AppEventsConstants.EVENT_NAME_DEACTIVATED_APP,
+ (millisecondsSpentInSession/DateUtils.SECOND_IN_MILLIS),
+ eventParams);
+ resetSession();
+ }
+
+ private static int getQuantaIndex(long timeBetweenSessions) {
+ int quantaIndex = 0;
+
+ while (
+ quantaIndex < INACTIVE_SECONDS_QUANTA.length &&
+ INACTIVE_SECONDS_QUANTA[quantaIndex] < timeBetweenSessions
+ ) {
+ ++quantaIndex;
+ }
+
+ return quantaIndex;
+ }
+
+ private void resetSession() {
+ isAppActive = false;
+ lastResumeTime = FIRST_TIME_LOAD_RESUME_TIME;
+ lastSuspendTime = FIRST_TIME_LOAD_RESUME_TIME;
+ interruptionCount = 0;
+ millisecondsSpentInSession = 0;
+ }
+
+ private boolean wasSuspendedEver() {
+ return lastSuspendTime != FIRST_TIME_LOAD_RESUME_TIME;
+ }
+
+ private boolean isColdLaunch() {
+ // On the very first call in the process lifecycle, this will always
+ // return true. After that, it will always return false.
+ boolean result = !isWarmLaunch;
+ isWarmLaunch = true;
+ return result;
+ }
+}
diff --git a/platforms/android/FacebookLib/src/com/facebook/GetTokenClient.java b/platforms/android/FacebookLib/src/com/facebook/GetTokenClient.java
index 23cb31bf8..c0f1a7015 100644
--- a/platforms/android/FacebookLib/src/com/facebook/GetTokenClient.java
+++ b/platforms/android/FacebookLib/src/com/facebook/GetTokenClient.java
@@ -18,7 +18,7 @@
import android.content.Context;
-import android.os.*;
+import android.os.Bundle;
import com.facebook.internal.NativeProtocol;
import com.facebook.internal.PlatformServiceClient;
diff --git a/platforms/android/FacebookLib/src/com/facebook/NativeAppCallAttachmentStore.java b/platforms/android/FacebookLib/src/com/facebook/NativeAppCallAttachmentStore.java
index dc4108656..3704be274 100644
--- a/platforms/android/FacebookLib/src/com/facebook/NativeAppCallAttachmentStore.java
+++ b/platforms/android/FacebookLib/src/com/facebook/NativeAppCallAttachmentStore.java
@@ -24,7 +24,10 @@
import java.io.*;
import java.net.URLEncoder;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
/**
* This class works in conjunction with {@link NativeAppCallContentProvider} to allow apps to attach binary
diff --git a/platforms/android/FacebookLib/src/com/facebook/NativeAppCallContentProvider.java b/platforms/android/FacebookLib/src/com/facebook/NativeAppCallContentProvider.java
index bde165f58..374547a1c 100644
--- a/platforms/android/FacebookLib/src/com/facebook/NativeAppCallContentProvider.java
+++ b/platforms/android/FacebookLib/src/com/facebook/NativeAppCallContentProvider.java
@@ -24,8 +24,9 @@
import android.util.Log;
import android.util.Pair;
-import java.io.*;
-import java.util.*;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.UUID;
/**
*
Implements a
diff --git a/platforms/android/FacebookLib/src/com/facebook/Request.java b/platforms/android/FacebookLib/src/com/facebook/Request.java
index 1bf5bc9fa..9b95938c8 100644
--- a/platforms/android/FacebookLib/src/com/facebook/Request.java
+++ b/platforms/android/FacebookLib/src/com/facebook/Request.java
@@ -42,15 +42,13 @@
import java.util.regex.Pattern;
/**
- * A single request to be sent to the Facebook Platform through either the Graph API or REST API. The Request class provides functionality
+ * A single request to be sent to the Facebook Platform through the Graph API. The Request class provides functionality
* relating to serializing and deserializing requests and responses, making calls in batches (with a single round-trip
* to the service) and making calls asynchronously.
*
- * The particular service endpoint that a request targets is determined by either a graph path (see the
- * {@link #setGraphPath(String) setGraphPath} method) or a REST method name (see the {@link #setRestMethod(String)
- * setRestMethod} method); a single request may not target both.
+ * The particular service endpoint that a request targets is determined by a graph path (see the
+ * {@link #setGraphPath(String) setGraphPath} method).
*
* A Request can be executed either anonymously or representing an authenticated user. In the former case, no Session
* needs to be specified, while in the latter, a Session that is in an opened state must be provided. If requests are
@@ -118,7 +116,6 @@ public class Request {
private HttpMethod httpMethod;
private String graphPath;
private GraphObject graphObject;
- private String restMethod;
private String batchEntryName;
private String batchEntryDependsOn;
private boolean batchEntryOmitResultOnSuccess = true;
@@ -266,26 +263,6 @@ public static Request newPostRequest(Session session, String graphPath, GraphObj
return request;
}
- /**
- * Creates a new Request configured to make a call to the Facebook REST API.
- *
- * @param session
- * the Session to use, or null; if non-null, the session must be in an opened state
- * @param restMethod
- * the method in the Facebook REST API to execute
- * @param parameters
- * additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers,
- * Bitmaps, Dates, or Byte arrays.
- * @param httpMethod
- * the HTTP method to use for the request; must be one of GET, POST, or DELETE
- * @return a Request that is ready to execute
- */
- public static Request newRestRequest(Session session, String restMethod, Bundle parameters, HttpMethod httpMethod) {
- Request request = new Request(session, null, parameters, httpMethod);
- request.setRestMethod(restMethod);
- return request;
- }
-
/**
* Creates a new Request configured to retrieve a user's own profile.
*
@@ -543,6 +520,7 @@ public static Request newStatusUpdateRequest(Session session, String message, Gr
* A `null` ID will be provided into the callback if a) there is no native Facebook app, b) no one is logged into
* it, or c) the app has previously called
* {@link Settings#setLimitEventAndDataUsage(android.content.Context, boolean)} with `true` for this user.
+ * You must call this method from a background thread for it to work properly.
*
* @param session
* the Session to issue the Request on, or null; if non-null, the session must be in an opened state.
@@ -577,6 +555,7 @@ public static Request newCustomAudienceThirdPartyIdRequest(Session session, Cont
* A `null` ID will be provided into the callback if a) there is no native Facebook app, b) no one is logged into
* it, or c) the app has previously called
* {@link Settings#setLimitEventAndDataUsage(android.content.Context, boolean)} ;} with `true` for this user.
+ * You must call this method from a background thread for it to work properly.
*
* @param session
* the Session to issue the Request on, or null; if non-null, the session must be in an opened state.
@@ -875,7 +854,7 @@ public final String getGraphPath() {
}
/**
- * Sets the graph path of this request. A graph path may not be set if a REST method has been specified.
+ * Sets the graph path of this request.
*
* @param graphPath
* the graph path for this request
@@ -945,25 +924,6 @@ public final void setParameters(Bundle parameters) {
this.parameters = parameters;
}
- /**
- * Returns the REST method to call for this request.
- *
- * @return the REST method
- */
- public final String getRestMethod() {
- return this.restMethod;
- }
-
- /**
- * Sets the REST method to call for this request. A REST method may not be set if a graph path has been specified.
- *
- * @param restMethod
- * the REST method to call
- */
- public final void setRestMethod(String restMethod) {
- this.restMethod = restMethod;
- }
-
/**
* Returns the Session associated with this request.
*
@@ -1139,30 +1099,6 @@ public static RequestAsyncTask executePostRequestAsync(Session session, String g
return newPostRequest(session, graphPath, graphObject, callback).executeAsync();
}
- /**
- * Starts a new Request configured to make a call to the Facebook REST API.
- *
- * This should only be called from the UI thread.
- *
- * This method is deprecated. Prefer to call Request.newRestRequest(...).executeAsync();
- *
- * @param session
- * the Session to use, or null; if non-null, the session must be in an opened state
- * @param restMethod
- * the method in the Facebook REST API to execute
- * @param parameters
- * additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers,
- * Bitmaps, Dates, or Byte arrays.
- * @param httpMethod
- * the HTTP method to use for the request; must be one of GET, POST, or DELETE
- * @return a RequestAsyncTask that is executing the request
- */
- @Deprecated
- public static RequestAsyncTask executeRestRequestAsync(Session session, String restMethod, Bundle parameters,
- HttpMethod httpMethod) {
- return newRestRequest(session, restMethod, parameters, httpMethod).executeAsync();
- }
-
/**
* Starts a new Request configured to retrieve a user's own profile.
*
@@ -1392,10 +1328,6 @@ public static HttpURLConnection toHttpConnection(Collection requests) {
*/
public static HttpURLConnection toHttpConnection(RequestBatch requests) {
- for (Request request : requests) {
- request.validate();
- }
-
URL url = null;
try {
if (requests.size() == 1) {
@@ -1702,8 +1634,8 @@ public static RequestAsyncTask executeConnectionAsync(Handler callbackHandler, H
@Override
public String toString() {
return new StringBuilder().append("{Request: ").append(" session: ").append(session).append(", graphPath: ")
- .append(graphPath).append(", graphObject: ").append(graphObject).append(", restMethod: ")
- .append(restMethod).append(", httpMethod: ").append(httpMethod).append(", parameters: ")
+ .append(graphPath).append(", graphObject: ").append(graphObject)
+ .append(", httpMethod: ").append(httpMethod).append(", parameters: ")
.append(parameters).append("}").toString();
}
@@ -1813,13 +1745,7 @@ final String getUrlForBatchedRequest() {
throw new FacebookException("Can't override URL for a batch request");
}
- String baseUrl;
- if (this.restMethod != null) {
- baseUrl = getRestPathWithVersion();
- } else {
- baseUrl = getGraphPathWithVersion();
- }
-
+ String baseUrl = getGraphPathWithVersion();
addCommonParameters();
return appendParametersToBaseUrl(baseUrl);
}
@@ -1829,18 +1755,13 @@ final String getUrlForSingleRequest() {
return overriddenURL.toString();
}
- String baseUrl;
- if (this.restMethod != null) {
- baseUrl = String.format("%s/%s", ServerProtocol.getRestUrlBase(), getRestPathWithVersion());
+ String graphBaseUrlBase;
+ if (this.getHttpMethod() == HttpMethod.POST && graphPath != null && graphPath.endsWith(VIDEOS_SUFFIX)) {
+ graphBaseUrlBase = ServerProtocol.getGraphVideoUrlBase();
} else {
- String graphBaseUrlBase;
- if (this.getHttpMethod() == HttpMethod.POST && graphPath != null && graphPath.endsWith(VIDEOS_SUFFIX)) {
- graphBaseUrlBase = ServerProtocol.getGraphVideoUrlBase();
- } else {
- graphBaseUrlBase = ServerProtocol.getGraphUrlBase();
- }
- baseUrl = String.format("%s/%s", graphBaseUrlBase, getGraphPathWithVersion());
+ graphBaseUrlBase = ServerProtocol.getGraphUrlBase();
}
+ String baseUrl = String.format("%s/%s", graphBaseUrlBase, getGraphPathWithVersion());
addCommonParameters();
return appendParametersToBaseUrl(baseUrl);
@@ -1854,14 +1775,6 @@ private String getGraphPathWithVersion() {
return String.format("%s/%s", this.version, this.graphPath);
}
- private String getRestPathWithVersion() {
- Matcher matcher = versionPattern.matcher(this.restMethod);
- if (matcher.matches()) {
- return this.restMethod;
- }
- return String.format("%s/%s/%s", this.version, ServerProtocol.REST_METHOD_BASE, this.restMethod);
- }
-
private static class Attachment {
private final Request request;
private final Object value;
@@ -1933,12 +1846,6 @@ public void writeString(String key, String value) throws IOException {
batch.put(batchEntry);
}
- private void validate() {
- if (graphPath != null && restMethod != null) {
- throw new IllegalArgumentException("Only one of a graph path or REST method may be specified per request.");
- }
- }
-
private static boolean hasOnProgressCallbacks(RequestBatch requests) {
for (RequestBatch.Callback callback : requests.getCallbacks()) {
if (callback instanceof RequestBatch.OnProgressCallback) {
diff --git a/platforms/android/FacebookLib/src/com/facebook/Response.java b/platforms/android/FacebookLib/src/com/facebook/Response.java
index a5e408d88..f5620256b 100644
--- a/platforms/android/FacebookLib/src/com/facebook/Response.java
+++ b/platforms/android/FacebookLib/src/com/facebook/Response.java
@@ -17,7 +17,10 @@
package com.facebook;
import android.content.Context;
-import com.facebook.internal.*;
+import com.facebook.internal.CacheableRequestBatch;
+import com.facebook.internal.FileLruCache;
+import com.facebook.internal.Logger;
+import com.facebook.internal.Utility;
import com.facebook.model.GraphObject;
import com.facebook.model.GraphObjectList;
import org.json.JSONArray;
@@ -52,6 +55,9 @@ public class Response {
*/
public static final String NON_JSON_RESPONSE_PROPERTY = "FACEBOOK_NON_JSON_RESULT";
+ // From v2.1 of the Graph API, write endpoints will now return valid JSON with the result as the value for the "success" key
+ public static final String SUCCESS_KEY = "success";
+
private static final int INVALID_SESSION_FACEBOOK_ERROR_CODE = 190;
private static final String CODE_KEY = "code";
diff --git a/platforms/android/FacebookLib/src/com/facebook/Session.java b/platforms/android/FacebookLib/src/com/facebook/Session.java
index d0d2b5656..4fe231168 100644
--- a/platforms/android/FacebookLib/src/com/facebook/Session.java
+++ b/platforms/android/FacebookLib/src/com/facebook/Session.java
@@ -16,6 +16,7 @@
package com.facebook;
+import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.*;
import android.content.pm.ResolveInfo;
@@ -24,7 +25,10 @@
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
-import com.facebook.internal.*;
+import com.facebook.internal.NativeProtocol;
+import com.facebook.internal.SessionAuthorizationType;
+import com.facebook.internal.Utility;
+import com.facebook.internal.Validate;
import com.facebook.model.GraphMultiResult;
import com.facebook.model.GraphObject;
import com.facebook.model.GraphObjectList;
@@ -124,8 +128,6 @@ public class Session implements Serializable {
private static final String PUBLISH_PERMISSION_PREFIX = "publish";
private static final String MANAGE_PERMISSION_PREFIX = "manage";
- private static final String BASIC_INFO_PERMISSION = "basic_info";
-
@SuppressWarnings("serial")
private static final Set OTHER_PUBLISH_PERMISSIONS = new HashSet() {{
add("ads_management");
@@ -190,6 +192,7 @@ private Object readResolve() {
* class should not be modified. If serializations formats change,
* create a new class SerializationProxyVx.
*/
+ @SuppressWarnings("UnusedDeclaration")
private static class SerializationProxyV2 implements Serializable {
private static final long serialVersionUID = 7663436173185080064L;
private final String applicationId;
diff --git a/platforms/android/FacebookLib/src/com/facebook/SessionDefaultAudience.java b/platforms/android/FacebookLib/src/com/facebook/SessionDefaultAudience.java
index 2fdac3d01..3f1aea80b 100644
--- a/platforms/android/FacebookLib/src/com/facebook/SessionDefaultAudience.java
+++ b/platforms/android/FacebookLib/src/com/facebook/SessionDefaultAudience.java
@@ -51,7 +51,7 @@ private SessionDefaultAudience(String protocol) {
nativeProtocolAudience = protocol;
}
- String getNativeProtocolAudience() {
+ public String getNativeProtocolAudience() {
return nativeProtocolAudience;
}
}
diff --git a/platforms/android/FacebookLib/src/com/facebook/Settings.java b/platforms/android/FacebookLib/src/com/facebook/Settings.java
index b4608a4f1..c08376c1d 100644
--- a/platforms/android/FacebookLib/src/com/facebook/Settings.java
+++ b/platforms/android/FacebookLib/src/com/facebook/Settings.java
@@ -30,13 +30,16 @@
import com.facebook.android.BuildConfig;
import com.facebook.internal.AttributionIdentifiers;
import com.facebook.internal.Utility;
-import com.facebook.model.GraphObject;
import com.facebook.internal.Validate;
+import com.facebook.model.GraphObject;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.Field;
-import java.util.*;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -58,6 +61,7 @@ public final class Settings {
private static volatile String facebookDomain = FACEBOOK_COM;
private static AtomicLong onProgressThreshold = new AtomicLong(65536);
private static volatile boolean platformCompatibilityEnabled;
+ private static volatile boolean isLoggingEnabled = BuildConfig.DEBUG;
private static final int DEFAULT_CORE_POOL_SIZE = 5;
private static final int DEFAULT_MAXIMUM_POOL_SIZE = 128;
@@ -180,10 +184,26 @@ public static final void clearLoggingBehaviors() {
*/
public static final boolean isLoggingBehaviorEnabled(LoggingBehavior behavior) {
synchronized (loggingBehaviors) {
- return BuildConfig.DEBUG && loggingBehaviors.contains(behavior);
+ return Settings.isLoggingEnabled() && loggingBehaviors.contains(behavior);
}
}
+ /**
+ * Indicates if logging is enabled.
+ */
+ public static final boolean isLoggingEnabled() {
+ return isLoggingEnabled;
+ }
+
+ /**
+ * Used to enable or disable logging, defaults to BuildConfig.DEBUG.
+ * @param enabled
+ * Logging is enabled if true, disabled if false.
+ */
+ public static final void setIsLoggingEnabled(boolean enabled) {
+ isLoggingEnabled = enabled;
+ }
+
/**
* Returns the Executor used by the SDK for non-AsyncTask background work.
*
@@ -364,7 +384,8 @@ static Response publishInstallAndWaitForResponse(
} else {
return new Response(null, null, null, graphObject, true);
}
- } else if (identifiers.getAndroidAdvertiserId() == null && identifiers.getAttributionId() == null) {
+ } else if (identifiers == null ||
+ (identifiers.getAndroidAdvertiserId() == null && identifiers.getAttributionId() == null)) {
throw new FacebookException("No attribution id available to send to server.");
} else {
if (!Utility.queryAppSettings(applicationId, false).supportsAttribution()) {
diff --git a/platforms/android/FacebookLib/src/com/facebook/TestSession.java b/platforms/android/FacebookLib/src/com/facebook/TestSession.java
index 821c1f95d..ef8b7dd39 100644
--- a/platforms/android/FacebookLib/src/com/facebook/TestSession.java
+++ b/platforms/android/FacebookLib/src/com/facebook/TestSession.java
@@ -20,13 +20,12 @@
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
-import com.facebook.model.GraphObject;
-import com.facebook.model.GraphObjectList;
import com.facebook.internal.Logger;
import com.facebook.internal.Utility;
import com.facebook.internal.Validate;
-import org.json.JSONException;
-import org.json.JSONObject;
+import com.facebook.model.GraphObject;
+import com.facebook.model.GraphObjectList;
+import com.facebook.model.GraphUser;
import java.util.*;
@@ -232,70 +231,48 @@ private static synchronized void retrieveTestAccountsForAppIfNeeded() {
appTestAccounts = new HashMap();
- // The data we need is split across two different FQL tables. We construct two queries, submit them
+ // The data we need is split across two different graph API queries. We construct two queries, submit them
// together (the second one refers to the first one), then cross-reference the results.
- // Get the test accounts for this app.
- String testAccountQuery = String.format("SELECT id,access_token FROM test_account WHERE app_id = %s",
- testApplicationId);
- // Get the user names for those accounts.
- String userQuery = "SELECT uid,name FROM user WHERE uid IN (SELECT id FROM #test_accounts)";
+ Request.setDefaultBatchApplicationId(testApplicationId);
Bundle parameters = new Bundle();
-
- // Build a JSON string that contains our queries and pass it as the 'q' parameter of the query.
- JSONObject multiquery;
- try {
- multiquery = new JSONObject();
- multiquery.put("test_accounts", testAccountQuery);
- multiquery.put("users", userQuery);
- } catch (JSONException exception) {
- throw new FacebookException(exception);
- }
- parameters.putString("q", multiquery.toString());
-
- // We need to authenticate as this app.
parameters.putString("access_token", getAppAccessToken());
- Request request = new Request(null, "fql", parameters, null);
- Response response = request.executeAndWait();
+ Request requestTestUsers = new Request(null, "app/accounts/test-users", parameters, null);
+ requestTestUsers.setBatchEntryName("testUsers");
+ requestTestUsers.setBatchEntryOmitResultOnSuccess(false);
- if (response.getError() != null) {
- throw response.getError().getException();
- }
+ Bundle testUserNamesParam = new Bundle();
+ testUserNamesParam.putString("access_token", getAppAccessToken());
+ testUserNamesParam.putString("ids", "{result=testUsers:$.data.*.id}");
+ testUserNamesParam.putString("fields", "name");
- FqlResponse fqlResponse = response.getGraphObjectAs(FqlResponse.class);
+ Request requestTestUserNames = new Request(null, "", testUserNamesParam, null);
+ requestTestUserNames.setBatchEntryDependsOn("testUsers");
- GraphObjectList fqlResults = fqlResponse.getData();
- if (fqlResults == null || fqlResults.size() != 2) {
- throw new FacebookException("Unexpected number of results from FQL query");
+ List responses = Request.executeBatchAndWait(requestTestUsers, requestTestUserNames);
+ if (responses == null || responses.size() != 2) {
+ throw new FacebookException("Unexpected number of results from TestUsers batch query");
}
- // We get back two sets of results. The first is from the test_accounts query, the second from the users query.
- Collection testAccounts = fqlResults.get(0).getFqlResultSet().castToListOf(TestAccount.class);
- Collection userAccounts = fqlResults.get(1).getFqlResultSet().castToListOf(UserAccount.class);
+ TestAccountsResponse testAccountsResponse = responses.get(0).getGraphObjectAs(TestAccountsResponse.class);
+ GraphObjectList testAccounts = testAccountsResponse.getData();
- // Use both sets of results to populate our static array of accounts.
- populateTestAccounts(testAccounts, userAccounts);
+ // Response should contain a map of test accounts: { id's => { GraphUser } }
+ GraphObject userAccountsMap = responses.get(1).getGraphObject();
+ populateTestAccounts(testAccounts, userAccountsMap);
return;
}
private static synchronized void populateTestAccounts(Collection testAccounts,
- Collection userAccounts) {
- // We get different sets of data from each of these queries. We want to combine them into a single data
- // structure. We have added a Name property to the TestAccount interface, even though we don't really get
- // a name back from the service from that query. We stick the Name from the corresponding UserAccount in it.
+ GraphObject userAccountsMap) {
for (TestAccount testAccount : testAccounts) {
+ GraphUser testUser = userAccountsMap.getPropertyAs(testAccount.getId(), GraphUser.class);
+ testAccount.setName(testUser.getName());
storeTestAccount(testAccount);
}
-
- for (UserAccount userAccount : userAccounts) {
- TestAccount testAccount = appTestAccounts.get(userAccount.getUid());
- if (testAccount != null) {
- testAccount.setName(userAccount.getName());
- }
- }
}
private static synchronized void storeTestAccount(TestAccount testAccount) {
@@ -442,7 +419,8 @@ private void deleteTestAccount(String testAccountId, String appAccessToken) {
GraphObject graphObject = response.getGraphObject();
if (error != null) {
Log.w(LOG_TAG, String.format("Could not delete test account %s: %s", testAccountId, error.getException().toString()));
- } else if (graphObject.getProperty(Response.NON_JSON_RESPONSE_PROPERTY) == (Boolean) false) {
+ } else if (graphObject.getProperty(Response.NON_JSON_RESPONSE_PROPERTY) == (Boolean) false
+ || graphObject.getProperty(Response.SUCCESS_KEY) == (Boolean) false) {
Log.w(LOG_TAG, String.format("Could not delete test account %s: unknown reason", testAccountId));
}
}
@@ -484,27 +462,14 @@ private interface TestAccount extends GraphObject {
String getAccessToken();
- // Note: We don't actually get Name from our FQL query. We fill it in by correlating with UserAccounts.
- String getName();
-
- void setName(String name);
- }
-
- private interface UserAccount extends GraphObject {
- String getUid();
-
+ // Note: We don't actually get Name from our accounts/test-users query. We fill it in by correlating with GraphUser.
String getName();
void setName(String name);
}
- private interface FqlResult extends GraphObject {
- GraphObjectList getFqlResultSet();
-
- }
-
- private interface FqlResponse extends GraphObject {
- GraphObjectList getData();
+ private interface TestAccountsResponse extends GraphObject {
+ GraphObjectList getData();
}
private static final class TestTokenCachingStrategy extends TokenCachingStrategy {
diff --git a/platforms/android/FacebookLib/src/com/facebook/UiLifecycleHelper.java b/platforms/android/FacebookLib/src/com/facebook/UiLifecycleHelper.java
index d1656b5d6..8d5b73024 100644
--- a/platforms/android/FacebookLib/src/com/facebook/UiLifecycleHelper.java
+++ b/platforms/android/FacebookLib/src/com/facebook/UiLifecycleHelper.java
@@ -269,14 +269,7 @@ private boolean handleFacebookDialogActivityResult(int requestCode, int resultCo
return true;
}
- String callIdString = data.getStringExtra(NativeProtocol.EXTRA_PROTOCOL_CALL_ID);
- UUID callId = null;
- if (callIdString != null) {
- try {
- callId = UUID.fromString(callIdString);
- } catch (IllegalArgumentException exception) {
- }
- }
+ UUID callId = NativeProtocol.getCallIdFromIntent(data);
// Was this result for the call we are waiting on?
if (callId != null && pendingFacebookDialogCall.getCallId().equals(callId)) {
diff --git a/platforms/android/FacebookLib/src/com/facebook/android/FbDialog.java b/platforms/android/FacebookLib/src/com/facebook/android/FbDialog.java
index 603e69280..e35bbaac4 100644
--- a/platforms/android/FacebookLib/src/com/facebook/android/FbDialog.java
+++ b/platforms/android/FacebookLib/src/com/facebook/android/FbDialog.java
@@ -18,7 +18,9 @@
import android.content.Context;
import android.os.Bundle;
-import com.facebook.*;
+import com.facebook.FacebookDialogException;
+import com.facebook.FacebookException;
+import com.facebook.FacebookOperationCanceledException;
import com.facebook.android.Facebook.DialogListener;
import com.facebook.widget.WebDialog;
diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/AttributionIdentifiers.java b/platforms/android/FacebookLib/src/com/facebook/internal/AttributionIdentifiers.java
index 1c8f63018..4efe7a3e6 100644
--- a/platforms/android/FacebookLib/src/com/facebook/internal/AttributionIdentifiers.java
+++ b/platforms/android/FacebookLib/src/com/facebook/internal/AttributionIdentifiers.java
@@ -19,8 +19,11 @@
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Looper;
import android.util.Log;
+import com.facebook.FacebookException;
+
import java.lang.reflect.Method;
/**
@@ -51,6 +54,11 @@ public class AttributionIdentifiers {
private static AttributionIdentifiers getAndroidId(Context context) {
AttributionIdentifiers identifiers = new AttributionIdentifiers();
try {
+ // We can't call getAdvertisingIdInfo on the main thread or the app will potentially
+ // freeze, if this is the case throw:
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ throw new FacebookException("getAndroidId cannot be called on the main thread.");
+ }
Method isGooglePlayServicesAvailable = Utility.getMethodQuietly(
"com.google.android.gms.common.GooglePlayServicesUtil",
"isGooglePlayServicesAvailable",
diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/ImageDownloader.java b/platforms/android/FacebookLib/src/com/facebook/internal/ImageDownloader.java
index dc55fb917..69bea7f4a 100644
--- a/platforms/android/FacebookLib/src/com/facebook/internal/ImageDownloader.java
+++ b/platforms/android/FacebookLib/src/com/facebook/internal/ImageDownloader.java
@@ -30,7 +30,8 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
-import java.util.*;
+import java.util.HashMap;
+import java.util.Map;
public class ImageDownloader {
private static final int DOWNLOAD_QUEUE_MAX_CONCURRENT = WorkQueue.DEFAULT_MAX_CONCURRENT;
diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/NativeProtocol.java b/platforms/android/FacebookLib/src/com/facebook/internal/NativeProtocol.java
index 7a7186c10..a2cf01912 100644
--- a/platforms/android/FacebookLib/src/com/facebook/internal/NativeProtocol.java
+++ b/platforms/android/FacebookLib/src/com/facebook/internal/NativeProtocol.java
@@ -25,7 +25,10 @@
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
-import com.facebook.*;
+import com.facebook.FacebookException;
+import com.facebook.FacebookOperationCanceledException;
+import com.facebook.SessionDefaultAudience;
+import com.facebook.Settings;
import java.util.*;
@@ -57,12 +60,26 @@ public final class NativeProtocol {
public static final int PROTOCOL_VERSION_20131107 = 20131107;
public static final int PROTOCOL_VERSION_20140204 = 20140204;
public static final int PROTOCOL_VERSION_20140324 = 20140324;
+ public static final int PROTOCOL_VERSION_20140701 = 20140701;
public static final String EXTRA_PROTOCOL_VERSION = "com.facebook.platform.protocol.PROTOCOL_VERSION";
public static final String EXTRA_PROTOCOL_ACTION = "com.facebook.platform.protocol.PROTOCOL_ACTION";
public static final String EXTRA_PROTOCOL_CALL_ID = "com.facebook.platform.protocol.CALL_ID";
public static final String EXTRA_GET_INSTALL_DATA_PACKAGE = "com.facebook.platform.extra.INSTALLDATA_PACKAGE";
+ public static final String EXTRA_PROTOCOL_BRIDGE_ARGS =
+ "com.facebook.platform.protocol.BRIDGE_ARGS";
+
+ public static final String EXTRA_PROTOCOL_METHOD_ARGS =
+ "com.facebook.platform.protocol.METHOD_ARGS";
+
+ public static final String EXTRA_PROTOCOL_METHOD_RESULTS =
+ "com.facebook.platform.protocol.RESULT_ARGS";
+
+ public static final String BRIDGE_ARG_APP_NAME_STRING = "app_name";
+ public static final String BRIDGE_ARG_ACTION_ID_STRING = "action_id";
+ public static final String BRIDGE_ARG_ERROR_BUNDLE = "error";
+
// Messages supported by PlatformService:
public static final int MESSAGE_GET_ACCESS_TOKEN_REQUEST = 0x10000;
public static final int MESSAGE_GET_ACCESS_TOKEN_REPLY = 0x10001;
@@ -126,12 +143,27 @@ public final class NativeProtocol {
public static final String EXTRA_DATA_FAILURES_FATAL = "com.facebook.platform.extra.DATA_FAILURES_FATAL";
public static final String EXTRA_PHOTOS = "com.facebook.platform.extra.PHOTOS";
+ public static final String METHOD_ARGS_PLACE_TAG = "PLACE";
+ public static final String METHOD_ARGS_FRIEND_TAGS = "FRIENDS";
+ public static final String METHOD_ARGS_LINK = "LINK";
+ public static final String METHOD_ARGS_IMAGE = "IMAGE";
+ public static final String METHOD_ARGS_TITLE = "TITLE";
+ public static final String METHOD_ARGS_SUBTITLE = "SUBTITLE";
+ public static final String METHOD_ARGS_DESCRIPTION = "DESCRIPTION";
+ public static final String METHOD_ARGS_REF = "REF";
+ public static final String METHOD_ARGS_DATA_FAILURES_FATAL = "DATA_FAILURES_FATAL";
+ public static final String METHOD_ARGS_PHOTOS = "PHOTOS";
+
// Extras supported for ACTION_OGACTIONPUBLISH_DIALOG:
public static final String EXTRA_ACTION = "com.facebook.platform.extra.ACTION";
public static final String EXTRA_ACTION_TYPE = "com.facebook.platform.extra.ACTION_TYPE";
public static final String EXTRA_PREVIEW_PROPERTY_NAME =
"com.facebook.platform.extra.PREVIEW_PROPERTY_NAME";
+ public static final String METHOD_ARGS_ACTION = "ACTION";
+ public static final String METHOD_ARGS_ACTION_TYPE = "ACTION_TYPE";
+ public static final String METHOD_ARGS_PREVIEW_PROPERTY_NAME = "PREVIEW_PROPERTY_NAME";
+
// OG objects will have this key to set to true if they should be created as part of OG Action publish
public static final String OPEN_GRAPH_CREATE_OBJECT_KEY = "fbsdk:create_object";
// Determines whether an image is user generated
@@ -157,9 +189,9 @@ public final class NativeProtocol {
public static final String ERROR_PERMISSION_DENIED = "PermissionDenied";
public static final String ERROR_SERVICE_DISABLED = "ServiceDisabled";
- public static final String AUDIENCE_ME = "SELF";
- public static final String AUDIENCE_FRIENDS = "ALL_FRIENDS";
- public static final String AUDIENCE_EVERYONE = "EVERYONE";
+ public static final String AUDIENCE_ME = "only_me";
+ public static final String AUDIENCE_FRIENDS = "friends";
+ public static final String AUDIENCE_EVERYONE = "everyone";
// Request codes for different categories of native protocol calls.
public static final int DIALOG_REQUEST_CODE = 0xfacf;
@@ -170,9 +202,6 @@ public final class NativeProtocol {
// Columns returned by PlatformProvider
private static final String PLATFORM_PROVIDER_VERSION_COLUMN = "version";
- // Broadcast action for asynchronously-executing AppCalls
- private static final String PLATFORM_ASYNC_APPCALL_ACTION = "com.facebook.platform.AppCallResultBroadcast";
-
private static abstract class NativeAppInfo {
abstract protected String getPackage();
@@ -308,7 +337,7 @@ static Intent validateServiceIntent(Context context, Intent intent, NativeAppInf
}
public static Intent createProxyAuthIntent(Context context, String applicationId, List permissions,
- String e2e, boolean isRerequest) {
+ String e2e, boolean isRerequest, SessionDefaultAudience defaultAudience) {
for (NativeAppInfo appInfo : facebookAppInfoList) {
Intent intent = new Intent()
.setClassName(appInfo.getPackage(), FACEBOOK_PROXY_AUTH_ACTIVITY)
@@ -323,6 +352,7 @@ public static Intent createProxyAuthIntent(Context context, String applicationId
intent.putExtra(ServerProtocol.DIALOG_PARAM_RESPONSE_TYPE, ServerProtocol.DIALOG_RESPONSE_TYPE_TOKEN);
intent.putExtra(ServerProtocol.DIALOG_PARAM_RETURN_SCOPES, ServerProtocol.DIALOG_RETURN_SCOPES_TRUE);
+ intent.putExtra(ServerProtocol.DIALOG_PARAM_DEFAULT_AUDIENCE, defaultAudience.getNativeProtocolAudience());
if (!Settings.getPlatformCompatibilityEnabled()) {
// Override the API Version for Auth
@@ -360,6 +390,7 @@ public static Intent createTokenRefreshIntent(Context context) {
// Note: be sure this stays sorted in descending order; add new versions at the beginning
private static final List KNOWN_PROTOCOL_VERSIONS =
Arrays.asList(
+ PROTOCOL_VERSION_20140701,
PROTOCOL_VERSION_20140324,
PROTOCOL_VERSION_20140204,
PROTOCOL_VERSION_20131107,
@@ -389,15 +420,43 @@ private static Intent findActivityIntent(Context context, String activityAction,
return intent;
}
- public static Intent createPlatformActivityIntent(Context context, String action, int version, Bundle extras) {
+ public static boolean isVersionCompatibleWithBucketedIntent(int version) {
+ return KNOWN_PROTOCOL_VERSIONS.contains(version) && version >= PROTOCOL_VERSION_20140701;
+ }
+
+ public static Intent createPlatformActivityIntent(
+ Context context,
+ String callId,
+ String action,
+ int version,
+ String applicationName,
+ Bundle extras) {
Intent intent = findActivityIntent(context, INTENT_ACTION_PLATFORM_ACTIVITY, action);
if (intent == null) {
return null;
}
- intent.putExtras(extras)
- .putExtra(EXTRA_PROTOCOL_VERSION, version)
- .putExtra(EXTRA_PROTOCOL_ACTION, action);
+ String applicationId = Utility.getMetadataApplicationId(context);
+
+ intent.putExtra(EXTRA_PROTOCOL_VERSION, version)
+ .putExtra(EXTRA_PROTOCOL_ACTION, action)
+ .putExtra(EXTRA_APPLICATION_ID, applicationId);
+
+ if (isVersionCompatibleWithBucketedIntent(version)) {
+ // This is a bucketed intent
+ Bundle bridgeArguments = new Bundle();
+ bridgeArguments.putString(BRIDGE_ARG_ACTION_ID_STRING, callId);
+ bridgeArguments.putString(BRIDGE_ARG_APP_NAME_STRING, applicationName);
+ intent.putExtra(EXTRA_PROTOCOL_BRIDGE_ARGS, bridgeArguments);
+
+ Bundle methodArguments = extras == null ? new Bundle() : extras;
+ intent.putExtra(EXTRA_PROTOCOL_METHOD_ARGS, methodArguments);
+ } else {
+ // This is the older flat intent
+ intent.putExtra(EXTRA_PROTOCOL_CALL_ID, callId);
+ intent.putExtra(EXTRA_APPLICATION_NAME, applicationName);
+ intent.putExtras(extras);
+ }
return intent;
}
@@ -415,8 +474,58 @@ public static Intent createPlatformServiceIntent(Context context) {
return null;
}
+ public static int getProtocolVersionFromIntent(Intent intent) {
+ return intent.getIntExtra(EXTRA_PROTOCOL_VERSION, 0);
+ }
+
+ public static UUID getCallIdFromIntent(Intent intent) {
+ int version = getProtocolVersionFromIntent(intent);
+ String callIdString = null;
+ if (isVersionCompatibleWithBucketedIntent(version)) {
+ Bundle bridgeArgs = intent.getBundleExtra(EXTRA_PROTOCOL_BRIDGE_ARGS);
+ if (bridgeArgs != null) {
+ callIdString = bridgeArgs.getString(BRIDGE_ARG_ACTION_ID_STRING);
+ }
+ } else {
+ callIdString = intent.getStringExtra(EXTRA_PROTOCOL_CALL_ID);
+ }
+
+ UUID callId = null;
+ if (callIdString != null) {
+ try {
+ callId = UUID.fromString(callIdString);
+ } catch (IllegalArgumentException exception) {
+ }
+ }
+ return callId;
+ }
+
+ public static Bundle getBridgeArgumentsFromIntent(Intent intent) {
+ int version = getProtocolVersionFromIntent(intent);
+ if (!isVersionCompatibleWithBucketedIntent(version)) {
+ return null;
+ }
+
+ return intent.getBundleExtra(EXTRA_PROTOCOL_BRIDGE_ARGS);
+ }
+
+ public static Bundle getSuccessResultsFromIntent(Intent resultIntent) {
+ int version = getProtocolVersionFromIntent(resultIntent);
+ Bundle extras = resultIntent.getExtras();
+ if (!isVersionCompatibleWithBucketedIntent(version) || extras == null) {
+ return extras;
+ }
+
+ return extras.getBundle(EXTRA_PROTOCOL_METHOD_RESULTS);
+ }
+
public static boolean isErrorResult(Intent resultIntent) {
- return resultIntent.hasExtra(STATUS_ERROR_TYPE);
+ Bundle bridgeArgs = getBridgeArgumentsFromIntent(resultIntent);
+ if (bridgeArgs != null) {
+ return bridgeArgs.containsKey(BRIDGE_ARG_ERROR_BUNDLE);
+ } else {
+ return resultIntent.hasExtra(STATUS_ERROR_TYPE);
+ }
}
public static Exception getErrorFromResult(Intent resultIntent) {
@@ -424,12 +533,27 @@ public static Exception getErrorFromResult(Intent resultIntent) {
return null;
}
- String type = resultIntent.getStringExtra(STATUS_ERROR_TYPE);
- String description = resultIntent.getStringExtra(STATUS_ERROR_DESCRIPTION);
+ Bundle bridgeArgs = getBridgeArgumentsFromIntent(resultIntent);
+ if (bridgeArgs != null) {
+ Bundle errorBundle = bridgeArgs.getBundle(BRIDGE_ARG_ERROR_BUNDLE);
+ if (errorBundle != null) {
+ return getErrorFromResult(errorBundle);
+ }
+ }
- if (type.equalsIgnoreCase(ERROR_USER_CANCELED)) {
+ return getErrorFromResult(resultIntent.getExtras());
+ }
+
+ public static Exception getErrorFromResult(Bundle errorBundle) {
+ // TODO This is not going to work for JS dialogs, where the keys are not STATUS_ERROR_TYPE etc.
+ // TODO However, it should keep existing dialogs functional
+ String type = errorBundle.getString(STATUS_ERROR_TYPE);
+ String description = errorBundle.getString(STATUS_ERROR_DESCRIPTION);
+
+ if (type != null && type.equalsIgnoreCase(ERROR_USER_CANCELED)) {
return new FacebookOperationCanceledException(description);
}
+
/* TODO parse error values and create appropriate exception class */
return new FacebookException(description);
}
diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/ServerProtocol.java b/platforms/android/FacebookLib/src/com/facebook/internal/ServerProtocol.java
index 06155ce5e..bc9beecfa 100644
--- a/platforms/android/FacebookLib/src/com/facebook/internal/ServerProtocol.java
+++ b/platforms/android/FacebookLib/src/com/facebook/internal/ServerProtocol.java
@@ -39,6 +39,7 @@ public final class ServerProtocol {
public static final String DIALOG_PARAM_RESPONSE_TYPE = "response_type";
public static final String DIALOG_PARAM_RETURN_SCOPES = "return_scopes";
public static final String DIALOG_PARAM_SCOPE = "scope";
+ public static final String DIALOG_PARAM_DEFAULT_AUDIENCE = "default_audience";
public static final String DIALOG_REREQUEST_AUTH_TYPE = "rerequest";
public static final String DIALOG_RESPONSE_TYPE_TOKEN = "token";
public static final String DIALOG_RETURN_SCOPES_TRUE = "true";
@@ -46,9 +47,7 @@ public final class ServerProtocol {
// URL components
private static final String GRAPH_VIDEO_URL_FORMAT = "https://graph-video.%s";
private static final String GRAPH_URL_FORMAT = "https://graph.%s";
- private static final String REST_URL_FORMAT = "https://api.%s";
- public static final String REST_METHOD_BASE = "method";
- public static final String GRAPH_API_VERSION = "v2.0";
+ public static final String GRAPH_API_VERSION = "v2.1";
private static final String LEGACY_API_VERSION = "v1.0";
@@ -69,10 +68,6 @@ public static final String getGraphVideoUrlBase() {
return String.format(GRAPH_VIDEO_URL_FORMAT, Settings.getFacebookDomain());
}
- public static final String getRestUrlBase() {
- return String.format(REST_URL_FORMAT, Settings.getFacebookDomain());
- }
-
public static final String getAPIVersion() {
if (Settings.getPlatformCompatibilityEnabled()) {
return LEGACY_API_VERSION;
diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/Utility.java b/platforms/android/FacebookLib/src/com/facebook/internal/Utility.java
index 41fd4e3b6..19a8f4047 100644
--- a/platforms/android/FacebookLib/src/com/facebook/internal/Utility.java
+++ b/platforms/android/FacebookLib/src/com/facebook/internal/Utility.java
@@ -17,27 +17,19 @@
package com.facebook.internal;
import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
-import android.os.BatteryManager;
-import android.os.Build;
import android.os.Bundle;
-import android.os.Environment;
import android.os.Parcelable;
-import android.os.StatFs;
import android.provider.Settings.Secure;
-import android.telephony.TelephonyManager;
import android.text.TextUtils;
-import android.util.DisplayMetrics;
import android.util.Log;
-import android.view.Display;
-import android.view.WindowManager;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
-import com.facebook.*;
+import com.facebook.FacebookException;
+import com.facebook.Request;
+import com.facebook.Settings;
import com.facebook.android.BuildConfig;
import com.facebook.model.GraphObject;
import org.json.JSONArray;
@@ -85,12 +77,6 @@ public final class Utility {
private static Map fetchedAppSettings =
new ConcurrentHashMap();
- private static int sNumCPUCores = 0;
- private static long sTotalExternalStorageBytes = -1;
- private static long sAvailableExternalStorageBytes = -1;
- private static String sCarrierName = null;
-
-
public static class FetchedAppSettings {
private boolean supportsAttribution;
private boolean supportsImplicitLogging;
@@ -366,13 +352,13 @@ public static void clearFacebookCookies(Context context) {
}
public static void logd(String tag, Exception e) {
- if (BuildConfig.DEBUG && tag != null && e != null) {
+ if (Settings.isLoggingEnabled() && tag != null && e != null) {
Log.d(tag, e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
public static void logd(String tag, String msg) {
- if (BuildConfig.DEBUG && tag != null && msg != null) {
+ if (Settings.isLoggingEnabled() && tag != null && msg != null) {
Log.d(tag, msg);
}
}
@@ -516,14 +502,6 @@ public static void setAppEventExtendedDeviceInfoParameters(GraphObject params, C
params.setProperty("extinfo", extraInfoArray.toString());
}
- private static void silentJSONObjectPut(JSONObject object, String key, T data) {
- try {
- object.put(key, data);
- } catch (JSONException e) {
- // Swallow
- }
- }
-
public static Method getMethodQuietly(Class> clazz, String methodName, Class>... parameterTypes) {
try {
return clazz.getMethod(methodName, parameterTypes);
diff --git a/platforms/android/FacebookLib/src/com/facebook/widget/FacebookDialog.java b/platforms/android/FacebookLib/src/com/facebook/widget/FacebookDialog.java
index 1322228ca..226e1206a 100644
--- a/platforms/android/FacebookLib/src/com/facebook/widget/FacebookDialog.java
+++ b/platforms/android/FacebookLib/src/com/facebook/widget/FacebookDialog.java
@@ -29,7 +29,10 @@
import com.facebook.internal.NativeProtocol;
import com.facebook.internal.Utility;
import com.facebook.internal.Validate;
-import com.facebook.model.*;
+import com.facebook.model.GraphObject;
+import com.facebook.model.GraphObjectList;
+import com.facebook.model.OpenGraphAction;
+import com.facebook.model.OpenGraphObject;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -341,9 +344,11 @@ public static boolean handleActivityResult(Context context, PendingCall appCall,
if (callback != null) {
if (NativeProtocol.isErrorResult(data)) {
Exception error = NativeProtocol.getErrorFromResult(data);
+
+ // TODO - data.getExtras() doesn't work for the bucketed protocol.
callback.onError(appCall, error, data.getExtras());
} else {
- callback.onComplete(appCall, data.getExtras());
+ callback.onComplete(appCall, NativeProtocol.getSuccessResultsFromIntent(data));
}
}
@@ -480,7 +485,14 @@ static private String getEventName(String action, boolean hasPhotos) {
return eventName;
}
- abstract static class Builder> {
+ /**
+ * Provides a base class for various FacebookDialog builders. This is public primarily to allow its use elsewhere
+ * in the Android SDK; developers are discouraged from constructing their own FacebookDialog builders as the
+ * internal API may change.
+ *
+ * @param The concrete base class of the builder.
+ */
+ public abstract static class Builder> {
final protected Activity activity;
final protected String applicationId;
final protected PendingCall appCall;
@@ -489,7 +501,12 @@ abstract static class Builder> {
protected HashMap imageAttachments = new HashMap();
protected HashMap imageAttachmentFiles = new HashMap();
- Builder(Activity activity) {
+ /**
+ * Constructor.
+ *
+ * @param activity the Activity which is presenting the native Share dialog; must not be null
+ */
+ public Builder(Activity activity) {
Validate.notNull(activity, "activity");
this.activity = activity;
@@ -549,16 +566,26 @@ public CONCRETE setFragment(Fragment fragment) {
public FacebookDialog build() {
validate();
- Bundle extras = new Bundle();
- putExtra(extras, NativeProtocol.EXTRA_APPLICATION_ID, applicationId);
- putExtra(extras, NativeProtocol.EXTRA_APPLICATION_NAME, applicationName);
- extras = setBundleExtras(extras);
-
String action = getActionForFeatures(getDialogFeatures());
int protocolVersion = getProtocolVersionForNativeDialog(activity, action,
getMinVersionForFeatures(getDialogFeatures()));
- Intent intent = NativeProtocol.createPlatformActivityIntent(activity, action, protocolVersion, extras);
+ Bundle extras = null;
+ if (NativeProtocol.isVersionCompatibleWithBucketedIntent(protocolVersion)) {
+ // Facebook app supports the new bucketed protocol
+ extras = getMethodArguments();
+ } else {
+ // Facebook app only supports the old flat protocol
+ extras = setBundleExtras(new Bundle());
+ }
+
+ Intent intent = NativeProtocol.createPlatformActivityIntent(
+ activity,
+ appCall.getCallId().toString(),
+ action,
+ protocolVersion,
+ applicationName,
+ extras);
if (intent == null) {
logDialogActivity(activity, fragment,
getEventName(action, extras.containsKey(NativeProtocol.EXTRA_PHOTOS)),
@@ -567,6 +594,7 @@ public FacebookDialog build() {
throw new FacebookException(
"Unable to create Intent; this likely means the Facebook app is not installed.");
}
+
appCall.setRequestIntent(intent);
return new FacebookDialog(activity, fragment, appCall, getOnPresentCallback());
@@ -636,9 +664,11 @@ List getImageAttachmentNames() {
return new ArrayList(imageAttachments.keySet());
}
- abstract Bundle setBundleExtras(Bundle extras);
+ protected abstract Bundle setBundleExtras(Bundle extras);
- void putExtra(Bundle extras, String key, String value) {
+ protected abstract Bundle getMethodArguments();
+
+ protected void putExtra(Bundle extras, String key, String value) {
if (value != null) {
extras.putString(key, value);
}
@@ -800,7 +830,7 @@ public CONCRETE setDataErrorsFatal(boolean dataErrorsFatal) {
}
@Override
- Bundle setBundleExtras(Bundle extras) {
+ protected Bundle setBundleExtras(Bundle extras) {
putExtra(extras, NativeProtocol.EXTRA_APPLICATION_ID, applicationId);
putExtra(extras, NativeProtocol.EXTRA_APPLICATION_NAME, applicationName);
putExtra(extras, NativeProtocol.EXTRA_TITLE, name);
@@ -818,6 +848,27 @@ Bundle setBundleExtras(Bundle extras) {
}
return extras;
}
+
+ @Override
+ protected Bundle getMethodArguments() {
+ Bundle methodArguments = new Bundle();
+
+ putExtra(methodArguments, NativeProtocol.METHOD_ARGS_TITLE, name);
+ putExtra(methodArguments, NativeProtocol.METHOD_ARGS_SUBTITLE, caption);
+ putExtra(methodArguments, NativeProtocol.METHOD_ARGS_DESCRIPTION, description);
+ putExtra(methodArguments, NativeProtocol.METHOD_ARGS_LINK, link);
+ putExtra(methodArguments, NativeProtocol.METHOD_ARGS_IMAGE, picture);
+ putExtra(methodArguments, NativeProtocol.METHOD_ARGS_PLACE_TAG, place);
+ putExtra(methodArguments, NativeProtocol.METHOD_ARGS_TITLE, name);
+ putExtra(methodArguments, NativeProtocol.METHOD_ARGS_REF, ref);
+
+ methodArguments.putBoolean(NativeProtocol.METHOD_ARGS_DATA_FAILURES_FATAL, dataErrorsFatal);
+ if (!Utility.isNullOrEmpty(friends)) {
+ methodArguments.putStringArrayList(NativeProtocol.METHOD_ARGS_FRIEND_TAGS, friends);
+ }
+
+ return methodArguments;
+ }
}
/**
@@ -937,7 +988,7 @@ void validate() {
}
@Override
- Bundle setBundleExtras(Bundle extras) {
+ protected Bundle setBundleExtras(Bundle extras) {
putExtra(extras, NativeProtocol.EXTRA_APPLICATION_ID, applicationId);
putExtra(extras, NativeProtocol.EXTRA_APPLICATION_NAME, applicationName);
putExtra(extras, NativeProtocol.EXTRA_PLACE_TAG, place);
@@ -948,6 +999,20 @@ Bundle setBundleExtras(Bundle extras) {
}
return extras;
}
+
+ @Override
+ protected Bundle getMethodArguments() {
+ Bundle methodArgs = new Bundle();
+
+ putExtra(methodArgs, NativeProtocol.METHOD_ARGS_PLACE_TAG, place);
+ methodArgs.putStringArrayList(NativeProtocol.METHOD_ARGS_PHOTOS, imageAttachmentUrls);
+
+ if (!Utility.isNullOrEmpty(friends)) {
+ methodArgs.putStringArrayList(NativeProtocol.METHOD_ARGS_FRIEND_TAGS, friends);
+ }
+
+ return methodArgs;
+ }
}
/**
@@ -1399,7 +1464,7 @@ void updateObjectAttachmentUrls(String objectProperty, List attachmentUr
}
@Override
- Bundle setBundleExtras(Bundle extras) {
+ protected Bundle setBundleExtras(Bundle extras) {
putExtra(extras, NativeProtocol.EXTRA_PREVIEW_PROPERTY_NAME, previewPropertyName);
putExtra(extras, NativeProtocol.EXTRA_ACTION_TYPE, actionType);
extras.putBoolean(NativeProtocol.EXTRA_DATA_FAILURES_FATAL, dataErrorsFatal);
@@ -1413,6 +1478,23 @@ Bundle setBundleExtras(Bundle extras) {
return extras;
}
+ @Override
+ protected Bundle getMethodArguments() {
+ Bundle methodArgs = new Bundle();
+
+ putExtra(methodArgs, NativeProtocol.METHOD_ARGS_PREVIEW_PROPERTY_NAME, previewPropertyName);
+ putExtra(methodArgs, NativeProtocol.METHOD_ARGS_ACTION_TYPE, actionType);
+ methodArgs.putBoolean(NativeProtocol.METHOD_ARGS_DATA_FAILURES_FATAL, dataErrorsFatal);
+
+ JSONObject jsonAction = action.getInnerJSONObject();
+ jsonAction = flattenChildrenOfGraphObject(jsonAction);
+
+ String jsonString = jsonAction.toString();
+ putExtra(methodArgs, NativeProtocol.METHOD_ARGS_ACTION, jsonString);
+
+ return methodArgs;
+ }
+
private JSONObject flattenChildrenOfGraphObject(JSONObject graphObject) {
try {
// Clone the existing object to avoid modifying it from under the caller.
@@ -1577,7 +1659,6 @@ private PendingCall(Parcel in) {
private void setRequestIntent(Intent requestIntent) {
this.requestIntent = requestIntent;
- this.requestIntent.putExtra(NativeProtocol.EXTRA_PROTOCOL_CALL_ID, callId.toString());
}
/**
diff --git a/platforms/android/FacebookLib/src/com/facebook/widget/GraphObjectAdapter.java b/platforms/android/FacebookLib/src/com/facebook/widget/GraphObjectAdapter.java
index 285ddeec2..bc0adbfb1 100644
--- a/platforms/android/FacebookLib/src/com/facebook/widget/GraphObjectAdapter.java
+++ b/platforms/android/FacebookLib/src/com/facebook/widget/GraphObjectAdapter.java
@@ -23,7 +23,7 @@
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.*;
-import com.facebook.*;
+import com.facebook.FacebookException;
import com.facebook.android.R;
import com.facebook.internal.ImageDownloader;
import com.facebook.internal.ImageRequest;
diff --git a/platforms/android/FacebookLib/src/com/facebook/widget/GraphObjectPagingLoader.java b/platforms/android/FacebookLib/src/com/facebook/widget/GraphObjectPagingLoader.java
index 17d6cb674..0f100ed03 100644
--- a/platforms/android/FacebookLib/src/com/facebook/widget/GraphObjectPagingLoader.java
+++ b/platforms/android/FacebookLib/src/com/facebook/widget/GraphObjectPagingLoader.java
@@ -20,9 +20,9 @@
import android.os.Handler;
import android.support.v4.content.Loader;
import com.facebook.*;
+import com.facebook.internal.CacheableRequestBatch;
import com.facebook.model.GraphObject;
import com.facebook.model.GraphObjectList;
-import com.facebook.internal.CacheableRequestBatch;
class GraphObjectPagingLoader extends Loader> {
private final Class graphObjectClass;
diff --git a/platforms/android/FacebookLib/src/com/facebook/widget/LoginButton.java b/platforms/android/FacebookLib/src/com/facebook/widget/LoginButton.java
index 828b21d30..6f548f4dc 100644
--- a/platforms/android/FacebookLib/src/com/facebook/widget/LoginButton.java
+++ b/platforms/android/FacebookLib/src/com/facebook/widget/LoginButton.java
@@ -33,17 +33,18 @@
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
-
import com.facebook.*;
import com.facebook.android.R;
import com.facebook.internal.AnalyticsEvents;
-import com.facebook.model.GraphUser;
import com.facebook.internal.SessionAuthorizationType;
import com.facebook.internal.SessionTracker;
import com.facebook.internal.Utility;
import com.facebook.internal.Utility.FetchedAppSettings;
+import com.facebook.model.GraphUser;
-import java.util.*;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
/**
* A Log In/Log Out button that maintains session state and logs
diff --git a/platforms/android/FacebookLib/src/com/facebook/widget/PickerFragment.java b/platforms/android/FacebookLib/src/com/facebook/widget/PickerFragment.java
index 780fdfa9f..4ef6cbd23 100644
--- a/platforms/android/FacebookLib/src/com/facebook/widget/PickerFragment.java
+++ b/platforms/android/FacebookLib/src/com/facebook/widget/PickerFragment.java
@@ -32,10 +32,13 @@
import android.view.ViewStub;
import android.view.animation.AlphaAnimation;
import android.widget.*;
-import com.facebook.*;
+import com.facebook.FacebookException;
+import com.facebook.Request;
+import com.facebook.Session;
+import com.facebook.SessionState;
import com.facebook.android.R;
-import com.facebook.model.GraphObject;
import com.facebook.internal.SessionTracker;
+import com.facebook.model.GraphObject;
import java.util.*;
diff --git a/platforms/android/FacebookLib/src/com/facebook/widget/PlacePickerFragment.java b/platforms/android/FacebookLib/src/com/facebook/widget/PlacePickerFragment.java
index 58b277583..043700161 100644
--- a/platforms/android/FacebookLib/src/com/facebook/widget/PlacePickerFragment.java
+++ b/platforms/android/FacebookLib/src/com/facebook/widget/PlacePickerFragment.java
@@ -35,9 +35,9 @@
import com.facebook.*;
import com.facebook.android.R;
import com.facebook.internal.AnalyticsEvents;
-import com.facebook.model.GraphPlace;
import com.facebook.internal.Logger;
import com.facebook.internal.Utility;
+import com.facebook.model.GraphPlace;
import java.util.*;
diff --git a/platforms/android/FacebookLib/src/com/facebook/widget/ToolTipPopup.java b/platforms/android/FacebookLib/src/com/facebook/widget/ToolTipPopup.java
index 58f64f357..0622416ce 100644
--- a/platforms/android/FacebookLib/src/com/facebook/widget/ToolTipPopup.java
+++ b/platforms/android/FacebookLib/src/com/facebook/widget/ToolTipPopup.java
@@ -16,11 +16,6 @@
package com.facebook.widget;
-import java.lang.ref.WeakReference;
-
-import com.facebook.android.R;
-import com.facebook.widget.LoginButton.ToolTipMode;
-
import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
@@ -31,6 +26,9 @@
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;
+import com.facebook.android.R;
+
+import java.lang.ref.WeakReference;
public class ToolTipPopup {
diff --git a/platforms/android/FacebookLib/src/com/facebook/widget/WebDialog.java b/platforms/android/FacebookLib/src/com/facebook/widget/WebDialog.java
index 84be24eec..9150f50a5 100644
--- a/platforms/android/FacebookLib/src/com/facebook/widget/WebDialog.java
+++ b/platforms/android/FacebookLib/src/com/facebook/widget/WebDialog.java
@@ -37,7 +37,8 @@
import android.widget.ImageView;
import android.widget.LinearLayout;
import com.facebook.*;
-import com.facebook.android.*;
+import com.facebook.android.R;
+import com.facebook.android.Util;
import com.facebook.internal.Logger;
import com.facebook.internal.ServerProtocol;
import com.facebook.internal.Utility;
@@ -51,7 +52,6 @@
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;
diff --git a/platforms/android/README.md b/platforms/android/README.md
index 57b23cd63..95c7b9ea1 100644
--- a/platforms/android/README.md
+++ b/platforms/android/README.md
@@ -19,15 +19,16 @@ For Android sample app remember to configure the project with your FB app id in
This plugin requires [Cordova CLI](http://cordova.apache.org/docs/en/3.5.0/guide_cli_index.md.html).
-To install the plugin in your app, execute the following (replace variables where necessary)...
+To install the plugin in your app, execute the following (replace variables where necessary):
+```sh
+# Create initial Cordova app
+$ cordova create myApp
+$ cd myApp/
+$ cordova platform add android
- cordova create myApp
-
- cd myApp/
-
- cordova platform add android
-
- cordova -d plugin add /Users/your/path/here/phonegap-facebook-plugin --variable APP_ID="123456789" --variable APP_NAME="myApplication"
+# Remember to replace APP_ID and APP_NAME variables
+$ cordova -d plugin add /path/to/cloned/phonegap-facebook-plugin --variable APP_ID="123456789" --variable APP_NAME="myApplication"
+```
## Setup with Eclipse
@@ -56,13 +57,13 @@ Follow the steps below:
android update project --subprojects --path "platforms/android" --target android-19 --library "CordovaLib"
- android update project --subprojects --path "platforms/android" --target android-19 --library "FacebookLib"
+ android update project --subprojects --path "platforms/android" --target android-19 --library "com.phonegap.plugins.facebookconnect/FacebookLib"
cd platforms/android/
ant clean
- cd FacebookLib
+ cd com.phonegap.plugins.facebookconnect/FacebookLib
ant clean
diff --git a/platforms/android/src/org/apache/cordova/facebook/ConnectPlugin.java b/platforms/android/src/org/apache/cordova/facebook/ConnectPlugin.java
index 811ca7534..9e4fb06d2 100644
--- a/platforms/android/src/org/apache/cordova/facebook/ConnectPlugin.java
+++ b/platforms/android/src/org/apache/cordova/facebook/ConnectPlugin.java
@@ -21,7 +21,6 @@
import org.json.JSONException;
import org.json.JSONObject;
-import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
@@ -30,9 +29,11 @@
import com.facebook.FacebookDialogException;
import com.facebook.FacebookException;
import com.facebook.FacebookOperationCanceledException;
+import com.facebook.FacebookAuthorizationException;
import com.facebook.FacebookRequestError;
import com.facebook.FacebookServiceException;
import com.facebook.Request;
+import com.facebook.Request.GraphUserCallback;
import com.facebook.Response;
import com.facebook.Session;
import com.facebook.SessionState;
@@ -45,643 +46,726 @@
public class ConnectPlugin extends CordovaPlugin {
- private static final String PUBLISH_PERMISSION_PREFIX = "publish";
- private static final String MANAGE_PERMISSION_PREFIX = "manage";
- @SuppressWarnings("serial")
- private static final Set OTHER_PUBLISH_PERMISSIONS = new HashSet() {
- {
- add("ads_management");
- add("create_event");
- add("rsvp_event");
- }
- };
- private final String TAG = "ConnectPlugin";
-
- private AppEventsLogger logger;
- private String applicationId = null;
- private CallbackContext loginContext = null;
- private CallbackContext showDialogContext = null;
- private CallbackContext graphContext = null;
- private Bundle paramBundle;
- private String method;
- private String graphPath;
- private String userID;
- private UiLifecycleHelper uiHelper;
- private boolean trackingPendingCall = false;
-
- @Override
- public void initialize(CordovaInterface cordova, CordovaWebView webView) {
- //Initialize UiLifecycleHelper
- uiHelper = new UiLifecycleHelper(cordova.getActivity(), null);
-
- // Init logger
- logger = AppEventsLogger.newLogger(cordova.getActivity());
-
- int appResId = cordova.getActivity().getResources().getIdentifier("fb_app_id", "string", cordova.getActivity().getPackageName());
- applicationId = cordova.getActivity().getString(appResId);
-
- // Set up the activity result callback to this class
- cordova.setActivityResultCallback(this);
-
- // Open a session if we have one cached
- Session session = new Session.Builder(cordova.getActivity()).setApplicationId(applicationId).build();
- if (session.getState() == SessionState.CREATED_TOKEN_LOADED) {
- Session.setActiveSession(session);
- // - Create the request
- Session.OpenRequest openRequest = new Session.OpenRequest(cordova.getActivity());
- // - Set the status change call back
- openRequest.setCallback(new Session.StatusCallback() {
- @Override
- public void call(Session session, SessionState state, Exception exception) {
- onSessionStateChange(state, exception);
- }
- });
- session.openForRead(openRequest);
- }
-
- // If we have a valid open session, get user's info
- if (session != null && session.isOpened()) {
- // Call this method to initialize the session state info
- onSessionStateChange(session.getState(), null);
- }
- super.initialize(cordova, webView);
- }
-
- @Override
- public void onResume(boolean multitasking) {
- super.onResume(multitasking);
- uiHelper.onResume();
- // Developers can observe how frequently users activate their app by logging an app activation event.
- AppEventsLogger.activateApp(cordova.getActivity());
- }
-
- protected void onSaveInstanceState(Bundle outState) {
- uiHelper.onSaveInstanceState(outState);
- }
-
- public void onPause() {
- uiHelper.onPause();
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- uiHelper.onDestroy();
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent intent) {
- super.onActivityResult(requestCode, resultCode, intent);
- Log.d(TAG, "activity result in plugin");
- if (trackingPendingCall) {
- uiHelper.onActivityResult(requestCode, resultCode, intent, new FacebookDialog.Callback() {
- @Override
- public void onError(FacebookDialog.PendingCall pendingCall, Exception error, Bundle data) {
- Log.e("Activity", String.format("Error: %s", error.toString()));
- handleError(error);
- }
-
- @Override
- public void onComplete(FacebookDialog.PendingCall pendingCall, Bundle data) {
- Log.i("Activity", "Success!");
- handleSuccess(data);
- }
- });
- } else {
- Session session = Session.getActiveSession();
-
- if (session != null && (loginContext != null || session.isOpened())) {
- session.onActivityResult(cordova.getActivity(), requestCode, resultCode, intent);
- }
- }
- trackingPendingCall = false;
- }
-
- @Override
- public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
-
- if (action.equals("login")) {
- Log.d(TAG, "login FB");
- // Get the permissions
- String[] arrayPermissions = new String[args.length()];
- for (int i = 0; i < args.length(); i++) {
- arrayPermissions[i] = args.getString(i);
- }
-
- List permissions = null;
- if (arrayPermissions.length > 0) {
- permissions = Arrays.asList(arrayPermissions);
- }
-
- // Get the currently active session
- Session session = Session.getActiveSession();
-
- // Set a pending callback to cordova
- loginContext = callbackContext;
- PluginResult pr = new PluginResult(PluginResult.Status.NO_RESULT);
- pr.setKeepCallback(true);
- loginContext.sendPluginResult(pr);
-
- // Check if the active session is open
- if (session != null && session.isOpened()) {
- // Reauthorize flow
- boolean publishPermissions = false;
- boolean readPermissions = false;
- // Figure out if this will be a read or publish reauthorize
- if (permissions == null) {
- // No permissions, read
- readPermissions = true;
- }
- // Loop through the permissions to see what
- // is being requested
- for (String permission : arrayPermissions) {
- if (isPublishPermission(permission)) {
- publishPermissions = true;
- } else {
- readPermissions = true;
- }
- // Break if we have a mixed bag, as this is an error
- if (publishPermissions && readPermissions) {
- break;
- }
- }
-
- if (publishPermissions && readPermissions) {
- callbackContext.error("Cannot ask for both read and publish permissions.");
- } else {
- // Set up the new permissions request
- Session.NewPermissionsRequest newPermissionsRequest = new Session.NewPermissionsRequest(cordova.getActivity(), permissions);
- // Set up the activity result callback to this class
- cordova.setActivityResultCallback(this);
- // Check for write permissions, the default is read (empty)
- if (publishPermissions) {
- // Request new publish permissions
- session.requestNewPublishPermissions(newPermissionsRequest);
- } else {
- // Request new read permissions
- session.requestNewReadPermissions(newPermissionsRequest);
- }
- }
- } else {
- // Initial login, build a new session open request.
-
- // - Create a new session and set the application ID
- session = new Session.Builder(cordova.getActivity()).setApplicationId(applicationId).build();
- Session.setActiveSession(session);
- // - Create the request
- Session.OpenRequest openRequest = new Session.OpenRequest(cordova.getActivity());
- // - Set the permissions
- openRequest.setPermissions(permissions);
- // - Set the status change call back
- openRequest.setCallback(new Session.StatusCallback() {
- @Override
- public void call(Session session, SessionState state, Exception exception) {
- onSessionStateChange(state, exception);
- }
- });
-
- // Can only ask for read permissions initially
- session.openForRead(openRequest);
- }
- return true;
- } else if (action.equals("logout")) {
-
- Session session = Session.getActiveSession();
- if (session != null) {
- if (session.isOpened()) {
- session.closeAndClearTokenInformation();
- userID = null;
- callbackContext.success();
- } else {
- // Session not open
- callbackContext.error("Session not open.");
- }
- } else {
- callbackContext.error("No valid session found, must call init and login before logout.");
- }
- return true;
- } else if (action.equals("getLoginStatus")) {
- callbackContext.success(getResponse());
- return true;
- } else if (action.equals("getAccessToken")) {
- Session session = Session.getActiveSession();
- if (session != null) {
- if (session.isOpened()) {
- callbackContext.success(session.getAccessToken());
- } else {
- // Session not open
- callbackContext.error("Session not open.");
- }
- } else {
- callbackContext
- .error("No valid session found, must call init and login before logout.");
- }
- return true;
- } else if (action.equals("logEvent")) {
- if (args.length() == 0) {
- // Not enough parameters
- callbackContext.error("Invalid arguments");
- return true;
- }
- String eventName = args.getString(0);
- if (args.length() == 1) {
- logger.logEvent(eventName);
- } else {
- // args is greater than 1
- JSONObject params = args.getJSONObject(1);
- Bundle parameters = new Bundle();
-
- Iterator> iterator = params.keys();
- while (iterator.hasNext()) {
- try {
- // Try get a String
- String value = params.getString((String) iterator.next());
- parameters.putString((String) iterator.next(), value);
- } catch (Exception e) {
- // Maybe it was an int
- Log.w(TAG, "Type in AppEvent parameters was not String for key: " + (String) iterator.next());
- try {
- int value = params.getInt((String) iterator.next());
- parameters.putInt((String) iterator.next(), value);
- } catch (Exception e2) {
- // Nope
- Log.e(TAG, "Unsupported type in AppEvent parameters for key: " + (String) iterator.next());
- }
- }
- }
- if (args.length() == 2) {
- logger.logEvent(eventName, parameters);
- }
- if (args.length() == 3) {
- double value = args.getDouble(2);
- logger.logEvent(eventName, value, parameters);
- }
- }
- callbackContext.success();
- return true;
- } else if (action.equals("logPurchase")) {
- /*
- * While calls to logEvent can be made to register purchase events,
- * there is a helper method that explicitly takes a currency indicator.
- */
- if (args.length() != 2) {
- callbackContext.error("Invalid arguments");
- return true;
- }
- int value = args.getInt(0);
- String currency = args.getString(1);
- logger.logPurchase(BigDecimal.valueOf(value), Currency.getInstance(currency));
- callbackContext.success();
- return true;
- } else if (action.equals("showDialog")) {
- Bundle collect = new Bundle();
- JSONObject params = null;
- try {
- params = args.getJSONObject(0);
- } catch (JSONException e) {
- params = new JSONObject();
- }
-
- final ConnectPlugin me = this;
- Iterator> iter = params.keys();
- while (iter.hasNext()) {
- String key = (String) iter.next();
- if (key.equals("method")) {
- try {
- this.method = params.getString(key);
- } catch (JSONException e) {
- Log.w(TAG, "Nonstring method parameter provided to dialog");
- }
- } else {
- try {
- collect.putString(key, params.getString(key));
- } catch (JSONException e) {
- // Need to handle JSON parameters
- Log.w(TAG, "Nonstring parameter provided to dialog discarded");
- }
- }
- }
- this.paramBundle = new Bundle(collect);
-
- // Begin by sending a callback pending notice to Cordova
- showDialogContext = callbackContext;
- PluginResult pr = new PluginResult(PluginResult.Status.NO_RESULT);
- pr.setKeepCallback(true);
- showDialogContext.sendPluginResult(pr);
-
- // Setup callback context
- final OnCompleteListener dialogCallback = new OnCompleteListener() {
-
- @Override
- public void onComplete(Bundle values, FacebookException exception) {
- if (exception != null) {
- handleError(exception);
- } else {
- handleSuccess(values);
- }
- }
- };
-
- if (this.method.equalsIgnoreCase("feed")) {
- Runnable runnable = new Runnable() {
- public void run() {
- WebDialog feedDialog = (new WebDialog.FeedDialogBuilder(me.cordova.getActivity(), Session.getActiveSession(), paramBundle)).setOnCompleteListener(dialogCallback).build();
- feedDialog.show();
- }
- };
- cordova.getActivity().runOnUiThread(runnable);
- } else if (this.method.equalsIgnoreCase("apprequests")) {
- Runnable runnable = new Runnable() {
- public void run() {
- WebDialog requestsDialog = (new WebDialog.RequestsDialogBuilder(me.cordova.getActivity(), Session.getActiveSession(), paramBundle)).setOnCompleteListener(dialogCallback)
- .build();
- requestsDialog.show();
- }
- };
- cordova.getActivity().runOnUiThread(runnable);
- } else if (this.method.equalsIgnoreCase("share") || this.method.equalsIgnoreCase("share_open_graph")) {
- if (FacebookDialog.canPresentShareDialog(me.cordova.getActivity(), FacebookDialog.ShareDialogFeature.SHARE_DIALOG)) {
- Runnable runnable = new Runnable() {
- public void run() {
- // Publish the post using the Share Dialog
- FacebookDialog shareDialog = new FacebookDialog.ShareDialogBuilder(me.cordova.getActivity())
- .setName(paramBundle.getString("name"))
- .setCaption(paramBundle.getString("caption"))
- .setDescription(paramBundle.getString("description"))
- .setLink(paramBundle.getString("link"))
- .setPicture(paramBundle.getString("picture"))
- .build();
- uiHelper.trackPendingDialogCall(shareDialog.present());
- }
- };
- this.trackingPendingCall = true;
- cordova.getActivity().runOnUiThread(runnable);
- } else {
- // Fallback. For example, publish the post using the Feed Dialog
- Runnable runnable = new Runnable() {
- public void run() {
- WebDialog feedDialog = (new WebDialog.FeedDialogBuilder(me.cordova.getActivity(), Session.getActiveSession(), paramBundle)).setOnCompleteListener(dialogCallback).build();
- feedDialog.show();
- }
- };
- cordova.getActivity().runOnUiThread(runnable);
- }
- } else {
- callbackContext.error("Unsupported dialog method.");
- }
- return true;
- } else if (action.equals("graphApi")) {
- graphContext = callbackContext;
- PluginResult pr = new PluginResult(PluginResult.Status.NO_RESULT);
- pr.setKeepCallback(true);
- graphContext.sendPluginResult(pr);
-
- graphPath = args.getString(0);
-
- JSONArray arr = args.getJSONArray(1);
-
- final List permissionsList = new ArrayList();
- for (int i = 0; i < arr.length(); i++) {
- permissionsList.add(arr.getString(i));
- }
-
- final Session session = Session.getActiveSession();
- final ConnectPlugin me = this;
-
- boolean publishPermissions = false;
- boolean readPermissions = false;
- if (permissionsList.size() > 0) {
- for (String permission : permissionsList) {
- if (isPublishPermission(permission)) {
- publishPermissions = true;
- } else {
- readPermissions = true;
- }
- // Break if we have a mixed bag, as this is an error
- if (publishPermissions && readPermissions) {
- break;
- }
- }
- if (publishPermissions && readPermissions) {
- graphContext.error("Cannot ask for both read and publish permissions.");
- } else {
- if (session.getPermissions().containsAll(permissionsList)) {
- makeGraphCall();
- } else {
- // Set up the new permissions request
- Session.NewPermissionsRequest newPermissionsRequest = new Session.NewPermissionsRequest(cordova.getActivity(), permissionsList);
- // Set up the activity result callback to this class
- cordova.setActivityResultCallback(me);
- // Check for write permissions, the default is read (empty)
- if (publishPermissions) {
- // Request new publish permissions
- session.requestNewPublishPermissions(newPermissionsRequest);
- } else {
- // Request new read permissions
- session.requestNewReadPermissions(newPermissionsRequest);
- }
- }
- }
- } else {
- makeGraphCall();
- }
- return true;
- }
- return false;
- }
-
- private void handleError(Exception exception) {
- String errMsg = "Facebook error: " + exception.getMessage();
- // User clicked "x"
- if (exception instanceof FacebookOperationCanceledException) {
- errMsg = "User cancelled dialog";
- } else if (exception instanceof FacebookDialogException) {
- // Dialog error
- errMsg = "Dialog error: " + exception.getMessage();
- } else if (exception instanceof FacebookServiceException) {
- FacebookRequestError error = ((FacebookServiceException) exception).getRequestError();
- if (error.getErrorCode() == 4201) {
- // User hit the cancel button in the WebView
- // Tried error.getErrorMessage() but it returns null
- // if though the URL says:
- // Redirect URL: fbconnect://success?error_code=4201&error_message=User+canceled+the+Dialog+flow
- errMsg = "User cancelled dialog";
- }
- }
- Log.e(TAG, errMsg);
- showDialogContext.error(errMsg);
- }
-
- private void handleSuccess(Bundle values) {
- // Handle a successful dialog:
- // Send the URL parameters back, for a requests dialog, the "request" parameter
- // will include the resulting request id. For a feed dialog, the "post_id"
- // parameter will include the resulting post id.
- // Note: If the user clicks on the Cancel button, the parameter will be empty
- if (values.size() > 0) {
- JSONObject response = new JSONObject();
- try {
- Set keys = values.keySet();
- for (String key : keys) {
- response.put(key, values.get(key));
- }
- } catch (JSONException e) {
- e.printStackTrace();
- }
- showDialogContext.success(response);
- } else {
- Log.e(TAG, "User cancelled dialog");
- showDialogContext.error("User cancelled dialog");
- }
- }
-
- private void getUserInfo(final Session session) {
- if (cordova != null) {
- Request.newMeRequest(session, new Request.GraphUserCallback() {
-
- @Override
- public void onCompleted(GraphUser user, Response response) {
- // Create a new result with response data
- if (loginContext != null) {
- GraphObject graphObject = response.getGraphObject();
- Log.d(TAG, "returning login object " + graphObject.getInnerJSONObject().toString());
- userID = user.getId();
- loginContext.success(getResponse());
- loginContext = null;
- }
- }
- }).executeAsync();
- }
- }
-
- private void makeGraphCall() {
- Session session = Session.getActiveSession();
-
- Request.Callback graphCallback = new Request.Callback() {
-
- @Override
- public void onCompleted(Response response) {
- if (graphContext != null) {
- if (response.getError() != null) {
- graphContext.error(response.getError().getErrorMessage());
- } else {
- GraphObject graphObject = response.getGraphObject();
- graphContext.success(graphObject.getInnerJSONObject());
- }
- graphPath = null;
- graphContext = null;
- }
- }
- };
-
- //If you're using the paging URLs they will be URLEncoded, let's decode them.
- try {
- graphPath = URLDecoder.decode(graphPath, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- }
-
- String[] urlParts = graphPath.split("\\?");
- String graphAction = urlParts[0];
- Request graphRequest = Request.newGraphPathRequest(null, graphAction, graphCallback);
- Bundle params = graphRequest.getParameters();
-
- if (urlParts.length > 1) {
- String[] queries = urlParts[1].split("&");
-
- for (String query : queries) {
- int splitPoint = query.indexOf("=");
- if (splitPoint > 0) {
- String key = query.substring(0, splitPoint);
- String value = query.substring(splitPoint + 1, query.length());
- params.putString(key, value);
- }
- }
- }
- params.putString("access_token", session.getAccessToken());
-
- graphRequest.setParameters(params);
- graphRequest.executeAsync();
- }
-
- /*
- * Handles session state changes
- */
- private void onSessionStateChange(SessionState state, Exception exception) {
- final Session session = Session.getActiveSession();
- // Check if the session is open
- if (state.isOpened()) {
- if (loginContext != null) {
- // Get user info
- getUserInfo(session);
- } else if (graphContext != null) {
- // Make the graph call
- makeGraphCall();
- }
- }
- }
-
- /*
- * Checks for publish permissions
- */
- private boolean isPublishPermission(String permission) {
- return permission != null && (permission.startsWith(PUBLISH_PERMISSION_PREFIX) || permission.startsWith(MANAGE_PERMISSION_PREFIX) || OTHER_PUBLISH_PERMISSIONS.contains(permission));
- }
-
- /**
- * Create a Facebook Response object that matches the one for the Javascript SDK
- * @return JSONObject - the response object
- */
- public JSONObject getResponse() {
- String response;
- Session session = Session.getActiveSession();
- if (session != null && session.isOpened()) {
- Date today = new Date();
- long expiresTimeInterval = (session.getExpirationDate().getTime() - today.getTime()) / 1000L;
- long expiresIn = (expiresTimeInterval > 0) ? expiresTimeInterval : 0;
- response = "{"
- + "\"status\": \"connected\","
- + "\"authResponse\": {"
- + "\"accessToken\": \"" + session.getAccessToken() + "\","
- + "\"expiresIn\": \"" + expiresIn + "\","
- + "\"session_key\": true,"
- + "\"sig\": \"...\","
- + "\"userID\": \"" + this.userID + "\""
- + "}"
- + "}";
- } else {
- response = "{"
- + "\"status\": \"unknown\""
- + "}";
- }
-
- try {
- return new JSONObject(response);
- } catch (JSONException e) {
-
- e.printStackTrace();
- }
- return new JSONObject();
- }
-
- private class WebDialogBuilderRunnable implements Runnable {
- private Context context;
- private Session session;
- private String method;
- private Bundle paramBundle;
- private OnCompleteListener dialogCallback;
-
- public WebDialogBuilderRunnable(Context context, Session session, String method, Bundle paramBundle, OnCompleteListener dialogCallback) {
- this.context = context;
- this.session = session;
- this.method = method;
- this.paramBundle = paramBundle;
- this.dialogCallback = dialogCallback;
- }
-
- public void run() {
- WebDialog shareDialog = (new WebDialog.Builder(context, session, method, paramBundle)).setOnCompleteListener(dialogCallback).build();
- shareDialog.show();
- }
- }
+ private static final int INVALID_ERROR_CODE = -2; //-1 is FacebookRequestError.INVALID_ERROR_CODE
+ private static final String PUBLISH_PERMISSION_PREFIX = "publish";
+ private static final String MANAGE_PERMISSION_PREFIX = "manage";
+ @SuppressWarnings("serial")
+ private static final Set OTHER_PUBLISH_PERMISSIONS = new HashSet() {
+ {
+ add("ads_management");
+ add("create_event");
+ add("rsvp_event");
+ }
+ };
+ private final String TAG = "ConnectPlugin";
+
+ private AppEventsLogger logger;
+ private String applicationId = null;
+ private CallbackContext loginContext = null;
+ private CallbackContext showDialogContext = null;
+ private CallbackContext graphContext = null;
+ private Bundle paramBundle;
+ private String method;
+ private String graphPath;
+ private String userID;
+ private UiLifecycleHelper uiHelper;
+ private boolean trackingPendingCall = false;
+
+ @Override
+ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
+ //Initialize UiLifecycleHelper
+ uiHelper = new UiLifecycleHelper(cordova.getActivity(), null);
+
+ // Init logger
+ logger = AppEventsLogger.newLogger(cordova.getActivity());
+
+ int appResId = cordova.getActivity().getResources().getIdentifier("fb_app_id", "string", cordova.getActivity().getPackageName());
+ applicationId = cordova.getActivity().getString(appResId);
+
+ // Set up the activity result callback to this class
+ cordova.setActivityResultCallback(this);
+
+ // Open a session if we have one cached
+ Session session = new Session.Builder(cordova.getActivity()).setApplicationId(applicationId).build();
+ if (session.getState() == SessionState.CREATED_TOKEN_LOADED) {
+ Session.setActiveSession(session);
+ // - Create the request
+ Session.OpenRequest openRequest = new Session.OpenRequest(cordova.getActivity());
+ // - Set the status change call back
+ openRequest.setCallback(new Session.StatusCallback() {
+ @Override
+ public void call(Session session, SessionState state, Exception exception) {
+ onSessionStateChange(state, exception);
+ }
+ });
+ session.openForRead(openRequest);
+ }
+
+ // If we have a valid open session, get user's info
+ if (checkActiveSession(session)) {
+ // Call this method to initialize the session state info
+ onSessionStateChange(session.getState(), null);
+ }
+ super.initialize(cordova, webView);
+ }
+
+ @Override
+ public void onResume(boolean multitasking) {
+ super.onResume(multitasking);
+ uiHelper.onResume();
+ // Developers can observe how frequently users activate their app by logging an app activation event.
+ AppEventsLogger.activateApp(cordova.getActivity());
+ }
+
+ protected void onSaveInstanceState(Bundle outState) {
+ uiHelper.onSaveInstanceState(outState);
+ }
+
+ public void onPause() {
+ uiHelper.onPause();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ uiHelper.onDestroy();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ super.onActivityResult(requestCode, resultCode, intent);
+ Log.d(TAG, "activity result in plugin: requestCode(" + requestCode + "), resultCode(" + resultCode + ")");
+ if (trackingPendingCall) {
+ uiHelper.onActivityResult(requestCode, resultCode, intent, new FacebookDialog.Callback() {
+ @Override
+ public void onError(FacebookDialog.PendingCall pendingCall, Exception error, Bundle data) {
+ Log.e("Activity", String.format("Error: %s", error.toString()));
+ handleError(error, showDialogContext);
+ }
+
+ @Override
+ public void onComplete(FacebookDialog.PendingCall pendingCall, Bundle data) {
+ Log.i("Activity", "Success!");
+ handleSuccess(data);
+ }
+ });
+ } else {
+ Session session = Session.getActiveSession();
+ if (session != null && loginContext != null) {
+ session.onActivityResult(cordova.getActivity(), requestCode, resultCode, intent);
+ }
+ }
+ trackingPendingCall = false;
+ }
+
+ @Override
+ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+
+ if (action.equals("login")) {
+ Log.d(TAG, "login FB");
+ // Get the permissions
+ String[] arrayPermissions = new String[args.length()];
+ for (int i = 0; i < args.length(); i++) {
+ arrayPermissions[i] = args.getString(i);
+ }
+
+ List permissions = null;
+ if (arrayPermissions.length > 0) {
+ permissions = Arrays.asList(arrayPermissions);
+ }
+
+ // Get the currently active session
+ Session session = Session.getActiveSession();
+
+ // Set a pending callback to cordova
+ loginContext = callbackContext;
+ PluginResult pr = new PluginResult(PluginResult.Status.NO_RESULT);
+ pr.setKeepCallback(true);
+ loginContext.sendPluginResult(pr);
+
+ // Check if the active session is open
+ if (checkActiveSession(session)) {
+ // Reauthorize flow
+ boolean publishPermissions = false;
+ boolean readPermissions = false;
+ // Figure out if this will be a read or publish reauthorize
+ if (permissions == null) {
+ // No permissions, read
+ readPermissions = true;
+ }
+ // Loop through the permissions to see what
+ // is being requested
+ for (String permission : arrayPermissions) {
+ if (isPublishPermission(permission)) {
+ publishPermissions = true;
+ } else {
+ readPermissions = true;
+ }
+ // Break if we have a mixed bag, as this is an error
+ if (publishPermissions && readPermissions) {
+ break;
+ }
+ }
+ if (publishPermissions && readPermissions) {
+ callbackContext.error("Cannot ask for both read and publish permissions.");
+ } else {
+ // Set up the new permissions request
+ Session.NewPermissionsRequest newPermissionsRequest = new Session.NewPermissionsRequest(cordova.getActivity(), permissions);
+ // Set up the activity result callback to this class
+ cordova.setActivityResultCallback(this);
+ // Check for write permissions, the default is read (empty)
+ if (publishPermissions) {
+ // Request new publish permissions
+ session.requestNewPublishPermissions(newPermissionsRequest);
+ } else {
+ // Request new read permissions
+ session.requestNewReadPermissions(newPermissionsRequest);
+ }
+ }
+ } else {
+ // Initial login, build a new session open request.
+
+ // - Create a new session and set the application ID
+ session = new Session.Builder(cordova.getActivity()).setApplicationId(applicationId).build();
+ // Set up the activity result callback to this class
+ cordova.setActivityResultCallback(this);
+
+ Session.setActiveSession(session);
+ // - Create the request
+ Session.OpenRequest openRequest = new Session.OpenRequest(cordova.getActivity());
+ // - Set the permissions
+ openRequest.setPermissions(permissions);
+ // - Set the status change call back
+ openRequest.setCallback(new Session.StatusCallback() {
+ @Override
+ public void call(Session session, SessionState state, Exception exception) {
+ onSessionStateChange(state, exception);
+ }
+ });
+
+ // Can only ask for read permissions initially
+ session.openForRead(openRequest);
+ }
+ return true;
+ } else if (action.equals("logout")) {
+
+ Session session = Session.getActiveSession();
+ if (checkActiveSession(session)) {
+ session.closeAndClearTokenInformation();
+ userID = null;
+ callbackContext.success();
+ } else {
+ if (session != null) {
+ // Session was existing, but was not open
+ callbackContext.error("Session not open.");
+ } else {
+ callbackContext.error("No valid session found, must call init and login before logout.");
+ }
+ }
+ return true;
+ } else if (action.equals("getLoginStatus")) {
+ Session session = Session.getActiveSession();
+ if (userID == null && Session.getActiveSession() != null && session.isOpened()) {
+ // We have no userID but a valid session, so must update the user info
+ // (Probably app was force stopped)
+ final CallbackContext _callbackContext = callbackContext;
+ getUserInfo(session, new GraphUserCallback() {
+ @Override
+ public void onCompleted(GraphUser user, Response response) {
+ // Request completed, userID was updated,
+ // recursive call to generate the correct response JSON
+ if (response.getError() != null) {
+ _callbackContext.error(getFacebookRequestErrorResponse(response.getError()));
+ } else {
+ userID = user.getId();
+ _callbackContext.success(getResponse());
+ }
+ }
+ });
+ } else {
+ callbackContext.success(getResponse());
+ }
+ return true;
+ } else if (action.equals("getAccessToken")) {
+ Session session = Session.getActiveSession();
+ if (checkActiveSession(session)) {
+ callbackContext.success(session.getAccessToken());
+ } else {
+ if (session == null) {
+ callbackContext.error("No valid session found, must call init and login before logout.");
+ } else {
+ // Session not open
+ callbackContext.error("Session not open.");
+ }
+ }
+ return true;
+ } else if (action.equals("logEvent")) {
+ if (args.length() == 0) {
+ // Not enough parameters
+ callbackContext.error("Invalid arguments");
+ return true;
+ }
+ String eventName = args.getString(0);
+ if (args.length() == 1) {
+ logger.logEvent(eventName);
+ } else {
+ // Arguments is greater than 1
+ JSONObject params = args.getJSONObject(1);
+ Bundle parameters = new Bundle();
+
+ Iterator> iterator = params.keys();
+ while (iterator.hasNext()) {
+ try {
+ // Try get a String
+ String key = (String) iterator.next();
+ String value = params.getString(key);
+ parameters.putString(key, value);
+ } catch (Exception e) {
+ // Maybe it was an int
+ Log.w(TAG, "Type in AppEvent parameters was not String for key: " + (String) iterator.next());
+ try {
+ String key = (String) iterator.next();
+ int value = params.getInt(key);
+ parameters.putInt(key, value);
+ } catch (Exception e2) {
+ // Nope
+ Log.e(TAG, "Unsupported type in AppEvent parameters for key: " + (String) iterator.next());
+ }
+ }
+ }
+ if (args.length() == 2) {
+ logger.logEvent(eventName, parameters);
+ }
+ if (args.length() == 3) {
+ double value = args.getDouble(2);
+ logger.logEvent(eventName, value, parameters);
+ }
+ }
+ callbackContext.success();
+ return true;
+ } else if (action.equals("logPurchase")) {
+ /*
+ * While calls to logEvent can be made to register purchase events,
+ * there is a helper method that explicitly takes a currency indicator.
+ */
+ if (args.length() != 2) {
+ callbackContext.error("Invalid arguments");
+ return true;
+ }
+ int value = args.getInt(0);
+ String currency = args.getString(1);
+ logger.logPurchase(BigDecimal.valueOf(value), Currency.getInstance(currency));
+ callbackContext.success();
+ return true;
+ } else if (action.equals("showDialog")) {
+ Session session = Session.getActiveSession();
+ if (!checkActiveSession(session)) {
+ callbackContext.error("No active session");
+ return true;
+ }
+ Bundle collect = new Bundle();
+ JSONObject params = null;
+ try {
+ params = args.getJSONObject(0);
+ } catch (JSONException e) {
+ params = new JSONObject();
+ }
+
+ final ConnectPlugin me = this;
+ Iterator> iter = params.keys();
+ while (iter.hasNext()) {
+ String key = (String) iter.next();
+ if (key.equals("method")) {
+ try {
+ this.method = params.getString(key);
+ } catch (JSONException e) {
+ Log.w(TAG, "Nonstring method parameter provided to dialog");
+ }
+ } else {
+ try {
+ collect.putString(key, params.getString(key));
+ } catch (JSONException e) {
+ // Need to handle JSON parameters
+ Log.w(TAG, "Nonstring parameter provided to dialog discarded");
+ }
+ }
+ }
+ this.paramBundle = new Bundle(collect);
+
+ // Begin by sending a callback pending notice to Cordova
+ showDialogContext = callbackContext;
+ PluginResult pr = new PluginResult(PluginResult.Status.NO_RESULT);
+ pr.setKeepCallback(true);
+ showDialogContext.sendPluginResult(pr);
+
+ // Setup callback context
+ final OnCompleteListener dialogCallback = new OnCompleteListener() {
+
+ @Override
+ public void onComplete(Bundle values, FacebookException exception) {
+ if (exception != null) {
+ handleError(exception, showDialogContext);
+ } else {
+ handleSuccess(values);
+ }
+ }
+ };
+
+ if (this.method.equalsIgnoreCase("feed")) {
+ Runnable runnable = new Runnable() {
+ public void run() {
+ WebDialog feedDialog = (new WebDialog.FeedDialogBuilder(me.cordova.getActivity(), Session.getActiveSession(), paramBundle)).setOnCompleteListener(dialogCallback).build();
+ feedDialog.show();
+ }
+ };
+ cordova.getActivity().runOnUiThread(runnable);
+ } else if (this.method.equalsIgnoreCase("apprequests")) {
+ Runnable runnable = new Runnable() {
+ public void run() {
+ WebDialog requestsDialog = (new WebDialog.RequestsDialogBuilder(me.cordova.getActivity(), Session.getActiveSession(), paramBundle)).setOnCompleteListener(dialogCallback)
+ .build();
+ requestsDialog.show();
+ }
+ };
+ cordova.getActivity().runOnUiThread(runnable);
+ } else if (this.method.equalsIgnoreCase("share") || this.method.equalsIgnoreCase("share_open_graph")) {
+ if (FacebookDialog.canPresentShareDialog(me.cordova.getActivity(), FacebookDialog.ShareDialogFeature.SHARE_DIALOG)) {
+ Runnable runnable = new Runnable() {
+ public void run() {
+ // Publish the post using the Share Dialog
+ FacebookDialog shareDialog = new FacebookDialog.ShareDialogBuilder(me.cordova.getActivity())
+ .setName(paramBundle.getString("name"))
+ .setCaption(paramBundle.getString("caption"))
+ .setDescription(paramBundle.getString("description"))
+ .setLink(paramBundle.getString("href"))
+ .setPicture(paramBundle.getString("picture"))
+ .build();
+ uiHelper.trackPendingDialogCall(shareDialog.present());
+ }
+ };
+ this.trackingPendingCall = true;
+ cordova.getActivity().runOnUiThread(runnable);
+ } else {
+ // Fallback. For example, publish the post using the Feed Dialog
+ Runnable runnable = new Runnable() {
+ public void run() {
+ WebDialog feedDialog = (new WebDialog.FeedDialogBuilder(me.cordova.getActivity(), Session.getActiveSession(), paramBundle)).setOnCompleteListener(dialogCallback).build();
+ feedDialog.show();
+ }
+ };
+ cordova.getActivity().runOnUiThread(runnable);
+ }
+ } else {
+ callbackContext.error("Unsupported dialog method.");
+ }
+ return true;
+ } else if (action.equals("graphApi")) {
+ graphContext = callbackContext;
+ PluginResult pr = new PluginResult(PluginResult.Status.NO_RESULT);
+ pr.setKeepCallback(true);
+ graphContext.sendPluginResult(pr);
+
+ graphPath = args.getString(0);
+
+ JSONArray arr = args.getJSONArray(1);
+
+ final List permissionsList = new ArrayList();
+ for (int i = 0; i < arr.length(); i++) {
+ permissionsList.add(arr.getString(i));
+ }
+
+ boolean publishPermissions = false;
+ boolean readPermissions = false;
+ if (permissionsList.size() > 0) {
+ for (String permission : permissionsList) {
+ if (isPublishPermission(permission)) {
+ publishPermissions = true;
+ } else {
+ readPermissions = true;
+ }
+ // Break if we have a mixed bag, as this is an error
+ if (publishPermissions && readPermissions) {
+ break;
+ }
+ }
+ if (publishPermissions && readPermissions) {
+ graphContext.error("Cannot ask for both read and publish permissions.");
+ } else {
+ Session session = Session.getActiveSession();
+ if (session.getPermissions().containsAll(permissionsList)) {
+ makeGraphCall();
+ } else {
+ // Set up the new permissions request
+ Session.NewPermissionsRequest newPermissionsRequest = new Session.NewPermissionsRequest(cordova.getActivity(), permissionsList);
+ // Set up the activity result callback to this class
+ cordova.setActivityResultCallback(this);
+ // Check for write permissions, the default is read (empty)
+ if (publishPermissions) {
+ // Request new publish permissions
+ session.requestNewPublishPermissions(newPermissionsRequest);
+ } else {
+ // Request new read permissions
+ session.requestNewReadPermissions(newPermissionsRequest);
+ }
+ }
+ }
+ } else {
+ makeGraphCall();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ // Simple active session check
+ private boolean checkActiveSession(Session session) {
+ if (session != null && session.isOpened()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void handleError(Exception exception, CallbackContext context) {
+ String errMsg = "Facebook error: " + exception.getMessage();
+ int errorCode = INVALID_ERROR_CODE;
+ // User clicked "x"
+ if (exception instanceof FacebookOperationCanceledException) {
+ errMsg = "User cancelled dialog";
+ errorCode = 4201;
+ } else if (exception instanceof FacebookDialogException) {
+ // Dialog error
+ errMsg = "Dialog error: " + exception.getMessage();
+ }
+
+ Log.e(TAG, exception.toString());
+ context.error(getErrorResponse(exception, errMsg, errorCode));
+ }
+
+ private void handleSuccess(Bundle values) {
+ // Handle a successful dialog:
+ // Send the URL parameters back, for a requests dialog, the "request" parameter
+ // will include the resulting request id. For a feed dialog, the "post_id"
+ // parameter will include the resulting post id.
+ // Note: If the user clicks on the Cancel button, the parameter will be empty
+ if (values.size() > 0) {
+ JSONObject response = new JSONObject();
+ try {
+ Set keys = values.keySet();
+ for (String key : keys) {
+ response.put(key, values.get(key));
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ showDialogContext.success(response);
+ } else {
+ Log.e(TAG, "User cancelled dialog");
+ showDialogContext.error("User cancelled dialog");
+ }
+ }
+
+ private void getUserInfo(final Session session, final Request.GraphUserCallback graphUserCb) {
+ if (cordova != null) {
+ Request.newMeRequest(session, graphUserCb).executeAsync();
+ }
+ }
+
+ private void makeGraphCall() {
+ Session session = Session.getActiveSession();
+
+ Request.Callback graphCallback = new Request.Callback() {
+
+ @Override
+ public void onCompleted(Response response) {
+ if (graphContext != null) {
+ if (response.getError() != null) {
+ graphContext.error(getFacebookRequestErrorResponse(response.getError()));
+ } else {
+ GraphObject graphObject = response.getGraphObject();
+ graphContext.success(graphObject.getInnerJSONObject());
+ }
+ graphPath = null;
+ graphContext = null;
+ }
+ }
+ };
+
+ //If you're using the paging URLs they will be URLEncoded, let's decode them.
+ try {
+ graphPath = URLDecoder.decode(graphPath, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+
+ String[] urlParts = graphPath.split("\\?");
+ String graphAction = urlParts[0];
+ Request graphRequest = Request.newGraphPathRequest(null, graphAction, graphCallback);
+ Bundle params = graphRequest.getParameters();
+
+ if (urlParts.length > 1) {
+ String[] queries = urlParts[1].split("&");
+
+ for (String query : queries) {
+ int splitPoint = query.indexOf("=");
+ if (splitPoint > 0) {
+ String key = query.substring(0, splitPoint);
+ String value = query.substring(splitPoint + 1, query.length());
+ params.putString(key, value);
+ }
+ }
+ }
+ params.putString("access_token", session.getAccessToken());
+
+ graphRequest.setParameters(params);
+ graphRequest.executeAsync();
+ }
+
+ /*
+ * Handles session state changes
+ */
+ private void onSessionStateChange(SessionState state, Exception exception) {
+ Log.d(TAG, "onSessionStateChange:" + state.toString());
+ if (exception != null && exception instanceof FacebookOperationCanceledException) {
+ // only handle FacebookOperationCanceledException to support
+ // SDK recovery behavior triggered by getUserInfo
+ Log.e(TAG, "exception:" + exception.toString());
+ handleError(exception, loginContext);
+ } else {
+ final Session session = Session.getActiveSession();
+ // Check if the session is open
+ if (state.isOpened()) {
+ if (loginContext != null) {
+ // Get user info
+ getUserInfo(session, new Request.GraphUserCallback() {
+ @Override
+ public void onCompleted(GraphUser user, Response response) {
+ // Create a new result with response data
+ if (loginContext != null) {
+ if (response.getError() != null) {
+ loginContext.error(getFacebookRequestErrorResponse(response.getError()));
+ } else {
+ GraphObject graphObject = response.getGraphObject();
+ Log.d(TAG, "returning login object " + graphObject.getInnerJSONObject().toString());
+ userID = user.getId();
+ loginContext.success(getResponse());
+ loginContext = null;
+ }
+ } else {
+ // Just update the userID in case we force quit the application before
+ userID = user.getId();
+ }
+ }
+ });
+ } else if (graphContext != null) {
+ // Make the graph call
+ makeGraphCall();
+ }
+ } else if (state == SessionState.CLOSED_LOGIN_FAILED && loginContext != null){
+ handleError(new FacebookAuthorizationException("Session was closed and was not closed normally"), loginContext);
+ }
+ }
+ }
+
+ /*
+ * Checks for publish permissions
+ */
+ private boolean isPublishPermission(String permission) {
+ return permission != null && (permission.startsWith(PUBLISH_PERMISSION_PREFIX) || permission.startsWith(MANAGE_PERMISSION_PREFIX) || OTHER_PUBLISH_PERMISSIONS.contains(permission));
+ }
+
+ /**
+ * Create a Facebook Response object that matches the one for the Javascript SDK
+ * @return JSONObject - the response object
+ */
+ public JSONObject getResponse() {
+ String response;
+ final Session session = Session.getActiveSession();
+ if (checkActiveSession(session)) {
+ Date today = new Date();
+ long expiresTimeInterval = (session.getExpirationDate().getTime() - today.getTime()) / 1000L;
+ long expiresIn = (expiresTimeInterval > 0) ? expiresTimeInterval : 0;
+ response = "{"
+ + "\"status\": \"connected\","
+ + "\"authResponse\": {"
+ + "\"accessToken\": \"" + session.getAccessToken() + "\","
+ + "\"expiresIn\": \"" + expiresIn + "\","
+ + "\"session_key\": true,"
+ + "\"sig\": \"...\","
+ + "\"userID\": \"" + userID + "\""
+ + "}"
+ + "}";
+ } else {
+ response = "{"
+ + "\"status\": \"unknown\""
+ + "}";
+ }
+ try {
+ return new JSONObject(response);
+ } catch (JSONException e) {
+
+ e.printStackTrace();
+ }
+ return new JSONObject();
+ }
+
+ public JSONObject getFacebookRequestErrorResponse(FacebookRequestError error) {
+
+ String response = "{"
+ + "\"errorCode\": \"" + error.getErrorCode() + "\","
+ + "\"errorType\": \"" + error.getErrorType() + "\","
+ + "\"errorMessage\": \"" + error.getErrorMessage() + "\"";
+
+ int messageId = error.getUserActionMessageId();
+
+ // Check for INVALID_MESSAGE_ID
+ if (messageId != 0) {
+ String errorUserMessage = cordova.getActivity().getResources().getString(messageId);
+ // Safe check for null
+ if (errorUserMessage != null) {
+ response += ",\"errorUserMessage\": \"" + cordova.getActivity().getResources().getString(error.getUserActionMessageId()) + "\"";
+ }
+ }
+
+ response += "}";
+
+ try {
+ return new JSONObject(response);
+ } catch (JSONException e) {
+
+ e.printStackTrace();
+ }
+ return new JSONObject();
+ }
+
+ public JSONObject getErrorResponse(Exception error, String message, int errorCode) {
+
+ if (error instanceof FacebookServiceException) {
+ return getFacebookRequestErrorResponse(((FacebookServiceException) error).getRequestError());
+ }
+
+ String response = "{";
+
+ if (error instanceof FacebookDialogException) {
+ errorCode = ((FacebookDialogException) error).getErrorCode();
+ }
+
+ if (errorCode != INVALID_ERROR_CODE) {
+ response += "\"errorCode\": \"" + errorCode + "\",";
+ }
+
+ if (message == null) {
+ message = error.getMessage();
+ }
+
+ response += "\"errorMessage\": \"" + message + "\"}";
+
+ try {
+ return new JSONObject(response);
+ } catch (JSONException e) {
+
+ e.printStackTrace();
+ }
+ return new JSONObject();
+ }
}
diff --git a/platforms/ios/CordovaLib/.gitignore b/platforms/ios/CordovaLib/.npmignore
similarity index 100%
rename from platforms/ios/CordovaLib/.gitignore
rename to platforms/ios/CordovaLib/.npmignore
diff --git a/platforms/ios/CordovaLib/Classes/CDVAvailability.h b/platforms/ios/CordovaLib/Classes/CDVAvailability.h
index 57e346ea9..6c711c8f5 100644
--- a/platforms/ios/CordovaLib/Classes/CDVAvailability.h
+++ b/platforms/ios/CordovaLib/Classes/CDVAvailability.h
@@ -49,6 +49,10 @@
#define __CORDOVA_3_3_0 30300
#define __CORDOVA_3_4_0 30400
#define __CORDOVA_3_4_1 30401
+#define __CORDOVA_3_5_0 30500
+#define __CORDOVA_3_6_0 30600
+#define __CORDOVA_3_6_1 30601
+#define __CORDOVA_3_6_3 30603
#define __CORDOVA_NA 99999 /* not available */
/*
@@ -59,7 +63,7 @@
#endif
*/
#ifndef CORDOVA_VERSION_MIN_REQUIRED
- #define CORDOVA_VERSION_MIN_REQUIRED __CORDOVA_3_4_0
+ #define CORDOVA_VERSION_MIN_REQUIRED __CORDOVA_3_6_3
#endif
/*
diff --git a/platforms/ios/CordovaLib/Classes/CDVCommandDelegate.h b/platforms/ios/CordovaLib/Classes/CDVCommandDelegate.h
index 040113667..04df6bc7c 100644
--- a/platforms/ios/CordovaLib/Classes/CDVCommandDelegate.h
+++ b/platforms/ios/CordovaLib/Classes/CDVCommandDelegate.h
@@ -31,10 +31,6 @@
- (NSString*)pathForResource:(NSString*)resourcepath;
- (id)getCommandInstance:(NSString*)pluginName;
-// Plugins should not be using this interface to call other plugins since it
-// will result in bogus callbacks being made.
-- (BOOL)execute:(CDVInvokedUrlCommand*)command CDV_DEPRECATED(2.2, "Use direct method calls instead.");
-
// Sends a plugin result to the JS. This is thread-safe.
- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId;
// Evaluates the given JS. This is thread-safe.
diff --git a/platforms/ios/CordovaLib/Classes/CDVCommandDelegateImpl.h b/platforms/ios/CordovaLib/Classes/CDVCommandDelegateImpl.h
index 7b41df7e1..053113431 100644
--- a/platforms/ios/CordovaLib/Classes/CDVCommandDelegateImpl.h
+++ b/platforms/ios/CordovaLib/Classes/CDVCommandDelegateImpl.h
@@ -26,7 +26,7 @@
@interface CDVCommandDelegateImpl : NSObject {
@private
__weak CDVViewController* _viewController;
- NSRegularExpression *_callbackIdPattern;
+ NSRegularExpression* _callbackIdPattern;
@protected
__weak CDVCommandQueue* _commandQueue;
BOOL _delayResponses;
diff --git a/platforms/ios/CordovaLib/Classes/CDVCommandDelegateImpl.m b/platforms/ios/CordovaLib/Classes/CDVCommandDelegateImpl.m
index a72ef463d..76f5ef4c8 100644
--- a/platforms/ios/CordovaLib/Classes/CDVCommandDelegateImpl.m
+++ b/platforms/ios/CordovaLib/Classes/CDVCommandDelegateImpl.m
@@ -95,9 +95,14 @@ - (void)evalJsHelper:(NSString*)js
}
}
-- (BOOL)isValidCallbackId:(NSString *)callbackId
+- (BOOL)isValidCallbackId:(NSString*)callbackId
{
- NSError *err = nil;
+ NSError* err = nil;
+
+ if (callbackId == nil) {
+ return NO;
+ }
+
// Initialize on first use
if (_callbackIdPattern == nil) {
// Catch any invalid characters in the callback id.
@@ -150,11 +155,6 @@ - (void)evalJs:(NSString*)js scheduledOnRunLoop:(BOOL)scheduledOnRunLoop
}
}
-- (BOOL)execute:(CDVInvokedUrlCommand*)command
-{
- return [_commandQueue execute:command];
-}
-
- (id)getCommandInstance:(NSString*)pluginName
{
return [_viewController getCommandInstance:pluginName];
diff --git a/platforms/ios/CordovaLib/Classes/CDVLocalStorage.m b/platforms/ios/CordovaLib/Classes/CDVLocalStorage.m
index 9dcf5c18b..5e2f4bbbf 100644
--- a/platforms/ios/CordovaLib/Classes/CDVLocalStorage.m
+++ b/platforms/ios/CordovaLib/Classes/CDVLocalStorage.m
@@ -340,6 +340,12 @@ + (void)__restoreLegacyDatabaseLocationsWithBackupType:(NSString*)backupType
NSMutableArray* backupInfo = [NSMutableArray arrayWithCapacity:0];
if ([backupType isEqualToString:@"cloud"]) {
+#ifdef DEBUG
+ NSLog(@"\n\nStarted backup to iCloud! Please be careful."
+ "\nYour application might be rejected by Apple if you store too much data."
+ "\nFor more information please read \"iOS Data Storage Guidelines\" at:"
+ "\nhttps://developer.apple.com/icloud/documentation/data-storage/\n\n");
+#endif
// We would like to restore old backups/caches databases to the new destination (nested in lib folder)
[backupInfo addObjectsFromArray:[self createBackupInfoWithTargetDir:appLibraryFolder backupDir:[appDocumentsFolder stringByAppendingPathComponent:@"Backups"] targetDirNests:YES backupDirNests:NO rename:YES]];
[backupInfo addObjectsFromArray:[self createBackupInfoWithTargetDir:appLibraryFolder backupDir:[appLibraryFolder stringByAppendingPathComponent:@"Caches"] targetDirNests:YES backupDirNests:NO rename:NO]];
diff --git a/platforms/ios/CordovaLib/Classes/CDVPlugin.h b/platforms/ios/CordovaLib/Classes/CDVPlugin.h
index 33ba1c4ba..5e8b2830a 100644
--- a/platforms/ios/CordovaLib/Classes/CDVPlugin.h
+++ b/platforms/ios/CordovaLib/Classes/CDVPlugin.h
@@ -27,6 +27,8 @@ extern NSString* const CDVPageDidLoadNotification;
extern NSString* const CDVPluginHandleOpenURLNotification;
extern NSString* const CDVPluginResetNotification;
extern NSString* const CDVLocalNotification;
+extern NSString* const CDVRemoteNotification;
+extern NSString* const CDVRemoteNotificationError;
@interface CDVPlugin : NSObject {}
@@ -56,9 +58,10 @@ extern NSString* const CDVLocalNotification;
- (id)appDelegate;
-// TODO(agrieve): Deprecate these in favour of using CDVCommandDelegate directly.
-- (NSString*)writeJavascript:(NSString*)javascript;
-- (NSString*)success:(CDVPluginResult*)pluginResult callbackId:(NSString*)callbackId;
-- (NSString*)error:(CDVPluginResult*)pluginResult callbackId:(NSString*)callbackId;
+- (NSString*)writeJavascript:(NSString*)javascript CDV_DEPRECATED(3.6, "Use the CDVCommandDelegate equivalent of evalJs:. This will be removed in 4.0.0");
+
+- (NSString*)success:(CDVPluginResult*)pluginResult callbackId:(NSString*)callbackId CDV_DEPRECATED(3.6, "Use the CDVCommandDelegate equivalent of sendPluginResult:callbackId. This will be removed in 4.0.0");
+
+- (NSString*)error:(CDVPluginResult*)pluginResult callbackId:(NSString*)callbackId CDV_DEPRECATED(3.6, "Use the CDVCommandDelegate equivalent of sendPluginResult:callbackId. This will be removed in 4.0.0");
@end
diff --git a/platforms/ios/CordovaLib/Classes/CDVPlugin.m b/platforms/ios/CordovaLib/Classes/CDVPlugin.m
index 8c932a029..ea81ddd1e 100644
--- a/platforms/ios/CordovaLib/Classes/CDVPlugin.m
+++ b/platforms/ios/CordovaLib/Classes/CDVPlugin.m
@@ -23,6 +23,8 @@ Licensed to the Apache Software Foundation (ASF) under one
NSString* const CDVPluginHandleOpenURLNotification = @"CDVPluginHandleOpenURLNotification";
NSString* const CDVPluginResetNotification = @"CDVPluginResetNotification";
NSString* const CDVLocalNotification = @"CDVLocalNotification";
+NSString* const CDVRemoteNotification = @"CDVRemoteNotification";
+NSString* const CDVRemoteNotificationError = @"CDVRemoteNotificationError";
@interface CDVPlugin ()
diff --git a/platforms/ios/CordovaLib/Classes/CDVPluginResult.h b/platforms/ios/CordovaLib/Classes/CDVPluginResult.h
index 11b537736..e624d4dec 100644
--- a/platforms/ios/CordovaLib/Classes/CDVPluginResult.h
+++ b/platforms/ios/CordovaLib/Classes/CDVPluginResult.h
@@ -18,6 +18,7 @@
*/
#import
+#import "CDVAvailability.h"
typedef enum {
CDVCommandStatus_NO_RESULT = 0,
@@ -61,8 +62,10 @@ typedef enum {
- (NSString*)argumentsAsJSON;
// These methods are used by the legacy plugin return result method
-- (NSString*)toJSONString;
-- (NSString*)toSuccessCallbackString:(NSString*)callbackId;
-- (NSString*)toErrorCallbackString:(NSString*)callbackId;
+- (NSString*)toJSONString CDV_DEPRECATED(3.6, "Only used by toSuccessCallbackString and toErrorCallbackString which are deprecated. This will be removed in 4.0.0");
+
+- (NSString*)toSuccessCallbackString:(NSString*)callbackId CDV_DEPRECATED(3.6, "Use the CDVCommandDelegate method sendPluginResult:callbackId instead. This will be removed in 4.0.0");
+
+- (NSString*)toErrorCallbackString:(NSString*)callbackId CDV_DEPRECATED(3.6, "Use the CDVCommandDelegate method sendPluginResult:callbackId instead. This will be removed in 4.0.0");
@end
diff --git a/platforms/ios/CordovaLib/Classes/CDVPluginResult.m b/platforms/ios/CordovaLib/Classes/CDVPluginResult.m
index af7c528be..2eb46cdb0 100644
--- a/platforms/ios/CordovaLib/Classes/CDVPluginResult.m
+++ b/platforms/ios/CordovaLib/Classes/CDVPluginResult.m
@@ -96,7 +96,7 @@ - (CDVPluginResult*)initWithStatus:(CDVCommandStatus)statusOrdinal message:(id)t
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal
{
- return [[self alloc] initWithStatus:statusOrdinal message:[org_apache_cordova_CommandStatusMsgs objectAtIndex:statusOrdinal]];
+ return [[self alloc] initWithStatus:statusOrdinal message:nil];
}
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsString:(NSString*)theMessage
diff --git a/platforms/ios/CordovaLib/Classes/CDVShared.h b/platforms/ios/CordovaLib/Classes/CDVShared.h
index c54567abc..68acc5c27 100644
--- a/platforms/ios/CordovaLib/Classes/CDVShared.h
+++ b/platforms/ios/CordovaLib/Classes/CDVShared.h
@@ -17,17 +17,6 @@
under the License.
*/
-#import
+// This file was emptied out in 3.6.0 release (July 2014).
+// It will be deleted in a future release.
#import
-
-@interface NSError (JSONMethods)
-
-- (NSString*)JSONRepresentation;
-
-@end
-
-@interface CLLocation (JSONMethods)
-
-- (NSString*)JSONRepresentation;
-
-@end
diff --git a/platforms/ios/CordovaLib/Classes/CDVShared.m b/platforms/ios/CordovaLib/Classes/CDVShared.m
deleted file mode 100644
index ac8223424..000000000
--- a/platforms/ios/CordovaLib/Classes/CDVShared.m
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- Licensed to the Apache Software Foundation (ASF) under one
- or more contributor license agreements. See the NOTICE file
- distributed with this work for additional information
- regarding copyright ownership. The ASF licenses this file
- to you 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.
- */
-
-#import "CDVShared.h"
-
-#pragma mark -
-#pragma mark CLLocation(JSONMethods)
-
-@implementation CLLocation (JSONMethods)
-
-- (NSString*)JSONRepresentation
-{
- return [NSString stringWithFormat:
- @"{ timestamp: %.00f, \
- coords: { latitude: %f, longitude: %f, altitude: %.02f, heading: %.02f, speed: %.02f, accuracy: %.02f, altitudeAccuracy: %.02f } \
- }",
- [self.timestamp timeIntervalSince1970] * 1000.0,
- self.coordinate.latitude,
- self.coordinate.longitude,
- self.altitude,
- self.course,
- self.speed,
- self.horizontalAccuracy,
- self.verticalAccuracy
- ];
-}
-
-@end
-
-#pragma mark NSError(JSONMethods)
-
-@implementation NSError (JSONMethods)
-
-- (NSString*)JSONRepresentation
-{
- return [NSString stringWithFormat:
- @"{ code: %ld, message: '%@'}",
- (long)self.code,
- [self localizedDescription]
- ];
-}
-
-@end
diff --git a/platforms/ios/CordovaLib/Classes/CDVURLProtocol.m b/platforms/ios/CordovaLib/Classes/CDVURLProtocol.m
index eeee91639..3ecb6a0c3 100644
--- a/platforms/ios/CordovaLib/Classes/CDVURLProtocol.m
+++ b/platforms/ios/CordovaLib/Classes/CDVURLProtocol.m
@@ -27,7 +27,9 @@ Licensed to the Apache Software Foundation (ASF) under one
#import "CDVViewController.h"
@interface CDVHTTPURLResponse : NSHTTPURLResponse
-@property (nonatomic) NSInteger statusCode;
+#ifndef __IPHONE_8_0
+ @property (nonatomic) NSInteger statusCode;
+#endif
@end
static CDVWhitelist* gWhitelist = nil;
@@ -166,8 +168,8 @@ - (void)startLoading
// We have the asset! Get the data and send it along.
ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation];
NSString* MIMEType = (__bridge_transfer NSString*)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)[assetRepresentation UTI], kUTTagClassMIMEType);
- Byte* buffer = (Byte*)malloc([assetRepresentation size]);
- NSUInteger bufferSize = [assetRepresentation getBytes:buffer fromOffset:0.0 length:[assetRepresentation size] error:nil];
+ Byte* buffer = (Byte*)malloc((unsigned long)[assetRepresentation size]);
+ NSUInteger bufferSize = [assetRepresentation getBytes:buffer fromOffset:0.0 length:(NSUInteger)[assetRepresentation size] error:nil];
NSData* data = [NSData dataWithBytesNoCopy:buffer length:bufferSize freeWhenDone:YES];
[self sendResponseWithResponseCode:200 data:data mimeType:MIMEType];
} else {
@@ -205,12 +207,21 @@ - (void)sendResponseWithResponseCode:(NSInteger)statusCode data:(NSData*)data mi
mimeType = @"text/plain";
}
NSString* encodingName = [@"text/plain" isEqualToString : mimeType] ? @"UTF-8" : nil;
- CDVHTTPURLResponse* response =
- [[CDVHTTPURLResponse alloc] initWithURL:[[self request] URL]
- MIMEType:mimeType
- expectedContentLength:[data length]
- textEncodingName:encodingName];
- response.statusCode = statusCode;
+
+#ifdef __IPHONE_8_0
+ NSHTTPURLResponse* response = [NSHTTPURLResponse alloc];
+#else
+ CDVHTTPURLResponse* response = [CDVHTTPURLResponse alloc];
+#endif
+
+ response = [response initWithURL:[[self request] URL]
+ MIMEType:mimeType
+ expectedContentLength:[data length]
+ textEncodingName:encodingName];
+
+#ifndef __IPHONE_8_0
+ response.statusCode = statusCode;
+#endif
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
if (data != nil) {
diff --git a/platforms/ios/CordovaLib/Classes/CDVViewController.m b/platforms/ios/CordovaLib/Classes/CDVViewController.m
index 3a5cd7d4a..66c1850fe 100644
--- a/platforms/ios/CordovaLib/Classes/CDVViewController.m
+++ b/platforms/ios/CordovaLib/Classes/CDVViewController.m
@@ -78,8 +78,9 @@ - (void)__init
self.supportedOrientations = [self parseInterfaceOrientations:
[[[NSBundle mainBundle] infoDictionary] objectForKey:@"UISupportedInterfaceOrientations"]];
+ [self printVersion];
[self printMultitaskingInfo];
- [self printDeprecationNotice];
+ [self printPlatformVersionWarning];
self.initialized = YES;
// load config.xml settings
@@ -118,10 +119,15 @@ - (void)viewWillDisappear:(BOOL)animated
[super viewWillDisappear:animated];
}
-- (void)printDeprecationNotice
+- (void)printVersion
{
- if (!IsAtLeastiOSVersion(@"5.0")) {
- NSLog(@"CRITICAL: For Cordova 2.0, you will need to upgrade to at least iOS 5.0 or greater. Your current version of iOS is %@.",
+ NSLog(@"Apache Cordova native platform version %@ is starting.", CDV_VERSION);
+}
+
+- (void)printPlatformVersionWarning
+{
+ if (!IsAtLeastiOSVersion(@"6.0")) {
+ NSLog(@"CRITICAL: For Cordova 3.5.0 and above, you will need to upgrade to at least iOS 6.0 or greater. Your current version of iOS is %@.",
[[UIDevice currentDevice] systemVersion]
);
}
diff --git a/platforms/ios/CordovaLib/Classes/CDVWebViewDelegate.h b/platforms/ios/CordovaLib/Classes/CDVWebViewDelegate.h
index dd7180731..4b60bab41 100644
--- a/platforms/ios/CordovaLib/Classes/CDVWebViewDelegate.h
+++ b/platforms/ios/CordovaLib/Classes/CDVWebViewDelegate.h
@@ -18,6 +18,7 @@
*/
#import
+#import "CDVAvailability.h"
/**
* Distinguishes top-level navigations from sub-frame navigations.
@@ -34,6 +35,8 @@
}
- (id)initWithDelegate:(NSObject *)delegate;
-- (BOOL)request:(NSURLRequest*)newRequest isFragmentIdentifierToRequest:(NSURLRequest*)originalRequest;
+- (BOOL)request:(NSURLRequest*)newRequest isFragmentIdentifierToRequest:(NSURLRequest*)originalRequest CDV_DEPRECATED(3.5, "Use request:isEqualToRequestAfterStrippingFragments: instead.");
+
+- (BOOL)request:(NSURLRequest*)newRequest isEqualToRequestAfterStrippingFragments:(NSURLRequest*)originalRequest;
@end
diff --git a/platforms/ios/CordovaLib/Classes/CDVWebViewDelegate.m b/platforms/ios/CordovaLib/Classes/CDVWebViewDelegate.m
index b3965655a..6bac3fae4 100644
--- a/platforms/ios/CordovaLib/Classes/CDVWebViewDelegate.m
+++ b/platforms/ios/CordovaLib/Classes/CDVWebViewDelegate.m
@@ -115,6 +115,11 @@ - (id)initWithDelegate:(NSObject *)delegate
}
- (BOOL)request:(NSURLRequest*)newRequest isFragmentIdentifierToRequest:(NSURLRequest*)originalRequest
+{
+ return [self request:newRequest isEqualToRequestAfterStrippingFragments:originalRequest];
+}
+
+- (BOOL)request:(NSURLRequest*)newRequest isEqualToRequestAfterStrippingFragments:(NSURLRequest*)originalRequest
{
if (originalRequest.URL && newRequest.URL) {
NSString* originalRequestUrl = [originalRequest.URL absoluteString];
@@ -204,11 +209,14 @@ - (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)re
VerboseLog(@"webView shouldLoad=%d (before) state=%d loadCount=%d URL=%@", shouldLoad, _state, _loadCount, request.URL);
if (shouldLoad) {
- BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]];
+ // When devtools refresh occurs, it blindly uses the same request object. If a history.replaceState() has occured, then
+ // mainDocumentURL != URL even though it's a top-level navigation.
+ BOOL isDevToolsRefresh = (request == webView.request);
+ BOOL isTopLevelNavigation = isDevToolsRefresh || [request.URL isEqual:[request mainDocumentURL]];
if (isTopLevelNavigation) {
// Ignore hash changes that don't navigate to a different page.
// webView.request does actually update when history.replaceState() gets called.
- if ([self request:request isFragmentIdentifierToRequest:webView.request]) {
+ if ([self request:request isEqualToRequestAfterStrippingFragments:webView.request]) {
NSString* prevURL = [self evalForCurrentURL:webView];
if ([prevURL isEqualToString:[request.URL absoluteString]]) {
VerboseLog(@"Page reload detected.");
diff --git a/platforms/ios/CordovaLib/Classes/CDVWhitelist.m b/platforms/ios/CordovaLib/Classes/CDVWhitelist.m
index 0095c55a5..8e3be7523 100644
--- a/platforms/ios/CordovaLib/Classes/CDVWhitelist.m
+++ b/platforms/ios/CordovaLib/Classes/CDVWhitelist.m
@@ -44,6 +44,13 @@ + (NSString*)regexFromPattern:(NSString*)pattern allowWildcards:(bool)allowWildc
if (allowWildcards) {
regex = [regex stringByReplacingOccurrencesOfString:@"\\*" withString:@".*"];
+
+ /* [NSURL path] has the peculiarity that a trailing slash at the end of a path
+ * will be omitted. This regex tweak compensates for that.
+ */
+ if ([regex hasSuffix:@"\\/.*"]) {
+ regex = [NSString stringWithFormat:@"%@(\\/.*)?", [regex substringToIndex:([regex length] - 4)]];
+ }
}
return [NSString stringWithFormat:@"%@$", regex];
}
@@ -55,14 +62,14 @@ - (id)initWithScheme:(NSString*)scheme host:(NSString*)host port:(NSString*)port
if ((scheme == nil) || [scheme isEqualToString:@"*"]) {
_scheme = nil;
} else {
- _scheme = [NSRegularExpression regularExpressionWithPattern:[CDVWhitelistPattern regexFromPattern:scheme allowWildcards:NO] options:0 error:nil];
+ _scheme = [NSRegularExpression regularExpressionWithPattern:[CDVWhitelistPattern regexFromPattern:scheme allowWildcards:NO] options:NSRegularExpressionCaseInsensitive error:nil];
}
if ([host isEqualToString:@"*"]) {
_host = nil;
} else if ([host hasPrefix:@"*."]) {
- _host = [NSRegularExpression regularExpressionWithPattern:[NSString stringWithFormat:@"([a-z0-9.-]*\\.)?%@", [CDVWhitelistPattern regexFromPattern:[host substringFromIndex:2] allowWildcards:false]] options:0 error:nil];
+ _host = [NSRegularExpression regularExpressionWithPattern:[NSString stringWithFormat:@"([a-z0-9.-]*\\.)?%@", [CDVWhitelistPattern regexFromPattern:[host substringFromIndex:2] allowWildcards:false]] options:NSRegularExpressionCaseInsensitive error:nil];
} else {
- _host = [NSRegularExpression regularExpressionWithPattern:[CDVWhitelistPattern regexFromPattern:host allowWildcards:NO] options:0 error:nil];
+ _host = [NSRegularExpression regularExpressionWithPattern:[CDVWhitelistPattern regexFromPattern:host allowWildcards:NO] options:NSRegularExpressionCaseInsensitive error:nil];
}
if ((port == nil) || [port isEqualToString:@"*"]) {
_port = nil;
@@ -162,7 +169,7 @@ - (void)addWhiteListEntry:(NSString*)origin
self.whitelist = nil;
self.permittedSchemes = nil;
} else { // specific access
- NSRegularExpression* parts = [NSRegularExpression regularExpressionWithPattern:@"^((\\*|[a-z-]+)://)?(((\\*\\.)?[^*/:]+)|\\*)?(:(\\d+))?(/.*)?" options:0 error:nil];
+ NSRegularExpression* parts = [NSRegularExpression regularExpressionWithPattern:@"^((\\*|[A-Za-z-]+)://)?(((\\*\\.)?[^*/:]+)|\\*)?(:(\\d+))?(/.*)?" options:0 error:nil];
NSTextCheckingResult* m = [parts firstMatchInString:origin options:NSMatchingAnchored range:NSMakeRange(0, [origin length])];
if (m != nil) {
NSRange r;
@@ -239,7 +246,7 @@ - (BOOL)URLIsAllowed:(NSURL*)url logFailure:(BOOL)logFailure
}
// Shortcut rejection: Check that the scheme is supported
- NSString* scheme = [url scheme];
+ NSString* scheme = [[url scheme] lowercaseString];
if (![self schemeIsAllowed:scheme]) {
if (logFailure) {
NSLog(@"%@", [self errorStringForURL:url]);
diff --git a/platforms/ios/CordovaLib/Classes/NSData+Base64.h b/platforms/ios/CordovaLib/Classes/NSData+Base64.h
index ffe9c830b..d5a9a6fa7 100644
--- a/platforms/ios/CordovaLib/Classes/NSData+Base64.h
+++ b/platforms/ios/CordovaLib/Classes/NSData+Base64.h
@@ -5,11 +5,20 @@
// Created by Matt Gallagher on 2009/06/03.
// Copyright 2009 Matt Gallagher. All rights reserved.
//
-// Permission is given to use this source code file, free of charge, in any
-// project, commercial or otherwise, entirely at your risk, with the condition
-// that any redistribution (in part or whole) of source code must retain
-// this copyright and permission notice. Attribution in compiled projects is
-// appreciated but not required.
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software. Permission is granted to anyone to
+// use this software for any purpose, including commercial applications, and to
+// alter it and redistribute it freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+// 3. This notice may not be removed or altered from any source
+// distribution.
//
#import
diff --git a/platforms/ios/CordovaLib/Classes/NSData+Base64.m b/platforms/ios/CordovaLib/Classes/NSData+Base64.m
index d0f21895a..c9d67f9b2 100644
--- a/platforms/ios/CordovaLib/Classes/NSData+Base64.m
+++ b/platforms/ios/CordovaLib/Classes/NSData+Base64.m
@@ -5,11 +5,20 @@
// Created by Matt Gallagher on 2009/06/03.
// Copyright 2009 Matt Gallagher. All rights reserved.
//
-// Permission is given to use this source code file, free of charge, in any
-// project, commercial or otherwise, entirely at your risk, with the condition
-// that any redistribution (in part or whole) of source code must retain
-// this copyright and permission notice. Attribution in compiled projects is
-// appreciated but not required.
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software. Permission is granted to anyone to
+// use this software for any purpose, including commercial applications, and to
+// alter it and redistribute it freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+// 3. This notice may not be removed or altered from any source
+// distribution.
//
#import "NSData+Base64.h"
@@ -17,7 +26,7 @@
//
// Mapping from 6 bit pattern to ASCII character.
//
-static unsigned char cdvbase64EncodeLookup[65] =
+static unsigned char base64EncodeLookup[65] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
//
@@ -28,7 +37,7 @@
//
// Mapping from ASCII character to 6 bit pattern.
//
-static unsigned char cdvbase64DecodeLookup[256] =
+static unsigned char base64DecodeLookup[256] =
{
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx,
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx,
@@ -51,8 +60,8 @@
//
// Fundamental sizes of the binary and base64 encode/decode units in bytes
//
-#define CDV_BINARY_UNIT_SIZE 3
-#define CDV_BASE64_UNIT_SIZE 4
+#define BINARY_UNIT_SIZE 3
+#define BASE64_UNIT_SIZE 4
//
// NewBase64Decode
@@ -64,7 +73,7 @@
// length - the length of the string or -1 (to specify strlen should be used)
// outputLength - if not-NULL, on output will contain the decoded length
//
-// returns the decoded buffer. Must be freed by caller. Length is given by
+// returns the decoded buffer. Must be free'd by caller. Length is given by
// outputLength.
//
void *CDVNewBase64Decode(
@@ -76,7 +85,8 @@
length = strlen(inputBuffer);
}
- size_t outputBufferSize = (length / CDV_BASE64_UNIT_SIZE) * CDV_BINARY_UNIT_SIZE;
+ size_t outputBufferSize =
+ ((length + BASE64_UNIT_SIZE - 1) / BASE64_UNIT_SIZE) * BINARY_UNIT_SIZE;
unsigned char* outputBuffer = (unsigned char*)malloc(outputBufferSize);
size_t i = 0;
@@ -86,17 +96,16 @@
//
// Accumulate 4 valid characters (ignore everything else)
//
- unsigned char accumulated[CDV_BASE64_UNIT_SIZE];
- bzero(accumulated, sizeof(unsigned char) * CDV_BASE64_UNIT_SIZE);
+ unsigned char accumulated[BASE64_UNIT_SIZE];
size_t accumulateIndex = 0;
while (i < length) {
- unsigned char decode = cdvbase64DecodeLookup[inputBuffer[i++]];
+ unsigned char decode = base64DecodeLookup[inputBuffer[i++]];
if (decode != xx) {
accumulated[accumulateIndex] = decode;
accumulateIndex++;
- if (accumulateIndex == CDV_BASE64_UNIT_SIZE) {
+ if (accumulateIndex == BASE64_UNIT_SIZE) {
break;
}
}
@@ -105,9 +114,17 @@
//
// Store the 6 bits from each of the 4 characters as 3 bytes
//
- outputBuffer[j] = (accumulated[0] << 2) | (accumulated[1] >> 4);
- outputBuffer[j + 1] = (accumulated[1] << 4) | (accumulated[2] >> 2);
- outputBuffer[j + 2] = (accumulated[2] << 6) | accumulated[3];
+ // (Uses improved bounds checking suggested by Alexandre Colucci)
+ //
+ if (accumulateIndex >= 2) {
+ outputBuffer[j] = (accumulated[0] << 2) | (accumulated[1] >> 4);
+ }
+ if (accumulateIndex >= 3) {
+ outputBuffer[j + 1] = (accumulated[1] << 4) | (accumulated[2] >> 2);
+ }
+ if (accumulateIndex >= 4) {
+ outputBuffer[j + 2] = (accumulated[2] << 6) | accumulated[3];
+ }
j += accumulateIndex - 1;
}
@@ -118,7 +135,7 @@
}
//
-// NewBase64Decode
+// NewBase64Encode
//
// Encodes the arbitrary data in the inputBuffer as base64 into a newly malloced
// output buffer.
@@ -130,7 +147,7 @@
// outputLength - if not-NULL, on output will contain the encoded length
// (not including terminating 0 char)
//
-// returns the encoded buffer. Must be freed by caller. Length is given by
+// returns the encoded buffer. Must be free'd by caller. Length is given by
// outputLength.
//
char *CDVNewBase64Encode(
@@ -143,16 +160,16 @@
#define MAX_NUM_PADDING_CHARS 2
#define OUTPUT_LINE_LENGTH 64
-#define INPUT_LINE_LENGTH ((OUTPUT_LINE_LENGTH / CDV_BASE64_UNIT_SIZE) * CDV_BINARY_UNIT_SIZE)
-#define CR_LF_SIZE 0
+#define INPUT_LINE_LENGTH ((OUTPUT_LINE_LENGTH / BASE64_UNIT_SIZE) * BINARY_UNIT_SIZE)
+#define CR_LF_SIZE 2
//
// Byte accurate calculation of final buffer size
//
size_t outputBufferSize =
- ((length / CDV_BINARY_UNIT_SIZE)
- + ((length % CDV_BINARY_UNIT_SIZE) ? 1 : 0))
- * CDV_BASE64_UNIT_SIZE;
+ ((length / BINARY_UNIT_SIZE)
+ + ((length % BINARY_UNIT_SIZE) ? 1 : 0))
+ * BASE64_UNIT_SIZE;
if (separateLines) {
outputBufferSize +=
(outputBufferSize / OUTPUT_LINE_LENGTH) * CR_LF_SIZE;
@@ -181,16 +198,16 @@
lineEnd = length;
}
- for (; i + CDV_BINARY_UNIT_SIZE - 1 < lineEnd; i += CDV_BINARY_UNIT_SIZE) {
+ for (; i + BINARY_UNIT_SIZE - 1 < lineEnd; i += BINARY_UNIT_SIZE) {
//
// Inner loop: turn 48 bytes into 64 base64 characters
//
- outputBuffer[j++] = cdvbase64EncodeLookup[(inputBuffer[i] & 0xFC) >> 2];
- outputBuffer[j++] = cdvbase64EncodeLookup[((inputBuffer[i] & 0x03) << 4)
+ outputBuffer[j++] = base64EncodeLookup[(inputBuffer[i] & 0xFC) >> 2];
+ outputBuffer[j++] = base64EncodeLookup[((inputBuffer[i] & 0x03) << 4)
| ((inputBuffer[i + 1] & 0xF0) >> 4)];
- outputBuffer[j++] = cdvbase64EncodeLookup[((inputBuffer[i + 1] & 0x0F) << 2)
+ outputBuffer[j++] = base64EncodeLookup[((inputBuffer[i + 1] & 0x0F) << 2)
| ((inputBuffer[i + 2] & 0xC0) >> 6)];
- outputBuffer[j++] = cdvbase64EncodeLookup[inputBuffer[i + 2] & 0x3F];
+ outputBuffer[j++] = base64EncodeLookup[inputBuffer[i + 2] & 0x3F];
}
if (lineEnd == length) {
@@ -209,17 +226,17 @@
//
// Handle the single '=' case
//
- outputBuffer[j++] = cdvbase64EncodeLookup[(inputBuffer[i] & 0xFC) >> 2];
- outputBuffer[j++] = cdvbase64EncodeLookup[((inputBuffer[i] & 0x03) << 4)
+ outputBuffer[j++] = base64EncodeLookup[(inputBuffer[i] & 0xFC) >> 2];
+ outputBuffer[j++] = base64EncodeLookup[((inputBuffer[i] & 0x03) << 4)
| ((inputBuffer[i + 1] & 0xF0) >> 4)];
- outputBuffer[j++] = cdvbase64EncodeLookup[(inputBuffer[i + 1] & 0x0F) << 2];
+ outputBuffer[j++] = base64EncodeLookup[(inputBuffer[i + 1] & 0x0F) << 2];
outputBuffer[j++] = '=';
} else if (i < length) {
//
// Handle the double '=' case
//
- outputBuffer[j++] = cdvbase64EncodeLookup[(inputBuffer[i] & 0xFC) >> 2];
- outputBuffer[j++] = cdvbase64EncodeLookup[(inputBuffer[i] & 0x03) << 4];
+ outputBuffer[j++] = base64EncodeLookup[(inputBuffer[i] & 0xFC) >> 2];
+ outputBuffer[j++] = base64EncodeLookup[(inputBuffer[i] & 0x03) << 4];
outputBuffer[j++] = '=';
outputBuffer[j++] = '=';
}
diff --git a/platforms/ios/CordovaLib/CordovaLib.xcodeproj/project.pbxproj b/platforms/ios/CordovaLib/CordovaLib.xcodeproj/project.pbxproj
index 7d437a206..6e6777705 100644
--- a/platforms/ios/CordovaLib/CordovaLib.xcodeproj/project.pbxproj
+++ b/platforms/ios/CordovaLib/CordovaLib.xcodeproj/project.pbxproj
@@ -8,7 +8,6 @@
/* Begin PBXBuildFile section */
1B701028177A61CF00AE11F4 /* CDVShared.h in Headers */ = {isa = PBXBuildFile; fileRef = 1B701026177A61CF00AE11F4 /* CDVShared.h */; settings = {ATTRIBUTES = (Public, ); }; };
- 1B701029177A61CF00AE11F4 /* CDVShared.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B701027177A61CF00AE11F4 /* CDVShared.m */; };
1F92F4A01314023E0046367C /* CDVPluginResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 1F92F49E1314023E0046367C /* CDVPluginResult.h */; settings = {ATTRIBUTES = (Public, ); }; };
1F92F4A11314023E0046367C /* CDVPluginResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F92F49F1314023E0046367C /* CDVPluginResult.m */; };
301F2F2A14F3C9CA003FE9FC /* CDV.h in Headers */ = {isa = PBXBuildFile; fileRef = 301F2F2914F3C9CA003FE9FC /* CDV.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -56,7 +55,6 @@
/* Begin PBXFileReference section */
1B701026177A61CF00AE11F4 /* CDVShared.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CDVShared.h; path = Classes/CDVShared.h; sourceTree = ""; };
- 1B701027177A61CF00AE11F4 /* CDVShared.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CDVShared.m; path = Classes/CDVShared.m; sourceTree = ""; };
1F92F49E1314023E0046367C /* CDVPluginResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CDVPluginResult.h; path = Classes/CDVPluginResult.h; sourceTree = ""; };
1F92F49F1314023E0046367C /* CDVPluginResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CDVPluginResult.m; path = Classes/CDVPluginResult.m; sourceTree = ""; };
301F2F2914F3C9CA003FE9FC /* CDV.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CDV.h; path = Classes/CDV.h; sourceTree = ""; };
@@ -204,7 +202,6 @@
30C684931407044A004C1A8E /* CDVURLProtocol.m */,
30C6847E1406CB38004C1A8E /* CDVWhitelist.h */,
1B701026177A61CF00AE11F4 /* CDVShared.h */,
- 1B701027177A61CF00AE11F4 /* CDVShared.m */,
30C6847F1406CB38004C1A8E /* CDVWhitelist.m */,
30E33AF013A7E24B00594D64 /* CDVPlugin.h */,
30E33AF113A7E24B00594D64 /* CDVPlugin.m */,
@@ -359,7 +356,6 @@
EB96673C16A8970A00D86CDF /* CDVUserAgentUtil.m in Sources */,
EBFF4DBC16D3FE2E008F452B /* CDVWebViewDelegate.m in Sources */,
7E14B5A91705050A0032169E /* CDVTimer.m in Sources */,
- 1B701029177A61CF00AE11F4 /* CDVShared.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -370,13 +366,6 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- "ARCHS[sdk=iphoneos*]" = armv7;
- "ARCHS[sdk=iphoneos6.*]" = (
- armv7,
- armv7s,
- );
- "ARCHS[sdk=iphoneos7.*]" = "$(ARCHS_STANDARD_INCLUDING_64_BIT)";
- "ARCHS[sdk=iphonesimulator*]" = "$(ARCHS_STANDARD_INCLUDING_64_BIT)";
CLANG_ENABLE_OBJC_ARC = YES;
COPY_PHASE_STRIP = NO;
DSTROOT = "/tmp/$(PROJECT_NAME).dst";
@@ -400,13 +389,6 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- "ARCHS[sdk=iphoneos*]" = armv7;
- "ARCHS[sdk=iphoneos6.*]" = (
- armv7,
- armv7s,
- );
- "ARCHS[sdk=iphoneos7.*]" = "$(ARCHS_STANDARD_INCLUDING_64_BIT)";
- "ARCHS[sdk=iphonesimulator*]" = "$(ARCHS_STANDARD_INCLUDING_64_BIT)";
CLANG_ENABLE_OBJC_ARC = YES;
DSTROOT = "/tmp/$(PROJECT_NAME).dst";
GCC_MODEL_TUNING = G5;
@@ -426,13 +408,6 @@
1DEB922308733DC00010E9CD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
- "ARCHS[sdk=iphoneos*]" = armv7;
- "ARCHS[sdk=iphoneos6.*]" = (
- armv7,
- armv7s,
- );
- "ARCHS[sdk=iphoneos7.*]" = "$(ARCHS_STANDARD_INCLUDING_64_BIT)";
- "ARCHS[sdk=iphonesimulator*]" = "$(ARCHS_STANDARD_INCLUDING_64_BIT)";
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -457,20 +432,12 @@
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = "1,2";
USER_HEADER_SEARCH_PATHS = "";
- VALID_ARCHS = "i386 armv7 armv7s arm64";
};
name = Debug;
};
1DEB922408733DC00010E9CD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
- "ARCHS[sdk=iphoneos*]" = armv7;
- "ARCHS[sdk=iphoneos6.*]" = (
- armv7,
- armv7s,
- );
- "ARCHS[sdk=iphoneos7.*]" = "$(ARCHS_STANDARD_INCLUDING_64_BIT)";
- "ARCHS[sdk=iphonesimulator*]" = "$(ARCHS_STANDARD_INCLUDING_64_BIT)";
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -492,7 +459,6 @@
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = "1,2";
- VALID_ARCHS = "i386 armv7 armv7s arm64";
};
name = Release;
};
diff --git a/platforms/ios/CordovaLib/VERSION b/platforms/ios/CordovaLib/VERSION
index 47b322c97..4a788a01d 100644
--- a/platforms/ios/CordovaLib/VERSION
+++ b/platforms/ios/CordovaLib/VERSION
@@ -1 +1 @@
-3.4.1
+3.6.3
diff --git a/platforms/ios/CordovaLib/cordova.js b/platforms/ios/CordovaLib/cordova.js
index 631573c41..05b79c7f8 100644
--- a/platforms/ios/CordovaLib/cordova.js
+++ b/platforms/ios/CordovaLib/cordova.js
@@ -1,5 +1,5 @@
// Platform: ios
-// 3.4.0
+// 3.6.3
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
@@ -19,7 +19,7 @@
under the License.
*/
;(function() {
-var CORDOVA_JS_BUILD_LABEL = '3.4.0';
+var CORDOVA_JS_BUILD_LABEL = '3.6.3';
// file: src/scripts/require.js
/*jshint -W079 */
@@ -265,7 +265,7 @@ var cordova = {
try {
cordova.callbackFromNative(callbackId, true, args.status, [args.message], args.keepCallback);
} catch (e) {
- console.log("Error in error callback: " + callbackId + " = "+e);
+ console.log("Error in success callback: " + callbackId + " = "+e);
}
},
@@ -392,6 +392,16 @@ base64.fromArrayBuffer = function(arrayBuffer) {
return uint8ToBase64(array);
};
+base64.toArrayBuffer = function(str) {
+ var decodedStr = typeof atob != 'undefined' ? atob(str) : new Buffer(str,'base64').toString('binary');
+ var arrayBuffer = new ArrayBuffer(decodedStr.length);
+ var array = new Uint8Array(arrayBuffer);
+ for (var i=0, len=decodedStr.length; i < len; i++) {
+ array[i] = decodedStr.charCodeAt(i);
+ }
+ return arrayBuffer;
+};
+
//------------------------------------------------------------------------------
/* This code is based on the performance tests at http://jsperf.com/b64tests
@@ -770,6 +780,7 @@ channel.createSticky('onNativeReady');
channel.createSticky('onCordovaReady');
// Event to indicate that all automatically loaded JS plugins are loaded and ready.
+// FIXME remove this
channel.createSticky('onPluginsReady');
// Event to indicate that Cordova is ready
@@ -815,7 +826,8 @@ var cordova = require('cordova'),
XHR_OPTIONAL_PAYLOAD: 3,
IFRAME_HASH_NO_PAYLOAD: 4,
// Bundling the payload turns out to be slower. Probably since it has to be URI encoded / decoded.
- IFRAME_HASH_WITH_PAYLOAD: 5
+ IFRAME_HASH_WITH_PAYLOAD: 5,
+ WK_WEBVIEW_BINDING: 6
},
bridgeMode,
execIframe,
@@ -910,7 +922,15 @@ function iOSExec() {
// Use IFRAME_NAV elsewhere since it's faster and XHR bridge
// seems to have bugs in newer OS's (CB-3900, CB-3359, CB-5457, CB-4970, CB-4998, CB-5134)
if (bridgeMode === undefined) {
- bridgeMode = navigator.userAgent.indexOf(' 5_') == -1 ? jsToNativeModes.IFRAME_NAV: jsToNativeModes.XHR_NO_PAYLOAD;
+ if (navigator.userAgent) {
+ bridgeMode = navigator.userAgent.indexOf(' 5_') == -1 ? jsToNativeModes.IFRAME_NAV: jsToNativeModes.XHR_NO_PAYLOAD;
+ } else {
+ bridgeMode = jsToNativeModes.IFRAME_NAV;
+ }
+ }
+
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.cordova && window.webkit.messageHandlers.cordova.postMessage) {
+ bridgeMode = jsToNativeModes.WK_WEBVIEW_BINDING;
}
var successCallback, failCallback, service, action, actionArgs, splitCommand;
@@ -943,6 +963,9 @@ function iOSExec() {
} catch (e) {}
}
+ // If actionArgs is not provided, default to an empty array
+ actionArgs = actionArgs || [];
+
// Register the callbacks and add the callbackId to the positional
// arguments if given.
if (successCallback || failCallback) {
@@ -959,63 +982,80 @@ function iOSExec() {
// effectively clone the command arguments in case they are mutated before
// the command is executed.
commandQueue.push(JSON.stringify(command));
-
- // If we're in the context of a stringByEvaluatingJavaScriptFromString call,
- // then the queue will be flushed when it returns; no need for a poke.
- // Also, if there is already a command in the queue, then we've already
- // poked the native side, so there is no reason to do so again.
- if (!isInContextOfEvalJs && commandQueue.length == 1) {
- switch (bridgeMode) {
- case jsToNativeModes.XHR_NO_PAYLOAD:
- case jsToNativeModes.XHR_WITH_PAYLOAD:
- case jsToNativeModes.XHR_OPTIONAL_PAYLOAD:
- // This prevents sending an XHR when there is already one being sent.
- // This should happen only in rare circumstances (refer to unit tests).
- if (execXhr && execXhr.readyState != 4) {
- execXhr = null;
- }
- // Re-using the XHR improves exec() performance by about 10%.
- execXhr = execXhr || new XMLHttpRequest();
- // Changing this to a GET will make the XHR reach the URIProtocol on 4.2.
- // For some reason it still doesn't work though...
- // Add a timestamp to the query param to prevent caching.
- execXhr.open('HEAD', "/!gap_exec?" + (+new Date()), true);
- if (!vcHeaderValue) {
- vcHeaderValue = /.*\((.*)\)/.exec(navigator.userAgent)[1];
- }
- execXhr.setRequestHeader('vc', vcHeaderValue);
- execXhr.setRequestHeader('rc', ++requestCount);
- if (shouldBundleCommandJson()) {
- execXhr.setRequestHeader('cmds', iOSExec.nativeFetchMessages());
- }
- execXhr.send(null);
- break;
- case jsToNativeModes.IFRAME_HASH_NO_PAYLOAD:
- case jsToNativeModes.IFRAME_HASH_WITH_PAYLOAD:
- execHashIframe = execHashIframe || createHashIframe();
- // Check if they've removed it from the DOM, and put it back if so.
- if (!execHashIframe.contentWindow) {
- execHashIframe = createHashIframe();
- }
- // The delegate method is called only when the hash changes, so toggle it back and forth.
- hashToggle = hashToggle ^ 3;
- var hashValue = '%0' + hashToggle;
- if (bridgeMode === jsToNativeModes.IFRAME_HASH_WITH_PAYLOAD) {
- hashValue += iOSExec.nativeFetchMessages();
- }
- execHashIframe.contentWindow.location.hash = hashValue;
- break;
- default:
- execIframe = execIframe || createExecIframe();
- // Check if they've removed it from the DOM, and put it back if so.
- if (!execIframe.contentWindow) {
- execIframe = createExecIframe();
+
+ if (bridgeMode === jsToNativeModes.WK_WEBVIEW_BINDING) {
+ window.webkit.messageHandlers.cordova.postMessage(command);
+ } else {
+ // If we're in the context of a stringByEvaluatingJavaScriptFromString call,
+ // then the queue will be flushed when it returns; no need for a poke.
+ // Also, if there is already a command in the queue, then we've already
+ // poked the native side, so there is no reason to do so again.
+ if (!isInContextOfEvalJs && commandQueue.length == 1) {
+ switch (bridgeMode) {
+ case jsToNativeModes.XHR_NO_PAYLOAD:
+ case jsToNativeModes.XHR_WITH_PAYLOAD:
+ case jsToNativeModes.XHR_OPTIONAL_PAYLOAD:
+ pokeNativeViaXhr();
+ break;
+ default: // iframe-based.
+ pokeNativeViaIframe();
}
- execIframe.src = "gap://ready";
}
}
}
+function pokeNativeViaXhr() {
+ // This prevents sending an XHR when there is already one being sent.
+ // This should happen only in rare circumstances (refer to unit tests).
+ if (execXhr && execXhr.readyState != 4) {
+ execXhr = null;
+ }
+ // Re-using the XHR improves exec() performance by about 10%.
+ execXhr = execXhr || new XMLHttpRequest();
+ // Changing this to a GET will make the XHR reach the URIProtocol on 4.2.
+ // For some reason it still doesn't work though...
+ // Add a timestamp to the query param to prevent caching.
+ execXhr.open('HEAD', "/!gap_exec?" + (+new Date()), true);
+ if (!vcHeaderValue) {
+ vcHeaderValue = /.*\((.*)\)/.exec(navigator.userAgent)[1];
+ }
+ execXhr.setRequestHeader('vc', vcHeaderValue);
+ execXhr.setRequestHeader('rc', ++requestCount);
+ if (shouldBundleCommandJson()) {
+ execXhr.setRequestHeader('cmds', iOSExec.nativeFetchMessages());
+ }
+ execXhr.send(null);
+}
+
+function pokeNativeViaIframe() {
+ // CB-5488 - Don't attempt to create iframe before document.body is available.
+ if (!document.body) {
+ setTimeout(pokeNativeViaIframe);
+ return;
+ }
+ if (bridgeMode === jsToNativeModes.IFRAME_HASH_NO_PAYLOAD || bridgeMode === jsToNativeModes.IFRAME_HASH_WITH_PAYLOAD) {
+ execHashIframe = execHashIframe || createHashIframe();
+ // Check if they've removed it from the DOM, and put it back if so.
+ if (!execHashIframe.contentWindow) {
+ execHashIframe = createHashIframe();
+ }
+ // The delegate method is called only when the hash changes, so toggle it back and forth.
+ hashToggle = hashToggle ^ 3;
+ var hashValue = '%0' + hashToggle;
+ if (bridgeMode === jsToNativeModes.IFRAME_HASH_WITH_PAYLOAD) {
+ hashValue += iOSExec.nativeFetchMessages();
+ }
+ execHashIframe.contentWindow.location.hash = hashValue;
+ } else {
+ execIframe = execIframe || createExecIframe();
+ // Check if they've removed it from the DOM, and put it back if so.
+ if (!execIframe.contentWindow) {
+ execIframe = createExecIframe();
+ }
+ execIframe.src = "gap://ready";
+ }
+}
+
iOSExec.jsToNativeModes = jsToNativeModes;
iOSExec.setJsToNativeBridgeMode = function(mode) {
@@ -1179,9 +1219,13 @@ modulemapper.clobbers('cordova/exec', 'Cordova.exec');
// Call the platform-specific initialization.
platform.bootstrap && platform.bootstrap();
-pluginloader.load(function() {
- channel.onPluginsReady.fire();
-});
+// Wrap in a setTimeout to support the use-case of having plugin JS appended to cordova.js.
+// The delay allows the attached modules to be defined before the plugin loader looks for them.
+setTimeout(function() {
+ pluginloader.load(function() {
+ channel.onPluginsReady.fire();
+ });
+}, 0);
/**
* Create all cordova objects once native side is ready.
@@ -1204,6 +1248,111 @@ channel.join(function() {
}, platformInitChannelsArray);
+});
+
+// file: src/common/init_b.js
+define("cordova/init_b", function(require, exports, module) {
+
+var channel = require('cordova/channel');
+var cordova = require('cordova');
+var platform = require('cordova/platform');
+
+var platformInitChannelsArray = [channel.onDOMContentLoaded, channel.onNativeReady];
+
+// setting exec
+cordova.exec = require('cordova/exec');
+
+function logUnfiredChannels(arr) {
+ for (var i = 0; i < arr.length; ++i) {
+ if (arr[i].state != 2) {
+ console.log('Channel not fired: ' + arr[i].type);
+ }
+ }
+}
+
+window.setTimeout(function() {
+ if (channel.onDeviceReady.state != 2) {
+ console.log('deviceready has not fired after 5 seconds.');
+ logUnfiredChannels(platformInitChannelsArray);
+ logUnfiredChannels(channel.deviceReadyChannelsArray);
+ }
+}, 5000);
+
+// Replace navigator before any modules are required(), to ensure it happens as soon as possible.
+// We replace it so that properties that can't be clobbered can instead be overridden.
+function replaceNavigator(origNavigator) {
+ var CordovaNavigator = function() {};
+ CordovaNavigator.prototype = origNavigator;
+ var newNavigator = new CordovaNavigator();
+ // This work-around really only applies to new APIs that are newer than Function.bind.
+ // Without it, APIs such as getGamepads() break.
+ if (CordovaNavigator.bind) {
+ for (var key in origNavigator) {
+ if (typeof origNavigator[key] == 'function') {
+ newNavigator[key] = origNavigator[key].bind(origNavigator);
+ }
+ }
+ }
+ return newNavigator;
+}
+if (window.navigator) {
+ window.navigator = replaceNavigator(window.navigator);
+}
+
+if (!window.console) {
+ window.console = {
+ log: function(){}
+ };
+}
+if (!window.console.warn) {
+ window.console.warn = function(msg) {
+ this.log("warn: " + msg);
+ };
+}
+
+// Register pause, resume and deviceready channels as events on document.
+channel.onPause = cordova.addDocumentEventHandler('pause');
+channel.onResume = cordova.addDocumentEventHandler('resume');
+channel.onDeviceReady = cordova.addStickyDocumentEventHandler('deviceready');
+
+// Listen for DOMContentLoaded and notify our channel subscribers.
+if (document.readyState == 'complete' || document.readyState == 'interactive') {
+ channel.onDOMContentLoaded.fire();
+} else {
+ document.addEventListener('DOMContentLoaded', function() {
+ channel.onDOMContentLoaded.fire();
+ }, false);
+}
+
+// _nativeReady is global variable that the native side can set
+// to signify that the native code is ready. It is a global since
+// it may be called before any cordova JS is ready.
+if (window._nativeReady) {
+ channel.onNativeReady.fire();
+}
+
+// Call the platform-specific initialization.
+platform.bootstrap && platform.bootstrap();
+
+/**
+ * Create all cordova objects once native side is ready.
+ */
+channel.join(function() {
+
+ platform.initialize && platform.initialize();
+
+ // Fire event to notify that all objects are created
+ channel.onCordovaReady.fire();
+
+ // Fire onDeviceReady event once page has fully loaded, all
+ // constructors have run and cordova info has been received from native
+ // side.
+ channel.join(function() {
+ require('cordova').fireDocumentEvent('deviceready');
+ }, channel.deviceReadyChannelsArray);
+
+}, platformInitChannelsArray);
+
});
// file: src/common/modulemapper.js
@@ -1327,43 +1476,51 @@ var modulemapper = require('cordova/modulemapper');
var urlutil = require('cordova/urlutil');
// Helper function to inject a