Related
In Cloud Functions, I have defined a function that makes somes updates using a batch that I commit. This commit is the return of the function. This function simply computes the number of likes of every post (likes and posts are two distinct collections in my Firestore database). Since the whole code is short and very simple to understand, I show it below.
The fact to like or unlike a post (to add or remove a like document from the likes collection) is done client-side by the app.
The fact to compute some statistics (number of likes per post for example) is down server-side in the following Cloud Function. (because if it was client side, it would be hackable, i. e. Bad statistics would be generated and saved + dealing with statistics doesn't concern the Android app directly and so it should definitely be computed server-side).
The important thing to note is: return batch.commit is the return of this Cloud Function.
exports.sortPostsByUsersPostsLikes = functions.https.onCall((data, context) => {
if(!context.auth) {
throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.');
}
const batch = admin.firestore().batch();
const posts = admin_firestore.collection('list_of_users_posts');
const likes = admin_firestore.collection('likes_of_users_posts');
const map_posts_id_with_number_of_likes = [];
likes.get().then(function(likes_docs) {
likes_docs.forEach(like_doc => {
if(!(like_doc.data().post in map_posts_id_with_number_of_likes)) {
map_posts_id_with_number_of_likes[like_doc.data().post] = 0;
}
map_posts_id_with_number_of_likes[like_doc.data().post] += 1;
});
return posts.get();
}).then(function(posts_docs) {
posts_docs.forEach(post_doc => {
if(post_doc.id in map_posts_id_with_number_of_likes) {
batch.update(post_doc.ref, "number_of_likes", map_posts_id_with_number_of_likes[post_doc.id]);
} else {
batch.update(post_doc.ref, "number_of_likes", 0);
}
});
return batch.commit();
}).catch(function(error) {
console.log("UNABLE TO SORT THE POSTS");
console.log(error);
throw new functions.https.HttpsError('unknown', 'An error occurred when trying to sort the posts.');
});
});
In my Android app, when the user, in the list of the posts, likes a post:
First, I add a like in the likes collection
When the like is successfully added in the likes collection in database, I refresh the list of the posts
When the list of the posts is shown (or refreshed), I call the above Cloud Function in order to re-compute the number of likes of the posts (soon, "of the shown posts").
When the number of likes of the posts is successfully recomputed, I show the posts (so the number of likes of each shown post would be correct).
Question
The problem is: at step 4., the number of likes of each shown post is NOT correct (sometimes it is, sometimes it is not). As if the Cloud Function didn't wait the batch commit ends. Is it a normal behavior? Is there any way to force the Cloud Function to wait for the batch's commit's success?
The code I use, in the Android app, to call the above Cloud Function and then, normally if it succeeds, to show the posts (so normally with the good number of likes, but it's not the case in practice) is:
FirebaseFunctions.getInstance()
.getHttpsCallable("sortPostsByUsersPostsLikes")
.call()
.continueWith(new Continuation<HttpsCallableResult, Void>() {
#Override
public Void then(#NonNull final Task<HttpsCallableResult> task) {
if(requireActivity().isDestroyed() || requireActivity().isFinishing()) {
return null;
}
if(!task.isSuccessful()) {
Exception e = task.getException();
if (e instanceof FirebaseFunctionsException) {
FirebaseFunctionsException ffe = (FirebaseFunctionsException) e;
if(ffe.getCode() == FirebaseFunctionsException.Code.UNKNOWN) {
miscellaneous.showErrorPopIn(requireActivity(), R.string.error_sortPostsByUsersPostsLikes);
}
}
return null;
}
postsDatabaseModel.getListOfPostsOfUser(the_posts_owner).get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
Returning the batch commit and adding a then doesn't work
I have tried the following but it doesn't work:
.then(function(posts_docs) {
posts_docs.forEach(post_doc => {
if(post_doc.id in map_posts_id_with_number_of_likes) {
batch.update(post_doc.ref, "number_of_likes", map_posts_id_with_number_of_likes[post_doc.id]);
} else {
batch.update(post_doc.ref, "number_of_likes", 0);
}
});
return batch.commit();
}).then(function() {
return true;
}).catch(function(error) {
You are correctly chaining the promises returned by the asynchronous methods but your don't return this entire chain. You should do as follows:
exports.sortPostsByUsersPostsLikes = functions.https.onCall((data, context) => {
if(!context.auth) {
throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.');
}
const batch = admin.firestore().batch();
const posts = admin_firestore.collection('list_of_users_posts');
const likes = admin_firestore.collection('likes_of_users_posts');
const map_posts_id_with_number_of_likes = [];
// SEE THE ADDITION OF RETURN BELOW
return likes.get().then(function(likes_docs) {
likes_docs.forEach(like_doc => {
if(!(like_doc.data().post in map_posts_id_with_number_of_likes)) {
map_posts_id_with_number_of_likes[like_doc.data().post] = 0;
}
map_posts_id_with_number_of_likes[like_doc.data().post] += 1;
});
return posts.get();
}).then(function(posts_docs) {
posts_docs.forEach(post_doc => {
if(post_doc.id in map_posts_id_with_number_of_likes) {
batch.update(post_doc.ref, "number_of_likes", map_posts_id_with_number_of_likes[post_doc.id]);
} else {
batch.update(post_doc.ref, "number_of_likes", 0);
}
});
return batch.commit();
}).catch(function(error) {
console.log("UNABLE TO SORT THE POSTS");
console.log(error);
throw new functions.https.HttpsError('unknown', 'An error occurred when trying to sort the posts.');
});
});
I would suggest you watch the 3 videos about "JavaScript Promises" from the Firebase video series (https://firebase.google.com/docs/functions/video-series/) which emphasize how important it is to return a Promise. Without that, the Cloud Function may terminate at anytime before all the asynchronous operatins are completed.
UPDATE FOLLOWING YOUR COMMENTS
If you want to log the fact that the Cloud Function was successful, you could do as follows:
exports.sortPostsByUsersPostsLikes = functions.https.onCall((data, context) => {
if(!context.auth) {
throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.');
}
//...
return likes.get().then(function(likes_docs) {
//...
return posts.get();
}).then(function(posts_docs) {
//...
return batch.commit();
}).then(function() {
console.log("SUCCESS")
return null;
})
.catch(function(error) {
//...
});
});
I am trying to get my friend name and ids with Graph API v2.0, but data returns empty:
{
"data": [
]
}
When I was using v1.0, everything was OK with the following request:
FBRequest* friendsRequest = [FBRequest requestForMyFriends];
[friendsRequest startWithCompletionHandler: ^(FBRequestConnection *connection,
NSDictionary* result,
NSError *error) {
NSArray* friends = [result objectForKey:#"data"];
NSLog(#"Found: %i friends", friends.count);
for (NSDictionary<FBGraphUser>* friend in friends) {
NSLog(#"I have a friend named %# with id %#", friend.name, friend.id);
}
}];
But now I cannot get friends!
In v2.0 of the Graph API, calling /me/friends returns the person's friends who also use the app.
In addition, in v2.0, you must request the user_friends permission from each user. user_friends is no longer included by default in every login. Each user must grant the user_friends permission in order to appear in the response to /me/friends. See the Facebook upgrade guide for more detailed information, or review the summary below.
If you want to access a list of non-app-using friends, there are two options:
If you want to let your people tag their friends in stories that they publish to Facebook using your App, you can use the /me/taggable_friends API. Use of this endpoint requires review by Facebook and should only be used for the case where you're rendering a list of friends in order to let the user tag them in a post.
If your App is a Game AND your Game supports Facebook Canvas, you can use the /me/invitable_friends endpoint in order to render a custom invite dialog, then pass the tokens returned by this API to the standard Requests Dialog.
In other cases, apps are no longer able to retrieve the full list of a user's friends (only those friends who have specifically authorized your app using the user_friends permission). This has been confirmed by Facebook as 'by design'.
For apps wanting allow people to invite friends to use an app, you can still use the Send Dialog on Web or the new Message Dialog on iOS and Android.
UPDATE: Facebook have published an FAQ on these changes here: https://developers.facebook.com/docs/apps/faq which explain all the options available to developers in order to invite friends etc.
Although Simon Cross's answer is accepted and correct, I thought I would beef it up a bit with an example (Android) of what needs to be done. I'll keep it as general as I can and focus on just the question. Personally I wound up storing things in a database so the loading was smooth, but that requires a CursorAdapter and ContentProvider which is a bit out of scope here.
I came here myself and then thought, now what?!
The Issue
Just like user3594351, I was noticing the friend data was blank. I found this out by using the FriendPickerFragment. What worked three months ago, no longer works. Even Facebook's examples broke. So my issue was 'How Do I create FriendPickerFragment by hand?
What Did Not Work
Option #1 from Simon Cross was not strong enough to invite friends to the app. Simon Cross also recommended the Requests Dialog, but that would only allow five requests at a time. The requests dialog also showed the same friends during any given Facebook logged in session. Not useful.
What Worked (Summary)
Option #2 with some hard work. You must make sure you fulfill Facebook's new rules: 1.) You're a game 2.) You have a Canvas app (Web Presence) 3.) Your app is registered with Facebook. It is all done on the Facebook developer website under Settings.
To emulate the friend picker by hand inside my app I did the following:
Create a tab activity that shows two fragments. Each fragment shows a list. One fragment for available friend (/me/friends) and another for invitable friends (/me/invitable_friends). Use the same fragment code to render both tabs.
Create an AsyncTask that will get the friend data from Facebook. Once that data is loaded, toss it to the adapter which will render the values to the screen.
Details
The AsynchTask
private class DownloadFacebookFriendsTask extends AsyncTask<FacebookFriend.Type, Boolean, Boolean> {
private final String TAG = DownloadFacebookFriendsTask.class.getSimpleName();
GraphObject graphObject;
ArrayList<FacebookFriend> myList = new ArrayList<FacebookFriend>();
#Override
protected Boolean doInBackground(FacebookFriend.Type... pickType) {
//
// Determine Type
//
String facebookRequest;
if (pickType[0] == FacebookFriend.Type.AVAILABLE) {
facebookRequest = "/me/friends";
} else {
facebookRequest = "/me/invitable_friends";
}
//
// Launch Facebook request and WAIT.
//
new Request(
Session.getActiveSession(),
facebookRequest,
null,
HttpMethod.GET,
new Request.Callback() {
public void onCompleted(Response response) {
FacebookRequestError error = response.getError();
if (error != null && response != null) {
Log.e(TAG, error.toString());
} else {
graphObject = response.getGraphObject();
}
}
}
).executeAndWait();
//
// Process Facebook response
//
//
if (graphObject == null) {
return false;
}
int numberOfRecords = 0;
JSONArray dataArray = (JSONArray) graphObject.getProperty("data");
if (dataArray.length() > 0) {
// Ensure the user has at least one friend ...
for (int i = 0; i < dataArray.length(); i++) {
JSONObject jsonObject = dataArray.optJSONObject(i);
FacebookFriend facebookFriend = new FacebookFriend(jsonObject, pickType[0]);
if (facebookFriend.isValid()) {
numberOfRecords++;
myList.add(facebookFriend);
}
}
}
// Make sure there are records to process
if (numberOfRecords > 0){
return true;
} else {
return false;
}
}
#Override
protected void onProgressUpdate(Boolean... booleans) {
// No need to update this, wait until the whole thread finishes.
}
#Override
protected void onPostExecute(Boolean result) {
if (result) {
/*
User the array "myList" to create the adapter which will control showing items in the list.
*/
} else {
Log.i(TAG, "Facebook Thread unable to Get/Parse friend data. Type = " + pickType);
}
}
}
The FacebookFriend class I created
public class FacebookFriend {
String facebookId;
String name;
String pictureUrl;
boolean invitable;
boolean available;
boolean isValid;
public enum Type {AVAILABLE, INVITABLE};
public FacebookFriend(JSONObject jsonObject, Type type) {
//
//Parse the Facebook Data from the JSON object.
//
try {
if (type == Type.INVITABLE) {
//parse /me/invitable_friend
this.facebookId = jsonObject.getString("id");
this.name = jsonObject.getString("name");
// Handle the picture data.
JSONObject pictureJsonObject = jsonObject.getJSONObject("picture").getJSONObject("data");
boolean isSilhouette = pictureJsonObject.getBoolean("is_silhouette");
if (!isSilhouette) {
this.pictureUrl = pictureJsonObject.getString("url");
} else {
this.pictureUrl = "";
}
this.invitable = true;
} else {
// Parse /me/friends
this.facebookId = jsonObject.getString("id");
this.name = jsonObject.getString("name");
this.available = true;
this.pictureUrl = "";
}
isValid = true;
} catch (JSONException e) {
Log.w("#", "Warnings - unable to process Facebook JSON: " + e.getLocalizedMessage());
}
}
}
Facebook has revised their policies now. You can’t get the whole friendlist anyway if your app does not have a Canvas implementation and if your app is not a game. Of course there’s also taggable_friends, but that one is for tagging only.
You will be able to pull the list of friends who have authorised the app only.
The apps that are using Graph API 1.0 will be working till April 30th, 2015 and after that it will be deprecated.
See the following to get more details on this:
User Friends
Facebook Application Development FAQ
In Swift 4.2 and Xcode 10.1:
If you want to get the friends list from Facebook, you need to submit your app for review in Facebook. See some of the Login Permissions:
Login Permissions
Here are the two steps:
1) First your app status is must be in Live
2) Get required permissions form Facebook.
1) Enable our app status live:
Go to the apps page and select your app
https://developers.facebook.com/apps/
Select status in the top right in Dashboard.
Submit privacy policy URL
Select category
Now our app is in Live status.
One step is completed.
2) Submit our app for review:
First send required requests.
Example: user_friends, user_videos, user_posts, etc.
Second, go to the Current Request page
Example: user_events
Submit all details
Like this submit for all requests (user_friends , user_events, user_videos, user_posts, etc.).
Finally submit your app for review.
If your review is accepted from Facebook's side, you are now eligible to read contacts, etc.
As Simon mentioned, this is not possible in the new Facebook API. Pure technically speaking you can do it via browser automation.
this is against Facebook policy, so depending on the country where you live, this may not be legal
you'll have to use your credentials / ask user for credentials and possibly store them (storing passwords even symmetrically encrypted is not a good idea)
when Facebook changes their API, you'll have to update the browser automation code as well (if you can't force updates of your application, you should put browser automation piece out as a webservice)
this is bypassing the OAuth concept
on the other hand, my feeling is that I'm owning my data including the list of my friends and Facebook shouldn't restrict me from accessing those via the API
Sample implementation using WatiN:
class FacebookUser
{
public string Name { get; set; }
public long Id { get; set; }
}
public IList<FacebookUser> GetFacebookFriends(string email, string password, int? maxTimeoutInMilliseconds)
{
var users = new List<FacebookUser>();
Settings.Instance.MakeNewIeInstanceVisible = false;
using (var browser = new IE("https://www.facebook.com"))
{
try
{
browser.TextField(Find.ByName("email")).Value = email;
browser.TextField(Find.ByName("pass")).Value = password;
browser.Form(Find.ById("login_form")).Submit();
browser.WaitForComplete();
}
catch (ElementNotFoundException)
{
// We're already logged in
}
browser.GoTo("https://www.facebook.com/friends");
var watch = new Stopwatch();
watch.Start();
Link previousLastLink = null;
while (maxTimeoutInMilliseconds.HasValue && watch.Elapsed.TotalMilliseconds < maxTimeoutInMilliseconds.Value)
{
var lastLink = browser.Links.Where(l => l.GetAttributeValue("data-hovercard") != null
&& l.GetAttributeValue("data-hovercard").Contains("user.php")
&& l.Text != null
).LastOrDefault();
if (lastLink == null || previousLastLink == lastLink)
{
break;
}
var ieElement = lastLink.NativeElement as IEElement;
if (ieElement != null)
{
var htmlElement = ieElement.AsHtmlElement;
htmlElement.scrollIntoView();
browser.WaitForComplete();
}
previousLastLink = lastLink;
}
var links = browser.Links.Where(l => l.GetAttributeValue("data-hovercard") != null
&& l.GetAttributeValue("data-hovercard").Contains("user.php")
&& l.Text != null
).ToList();
var idRegex = new Regex("id=(?<id>([0-9]+))");
foreach (var link in links)
{
string hovercard = link.GetAttributeValue("data-hovercard");
var match = idRegex.Match(hovercard);
long id = 0;
if (match.Success)
{
id = long.Parse(match.Groups["id"].Value);
}
users.Add(new FacebookUser
{
Name = link.Text,
Id = id
});
}
}
return users;
}
Prototype with implementation of this approach (using C#/WatiN) see https://github.com/svejdo1/ShadowApi. It is also allowing dynamic update of Facebook connector that is retrieving a list of your contacts.
Try /me/taggable_friends?limit=5000 using your JavaScript code
Or
try the Graph API:
https://graph.facebook.com/v2.3/user_id_here/taggable_friends?access_token=
If you are still struggling with this issue on a development mode.
Follow the same process as mentioned below:
create a test app of your main app,
create test users, automatically install app for test users and assign them 'user_friend' permission.
Add your test users as a friend with each other.
I followed the same process after going through alot of research and finally it worked.
In the Facebook SDK Graph API v2.0 or above, you must request the user_friends permission from each user in the time of Facebook login since user_friends is no longer included by default in every login; we have to add that.
Each user must grant the user_friends permission in order to appear in the response to /me/friends.
let fbLoginManager : FBSDKLoginManager = FBSDKLoginManager()
fbLoginManager.loginBehavior = FBSDKLoginBehavior.web
fbLoginManager.logIn(withReadPermissions: ["email","user_friends","public_profile"], from: self) { (result, error) in
if (error == nil) {
let fbloginresult : FBSDKLoginManagerLoginResult = result!
if fbloginresult.grantedPermissions != nil {
if (fbloginresult.grantedPermissions.contains("email")) {
// Do the stuff
}
else {
}
}
else {
}
}
}
So at the time of Facebook login, it prompts with a screen which contain all the permissions:
If the user presses the Continue button, the permissions will be set. When you access the friends list using Graph API, your friends who logged into the application as above will be listed
if ((FBSDKAccessToken.current()) != nil) {
FBSDKGraphRequest(graphPath: "/me/friends", parameters: ["fields" : "id,name"]).start(completionHandler: { (connection, result, error) -> Void in
if (error == nil) {
print(result!)
}
})
}
The output will contain the users who granted the user_friends permission at the time of login to your application through Facebook.
{
data = (
{
id = xxxxxxxxxx;
name = "xxxxxxxx";
}
);
paging = {
cursors = {
after = xxxxxx;
before = xxxxxxx;
};
};
summary = {
"total_count" = 8;
};
}
I am creating my first app engine app and having problems with authenticating the users.
I have followed the https://cloud.google.com/appengine/docs/python/endpoints/consume_android#making_authenticated_calls - seems a bit magical, that I just "setAccountName" and it is supposed to work, but w/e, I guess that it should load the app scopes from Android Audience and then just check if the account name I passed has actually logged into the device.
The API call works, passes the authentication, but sadly - the "endpoints.get_current_user()" function on the backend returns None.
So I kept digging, but I can't seem to find anything on the topic. Best thing I have found is http://blog.notdot.net/2010/05/Authenticating-against-App-Engine-from-an-Android-app - but that's an article from 6 years ago and the author uses HTTP client and nothing related to endpoints libs.
All I can think of would be to follow some "non-endpoints" way of adding "login with Google" to my app and then try to pass the credentials I would get to my API builder, but that just feels wrong, like there should be an easier way to do that.
So, am I missing some step, that was not mentioned in https://cloud.google.com/appengine/docs/python/endpoints/consume_android#making_authenticated_calls ?
Actual code (slightly simplified) below:
Backend:
auth_api = endpoints.api(
name='auth_api',
version='v1.0',
auth_level=endpoints.AUTH_LEVEL.REQUIRED,
allowed_client_ids=[
ANDROID_CLIENT_ID,
WEB_CLIENT_ID,
endpoints.API_EXPLORER_CLIENT_ID,
],
audiences=[
WEB_CLIENT_ID,
endpoints.API_EXPLORER_CLIENT_ID,
],
scopes=[
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email',
],
)
#auth_api.api_class(resource_name='rating')
class RatingHandler(remote.Service):
#endpoints.method(
message_types.VoidMessage,
RatingsMessage,
path='rating/getRatings',
http_method='GET',
)
def getRatings(self, request):
rating_query = Rating.query(
ancestor=ndb.Key(
Account,
endpoints.get_current_user().user_id(), // ERROR! endpoints.get_current_user() is None
)
).order(-Rating.rating)
Client:
// Somewhere these lines exist
if (credential == null || credential.getSelectedAccountName() == null) {
startActivityForResult(
AuthUtils.getCredentials(getActivity()).newChooseAccountIntent(),
AuthUtils.REQUEST_ACCOUNT_PICKER
);
} else {
LoadRatings();
}
#Override
public void onActivityResult(
int requestCode,
int resultCode,
Intent data
) {
super.onActivityResult(requestCode, resultCode, data);
if (data != null && data.getExtras() != null) {
String accountName =
data.getExtras().getString(
AccountManager.KEY_ACCOUNT_NAME
);
if (accountName != null) {
credential = GoogleAccountCredential.usingAudience(
getApplicationContext(),
"server:client_id:" + Constants.ANDROID_AUDIENCE
);
credential.setSelectedAccountName(accountName);
LoadRatings();
}
}
}
public void LoadRatings() {
// AuthApi was auto-generated by Google App Engine based on my Backend
AuthApi.Builder api = new AuthApi.Builder(
AndroidHttp.newCompatibleTransport(),
new AndroidJsonFactory(),
credential
).setApplicationName(getPackageName());
AuthApi service = api.build();
try {
ApiMessagesRatingRatedBeerListMessage result = service.rating().getRatings().
// some stuff done with result, but the Exception is thrown in line above
OK, I figured it out. When I removed "scopes" from API declaration, it works. I'm not sure how I am going to access user's email / profile yet, but that's at least a step forward.
This issue has been actually brought up before - How to add more scopes in GoogleCloud Endpoints - sadly, without any answers
You won't have a User object (Entity that is) on the backend created for you. You have to do that yourself. For example:
#Entity
public class AppEngineUser {
#Id
private String email;
private User user;
private AppEngineUser() {}
public AppEngineUser(User user) {
this.user = user;
this.email = user.getEmail();
}
public User getUser() {
return user;
}
public Key<AppEngineUser> getKey() {
return Key.create(AppEngineUser.class, email);
}
}
When you create an API method and specify a User object like this:
#ApiMethod(name = "insertRecord", path = "insert_record", httpMethod = HttpMethod.POST)
public Record insertRecord(User user, Record record)
// check if google user is authenticated
throws UnauthorizedException {
if (user == null) {
throw new UnauthorizedException("Authorization required");
}
// user is authenticated... do some stuff!
}
The User object is an injected type. It is actually: com.google.appengine.api.users.User. See "injected types" on https://cloud.google.com/appengine/docs/java/endpoints/paramreturn_types.
What this means is that GAE injects the Google user object if the user provided the correct credentials to their Google+ account. If they did not, then User will be null. If it is, you can throw an UnauthorizedException the way the above method does.
You can now get things like the User's gmail address among other things if the user object is not null. From there, you must store those values in your own custom entity, such as AppEngineUser and save it to the datastore. Then you can do other things with it later, such as load it, check if the user is registered and whatever else all by yourself.
Hope that helps!
I am using couchbase mobile for an application and I want to use facebook for authentication. As per documentation, couchbase offers it's own implementation for authentication, the only required thing would be the token which I retrieve from the android facebook login flow.
The code for Synchronize class looks something like this:
public class Synchronize {
public Replication pullReplication;
public Replication pushReplication;
public static class Builder {
public Replication pullReplication;
public Replication pushReplication;
public Builder(Database database, String url, Boolean continuousPull) {
if (pullReplication == null && pushReplication == null) {
URL syncUrl;
try {
syncUrl = new URL(url);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
pullReplication = database.createPullReplication(syncUrl);
pullReplication.setContinuous(true);
pushReplication = database.createPushReplication(syncUrl);
pushReplication.setContinuous(true);
}
}
public Builder facebookAuth(String token) {
if (!TextUtils.isEmpty(token)) {
Authenticator facebookAuthenticator = AuthenticatorFactory.createFacebookAuthenticator(token);
pullReplication.setAuthenticator(facebookAuthenticator);
pushReplication.setAuthenticator(facebookAuthenticator);
}
return this;
}
public Builder basicAuth(String username, String password) {
Authenticator basicAuthenticator = AuthenticatorFactory.createBasicAuthenticator(username, password);
pullReplication.setAuthenticator(basicAuthenticator);
pushReplication.setAuthenticator(basicAuthenticator);
return this;
}
public Builder addChangeListener(Replication.ChangeListener changeListener) {
pullReplication.addChangeListener(changeListener);
pushReplication.addChangeListener(changeListener);
return this;
}
public Synchronize build() {
return new Synchronize(this);
}
}
private Synchronize(Builder builder) {
pullReplication = builder.pullReplication;
pushReplication = builder.pushReplication;
}
public void start() {
pullReplication.start();
pushReplication.start();
}
public void destroyReplications() {
if (pullReplication != null && pushReplication != null) {
pullReplication.stop();
pushReplication.stop();
pullReplication.deleteCookie("SyncGatewaySession");
pushReplication.deleteCookie("SyncGatewaySession");
pullReplication = null;
pushReplication = null;
}
}
}
And I use it like this:
...
public void startReplicationSync(String facebookAccessToken) {
if (sync != null) {
sync.destroyReplications();
}
final String url = BuildConfig.URL_HOST + ":" + BuildConfig.URL_PORT + "/" + DATABASE_NAME;
sync = new Synchronize.Builder(databaseManager.getDatabase(), url, true)
.facebookAuth(facebookAccessToken)
.addChangeListener(getReplicationChangeListener())
.build();
sync.start();
}
...
My sync gateway json config file:
{
"interface":":4984",
"adminInterface":":4985",
"log":["REST"],
"facebook":{
"register" : true
},
"databases":{
"sync_gateway":{
"server":"http://localhost:8091",
"bucket":"sync_gateway",
"users": {
"GUEST": {"disabled": false}
},
"sync":`function(doc) {channel(doc.channels);}`
}
}
}
I also tried with "GUEST": {"disabled": true}, no luck
My problem is that if I do this
pullReplication.setAuthenticator(facebookAuthenticator);
pushReplication.setAuthenticator(facebookAuthenticator);
Nothing will ever get replicated/pulled from the server. However if I don't set an authenticator, everything is pulled. Is it something I am doing wrong? I really need to use the authenticator in order to prevent some documents to not being replicated for non-authenticated users.
Note! The token is good, as if I am looking in the users section of sync gateway admin, I can see the right profile id of the logged in user token I passed to the couchbase facebook authenticator.
In the Sync Gateway config you provided, the Sync Function is function(doc, oldDoc) {channel(doc.channels);} which means that if the document processed by Sync Gateway contains a string(s) under the channels field, the document will be mapped to this/these channel(s). Let's assume the following config file:
{
"log": ["CRUD"],
"databases": {
"db": {
"server": "walrus:",
"users": {
"GUEST": {"disabled": false, "admin_channels": ["*"]}
},
"sync": `
function sync(doc, oldDoc) {
channel(doc.channels);
}
`
}
}
}
If the channels field doesn't exist then the document will be mapped to a channel called undefined. But the GUEST account has access to the * channel (a placeholder to represent all channels). So, all unauthenticated replications will pull all documents. Let's now introduce the facebook login field in the config file. This time, replications authenticated with a facebook token represent a new user which has only access to the ! channel by default (watch this screencast to understand the ! channel, a.k.a the public channel https://www.youtube.com/watch?v=DKmb5mj9pMI). To give a user access to other channels, you must use the access API call in the Sync Function (read more about all Sync Function API calls here).
In the case of facebook authentication, the user's facebook ID is used to represent the user name. Supposing that the document has a property holding the user's facebook ID (user_id: FACEBOOK_ID), you can map the document to a channel and give the user access to it. The new Sync Function would look like this:
function(doc, oldDoc) {
channel(doc._id);
access(doc.user_id, doc._id);
}
You can retrieve the user's facebook ID with the Facebook Android SDK and save on a document field.
I create a room and it gets successfully made. And my onRoomCreated method gets called...
#Override
public void onRoomCreated(int statusCode, Room room) {
mRoomId = room.getRoomId();
Intent i = Games.RealTimeMultiplayer.getWaitingRoomIntent(gApiClient, room, 2);
startActivityForResult(i, RC_WAITING_ROOM);
}
Then in my onActivityResult...
Room r = data.getExtras().getParcelable(Multiplayer.EXTRA_ROOM);
ArrayList<String> invitees = new ArrayList<String>();
for (Participant p : r.getParticipants()) {
invitees.add(p.getPlayer().getPlayerId()); //<---NULL POINTER!
}
I get that null pointer. Why?
EDIT: The android docs say this about the getPlayer() method...
Returns the Player that this participant represents. Note that this may be null if the identity of the player is unknown. This occurs in automatching scenarios where some players are not permitted to see the real identity of others.
That is why I am getting null, because my room is through auto-matching.
Now the question is. How can I create a turnbasedgame using only participant IDs? Not Player IDs
Now that I see what you are asking more clearly (my fault, not yours), here is how I do it:
(for clarification I use LibGDX, so may be some interface stuff you don't need, and I am still using GamesClient not the new API methods, but is for all intents the same)
First, the final call I look to start my game is onRoomConnected
#Override
public void onRoomConnected(int statusCode, Room room) {
//dLog("onRoomConnected");
mRoomCurrent = room;
mParticipants = room.getParticipants();
mMyID = room.getParticipantId(aHelper.getGamesClient().getCurrentPlayerId());
//dLog("The id is " + mMyID);
try {
bWaitRoomDismissedFromCode = true;
finishActivity(RC_WAITING_ROOM);
} catch (Exception e) {
//dLog("would have errored out in waiting room");
}
//tell the Game the room is connected
if (statusCode == GamesClient.STATUS_OK) {
theGameInterface.onRoomConnected(room.getParticipantIds(), mMyID, room.getCreationTimestamp() );
} else {
leaveRoom();
}
}
So, now have all the participantIDs.. now in my Game code (where I sent that List of Ids), I sort the list of IDs so that in determining Player order, it is the same methodology for all Players. First I build my opponents.
private void buildOpponents() {
// this creates a new opponent with a View on the Stage()
//sort the participants the same for all players
sortParticipantIDs();
for (String s : mParticipantIds) {
if(s.contains(mMyID) || mMyID.contains(s)) continue;
newOpponentWindow ow = new newOpponentWindow(s, MyAssetManager.getMySkin(), getStage());
Opponent o = new Opponent(this, s);
mapOpponents.put(s, o);
o.setWindow(ow);
getStage().addActor(ow);
}
setOpponentWindowPositions();
}
Then after some more setup I start Play and my first Time through, I have chosen that whoever is the top ID gets the honor of starting (I find this randomizes play enough, without having to do another method.. .but you can let the top ID do another method, and send that out to the other Players) Note this checks over my Opponents to determine Starting Player if someone leaves the room later in the game as well.
private boolean determineIfStartingBidder() {
Collections.sort(mParticipantIds);
// now look thru list
// if the number is mine then return true
// if the number is not mine.. and opponent is not Out of Game or Disconnected.. then return false
for (String s : mParticipantIds) {
if(s.contains(mMyID) || mMyID.contains(s)){
return true;
}
if(mapOpponents.get(s).getCurrentState() == currentState.DISCONNECTED || mapOpponents.get(s).getCurrentState() == currentState.OUTOFGAME ||
mapOpponents.get(s).getCurrentState() == currentState.LOSTGAME) {
continue;
}
return false;
}
return false;
}
Then in your game logic, just go through your ParticipantID list in whatever manner makes sense to pass the baton around! This works well, since all the calls for passing messages require the ParticipantID, and are there for easy grab n go!
Prior Answer Below ------------------------------------------------
try
data.getParcelableExtra(Multiplayer.EXTRA_ROOM);
no need for the getExtras