/** * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. * * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, * copy, modify, and distribute this software in source code or binary form for use * in connection with the web services and APIs provided by Facebook. * * As with any software that integrates with the Facebook platform, your use of * this software is subject to the Facebook Developer Principles and Policies * [http://developers.facebook.com/policy/]. This copyright notice shall be * included in all copies or substantial portions of the software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.facebook; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import com.facebook.internal.Utility; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * This class manages Facebook test users. */ public class TestUserManager { private static final String LOG_TAG = "TestUserManager"; private enum Mode { PRIVATE, SHARED, } private String testApplicationSecret; private String testApplicationId; private Map<String, JSONObject> appTestAccounts; /** * Constructor. * * @param testApplicationSecret The application secret. * @param testApplicationId The application id. */ public TestUserManager(String testApplicationSecret, String testApplicationId) { if (Utility.isNullOrEmpty(testApplicationId) || Utility.isNullOrEmpty(testApplicationSecret)) { throw new FacebookException("Must provide app ID and secret"); } this.testApplicationSecret = testApplicationSecret; this.testApplicationId = testApplicationId; } /** * Gets the access token of the private test user for the application with the requested * permissions. * * @param permissions The requested permissions. * @return The access token of the private test user for the application. */ public AccessToken getAccessTokenForPrivateUser(List<String> permissions) { return getAccessTokenForUser(permissions, Mode.PRIVATE, null); } /** * Gets the access token of the shared test user for the application with the requested * permissions. * * @param permissions The requested permissions. * @return The access token of the shared test user for the application. */ public AccessToken getAccessTokenForSharedUser(List<String> permissions) { return getAccessTokenForSharedUser(permissions, null); } /** * Gets the access token of the shared test user with the tag for the application with the * requested permissions. * * @param permissions The requested permissions. * @param uniqueUserTag The user tag. * @return The requested shared user. */ public AccessToken getAccessTokenForSharedUser( List<String> permissions, String uniqueUserTag) { return getAccessTokenForUser(permissions, Mode.SHARED, uniqueUserTag); } /** * Getter for the test application id. * * @return The test application id. */ public synchronized String getTestApplicationId() { return testApplicationId; } /** * Getter for the test application secret. * * @return The test application secret. */ public synchronized String getTestApplicationSecret() { return testApplicationSecret; } private AccessToken getAccessTokenForUser( List<String> permissions, Mode mode, String uniqueUserTag) { retrieveTestAccountsForAppIfNeeded(); if (Utility.isNullOrEmpty(permissions)) { permissions = Arrays.asList("email", "publish_actions"); } JSONObject testAccount = null; if (mode == Mode.PRIVATE) { testAccount = createTestAccount(permissions, mode, uniqueUserTag); } else { testAccount = findOrCreateSharedTestAccount(permissions, mode, uniqueUserTag); } return new AccessToken( testAccount.optString("access_token"), testApplicationId, testAccount.optString("id"), permissions, null, AccessTokenSource.TEST_USER, null, null); } private synchronized void retrieveTestAccountsForAppIfNeeded() { if (appTestAccounts != null) { return; } appTestAccounts = new HashMap<String, JSONObject>(); // The data we need is split across two different graph API queries. We construct two // queries, submit them together (the second one depends on the first one), then // cross-reference the results. GraphRequest.setDefaultBatchApplicationId(testApplicationId); Bundle parameters = new Bundle(); parameters.putString("access_token", getAppAccessToken()); GraphRequest requestTestUsers = new GraphRequest(null, "app/accounts/test-users", parameters, null); requestTestUsers.setBatchEntryName("testUsers"); requestTestUsers.setBatchEntryOmitResultOnSuccess(false); Bundle testUserNamesParam = new Bundle(); testUserNamesParam.putString("access_token", getAppAccessToken()); testUserNamesParam.putString("ids", "{result=testUsers:$.data.*.id}"); testUserNamesParam.putString("fields", "name"); GraphRequest requestTestUserNames = new GraphRequest(null, "", testUserNamesParam, null); requestTestUserNames.setBatchEntryDependsOn("testUsers"); List<GraphResponse> responses = GraphRequest.executeBatchAndWait(requestTestUsers, requestTestUserNames); if (responses == null || responses.size() != 2) { throw new FacebookException("Unexpected number of results from TestUsers batch query"); } JSONObject testAccountsResponse = responses.get(0).getJSONObject(); JSONArray testAccounts = testAccountsResponse.optJSONArray("data"); // Response should contain a map of test accounts: { id's => { user } } JSONObject userAccountsMap = responses.get(1).getJSONObject(); populateTestAccounts(testAccounts, userAccountsMap); } private synchronized void populateTestAccounts(JSONArray testAccounts, JSONObject userAccountsMap) { for (int i = 0; i < testAccounts.length(); ++i) { JSONObject testAccount = testAccounts.optJSONObject(i); JSONObject testUser = userAccountsMap.optJSONObject(testAccount.optString("id")); try { testAccount.put("name", testUser.optString("name")); } catch (JSONException e) { Log.e(LOG_TAG, "Could not set name", e); } storeTestAccount(testAccount); } } private synchronized void storeTestAccount(JSONObject testAccount) { appTestAccounts.put(testAccount.optString("id"), testAccount); } private synchronized JSONObject findTestAccountMatchingIdentifier(String identifier) { for (JSONObject testAccount : appTestAccounts.values()) { if (testAccount.optString("name").contains(identifier)) { return testAccount; } } return null; } final String getAppAccessToken() { return testApplicationId + "|" + testApplicationSecret; } private JSONObject findOrCreateSharedTestAccount(List<String> permissions, Mode mode, String uniqueUserTag) { JSONObject testAccount = findTestAccountMatchingIdentifier( getSharedTestAccountIdentifier(permissions, uniqueUserTag)); if (testAccount != null) { return testAccount; } else { return createTestAccount(permissions, mode, uniqueUserTag); } } private String getSharedTestAccountIdentifier(List<String> permissions, String uniqueUserTag) { // We use long even though hashes are ints to avoid sign issues. long permissionsHash = getPermissionsString(permissions).hashCode() & 0xffffffffL; long userTagHash = (uniqueUserTag != null) ? uniqueUserTag.hashCode() & 0xffffffffL : 0; long combinedHash = permissionsHash ^ userTagHash; return validNameStringFromInteger(combinedHash); } private String validNameStringFromInteger(long i) { String s = Long.toString(i); StringBuilder result = new StringBuilder("Perm"); // We know each character is a digit. Convert it into a letter 'a'-'j'. Avoid repeated // characters that might make Facebook reject the name by converting every other repeated // character into one 10 higher ('k'-'t'). char lastChar = 0; for (char c : s.toCharArray()) { if (c == lastChar) { c += 10; } result.append((char) (c + 'a' - '0')); lastChar = c; } return result.toString(); } private JSONObject createTestAccount( List<String> permissions, Mode mode, String uniqueUserTag) { Bundle parameters = new Bundle(); parameters.putString("installed", "true"); parameters.putString("permissions", getPermissionsString(permissions)); parameters.putString("access_token", getAppAccessToken()); // If we're in shared mode, we want to rename this user to encode its permissions, so we can // find it later. If we're in private mode, don't bother renaming it since we're just going // to delete it at the end. if (mode == Mode.SHARED) { parameters.putString("name", String.format("Shared %s Testuser", getSharedTestAccountIdentifier(permissions, uniqueUserTag))); } String graphPath = String.format("%s/accounts/test-users", testApplicationId); GraphRequest createUserRequest = new GraphRequest(null, graphPath, parameters, HttpMethod.POST); GraphResponse response = createUserRequest.executeAndWait(); FacebookRequestError error = response.getError(); JSONObject testAccount = response.getJSONObject(); if (error != null) { return null; } else { assert testAccount != null; // If we are in shared mode, store this new account in the dictionary so we can re-use // it later. if (mode == Mode.SHARED) { // Remember the new name we gave it, since we didn't get it back in the results of // the create request. try { testAccount.put("name", parameters.getString("name")); } catch (JSONException e) { Log.e(LOG_TAG, "Could not set name", e); } storeTestAccount(testAccount); } return testAccount; } } private String getPermissionsString(List<String> permissions) { return TextUtils.join(",", permissions); } }