Base Configuration

This commit is contained in:
2018-03-25 13:13:01 +02:00
commit bcac6a3b85
334 changed files with 56965 additions and 0 deletions

View File

@ -0,0 +1,52 @@
apply plugin: 'com.android.library'
apply plugin: 'com.github.kt3k.coveralls'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.1'
}
}
android {
compileSdkVersion 26
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName project.version
consumerProguardFiles 'release-proguard.pro'
}
packagingOptions {
exclude '**/BuildConfig.class'
}
lintOptions {
abortOnError false
}
buildTypes {
debug {
testCoverageEnabled = true
}
}
}
//ext {
// okhttpVersion = '3.9.1'
//}
dependencies {
api "com.android.support:support-annotations:$supportLibVersion"
api 'com.parse.bolts:bolts-tasks:1.4.0'
api "com.squareup.okhttp3:okhttp:$okhttpVersion"
testImplementation 'org.robolectric:robolectric:3.3.2'
testImplementation 'org.skyscreamer:jsonassert:1.5.0'
testImplementation 'org.mockito:mockito-core:1.10.19'
testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion"
}

View File

@ -0,0 +1,7 @@
-keepnames class com.parse.** { *; }
# Required for Parse
-keepattributes *Annotation*
-keepattributes Signature
# https://github.com/square/okio#proguard
-dontwarn okio.**

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2015-present, Parse, LLC.
~ All rights reserved.
~
~ This source code is licensed under the BSD-style license found in the
~ LICENSE file in the root directory of this source tree. An additional grant
~ of patent rights can be found in the PATENTS file in the same directory.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.parse">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application>
<service
android:name=".PushServiceApi26"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
</application>
</manifest>

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.List;
import bolts.Continuation;
import bolts.Task;
/**
* {@code AbstractParseQueryController} is an abstract implementation of
* {@link ParseQueryController}, which implements {@link ParseQueryController#getFirstAsync}.
*/
/** package */ abstract class AbstractQueryController implements ParseQueryController {
@Override
public <T extends ParseObject> Task<T> getFirstAsync(ParseQuery.State<T> state, ParseUser user,
Task<Void> cancellationToken) {
return findAsync(state, user, cancellationToken).continueWith(new Continuation<List<T>, T>() {
@Override
public T then(Task<List<T>> task) throws Exception {
if (task.isFaulted()) {
throw task.getError();
}
if (task.getResult() != null && task.getResult().size() > 0) {
return task.getResult().get(0);
}
throw new ParseException(ParseException.OBJECT_NOT_FOUND, "no results found for query");
}
});
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.Map;
/**
* Provides a general interface for delegation of third party authentication callbacks.
*/
public interface AuthenticationCallback {
/**
* Called when restoring third party authentication credentials that have been serialized,
* such as session keys, etc.
* <p />
* <strong>Note:</strong> This will be executed on a background thread.
*
* @param authData
* The auth data for the provider. This value may be {@code null} when
* unlinking an account.
*
* @return {@code true} iff the {@code authData} was successfully synchronized or {@code false}
* if user should no longer be associated because of bad {@code authData}.
*/
boolean onRestore(Map<String, String> authData);
}

View File

@ -0,0 +1,175 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.List;
import java.util.concurrent.Callable;
import bolts.Continuation;
import bolts.Task;
/** package */ class CacheQueryController extends AbstractQueryController {
private final NetworkQueryController networkController;
public CacheQueryController(NetworkQueryController network) {
networkController = network;
}
@Override
public <T extends ParseObject> Task<List<T>> findAsync(
final ParseQuery.State<T> state,
final ParseUser user,
final Task<Void> cancellationToken) {
final String sessionToken = user != null ? user.getSessionToken() : null;
CommandDelegate<List<T>> callbacks = new CommandDelegate<List<T>>() {
@Override
public Task<List<T>> runOnNetworkAsync() {
return networkController.findAsync(state, sessionToken, cancellationToken);
}
@Override
public Task<List<T>> runFromCacheAsync() {
return findFromCacheAsync(state, sessionToken);
}
};
return runCommandWithPolicyAsync(callbacks, state.cachePolicy());
}
@Override
public <T extends ParseObject> Task<Integer> countAsync(
final ParseQuery.State<T> state,
final ParseUser user,
final Task<Void> cancellationToken) {
final String sessionToken = user != null ? user.getSessionToken() : null;
CommandDelegate<Integer> callbacks = new CommandDelegate<Integer>() {
@Override
public Task<Integer> runOnNetworkAsync() {
return networkController.countAsync(state, sessionToken, cancellationToken);
}
@Override
public Task<Integer> runFromCacheAsync() {
return countFromCacheAsync(state, sessionToken);
}
};
return runCommandWithPolicyAsync(callbacks, state.cachePolicy());
}
/**
* Retrieves the results of the last time {@link ParseQuery#find()} was called on a query
* identical to this one.
*
* @param sessionToken The user requesting access.
* @return A list of {@link ParseObject}s corresponding to this query. Returns null if there is no
* cache for this query.
*/
private <T extends ParseObject> Task<List<T>> findFromCacheAsync(
final ParseQuery.State<T> state, String sessionToken) {
final String cacheKey = ParseRESTQueryCommand.findCommand(state, sessionToken).getCacheKey();
return Task.call(new Callable<List<T>>() {
@Override
public List<T> call() throws Exception {
JSONObject cached = ParseKeyValueCache.jsonFromKeyValueCache(cacheKey, state.maxCacheAge());
if (cached == null) {
throw new ParseException(ParseException.CACHE_MISS, "results not cached");
}
try {
return networkController.convertFindResponse(state, cached);
} catch (JSONException e) {
throw new ParseException(ParseException.CACHE_MISS, "the cache contains corrupted json");
}
}
}, Task.BACKGROUND_EXECUTOR);
}
/**
* Retrieves the results of the last time {@link ParseQuery#count()} was called on a query
* identical to this one.
*
* @param sessionToken The user requesting access.
* @return A list of {@link ParseObject}s corresponding to this query. Returns null if there is no
* cache for this query.
*/
private <T extends ParseObject> Task<Integer> countFromCacheAsync(
final ParseQuery.State<T> state, String sessionToken) {
final String cacheKey = ParseRESTQueryCommand.countCommand(state, sessionToken).getCacheKey();
return Task.call(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
JSONObject cached = ParseKeyValueCache.jsonFromKeyValueCache(cacheKey, state.maxCacheAge());
if (cached == null) {
throw new ParseException(ParseException.CACHE_MISS, "results not cached");
}
try {
return cached.getInt("count");
} catch (JSONException e) {
throw new ParseException(ParseException.CACHE_MISS, "the cache contains corrupted json");
}
}
}, Task.BACKGROUND_EXECUTOR);
}
private <TResult> Task<TResult> runCommandWithPolicyAsync(final CommandDelegate<TResult> c,
ParseQuery.CachePolicy policy) {
switch (policy) {
case IGNORE_CACHE:
case NETWORK_ONLY:
return c.runOnNetworkAsync();
case CACHE_ONLY:
return c.runFromCacheAsync();
case CACHE_ELSE_NETWORK:
return c.runFromCacheAsync().continueWithTask(new Continuation<TResult, Task<TResult>>() {
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
@Override
public Task<TResult> then(Task<TResult> task) throws Exception {
if (task.getError() instanceof ParseException) {
return c.runOnNetworkAsync();
}
return task;
}
});
case NETWORK_ELSE_CACHE:
return c.runOnNetworkAsync().continueWithTask(new Continuation<TResult, Task<TResult>>() {
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
@Override
public Task<TResult> then(Task<TResult> task) throws Exception {
Exception error = task.getError();
if (error instanceof ParseException &&
((ParseException) error).getCode() == ParseException.CONNECTION_FAILED) {
return c.runFromCacheAsync();
}
// Either the query succeeded, or there was an an error with the query, not the
// network
return task;
}
});
case CACHE_THEN_NETWORK:
throw new RuntimeException(
"You cannot use the cache policy CACHE_THEN_NETWORK with find()");
default:
throw new RuntimeException("Unknown cache policy: " + policy);
}
}
/**
* A callback that will be used to tell runCommandWithPolicy how to perform the command on the
* network and form the cache.
*/
private interface CommandDelegate<T> {
// Fetches data from the network.
Task<T> runOnNetworkAsync();
// Fetches data from the cache.
Task<T> runFromCacheAsync();
}
}

View File

@ -0,0 +1,162 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import bolts.Continuation;
import bolts.Task;
/** package */ class CachedCurrentInstallationController
implements ParseCurrentInstallationController {
/* package */ static final String TAG = "com.parse.CachedCurrentInstallationController";
/*
* Note about lock ordering:
*
* You must NOT acquire the ParseInstallation instance mutex (the "mutex" field in ParseObject)
* while holding this current installation lock. (We used to use the ParseInstallation.class lock,
* but moved on to an explicit lock object since anyone could acquire the ParseInstallation.class
* lock as ParseInstallation is a public class.) Acquiring the instance mutex while holding this
* current installation lock will lead to a deadlock. Here is an example:
* https://phabricator.fb.com/P3251091
*/
private final Object mutex = new Object();
private final TaskQueue taskQueue = new TaskQueue();
private final ParseObjectStore<ParseInstallation> store;
private final InstallationId installationId;
// The "current installation" is the installation for this device. Protected by
// mutex.
/* package for test */ ParseInstallation currentInstallation;
public CachedCurrentInstallationController(
ParseObjectStore<ParseInstallation> store, InstallationId installationId) {
this.store = store;
this.installationId = installationId;
}
@Override
public Task<Void> setAsync(final ParseInstallation installation) {
if (!isCurrent(installation)) {
return Task.forResult(null);
}
return taskQueue.enqueue(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> toAwait) throws Exception {
return toAwait.continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return store.setAsync(installation);
}
}).continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
installationId.set(installation.getInstallationId());
return task;
}
}, ParseExecutors.io());
}
});
}
@Override
public Task<ParseInstallation> getAsync() {
synchronized (mutex) {
if (currentInstallation != null) {
return Task.forResult(currentInstallation);
}
}
return taskQueue.enqueue(new Continuation<Void, Task<ParseInstallation>>() {
@Override
public Task<ParseInstallation> then(Task<Void> toAwait) throws Exception {
return toAwait.continueWithTask(new Continuation<Void, Task<ParseInstallation>>() {
@Override
public Task<ParseInstallation> then(Task<Void> task) throws Exception {
synchronized (mutex) {
if (currentInstallation != null) {
return Task.forResult(currentInstallation);
}
}
return store.getAsync().continueWith(new Continuation<ParseInstallation, ParseInstallation>() {
@Override
public ParseInstallation then(Task<ParseInstallation> task) throws Exception {
ParseInstallation current = task.getResult();
if (current == null) {
current = ParseObject.create(ParseInstallation.class);
current.updateDeviceInfo(installationId);
} else {
installationId.set(current.getInstallationId());
PLog.v(TAG, "Successfully deserialized Installation object");
}
synchronized (mutex) {
currentInstallation = current;
}
return current;
}
}, ParseExecutors.io());
}
});
}
});
}
@Override
public Task<Boolean> existsAsync() {
synchronized (mutex) {
if (currentInstallation != null) {
return Task.forResult(true);
}
}
return taskQueue.enqueue(new Continuation<Void, Task<Boolean>>() {
@Override
public Task<Boolean> then(Task<Void> toAwait) throws Exception {
return toAwait.continueWithTask(new Continuation<Void, Task<Boolean>>() {
@Override
public Task<Boolean> then(Task<Void> task) throws Exception {
return store.existsAsync();
}
});
}
});
}
@Override
public void clearFromMemory() {
synchronized (mutex) {
currentInstallation = null;
}
}
@Override
public void clearFromDisk() {
synchronized (mutex) {
currentInstallation = null;
}
try {
installationId.clear();
ParseTaskUtils.wait(store.deleteAsync());
} catch (ParseException e) {
// ignored
}
}
@Override
public boolean isCurrent(ParseInstallation installation) {
synchronized (mutex) {
return currentInstallation == installation;
}
}
}

View File

@ -0,0 +1,290 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.Arrays;
import java.util.Map;
import bolts.Continuation;
import bolts.Task;
/** package */ class CachedCurrentUserController implements ParseCurrentUserController {
/**
* Lock used to synchronize current user modifications and access.
*
* Note about lock ordering:
*
* You must NOT acquire the ParseUser instance mutex (the "mutex" field in ParseObject) while
* holding this static initialization lock. Doing so will cause a deadlock. Here's an example:
* https://phabricator.fb.com/P17182641
*/
private final Object mutex = new Object();
private final TaskQueue taskQueue = new TaskQueue();
private final ParseObjectStore<ParseUser> store;
/* package */ ParseUser currentUser;
// Whether currentUser is known to match the serialized version on disk. This is useful for saving
// a filesystem check if you try to load currentUser frequently while there is none on disk.
/* package */ boolean currentUserMatchesDisk = false;
public CachedCurrentUserController(ParseObjectStore<ParseUser> store) {
this.store = store;
}
@Override
public Task<Void> setAsync(final ParseUser user) {
return taskQueue.enqueue(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> toAwait) throws Exception {
return toAwait.continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
ParseUser oldCurrentUser;
synchronized (mutex) {
oldCurrentUser = currentUser;
}
if (oldCurrentUser != null && oldCurrentUser != user) {
// We don't need to revoke the token since we're not explicitly calling logOut
// We don't need to remove persisted files since we're overwriting them
return oldCurrentUser.logOutAsync(false).continueWith(new Continuation<Void, Void>() {
@Override
public Void then(Task<Void> task) throws Exception {
return null; // ignore errors
}
});
}
return task;
}
}).onSuccessTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
user.setIsCurrentUser(true);
return user.synchronizeAllAuthDataAsync();
}
}).onSuccessTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return store.setAsync(user).continueWith(new Continuation<Void, Void>() {
@Override
public Void then(Task<Void> task) throws Exception {
synchronized (mutex) {
currentUserMatchesDisk = !task.isFaulted();
currentUser = user;
}
return null;
}
});
}
});
}
});
}
@Override
public Task<Void> setIfNeededAsync(ParseUser user) {
synchronized (mutex) {
if (!user.isCurrentUser() || currentUserMatchesDisk) {
return Task.forResult(null);
}
}
return setAsync(user);
}
@Override
public Task<ParseUser> getAsync() {
return getAsync(ParseUser.isAutomaticUserEnabled());
}
@Override
public Task<Boolean> existsAsync() {
synchronized (mutex) {
if (currentUser != null) {
return Task.forResult(true);
}
}
return taskQueue.enqueue(new Continuation<Void, Task<Boolean>>() {
@Override
public Task<Boolean> then(Task<Void> toAwait) throws Exception {
return toAwait.continueWithTask(new Continuation<Void, Task<Boolean>>() {
@Override
public Task<Boolean> then(Task<Void> task) throws Exception {
return store.existsAsync();
}
});
}
});
}
@Override
public boolean isCurrent(ParseUser user) {
synchronized (mutex) {
return currentUser == user;
}
}
@Override
public void clearFromMemory() {
synchronized (mutex) {
currentUser = null;
currentUserMatchesDisk = false;
}
}
@Override
public void clearFromDisk() {
synchronized (mutex) {
currentUser = null;
currentUserMatchesDisk = false;
}
try {
ParseTaskUtils.wait(store.deleteAsync());
} catch (ParseException e) {
// ignored
}
}
@Override
public Task<String> getCurrentSessionTokenAsync() {
return getAsync(false).onSuccess(new Continuation<ParseUser, String>() {
@Override
public String then(Task<ParseUser> task) throws Exception {
ParseUser user = task.getResult();
return user != null ? user.getSessionToken() : null;
}
});
}
@Override
public Task<Void> logOutAsync() {
return taskQueue.enqueue(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> toAwait) throws Exception {
// We can parallelize disk and network work, but only after we restore the current user from
// disk.
final Task<ParseUser> userTask = getAsync(false);
return Task.whenAll(Arrays.asList(userTask, toAwait)).continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
Task<Void> logOutTask = userTask.onSuccessTask(new Continuation<ParseUser, Task<Void>>() {
@Override
public Task<Void> then(Task<ParseUser> task) throws Exception {
ParseUser user = task.getResult();
if (user == null) {
return task.cast();
}
return user.logOutAsync();
}
});
Task<Void> diskTask = store.deleteAsync().continueWith(new Continuation<Void, Void>() {
@Override
public Void then(Task<Void> task) throws Exception {
boolean deleted = !task.isFaulted();
synchronized (mutex) {
currentUserMatchesDisk = deleted;
currentUser = null;
}
return null;
}
});
return Task.whenAll(Arrays.asList(logOutTask, diskTask));
}
});
}
});
}
@Override
public Task<ParseUser> getAsync(final boolean shouldAutoCreateUser) {
synchronized (mutex) {
if (currentUser != null) {
return Task.forResult(currentUser);
}
}
return taskQueue.enqueue(new Continuation<Void, Task<ParseUser>>() {
@Override
public Task<ParseUser> then(Task<Void> toAwait) throws Exception {
return toAwait.continueWithTask(new Continuation<Void, Task<ParseUser>>() {
@Override
public Task<ParseUser> then(Task<Void> ignored) throws Exception {
ParseUser current;
boolean matchesDisk;
synchronized (mutex) {
current = currentUser;
matchesDisk = currentUserMatchesDisk;
}
if (current != null) {
return Task.forResult(current);
}
if (matchesDisk) {
if (shouldAutoCreateUser) {
return Task.forResult(lazyLogIn());
}
return null;
}
return store.getAsync().continueWith(new Continuation<ParseUser, ParseUser>() {
@Override
public ParseUser then(Task<ParseUser> task) throws Exception {
ParseUser current = task.getResult();
boolean matchesDisk = !task.isFaulted();
synchronized (mutex) {
currentUser = current;
currentUserMatchesDisk = matchesDisk;
}
if (current != null) {
synchronized (current.mutex) {
current.setIsCurrentUser(true);
}
return current;
}
if (shouldAutoCreateUser) {
return lazyLogIn();
}
return null;
}
});
}
});
}
});
}
private ParseUser lazyLogIn() {
Map<String, String> authData = ParseAnonymousUtils.getAuthData();
return lazyLogIn(ParseAnonymousUtils.AUTH_TYPE, authData);
}
/* package for tests */ ParseUser lazyLogIn(String authType, Map<String, String> authData) {
// Note: if authType != ParseAnonymousUtils.AUTH_TYPE the user is not "lazy".
ParseUser user = ParseObject.create(ParseUser.class);
synchronized (user.mutex) {
user.setIsCurrentUser(true);
user.putAuthData(authType, authData);
}
synchronized (mutex) {
currentUserMatchesDisk = false;
currentUser = user;
}
return user;
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/**
* A {@code ConfigCallback} is used to run code after {@link ParseConfig#getInBackground()} is used
* to fetch a new configuration object from the server in a background thread.
* <p>
* The easiest way to use a {@code ConfigCallback} is through an anonymous inner class. Override the
* {@code done} function to specify what the callback should do after the fetch is complete.
* The {@code done} function will be run in the UI thread, while the fetch happens in a
* background thread. This ensures that the UI does not freeze while the fetch happens.
* <p/>
* <pre>
* ParseConfig.getInBackground(new ConfigCallback() {
* public void done(ParseConfig config, ParseException e) {
* if (e == null) {
* configFetchSuccess(object);
* } else {
* configFetchFailed(e);
* }
* }
* });
* </pre>
*/
public interface ConfigCallback extends ParseCallback2<ParseConfig, ParseException> {
/**
* Override this function with the code you want to run after the fetch is complete.
*
* @param config
* A new {@code ParseConfig} instance from the server, or {@code null} if it did not
* succeed.
* @param e
* The exception raised by the fetch, or {@code null} if it succeeded.
*/
@Override
void done(ParseConfig config, ParseException e);
}

View File

@ -0,0 +1,96 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ReceiverCallNotAllowedException;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** package */ class ConnectivityNotifier extends BroadcastReceiver {
private static final String TAG = "com.parse.ConnectivityNotifier";
public interface ConnectivityListener {
void networkConnectivityStatusChanged(Context context, Intent intent);
}
private static final ConnectivityNotifier singleton = new ConnectivityNotifier();
public static ConnectivityNotifier getNotifier(Context context) {
singleton.tryToRegisterForNetworkStatusNotifications(context);
return singleton;
}
public static boolean isConnected(Context context) {
ConnectivityManager connectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager == null) {
return false;
}
NetworkInfo network = connectivityManager.getActiveNetworkInfo();
return network != null && network.isConnected();
}
private Set<ConnectivityListener> listeners = new HashSet<>();
private boolean hasRegisteredReceiver = false;
private final Object lock = new Object();
public void addListener(ConnectivityListener delegate) {
synchronized (lock) {
listeners.add(delegate);
}
}
public void removeListener(ConnectivityListener delegate) {
synchronized (lock) {
listeners.remove(delegate);
}
}
private boolean tryToRegisterForNetworkStatusNotifications(Context context) {
synchronized (lock) {
if (hasRegisteredReceiver) {
return true;
}
try {
if (context == null) {
return false;
}
context = context.getApplicationContext();
context.registerReceiver(this, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
hasRegisteredReceiver = true;
return true;
} catch (ReceiverCallNotAllowedException e) {
// In practice, this only happens with the push service, which will trigger a retry soon afterwards.
PLog.v(TAG, "Cannot register a broadcast receiver because the executing " +
"thread is currently in a broadcast receiver. Will try again later.");
return false;
}
}
}
@Override
public void onReceive(Context context, Intent intent) {
List<ConnectivityListener> listenersCopy;
synchronized (lock) {
listenersCopy = new ArrayList<>(listeners);
}
for (ConnectivityListener delegate : listenersCopy) {
delegate.networkConnectivityStatusChanged(context, intent);
}
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/**
* A {@code CountCallback} is used to run code after a {@link ParseQuery} is used to count objects
* matching a query in a background thread.
* <p/>
* The easiest way to use a {@code CountCallback} is through an anonymous inner class. Override the
* {@code done} function to specify what the callback should do after the count is complete.
* The {@code done} function will be run in the UI thread, while the count happens in a
* background thread. This ensures that the UI does not freeze while the fetch happens.
* <p/>
* For example, this sample code counts objects of class {@code "MyClass"}. It calls a
* different function depending on whether the count succeeded or not.
* <p/>
* <pre>
* ParseQuery&lt;ParseObject&gt; query = ParseQuery.getQuery(&quot;MyClass&quot;);
* query.countInBackground(new CountCallback() {
* public void done(int count, ParseException e) {
* if (e == null) {
* objectsWereCountedSuccessfully(count);
* } else {
* objectCountingFailed();
* }
* }
* });
* </pre>
*/
// FYI, this does not extend ParseCallback2 since the first param is `int`, which can't be used
// in a generic.
public interface CountCallback {
/**
* Override this function with the code you want to run after the count is complete.
*
* @param count
* The number of objects matching the query, or -1 if it failed.
* @param e
* The exception raised by the count, or null if it succeeded.
*/
void done(int count, ParseException e);
}

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/**
* A {@code DeleteCallback} is used to run code after saving a {@link ParseObject} in a background
* thread.
* <p/>
* The easiest way to use a {@code DeleteCallback} is through an anonymous inner class. Override the
* {@code done} function to specify what the callback should do after the delete is complete.
* The {@code done} function will be run in the UI thread, while the delete happens in a
* background thread. This ensures that the UI does not freeze while the delete happens.
* <p/>
* For example, this sample code deletes the object {@code myObject} and calls a different
* function depending on whether the save succeeded or not.
* <p/>
* <pre>
* myObject.deleteInBackground(new DeleteCallback() {
* public void done(ParseException e) {
* if (e == null) {
* myObjectWasDeletedSuccessfully();
* } else {
* myObjectDeleteDidNotSucceed();
* }
* }
* });
* </pre>
*/
public interface DeleteCallback extends ParseCallback1<ParseException> {
/**
* Override this function with the code you want to run after the delete is complete.
*
* @param e
* The exception raised by the delete, or {@code null} if it succeeded.
*/
@Override
void done(ParseException e);
}

View File

@ -0,0 +1,191 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import com.parse.http.ParseHttpRequest;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import bolts.Continuation;
import bolts.Task;
/**
* Properties
* - time
* Used for sort order when querying for all EventuallyPins
* - type
* TYPE_SAVE or TYPE_DELETE
* - object
* The object that the operation should notify when complete
* - operationSetUUID
* The operationSet to be completed
* - sessionToken
* The user that instantiated the operation
*/
@ParseClassName("_EventuallyPin")
/** package */ class EventuallyPin extends ParseObject {
public static final String PIN_NAME = "_eventuallyPin";
public static final int TYPE_SAVE = 1;
public static final int TYPE_DELETE = 2;
public static final int TYPE_COMMAND = 3;
public EventuallyPin() {
super("_EventuallyPin");
}
@Override
boolean needsDefaultACL() {
return false;
}
public String getUUID() {
return getString("uuid");
}
public int getType() {
return getInt("type");
}
public ParseObject getObject() {
return getParseObject("object");
}
public String getOperationSetUUID() {
return getString("operationSetUUID");
}
public String getSessionToken() {
return getString("sessionToken");
}
public ParseRESTCommand getCommand() throws JSONException {
JSONObject json = getJSONObject("command");
ParseRESTCommand command = null;
if (ParseRESTCommand.isValidCommandJSONObject(json)) {
command = ParseRESTCommand.fromJSONObject(json);
} else if (ParseRESTCommand.isValidOldFormatCommandJSONObject(json)) {
// do nothing
} else {
throw new JSONException("Failed to load command from JSON.");
}
return command;
}
public static Task<EventuallyPin> pinEventuallyCommand(ParseObject object,
ParseRESTCommand command) {
int type = TYPE_COMMAND;
JSONObject json = null;
if (command.httpPath.startsWith("classes")) {
if (command.method == ParseHttpRequest.Method.POST ||
command.method == ParseHttpRequest.Method.PUT) {
type = TYPE_SAVE;
} else if (command.method == ParseHttpRequest.Method.DELETE) {
type = TYPE_DELETE;
}
} else {
json = command.toJSONObject();
}
return pinEventuallyCommand(
type,
object,
command.getOperationSetUUID(),
command.getSessionToken(),
json);
}
/**
* @param type
* Type of the command: TYPE_SAVE, TYPE_DELETE, TYPE_COMMAND
* @param obj
* (Optional) Object the operation is being executed on. Required for TYPE_SAVE and
* TYPE_DELETE.
* @param operationSetUUID
* (Optional) UUID of the ParseOperationSet that is paired with the ParseCommand.
* Required for TYPE_SAVE and TYPE_DELETE.
* @param sessionToken
* (Optional) The sessionToken for the command. Required for TYPE_SAVE and TYPE_DELETE.
* @param command
* (Optional) JSON representation of the ParseCommand. Required for TYPE_COMMAND.
* @return
* Returns a task that is resolved when the command is pinned.
*/
private static Task<EventuallyPin> pinEventuallyCommand(int type, ParseObject obj,
String operationSetUUID, String sessionToken, JSONObject command) {
final EventuallyPin pin = new EventuallyPin();
pin.put("uuid", UUID.randomUUID().toString());
pin.put("time", new Date());
pin.put("type", type);
if (obj != null) {
pin.put("object", obj);
}
if (operationSetUUID != null) {
pin.put("operationSetUUID", operationSetUUID);
}
if (sessionToken != null) {
pin.put("sessionToken", sessionToken);
}
if (command != null) {
pin.put("command", command);
}
return pin.pinInBackground(PIN_NAME).continueWith(new Continuation<Void, EventuallyPin>() {
@Override
public EventuallyPin then(Task<Void> task) throws Exception {
return pin;
}
});
}
public static Task<List<EventuallyPin>> findAllPinned() {
return findAllPinned(null);
}
public static Task<List<EventuallyPin>> findAllPinned(Collection<String> excludeUUIDs) {
ParseQuery<EventuallyPin> query = new ParseQuery<>(EventuallyPin.class)
.fromPin(PIN_NAME)
.ignoreACLs()
.orderByAscending("time");
if (excludeUUIDs != null) {
query.whereNotContainedIn("uuid", excludeUUIDs);
}
// We need pass in a null user because we don't want the query to fetch the current user
// from LDS.
return query.findInBackground().continueWithTask(new Continuation<List<EventuallyPin>, Task<List<EventuallyPin>>>() {
@Override
public Task<List<EventuallyPin>> then(Task<List<EventuallyPin>> task) throws Exception {
final List<EventuallyPin> pins = task.getResult();
List<Task<Void>> tasks = new ArrayList<>();
for (EventuallyPin pin : pins) {
ParseObject object = pin.getObject();
if (object != null) {
tasks.add(object.fetchFromLocalDatastoreAsync().makeVoid());
}
}
return Task.whenAll(tasks).continueWithTask(new Continuation<Void, Task<List<EventuallyPin>>>() {
@Override
public Task<List<EventuallyPin>> then(Task<Void> task) throws Exception {
return Task.forResult(pins);
}
});
}
});
}
}

View File

@ -0,0 +1,139 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.Callable;
import bolts.Task;
/** package */ class FileObjectStore<T extends ParseObject> implements ParseObjectStore<T> {
private static ParseObjectSubclassingController getSubclassingController() {
return ParseCorePlugins.getInstance().getSubclassingController();
}
/**
* Saves the {@code ParseObject} to the a file on disk as JSON in /2/ format.
*
* @param coder
* Current coder to encode the ParseObject.
* @param current
* ParseObject which needs to be saved to disk.
* @param file
* The file to save the object to.
*
* @see #getFromDisk(ParseObjectCurrentCoder, File, ParseObject.State.Init)
*/
private static void saveToDisk(
ParseObjectCurrentCoder coder, ParseObject current, File file) {
JSONObject json = coder.encode(current.getState(), null, PointerEncoder.get());
try {
ParseFileUtils.writeJSONObjectToFile(file, json);
} catch (IOException e) {
//TODO(grantland): We should do something if this fails...
}
}
/**
* Retrieves a {@code ParseObject} from a file on disk in /2/ format.
*
* @param coder
* Current coder to decode the ParseObject.
* @param file
* The file to retrieve the object from.
* @param builder
* An empty builder which is used to generate a empty state and rebuild a ParseObject.
* @return The {@code ParseObject} that was retrieved. If the file wasn't found, or the contents
* of the file is an invalid {@code ParseObject}, returns {@code null}.
*
* @see #saveToDisk(ParseObjectCurrentCoder, ParseObject, File)
*/
private static <T extends ParseObject> T getFromDisk(
ParseObjectCurrentCoder coder, File file, ParseObject.State.Init builder) {
JSONObject json;
try {
json = ParseFileUtils.readFileToJSONObject(file);
} catch (IOException | JSONException e) {
return null;
}
ParseObject.State newState = coder.decode(builder, json, ParseDecoder.get())
.isComplete(true)
.build();
return ParseObject.from(newState);
}
private final String className;
private final File file;
private final ParseObjectCurrentCoder coder;
public FileObjectStore(Class<T> clazz, File file, ParseObjectCurrentCoder coder) {
this(getSubclassingController().getClassName(clazz), file, coder);
}
public FileObjectStore(String className, File file, ParseObjectCurrentCoder coder) {
this.className = className;
this.file = file;
this.coder = coder;
}
@Override
public Task<Void> setAsync(final T object) {
return Task.call(new Callable<Void>() {
@Override
public Void call() throws Exception {
saveToDisk(coder, object, file);
//TODO (grantland): check to see if this failed? We currently don't for legacy reasons.
return null;
}
}, ParseExecutors.io());
}
@Override
public Task<T> getAsync() {
return Task.call(new Callable<T>() {
@Override
public T call() throws Exception {
if (!file.exists()) {
return null;
}
return getFromDisk(coder, file, ParseObject.State.newBuilder(className));
}
}, ParseExecutors.io());
}
@Override
public Task<Boolean> existsAsync() {
return Task.call(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return file.exists();
}
}, ParseExecutors.io());
}
@Override
public Task<Void> deleteAsync() {
return Task.call(new Callable<Void>() {
@Override
public Void call() throws Exception {
if (file.exists() && !ParseFileUtils.deleteQuietly(file)) {
throw new RuntimeException("Unable to delete");
}
return null;
}
}, ParseExecutors.io());
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.List;
/**
* A {@code FindCallback} is used to run code after a {@link ParseQuery} is used to fetch a list of
* {@link ParseObject}s in a background thread.
* <p/>
* The easiest way to use a {@code FindCallback} is through an anonymous inner class. Override the
* {@code done} function to specify what the callback should do after the fetch is complete.
* The {@code done} function will be run in the UI thread, while the fetch happens in a
* background thread. This ensures that the UI does not freeze while the fetch happens.
* <p/>
* For example, this sample code fetches all objects of class {@code "MyClass"}. It calls a
* different function depending on whether the fetch succeeded or not.
* <p/>
* <pre>
* ParseQuery&lt;ParseObject&gt; query = ParseQuery.getQuery(&quot;MyClass&quot;);
* query.findInBackground(new FindCallback&lt;ParseObject&gt;() {
* public void done(List&lt;ParseObject&gt; objects, ParseException e) {
* if (e == null) {
* objectsWereRetrievedSuccessfully(objects);
* } else {
* objectRetrievalFailed();
* }
* }
* });
* </pre>
*/
public interface FindCallback<T extends ParseObject> extends ParseCallback2<List<T>, ParseException> {
/**
* Override this function with the code you want to run after the fetch is complete.
*
* @param objects
* The objects that were retrieved, or null if it did not succeed.
* @param e
* The exception raised by the save, or null if it succeeded.
*/
@Override
void done(List<T> objects, ParseException e);
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/**
* A {@code FunctionCallback} is used to run code after {@link ParseCloud#callFunction} is used to
* run a Cloud Function in a background thread.
* <p/>
* The easiest way to use a {@code FunctionCallback} is through an anonymous inner class. Override
* the {@code done} function to specify what the callback should do after the cloud function is
* complete. The {@code done} function will be run in the UI thread, while the fetch happens in
* a background thread. This ensures that the UI does not freeze while the fetch happens.
* <p/>
* For example, this sample code calls a cloud function {@code "MyFunction"} with
* {@code params} and calls a different function depending on whether the function succeeded.
* <p/>
* <pre>
* ParseCloud.callFunctionInBackground(&quot;MyFunction&quot;new, params, FunctionCallback<ParseObject>() {
* public void done(ParseObject object, ParseException e) {
* if (e == null) {
* cloudFunctionSucceeded(object);
* } else {
* cloudFunctionFailed();
* }
* }
* });
* </pre>
*
* @param <T>
* The type of object returned by the Cloud Function.
*/
public interface FunctionCallback<T> extends ParseCallback2<T, ParseException> {
/**
* Override this function with the code you want to run after the cloud function is complete.
*
* @param object
* The object that was returned by the cloud function.
* @param e
* The exception raised by the cloud call, or {@code null} if it succeeded.
*/
@Override
void done(T object, ParseException e);
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.CallSuper;
/**
* @exclude
*/
public class GcmBroadcastReceiver extends BroadcastReceiver {
@Override
@CallSuper
public void onReceive(Context context, Intent intent) {
PushServiceUtils.runService(context, intent);
}
}

View File

@ -0,0 +1,211 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.ref.WeakReference;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import bolts.Task;
/**
* Proxy Service while running in GCM mode.
*
* We use an {@link ExecutorService} so that we can operate like a ghetto
* {@link android.app.IntentService} where all incoming {@link Intent}s will be handled
* sequentially.
*/
/** package */ class GcmPushHandler implements PushHandler {
private static final String TAG = "GcmPushHandler";
static final String REGISTER_RESPONSE_ACTION = "com.google.android.c2dm.intent.REGISTRATION";
static final String RECEIVE_PUSH_ACTION = "com.google.android.c2dm.intent.RECEIVE";
GcmPushHandler() {}
@NonNull
@Override
public SupportLevel isSupported() {
if (!ManifestInfo.isGooglePlayServicesAvailable()) {
return SupportLevel.MISSING_REQUIRED_DECLARATIONS;
}
return getManifestSupportLevel();
}
private SupportLevel getManifestSupportLevel() {
Context context = Parse.getApplicationContext();
String[] requiredPermissions = new String[] {
"android.permission.INTERNET",
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.WAKE_LOCK",
"com.google.android.c2dm.permission.RECEIVE",
context.getPackageName() + ".permission.C2D_MESSAGE"
};
if (!ManifestInfo.hasRequestedPermissions(context, requiredPermissions)) {
return SupportLevel.MISSING_REQUIRED_DECLARATIONS;
}
String packageName = context.getPackageName();
String rcvrPermission = "com.google.android.c2dm.permission.SEND";
Intent[] intents = new Intent[] {
new Intent(GcmPushHandler.RECEIVE_PUSH_ACTION)
.setPackage(packageName)
.addCategory(packageName),
new Intent(GcmPushHandler.REGISTER_RESPONSE_ACTION)
.setPackage(packageName)
.addCategory(packageName),
};
if (!ManifestInfo.checkReceiver(GcmBroadcastReceiver.class, rcvrPermission, intents)) {
return SupportLevel.MISSING_REQUIRED_DECLARATIONS;
}
String[] optionalPermissions = new String[] {
"android.permission.VIBRATE"
};
if (!ManifestInfo.hasGrantedPermissions(context, optionalPermissions)) {
return SupportLevel.MISSING_OPTIONAL_DECLARATIONS;
}
return SupportLevel.SUPPORTED;
}
@Nullable
@Override
public String getWarningMessage(SupportLevel level) {
switch (level) {
case SUPPORTED: return null;
case MISSING_OPTIONAL_DECLARATIONS: return "Using GCM for Parse Push, " +
"but the app manifest is missing some optional " +
"declarations that should be added for maximum reliability. Please " +
getWarningMessage();
case MISSING_REQUIRED_DECLARATIONS:
if (ManifestInfo.isGooglePlayServicesAvailable()) {
return "Cannot use GCM for push because the app manifest is missing some " +
"required declarations. Please " + getWarningMessage();
} else {
return "Cannot use GCM for push on this device because Google Play " +
"Services is not available. Install Google Play Services from the Play Store.";
}
}
return null;
}
static String getWarningMessage() {
String packageName = Parse.getApplicationContext().getPackageName();
String gcmPackagePermission = packageName + ".permission.C2D_MESSAGE";
return "make sure that these permissions are declared as children " +
"of the root <manifest> element:\n" +
"\n" +
"<uses-permission android:name=\"android.permission.INTERNET\" />\n" +
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n" +
"<uses-permission android:name=\"android.permission.VIBRATE\" />\n" +
"<uses-permission android:name=\"android.permission.WAKE_LOCK\" />\n" +
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\" />\n" +
"<uses-permission android:name=\"com.google.android.c2dm.permission.RECEIVE\" />\n" +
"<permission android:name=\"" + gcmPackagePermission + "\" " +
"android:protectionLevel=\"signature\" />\n" +
"<uses-permission android:name=\"" + gcmPackagePermission + "\" />\n" +
"\n" +
"Also, please make sure that these services and broadcast receivers are declared as " +
"children of the <application> element:\n" +
"\n" +
"<service android:name=\"com.parse.PushService\" />\n" +
"<receiver android:name=\"com.parse.GcmBroadcastReceiver\" " +
"android:permission=\"com.google.android.c2dm.permission.SEND\">\n" +
" <intent-filter>\n" +
" <action android:name=\"com.google.android.c2dm.intent.RECEIVE\" />\n" +
" <action android:name=\"com.google.android.c2dm.intent.REGISTRATION\" />\n" +
" <category android:name=\"" + packageName + "\" />\n" +
" </intent-filter>\n" +
"</receiver>\n" +
"<receiver android:name=\"com.parse.ParsePushBroadcastReceiver\"" +
" android:exported=false>\n" +
" <intent-filter>\n" +
" <action android:name=\"com.parse.push.intent.RECEIVE\" />\n" +
" <action android:name=\"com.parse.push.intent.OPEN\" />\n" +
" <action android:name=\"com.parse.push.intent.DELETE\" />\n" +
" </intent-filter>\n" +
"</receiver>";
}
@Override
public Task<Void> initialize() {
return GcmRegistrar.getInstance().registerAsync();
}
@WorkerThread
@Override
public void handlePush(Intent intent) {
if (intent != null) {
String action = intent.getAction();
if (REGISTER_RESPONSE_ACTION.equals(action)) {
handleGcmRegistrationIntent(intent);
} else if (RECEIVE_PUSH_ACTION.equals(action)) {
handleGcmPushIntent(intent);
} else {
PLog.e(TAG, "PushService got unknown intent in GCM mode: " + intent);
}
}
}
@WorkerThread
private void handleGcmRegistrationIntent(Intent intent) {
try {
// Have to block here since we are already in a background thread and as soon as we return,
// PushService may exit.
GcmRegistrar.getInstance().handleRegistrationIntentAsync(intent).waitForCompletion();
} catch (InterruptedException e) {
// do nothing
}
}
@WorkerThread
private void handleGcmPushIntent(Intent intent) {
String messageType = intent.getStringExtra("message_type");
if (messageType != null) {
/*
* The GCM docs reserve the right to use the message_type field for new actions, but haven't
* documented what those new actions are yet. For forwards compatibility, ignore anything
* with a message_type field.
*/
PLog.i(TAG, "Ignored special message type " + messageType + " from GCM via intent " + intent);
} else {
String pushId = intent.getStringExtra("push_id");
String timestamp = intent.getStringExtra("time");
String dataString = intent.getStringExtra("data");
String channel = intent.getStringExtra("channel");
JSONObject data = null;
if (dataString != null) {
try {
data = new JSONObject(dataString);
} catch (JSONException e) {
PLog.e(TAG, "Ignoring push because of JSON exception while processing: " + dataString, e);
return;
}
}
PushRouter.getInstance().handlePush(pushId, timestamp, channel, data);
}
}
}

View File

@ -0,0 +1,405 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.SystemClock;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import bolts.Continuation;
import bolts.Task;
import bolts.TaskCompletionSource;
/**
* A class that manages registering for GCM and updating the registration if it is out of date,
* used by {@link com.parse.GcmPushHandler}.
*/
/** package */ class GcmRegistrar {
private static final String TAG = "com.parse.GcmRegistrar";
private static final String REGISTRATION_ID_EXTRA = "registration_id";
private static final String ERROR_EXTRA = "error";
private static final String SENDER_ID_EXTRA = "com.parse.push.gcm_sender_id";
public static final String REGISTER_ACTION = "com.google.android.c2dm.intent.REGISTER";
private static final String FILENAME_DEVICE_TOKEN_LAST_MODIFIED = "deviceTokenLastModified";
private long localDeviceTokenLastModified;
private final Object localDeviceTokenLastModifiedMutex = new Object();
public static GcmRegistrar getInstance() {
return Singleton.INSTANCE;
}
private static class Singleton {
public static final GcmRegistrar INSTANCE = new GcmRegistrar(Parse.getApplicationContext());
}
private static String actualSenderIDFromExtra(Object senderIDExtra) {
if (!(senderIDExtra instanceof String)) {
return null;
}
String senderID = (String)senderIDExtra;
if (!senderID.startsWith("id:")) {
return null;
}
return senderID.substring(3);
}
private final Object lock = new Object();
private Request request = null;
private Context context = null;
// This a package-level constructor for unit testing only. Otherwise, use getInstance().
/* package */ GcmRegistrar(Context context) {
this.context = context;
}
/**
* Does nothing if the client already has a valid GCM registration id. Otherwise, sends out a
* GCM registration request and saves the resulting registration id to the server via
* ParseInstallation.
*/
public Task<Void> registerAsync() {
if (ManifestInfo.getPushType() != PushType.GCM) {
return Task.forResult(null);
}
synchronized (lock) {
/*
* If we don't yet have a device token, mark this installation as wanting to use GCM by
* setting its pushType to GCM. If the registration does not succeed (because the device
* is offline, for instance), then update() will re-register for a GCM device token at
* next app initialize time.
*/
final ParseInstallation installation = ParseInstallation.getCurrentInstallation();
// Check whether we need to send registration request, if installation does not
// have device token or local device token is stale, we need to send request.
Task<Boolean> checkTask = installation.getDeviceToken() == null
? Task.forResult(true)
: isLocalDeviceTokenStaleAsync();
return checkTask.onSuccessTask(new Continuation<Boolean, Task<Void>>() {
@Override
public Task<Void> then(Task<Boolean> task) throws Exception {
if (!task.getResult()) {
return Task.forResult(null);
}
if (installation.getPushType() != PushType.GCM) {
installation.setPushType(PushType.GCM);
}
// We do not need to wait sendRegistrationRequestAsync, since this task will finish
// after we get the response from GCM, if we wait for this task, it will block our test.
sendRegistrationRequestAsync();
return Task.forResult(null);
}
});
}
}
private Task<Void> sendRegistrationRequestAsync() {
synchronized (lock) {
if (request != null) {
return Task.forResult(null);
}
// Look for an element like this as a child of the <application> element:
//
// <meta-data android:name="com.parse.push.gcm_sender_id"
// android:value="id:567327206255" />
//
// The reason why the "id:" prefix is necessary is because Android treats any metadata value
// that is a string of digits as an integer. So the call to Bundle.getString() will actually
// return null for `android:value="567327206255"`. Additionally, Bundle.getInteger() returns
// a 32-bit integer. For `android:value="567327206255"`, this returns a truncated integer
// because 567327206255 is larger than the largest 32-bit integer.
Bundle metaData = ManifestInfo.getApplicationMetadata(context);
String senderID = null;
if (metaData != null) {
Object senderIDExtra = metaData.get(SENDER_ID_EXTRA);
if (senderIDExtra != null) {
senderID = actualSenderIDFromExtra(senderIDExtra);
if (senderID == null) {
PLog.e(TAG, "Found " + SENDER_ID_EXTRA + " <meta-data> element with value \"" +
senderIDExtra.toString() + "\", but the value is missing the expected \"id:\" " +
"prefix.");
return null;
}
}
}
if (senderID == null) {
PLog.e(TAG, "You must provide " + SENDER_ID_EXTRA + " in your AndroidManifest.xml\n" +
"Make sure to prefix with the value with id:\n\n" +
"<meta-data\n" +
" android:name=\"com.parse.push.gcm_sender_id\"\n" +
" android:value=\"id:<YOUR_GCM_SENDER_ID>\" />");
return null;
}
request = Request.createAndSend(context, senderID);
return request.getTask().continueWith(new Continuation<String, Void>() {
@Override
public Void then(Task<String> task) {
Exception e = task.getError();
if (e != null) {
PLog.e(TAG, "Got error when trying to register for GCM push", e);
}
synchronized (lock) {
request = null;
}
return null;
}
});
}
}
/**
* Should be called by a broadcast receiver or service to handle the GCM registration response
* intent (com.google.android.c2dm.intent.REGISTRATION).
*/
Task<Void> handleRegistrationIntentAsync(Intent intent) {
List<Task<Void>> tasks = new ArrayList<>();
/*
* We have to parse the response here because GCM may send us a new registration_id
* out-of-band without a request in flight.
*/
String registrationId = intent.getStringExtra(REGISTRATION_ID_EXTRA);
if (registrationId != null && registrationId.length() > 0) {
PLog.v(TAG, "Received deviceToken <" + registrationId + "> from GCM.");
ParseInstallation installation = ParseInstallation.getCurrentInstallation();
// Compare the new deviceToken with the old deviceToken, we only update the
// deviceToken if the new one is different from the old one. This does not follow google
// guide strictly. But we find most of the time if user just update the app, the
// registrationId does not change so there is no need to save it again.
if (!registrationId.equals(installation.getDeviceToken())) {
installation.setPushType(PushType.GCM);
installation.setDeviceToken(registrationId);
tasks.add(installation.saveInBackground());
}
// We need to update the last modified even the deviceToken is the same. Otherwise when the
// app is opened again, isDeviceTokenStale() will always return false so we will send
// request to GCM every time.
tasks.add(updateLocalDeviceTokenLastModifiedAsync());
}
synchronized (lock) {
if (request != null) {
request.onReceiveResponseIntent(intent);
}
}
return Task.whenAll(tasks);
}
// Only used by tests.
/* package */ int getRequestIdentifier() {
synchronized (lock) {
return request != null ? request.identifier : 0;
}
}
/** package for tests */ Task<Boolean> isLocalDeviceTokenStaleAsync() {
return getLocalDeviceTokenLastModifiedAsync().onSuccessTask(new Continuation<Long, Task<Boolean>>() {
@Override
public Task<Boolean> then(Task<Long> task) throws Exception {
long localDeviceTokenLastModified = task.getResult();
return Task.forResult(localDeviceTokenLastModified != ManifestInfo.getLastModified());
}
});
}
/** package for tests */ Task<Void> updateLocalDeviceTokenLastModifiedAsync() {
return Task.call(new Callable<Void>() {
@Override
public Void call() throws Exception {
synchronized (localDeviceTokenLastModifiedMutex) {
localDeviceTokenLastModified = ManifestInfo.getLastModified();
final String localDeviceTokenLastModifiedStr =
String.valueOf(localDeviceTokenLastModified);
try {
ParseFileUtils.writeStringToFile(getLocalDeviceTokenLastModifiedFile(),
localDeviceTokenLastModifiedStr, "UTF-8");
} catch (IOException e) {
// do nothing
}
}
return null;
}
}, Task.BACKGROUND_EXECUTOR);
}
private Task<Long> getLocalDeviceTokenLastModifiedAsync() {
return Task.call(new Callable<Long>() {
@Override
public Long call() throws Exception {
synchronized (localDeviceTokenLastModifiedMutex) {
if (localDeviceTokenLastModified == 0) {
try {
String localDeviceTokenLastModifiedStr = ParseFileUtils.readFileToString(
getLocalDeviceTokenLastModifiedFile(), "UTF-8");
localDeviceTokenLastModified = Long.valueOf(localDeviceTokenLastModifiedStr);
} catch (IOException e) {
localDeviceTokenLastModified = 0;
}
}
return localDeviceTokenLastModified;
}
}
}, Task.BACKGROUND_EXECUTOR);
}
/** package for tests */ static File getLocalDeviceTokenLastModifiedFile() {
File dir = Parse.getParseCacheDir("GCMRegistrar");
return new File(dir, FILENAME_DEVICE_TOKEN_LAST_MODIFIED);
}
/** package for tests */ static void deleteLocalDeviceTokenLastModifiedFile() {
ParseFileUtils.deleteQuietly(getLocalDeviceTokenLastModifiedFile());
}
/**
* Encapsulates the a GCM registration request-response, potentially using AlarmManager to
* schedule retries if the GCM service is not available.
*/
private static class Request {
private static final String RETRY_ACTION = "com.parse.RetryGcmRegistration";
private static final int MAX_RETRIES = 5;
private static final int BACKOFF_INTERVAL_MS = 3000;
final private Context context;
final private String senderId;
final private Random random;
final private int identifier;
final private TaskCompletionSource<String> tcs;
final private PendingIntent appIntent;
final private AtomicInteger tries;
final private PendingIntent retryIntent;
final private BroadcastReceiver retryReceiver;
public static Request createAndSend(Context context, String senderId) {
Request request = new Request(context, senderId);
request.send();
return request;
}
private Request(Context context, String senderId) {
this.context = context;
this.senderId = senderId;
this.random = new Random();
this.identifier = this.random.nextInt();
this.tcs = new TaskCompletionSource<>();
this.appIntent = PendingIntent.getBroadcast(this.context, identifier, new Intent(), 0);
this.tries = new AtomicInteger(0);
String packageName = this.context.getPackageName();
Intent intent = new Intent(RETRY_ACTION).setPackage(packageName);
intent.addCategory(packageName);
intent.putExtra("random", identifier);
this.retryIntent = PendingIntent.getBroadcast(this.context, identifier, intent, 0);
this.retryReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && intent.getIntExtra("random", 0) == identifier) {
send();
}
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(RETRY_ACTION);
filter.addCategory(packageName);
context.registerReceiver(this.retryReceiver, filter);
}
public Task<String> getTask() {
return tcs.getTask();
}
private void send() {
Intent intent = new Intent(REGISTER_ACTION);
intent.setPackage("com.google.android.gsf");
intent.putExtra("sender", senderId);
intent.putExtra("app", appIntent);
ComponentName name = null;
try {
name = context.startService(intent);
} catch (SecurityException exception) {
// do nothing
}
if (name == null) {
finish(null, "GSF_PACKAGE_NOT_AVAILABLE");
}
tries.incrementAndGet();
PLog.v(TAG, "Sending GCM registration intent");
}
public void onReceiveResponseIntent(Intent intent) {
String registrationId = intent.getStringExtra(REGISTRATION_ID_EXTRA);
String error = intent.getStringExtra(ERROR_EXTRA);
if (registrationId == null && error == null) {
PLog.e(TAG, "Got no registration info in GCM onReceiveResponseIntent");
return;
}
// Retry with exponential backoff if GCM isn't available.
if ("SERVICE_NOT_AVAILABLE".equals(error) && tries.get() < MAX_RETRIES) {
AlarmManager manager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
int alarmType = AlarmManager.ELAPSED_REALTIME_WAKEUP;
long delay = (1 << tries.get()) * BACKOFF_INTERVAL_MS + random.nextInt(BACKOFF_INTERVAL_MS);
long start = SystemClock.elapsedRealtime() + delay;
manager.set(alarmType, start, retryIntent);
} else {
finish(registrationId, error);
}
}
private void finish(String registrationId, String error) {
boolean didSetResult;
if (registrationId != null) {
didSetResult = tcs.trySetResult(registrationId);
} else {
didSetResult = tcs.trySetError(new Exception("GCM registration error: " + error));
}
if (didSetResult) {
appIntent.cancel();
retryIntent.cancel();
context.unregisterReceiver(this.retryReceiver);
}
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/**
* A {@code GetCallback} is used to run code after a {@link ParseQuery} is used to fetch a
* {@link ParseObject} in a background thread.
* <p/>
* The easiest way to use a {@code GetCallback} is through an anonymous inner class. Override the
* {@code done} function to specify what the callback should do after the fetch is complete.
* The {@code done} function will be run in the UI thread, while the fetch happens in a
* background thread. This ensures that the UI does not freeze while the fetch happens.
* <p/>
* For example, this sample code fetches an object of class {@code "MyClass"} and id
* {@code myId}. It calls a different function depending on whether the fetch succeeded or not.
* <p/>
* <pre>
* ParseQuery&lt;ParseObject&gt; query = ParseQuery.getQuery(&quot;MyClass&quot;);
* query.getInBackground(myId, new GetCallback&lt;ParseObject&gt;() {
* public void done(ParseObject object, ParseException e) {
* if (e == null) {
* objectWasRetrievedSuccessfully(object);
* } else {
* objectRetrievalFailed();
* }
* }
* });
* </pre>
*/
public interface GetCallback<T extends ParseObject> extends ParseCallback2<T, ParseException> {
/**
* Override this function with the code you want to run after the fetch is complete.
*
* @param object
* The object that was retrieved, or {@code null} if it did not succeed.
* @param e
* The exception raised by the fetch, or {@code null} if it succeeded.
*/
@Override
void done(T object, ParseException e);
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/**
* A {@code GetDataCallback} is used to run code after a {@link ParseFile} fetches its data on a
* background thread.
* <p/>
* The easiest way to use a {@code GetDataCallback} is through an anonymous inner class. Override
* the {@code done} function to specify what the callback should do after the fetch is complete.
* The {@code done} function will be run in the UI thread, while the fetch happens in a
* background thread. This ensures that the UI does not freeze while the fetch happens.
* <p/>
* <pre>
* file.getDataInBackground(new GetDataCallback() {
* public void done(byte[] data, ParseException e) {
* // ...
* }
* });
* </pre>
*/
public interface GetDataCallback extends ParseCallback2<byte[], ParseException> {
/**
* Override this function with the code you want to run after the fetch is complete.
*
* @param data
* The data that was retrieved, or {@code null} if it did not succeed.
* @param e
* The exception raised by the fetch, or {@code null} if it succeeded.
*/
@Override
void done(byte[] data, ParseException e);
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.io.InputStream;
/**
* A {@code GetDataStreamCallback} is used to run code after a {@link ParseFile} fetches its data on
* a background thread.
* <p/>
* The easiest way to use a {@code GetDataStreamCallback} is through an anonymous inner class.
* Override the {@code done} function to specify what the callback should do after the fetch is
* complete. The {@code done} function will be run in the UI thread, while the fetch happens in a
* background thread. This ensures that the UI does not freeze while the fetch happens.
* <p/>
* <pre>
* file.getDataStreamInBackground(new GetDataStreamCallback() {
* public void done(InputSteam input, ParseException e) {
* // ...
* }
* });
* </pre>
*/
public interface GetDataStreamCallback extends ParseCallback2<InputStream, ParseException> {
/**
* Override this function with the code you want to run after the fetch is complete.
*
* @param input
* The data that was retrieved, or {@code null} if it did not succeed.
* @param e
* The exception raised by the fetch, or {@code null} if it succeeded.
*/
@Override
void done(InputStream input, ParseException e);
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.io.File;
/**
* A {@code GetFileCallback} is used to run code after a {@link ParseFile} fetches its data on
* a background thread.
* <p/>
* The easiest way to use a {@code GetFileCallback} is through an anonymous inner class.
* Override the {@code done} function to specify what the callback should do after the fetch is
* complete. The {@code done} function will be run in the UI thread, while the fetch happens in a
* background thread. This ensures that the UI does not freeze while the fetch happens.
* <p/>
* <pre>
* file.getFileInBackground(new GetFileCallback() {
* public void done(File file, ParseException e) {
* // ...
* }
* });
* </pre>
*/
public interface GetFileCallback extends ParseCallback2<File, ParseException> {
/**
* Override this function with the code you want to run after the fetch is complete.
*
* @param file
* The data that was retrieved, or {@code null} if it did not succeed.
* @param e
* The exception raised by the fetch, or {@code null} if it succeeded.
*/
@Override
void done(File file, ParseException e);
}

View File

@ -0,0 +1,90 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.UUID;
/**
* Since we cannot save dirty ParseObjects to disk and we must be able to persist UUIDs across
* restarts even if the ParseInstallation is not saved, we use this legacy file still as a
* boostrapping environment as well until the full ParseInstallation is cached to disk.
*
* TODO: Allow dirty objects to be saved to disk.
*/
/* package */ class InstallationId {
private static final String TAG = "InstallationId";
private final Object lock = new Object();
private final File file;
private String installationId;
public InstallationId(File file) {
this.file = file;
}
/**
* Loads the installationId from memory, then tries to loads the legacy installationId from disk
* if it is present, or creates a new random UUID.
*/
public String get() {
synchronized (lock) {
if (installationId == null) {
try {
installationId = ParseFileUtils.readFileToString(file, "UTF-8");
} catch (FileNotFoundException e) {
PLog.i(TAG, "Couldn't find existing installationId file. Creating one instead.");
} catch (IOException e) {
PLog.e(TAG, "Unexpected exception reading installation id from disk", e);
}
}
if (installationId == null) {
setInternal(UUID.randomUUID().toString());
}
}
return installationId;
}
/**
* Sets the installationId and persists it to disk.
*/
public void set(String newInstallationId) {
synchronized (lock) {
if (ParseTextUtils.isEmpty(newInstallationId)
|| newInstallationId.equals(get())) {
return;
}
setInternal(newInstallationId);
}
}
private void setInternal(String newInstallationId) {
synchronized (lock) {
try {
ParseFileUtils.writeStringToFile(file, newInstallationId, "UTF-8");
} catch (IOException e) {
PLog.e(TAG, "Unexpected exception writing installation id to disk", e);
}
installationId = newInstallationId;
}
}
/* package for tests */ void clear() {
synchronized (lock) {
installationId = null;
ParseFileUtils.deleteQuietly(file);
}
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.Map;
/**
* A subclass of <code>ParseDecoder</code> which can keep <code>ParseObject</code> that
* has been fetched instead of creating a new instance.
*/
/** package */ class KnownParseObjectDecoder extends ParseDecoder {
private Map<String, ParseObject> fetchedObjects;
public KnownParseObjectDecoder(Map<String, ParseObject> fetchedObjects) {
super();
this.fetchedObjects = fetchedObjects;
}
/**
* If the object has been fetched, the fetched object will be returned. Otherwise a
* new created object will be returned.
*/
@Override
protected ParseObject decodePointer(String className, String objectId) {
if (fetchedObjects != null && fetchedObjects.containsKey(objectId)) {
return fetchedObjects.get(objectId);
}
return super.decodePointer(className, objectId);
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright (C) 2007 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.parse;
import java.util.AbstractList;
import java.util.List;
/**
* Static utility methods pertaining to {@link List} instances. Also see this
* class's counterparts {@link Sets}, {@link Maps} and {@link Queues}.
*
* <p>See the Guava User Guide article on <a href=
* "https://github.com/google/guava/wiki/CollectionUtilitiesExplained#lists">
* {@code Lists}</a>.
*
* @author Kevin Bourrillion
* @author Mike Bostock
* @author Louis Wasserman
* @since 2.0
*/
/** package */ class Lists {
/**
* Returns consecutive sublists of a list, each of the same size (the final list may be smaller).
* For example, partitioning a list containing [a, b, c, d, e] with a partition size of 3 yields
* [[a, b, c], [d, e]] -- an outer list containing two inner lists of three and two elements, all
* in the original order.
*
* The outer list is unmodifiable, but reflects the latest state of the source list. The inner
* lists are sublist views of the original list, produced on demand using List.subList(int, int),
* and are subject to all the usual caveats about modification as explained in that API.
*
* @param list the list to return consecutive sublists of
* @param size the desired size of each sublist (the last may be smaller)
* @return a list of consecutive sublists
*/
/* package */ static <T> List<List<T>> partition(List<T> list, int size) {
return new Partition<>(list, size);
}
private static class Partition<T> extends AbstractList<List<T>> {
private final List<T> list;
private final int size;
public Partition(List<T> list, int size) {
this.list = list;
this.size = size;
}
@Override
public List<T> get(int location) {
int start = location * size;
int end = Math.min(start + size, list.size());
return list.subList(start, end);
}
@Override
public int size() {
return (int) Math.ceil((double)list.size() / size);
}
}
}

View File

@ -0,0 +1,205 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.io.File;
import java.io.IOException;
import java.util.Random;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Manages a set of local ids and possible mappings to global Parse objectIds. This class is
* thread-safe.
*/
/** package */ class LocalIdManager {
/**
* Internal class representing all the information we know about a local id.
*/
private static class MapEntry {
String objectId;
int retainCount;
}
// Path to the local id storage on disk.
private final File diskPath;
// Random generator for inventing new ids.
private final Random random;
/**
* Creates a new LocalIdManager with default options.
*/
/* package for tests */ LocalIdManager(File root) {
diskPath = new File(root, "LocalId");
random = new Random();
}
/**
* Returns true if localId has the right basic format for a local id.
*/
private boolean isLocalId(String localId) {
if (!localId.startsWith("local_")) {
return false;
}
for (int i = 6; i < localId.length(); ++i) {
char c = localId.charAt(i);
if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f')) {
return false;
}
}
return true;
}
/**
* Grabs one entry in the local id map off the disk.
*/
private synchronized MapEntry getMapEntry(String localId) {
if (!isLocalId(localId)) {
throw new IllegalStateException("Tried to get invalid local id: \"" + localId + "\".");
}
try {
JSONObject json = ParseFileUtils.readFileToJSONObject(new File(diskPath, localId));
MapEntry entry = new MapEntry();
entry.retainCount = json.optInt("retainCount", 0);
entry.objectId = json.optString("objectId", null);
return entry;
} catch (IOException | JSONException e) {
return new MapEntry();
}
}
/**
* Writes one entry to the local id map on disk.
*/
private synchronized void putMapEntry(String localId, MapEntry entry) {
if (!isLocalId(localId)) {
throw new IllegalStateException("Tried to get invalid local id: \"" + localId + "\".");
}
JSONObject json = new JSONObject();
try {
json.put("retainCount", entry.retainCount);
if (entry.objectId != null) {
json.put("objectId", entry.objectId);
}
} catch (JSONException je) {
throw new IllegalStateException("Error creating local id map entry.", je);
}
File file = new File(diskPath, localId);
if (!diskPath.exists()) {
diskPath.mkdirs();
}
try {
ParseFileUtils.writeJSONObjectToFile(file, json);
} catch (IOException e) {
//TODO (grantland): We should do something if this fails...
}
}
/**
* Removes an entry from the local id map on disk.
*/
private synchronized void removeMapEntry(String localId) {
if (!isLocalId(localId)) {
throw new IllegalStateException("Tried to get invalid local id: \"" + localId + "\".");
}
File file = new File(diskPath, localId);
ParseFileUtils.deleteQuietly(file);
}
/**
* Creates a new local id.
*/
synchronized String createLocalId() {
long localIdNumber = random.nextLong();
String localId = "local_" + Long.toHexString(localIdNumber);
if (!isLocalId(localId)) {
throw new IllegalStateException("Generated an invalid local id: \"" + localId + "\". "
+ "This should never happen. Open a bug at https://github.com/parse-community/parse-server");
}
return localId;
}
/**
* Increments the retain count of a local id on disk.
*/
synchronized void retainLocalIdOnDisk(String localId) {
MapEntry entry = getMapEntry(localId);
entry.retainCount++;
putMapEntry(localId, entry);
}
/**
* Decrements the retain count of a local id on disk. If the retain count hits zero, the id is
* forgotten forever.
*/
synchronized void releaseLocalIdOnDisk(String localId) {
MapEntry entry = getMapEntry(localId);
entry.retainCount--;
if (entry.retainCount > 0) {
putMapEntry(localId, entry);
} else {
removeMapEntry(localId);
}
}
/**
* Returns the objectId associated with a given local id. Returns null if no objectId is yet known
* for the local id.
*/
synchronized String getObjectId(String localId) {
MapEntry entry = getMapEntry(localId);
return entry.objectId;
}
/**
* Sets the objectId associated with a given local id.
*/
synchronized void setObjectId(String localId, String objectId) {
MapEntry entry = getMapEntry(localId);
if (entry.retainCount > 0) {
if (entry.objectId != null) {
throw new IllegalStateException(
"Tried to set an objectId for a localId that already has one.");
}
entry.objectId = objectId;
putMapEntry(localId, entry);
}
}
/**
* Clears all local ids from the map. Returns true is the cache was already empty.
*/
synchronized boolean clear() throws IOException {
String[] files = diskPath.list();
if (files == null) {
return false;
}
if (files.length == 0) {
return false;
}
for (String fileName : files) {
File file = new File(diskPath, fileName);
if (!file.delete()) {
throw new IOException("Unable to delete file " + fileName + " in localId cache.");
}
}
return true;
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/**
* A {@code LocationCallback} is used to run code after a Location has been fetched by
* {@link com.parse.ParseGeoPoint#getCurrentLocationInBackground(long, android.location.Criteria)}.
* <p/>
* The easiest way to use a {@code LocationCallback} is through an anonymous inner class. Override the
* {@code done} function to specify what the callback should do after the location has been
* fetched. The {@code done} function will be run in the UI thread, while the location check
* happens in a background thread. This ensures that the UI does not freeze while the fetch happens.
* <p/>
* For example, this sample code defines a timeout for fetching the user's current location, and
* provides a callback. Within the callback, the success and failure cases are handled differently.
* <p/>
* <pre>
* ParseGeoPoint.getCurrentLocationAsync(1000, new LocationCallback() {
* public void done(ParseGeoPoint geoPoint, ParseException e) {
* if (e == null) {
* // do something with your new ParseGeoPoint
* } else {
* // handle your error
* e.printStackTrace();
* }
* }
* });
* </pre>
*/
public interface LocationCallback extends ParseCallback2<ParseGeoPoint, ParseException> {
/**
* Override this function with the code you want to run after the location fetch is complete.
*
* @param geoPoint
* The {@link ParseGeoPoint} returned by the location fetch.
* @param e
* The exception raised by the location fetch, or {@code null} if it succeeded.
*/
@Override
void done(ParseGeoPoint geoPoint, ParseException e);
}

View File

@ -0,0 +1,118 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.content.Context;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import bolts.Capture;
import bolts.Task;
import bolts.TaskCompletionSource;
/**
* LocationNotifier is a wrapper around fetching the current device's location. It looks for the GPS
* and Network LocationProviders by default (printStackTrace()'ing if, for example, the app doesn't
* have the correct permissions in its AndroidManifest.xml). This class is intended to be used for a
* <i>single</i> location update.
* <p>
* When testing, if a fakeLocation is provided (via setFakeLocation()), we don't wait for the
* LocationManager to fire or for the timer to run out; instead, we build a local LocationListener,
* then call the onLocationChanged() method manually.
*/
/** package */ class LocationNotifier {
private static Location fakeLocation = null;
/**
* Asynchronously gets the current location of the device.
*
* This will request location updates from the best provider that match the given criteria
* and return the first location received.
*
* You can customize the criteria to meet your specific needs.
* * For higher accuracy, you can set {@link Criteria#setAccuracy(int)}, however result in longer
* times for a fix.
* * For better battery efficiency and faster location fixes, you can set
* {@link Criteria#setPowerRequirement(int)}, however, this will result in lower accuracy.
*
* @param context
* The context used to request location updates.
* @param timeout
* The number of milliseconds to allow before timing out.
* @param criteria
* The application criteria for selecting a location provider.
*
* @see android.location.LocationManager#getBestProvider(android.location.Criteria, boolean)
* @see android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener)
*/
/* package */ static Task<Location> getCurrentLocationAsync(Context context,
long timeout, Criteria criteria) {
final TaskCompletionSource<Location> tcs = new TaskCompletionSource<>();
final Capture<ScheduledFuture<?>> timeoutFuture = new Capture<>();
final LocationManager manager =
(LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
final LocationListener listener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
if (location == null) {
return;
}
timeoutFuture.get().cancel(true);
tcs.trySetResult(location);
manager.removeUpdates(this);
}
@Override
public void onProviderDisabled(String provider) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
};
timeoutFuture.set(ParseExecutors.scheduled().schedule(new Runnable() {
@Override
public void run() {
tcs.trySetError(new ParseException(ParseException.TIMEOUT, "Location fetch timed out."));
manager.removeUpdates(listener);
}
}, timeout, TimeUnit.MILLISECONDS));
String provider = manager.getBestProvider(criteria, true);
if (provider != null) {
manager.requestLocationUpdates(provider, /* minTime */ 0, /* minDistance */ 0.0f, listener);
}
if (fakeLocation != null) {
listener.onLocationChanged(fakeLocation);
}
return tcs.getTask();
}
/**
* Helper method for testing.
*/
/* package */ static void setFakeLocation(Location location) {
fakeLocation = location;
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
import java.util.WeakHashMap;
import java.util.concurrent.locks.Lock;
/** package */ class LockSet {
private static final WeakHashMap<Lock, Long> stableIds = new WeakHashMap<>();
private static long nextStableId = 0L;
private final Set<Lock> locks;
public LockSet(Collection<Lock> locks) {
this.locks = new TreeSet<>(new Comparator<Lock>() {
@Override
public int compare(Lock lhs, Lock rhs) {
Long lhsId = getStableId(lhs);
Long rhsId = getStableId(rhs);
return lhsId.compareTo(rhsId);
}
});
this.locks.addAll(locks);
}
private static Long getStableId(Lock lock) {
synchronized (stableIds) {
if (stableIds.containsKey(lock)) {
return stableIds.get(lock);
}
long id = nextStableId++;
stableIds.put(lock, id);
return id;
}
}
public void lock() {
for (Lock l : locks) {
l.lock();
}
}
public void unlock() {
for (Lock l : locks) {
l.unlock();
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/**
* A {@code LogInCallback} is used to run code after logging in a user.
* <p/>
* The easiest way to use a {@code LogInCallback} is through an anonymous inner class. Override the
* {@code done} function to specify what the callback should do after the login is complete.
* The {@code done} function will be run in the UI thread, while the login happens in a
* background thread. This ensures that the UI does not freeze while the save happens.
* <p/>
* For example, this sample code logs in a user and calls a different function depending on whether
* the login succeeded or not.
* <p/>
* <pre>
* ParseUser.logInInBackground(&quot;username&quot;, &quot;password&quot;, new LogInCallback() {
* public void done(ParseUser user, ParseException e) {
* if (e == null &amp;&amp; user != null) {
* loginSuccessful();
* } else if (user == null) {
* usernameOrPasswordIsInvalid();
* } else {
* somethingWentWrong();
* }
* }
* });
* </pre>
*/
public interface LogInCallback extends ParseCallback2<ParseUser, ParseException> {
/**
* Override this function with the code you want to run after the save is complete.
*
* @param user
* The user that logged in, if the username and password is valid.
* @param e
* The exception raised by the login, or {@code null} if it succeeded.
*/
@Override
void done(ParseUser user, ParseException e);
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/**
* A {@code LogOutCallback} is used to run code after logging out a user.
* <p/>
* The easiest way to use a {@code LogOutCallback} is through an anonymous inner class. Override the
* {@code done} function to specify what the callback should do after the login is complete.
* The {@code done} function will be run in the UI thread, while the login happens in a
* background thread. This ensures that the UI does not freeze while the save happens.
* <p/>
* For example, this sample code logs out a user and calls a different function depending on whether
* the log out succeeded or not.
* <p/>
* <pre>
* ParseUser.logOutInBackground(new LogOutCallback() {
* public void done(ParseException e) {
* if (e == null) {
* logOutSuccessful();
* } else {
* somethingWentWrong();
* }
* }
* });
* </pre>
*/
public interface LogOutCallback extends ParseCallback1<ParseException> {
/**
* Override this function with the code you want to run after the save is complete.
*
* @param e
* The exception raised by the log out, or {@code null} if it succeeded.
*/
@Override
void done(ParseException e);
}

View File

@ -0,0 +1,354 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Bundle;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* A utility class for retrieving app metadata such as the app name, default icon, whether or not
* the app declares the correct permissions for push, etc.
*/
/** package */ class ManifestInfo {
private static final String TAG = "com.parse.ManifestInfo";
private static final Object lock = new Object();
private static long lastModified = -1;
/* package */ static int versionCode = -1;
/* package */ static String versionName = null;
private static int iconId = 0;
private static String displayName = null;
private static PushType pushType;
/**
* Returns the last time this application's APK was modified on disk. This is a proxy for both
* version changes and if the APK has been restored from backup onto a different device.
*/
public static long getLastModified() {
synchronized (lock) {
if (lastModified == -1) {
File apkPath = new File(getContext().getApplicationInfo().sourceDir);
lastModified = apkPath.lastModified();
}
}
return lastModified;
}
/**
* Returns the version code for this app, as specified by the android:versionCode attribute in the
* <manifest> element of the manifest.
*/
public static int getVersionCode() {
synchronized (lock) {
if (versionCode == -1) {
try {
versionCode = getPackageManager().getPackageInfo(getContext().getPackageName(), 0).versionCode;
} catch (NameNotFoundException e) {
PLog.e(TAG, "Couldn't find info about own package", e);
}
}
}
return versionCode;
}
/**
* Returns the version name for this app, as specified by the android:versionName attribute in the
* <manifest> element of the manifest.
*/
public static String getVersionName() {
synchronized (lock) {
if (versionName == null) {
try {
versionName = getPackageManager().getPackageInfo(getContext().getPackageName(), 0).versionName;
} catch (NameNotFoundException e) {
PLog.e(TAG, "Couldn't find info about own package", e);
}
}
}
return versionName;
}
/**
* Returns the display name of the app used by the app launcher, as specified by the android:label
* attribute in the <application> element of the manifest.
*/
public static String getDisplayName(Context context) {
synchronized (lock) {
if (displayName == null) {
ApplicationInfo appInfo = context.getApplicationInfo();
displayName = context.getPackageManager().getApplicationLabel(appInfo).toString();
}
}
return displayName;
}
/**
* Returns the default icon id used by this application, as specified by the android:icon
* attribute in the <application> element of the manifest.
*/
public static int getIconId() {
synchronized (lock) {
if (iconId == 0) {
iconId = getContext().getApplicationInfo().icon;
}
}
return iconId;
}
/**
* Returns whether the given action has an associated receiver defined in the manifest.
*/
/* package */ static boolean hasIntentReceiver(String action) {
return !getIntentReceivers(action).isEmpty();
}
/**
* Returns a list of ResolveInfo objects corresponding to the BroadcastReceivers with Intent Filters
* specifying the given action within the app's package.
*/
/* package */ static List<ResolveInfo> getIntentReceivers(String... actions) {
Context context = getContext();
PackageManager pm = context.getPackageManager();
String packageName = context.getPackageName();
List<ResolveInfo> list = new ArrayList<>();
for (String action : actions) {
list.addAll(pm.queryBroadcastReceivers(
new Intent(action),
PackageManager.GET_INTENT_FILTERS));
}
for (int i = list.size() - 1; i >= 0; --i) {
String receiverPackageName = list.get(i).activityInfo.packageName;
if (!receiverPackageName.equals(packageName)) {
list.remove(i);
}
}
return list;
}
// Should only be used for tests.
static void setPushType(PushType newPushType) {
synchronized (lock) {
pushType = newPushType;
}
}
/**
* Inspects the app's manifest and returns whether the manifest contains required declarations to
* be able to use GCM for push.
*/
public static PushType getPushType() {
synchronized (lock) {
if (pushType == null) {
pushType = findPushType();
PLog.v(TAG, "Using " + pushType + " for push.");
}
}
return pushType;
}
private static PushType findPushType() {
if (!ParsePushBroadcastReceiver.isSupported()) {
return PushType.NONE;
}
if (!PushServiceUtils.isSupported()) {
return PushType.NONE;
}
// Ordered by preference.
PushType[] types = PushType.types();
for (PushType type : types) {
PushHandler handler = PushHandler.Factory.create(type);
PushHandler.SupportLevel level = handler.isSupported();
String message = handler.getWarningMessage(level);
switch (level) {
case MISSING_REQUIRED_DECLARATIONS: // Can't use. notify.
if (message != null) PLog.e(TAG, message);
break;
case MISSING_OPTIONAL_DECLARATIONS: // Using anyway.
if (message != null) PLog.w(TAG, message);
return type;
case SUPPORTED:
return type;
}
}
return PushType.NONE;
}
/*
* Returns a message that can be written to the system log if an app expects push to be enabled,
* but push isn't actually enabled because the manifest is misconfigured.
*/
static String getPushDisabledMessage() {
return "Push is not configured for this app because the app manifest is missing required " +
"declarations. To configure GCM, please add the following declarations to your app manifest: " +
GcmPushHandler.getWarningMessage();
}
private static Context getContext() {
return Parse.getApplicationContext();
}
private static PackageManager getPackageManager() {
return getContext().getPackageManager();
}
private static ApplicationInfo getApplicationInfo(Context context, int flags) {
try {
return context.getPackageManager().getApplicationInfo(context.getPackageName(), flags);
} catch (NameNotFoundException e) {
return null;
}
}
/**
* @return A {@link Bundle} if meta-data is specified in AndroidManifest, otherwise null.
*/
public static Bundle getApplicationMetadata(Context context) {
ApplicationInfo info = getApplicationInfo(context, PackageManager.GET_META_DATA);
if (info != null) {
return info.metaData;
}
return null;
}
private static PackageInfo getPackageInfo(String name) {
PackageInfo info = null;
try {
info = getPackageManager().getPackageInfo(name, 0);
} catch (NameNotFoundException e) {
// do nothing
}
return info;
}
static ServiceInfo getServiceInfo(Class<? extends Service> clazz) {
ServiceInfo info = null;
try {
info = getPackageManager().getServiceInfo(new ComponentName(getContext(), clazz), 0);
} catch (NameNotFoundException e) {
// do nothing
}
return info;
}
private static ActivityInfo getReceiverInfo(Class<? extends BroadcastReceiver> clazz) {
ActivityInfo info = null;
try {
info = getPackageManager().getReceiverInfo(new ComponentName(getContext(), clazz), 0);
} catch (NameNotFoundException e) {
// do nothing
}
return info;
}
/**
* Returns {@code true} if this package has requested all of the listed permissions.
* <p />
* <strong>Note:</strong> This package might have requested all the permissions, but may not
* be granted all of them.
*/
static boolean hasRequestedPermissions(Context context, String... permissions) {
String packageName = context.getPackageName();
try {
PackageInfo pi = context.getPackageManager().getPackageInfo(
packageName, PackageManager.GET_PERMISSIONS);
if (pi.requestedPermissions == null) {
return false;
}
return Arrays.asList(pi.requestedPermissions).containsAll(Arrays.asList(permissions));
} catch (NameNotFoundException e) {
PLog.e(TAG, "Couldn't find info about own package", e);
return false;
}
}
/**
* Returns {@code true} if this package has been granted all of the listed permissions.
* <p />
* <strong>Note:</strong> This package might have requested all the permissions, but may not
* be granted all of them.
*/
static boolean hasGrantedPermissions(Context context, String... permissions) {
String packageName = context.getPackageName();
PackageManager packageManager = context.getPackageManager();
for (String permission : permissions) {
if (packageManager.checkPermission(permission, packageName) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
private static boolean checkResolveInfo(Class<? extends BroadcastReceiver> clazz, List<ResolveInfo> infoList, String permission) {
for (ResolveInfo info : infoList) {
if (info.activityInfo != null) {
final Class resolveInfoClass;
try {
resolveInfoClass = Class.forName(info.activityInfo.name);
} catch (ClassNotFoundException e) {
break;
}
if (clazz.isAssignableFrom(resolveInfoClass) && (permission == null || permission.equals(info.activityInfo.permission))) {
return true;
}
}
}
return false;
}
static boolean checkReceiver(Class<? extends BroadcastReceiver> clazz, String permission, Intent[] intents) {
for (Intent intent : intents) {
List<ResolveInfo> receivers = getPackageManager().queryBroadcastReceivers(intent, 0);
if (receivers.isEmpty()) {
return false;
}
if (!checkResolveInfo(clazz, receivers, permission)) {
return false;
}
}
return true;
}
static boolean isGooglePlayServicesAvailable() {
return getPackageInfo("com.google.android.gsf") != null;
}
}

View File

@ -0,0 +1,151 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import bolts.Continuation;
import bolts.Task;
/** package */ class NetworkObjectController implements ParseObjectController {
private ParseHttpClient client;
private ParseObjectCoder coder;
public NetworkObjectController(ParseHttpClient client) {
this.client = client;
this.coder = ParseObjectCoder.get();
}
@Override
public Task<ParseObject.State> fetchAsync(
final ParseObject.State state, String sessionToken, final ParseDecoder decoder) {
final ParseRESTCommand command = ParseRESTObjectCommand.getObjectCommand(
state.objectId(),
state.className(),
sessionToken);
return command.executeAsync(client).onSuccess(new Continuation<JSONObject, ParseObject.State>() {
@Override
public ParseObject.State then(Task<JSONObject> task) throws Exception {
JSONObject result = task.getResult();
// Copy and clear to create an new empty instance of the same type as `state`
ParseObject.State.Init<?> builder = state.newBuilder().clear();
return coder.decode(builder, result, decoder)
.isComplete(true)
.build();
}
});
}
@Override
public Task<ParseObject.State> saveAsync(
final ParseObject.State state,
final ParseOperationSet operations,
String sessionToken,
final ParseDecoder decoder) {
/*
* Get the JSON representation of the object, and use some of the information to construct the
* command.
*/
JSONObject objectJSON = coder.encode(state, operations, PointerEncoder.get());
ParseRESTObjectCommand command = ParseRESTObjectCommand.saveObjectCommand(
state,
objectJSON,
sessionToken);
return command.executeAsync(client).onSuccess(new Continuation<JSONObject, ParseObject.State>() {
@Override
public ParseObject.State then(Task<JSONObject> task) throws Exception {
JSONObject result = task.getResult();
// Copy and clear to create an new empty instance of the same type as `state`
ParseObject.State.Init<?> builder = state.newBuilder().clear();
return coder.decode(builder, result, decoder)
.isComplete(false)
.build();
}
});
}
@Override
public List<Task<ParseObject.State>> saveAllAsync(
List<ParseObject.State> states,
List<ParseOperationSet> operationsList,
String sessionToken,
List<ParseDecoder> decoders) {
int batchSize = states.size();
List<ParseRESTObjectCommand> commands = new ArrayList<>(batchSize);
ParseEncoder encoder = PointerEncoder.get();
for (int i = 0; i < batchSize; i++) {
ParseObject.State state = states.get(i);
ParseOperationSet operations = operationsList.get(i);
JSONObject objectJSON = coder.encode(state, operations, encoder);
ParseRESTObjectCommand command = ParseRESTObjectCommand.saveObjectCommand(
state, objectJSON, sessionToken);
commands.add(command);
}
final List<Task<JSONObject>> batchTasks =
ParseRESTObjectBatchCommand.executeBatch(client, commands, sessionToken);
final List<Task<ParseObject.State>> tasks = new ArrayList<>(batchSize);
for (int i = 0; i < batchSize; i++) {
final ParseObject.State state = states.get(i);
final ParseDecoder decoder = decoders.get(i);
tasks.add(batchTasks.get(i).onSuccess(new Continuation<JSONObject, ParseObject.State>() {
@Override
public ParseObject.State then(Task<JSONObject> task) throws Exception {
JSONObject result = task.getResult();
// Copy and clear to create an new empty instance of the same type as `state`
ParseObject.State.Init<?> builder = state.newBuilder().clear();
return coder.decode(builder, result, decoder)
.isComplete(false)
.build();
}
}));
}
return tasks;
}
@Override
public Task<Void> deleteAsync(ParseObject.State state, String sessionToken) {
ParseRESTObjectCommand command = ParseRESTObjectCommand.deleteObjectCommand(
state, sessionToken);
return command.executeAsync(client).makeVoid();
}
@Override
public List<Task<Void>> deleteAllAsync(
List<ParseObject.State> states, String sessionToken) {
int batchSize = states.size();
List<ParseRESTObjectCommand> commands = new ArrayList<>(batchSize);
for (int i = 0; i < batchSize; i++) {
ParseObject.State state = states.get(i);
ParseRESTObjectCommand command = ParseRESTObjectCommand.deleteObjectCommand(
state, sessionToken);
commands.add(command);
}
final List<Task<JSONObject>> batchTasks =
ParseRESTObjectBatchCommand.executeBatch(client, commands, sessionToken);
List<Task<Void>> tasks = new ArrayList<>(batchSize);
for (int i = 0; i < batchSize; i++) {
tasks.add(batchTasks.get(i).makeVoid());
}
return tasks;
}
}

View File

@ -0,0 +1,148 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import bolts.Continuation;
import bolts.Task;
/** package */ class NetworkQueryController extends AbstractQueryController {
private static final String TAG = "NetworkQueryController";
private final ParseHttpClient restClient;
public NetworkQueryController(ParseHttpClient restClient) {
this.restClient = restClient;
}
@Override
public <T extends ParseObject> Task<List<T>> findAsync(
ParseQuery.State<T> state, ParseUser user, Task<Void> cancellationToken) {
String sessionToken = user != null ? user.getSessionToken() : null;
return findAsync(state, sessionToken, cancellationToken);
}
@Override
public <T extends ParseObject> Task<Integer> countAsync(
ParseQuery.State<T> state, ParseUser user, Task<Void> cancellationToken) {
String sessionToken = user != null ? user.getSessionToken() : null;
return countAsync(state, sessionToken, cancellationToken);
}
/**
* Retrieves a list of {@link ParseObject}s that satisfy this query from the source.
*
* @return A list of all {@link ParseObject}s obeying the conditions set in this query.
*/
/* package */ <T extends ParseObject> Task<List<T>> findAsync(
final ParseQuery.State<T> state,
String sessionToken,
Task<Void> ct) {
final long queryStart = System.nanoTime();
final ParseRESTCommand command = ParseRESTQueryCommand.findCommand(state, sessionToken);
final long querySent = System.nanoTime();
return command.executeAsync(restClient, ct).onSuccess(new Continuation<JSONObject, List<T>>() {
@Override
public List<T> then(Task<JSONObject> task) throws Exception {
JSONObject json = task.getResult();
// Cache the results, unless we are ignoring the cache
ParseQuery.CachePolicy policy = state.cachePolicy();
if (policy != null && (policy != ParseQuery.CachePolicy.IGNORE_CACHE)) {
ParseKeyValueCache.saveToKeyValueCache(command.getCacheKey(), json.toString());
}
long queryReceived = System.nanoTime();
List<T> response = convertFindResponse(state, task.getResult());
long objectsParsed = System.nanoTime();
if (json.has("trace")) {
Object serverTrace = json.get("trace");
PLog.d("ParseQuery",
String.format("Query pre-processing took %f seconds\n" +
"%s\n" +
"Client side parsing took %f seconds\n",
(querySent - queryStart) / (1000.0f * 1000.0f),
serverTrace,
(objectsParsed - queryReceived) / (1000.0f * 1000.0f)));
}
return response;
}
}, Task.BACKGROUND_EXECUTOR);
}
/* package */ <T extends ParseObject> Task<Integer> countAsync(
final ParseQuery.State<T> state,
String sessionToken,
Task<Void> ct) {
final ParseRESTCommand command = ParseRESTQueryCommand.countCommand(state, sessionToken);
return command.executeAsync(restClient, ct).onSuccessTask(new Continuation<JSONObject, Task<JSONObject>>() {
@Override
public Task<JSONObject> then(Task<JSONObject> task) throws Exception {
// Cache the results, unless we are ignoring the cache
ParseQuery.CachePolicy policy = state.cachePolicy();
if (policy != null && policy != ParseQuery.CachePolicy.IGNORE_CACHE) {
JSONObject result = task.getResult();
ParseKeyValueCache.saveToKeyValueCache(command.getCacheKey(), result.toString());
}
return task;
}
}, Task.BACKGROUND_EXECUTOR).onSuccess(new Continuation<JSONObject, Integer>() {
@Override
public Integer then(Task<JSONObject> task) throws Exception {
// Convert response
return task.getResult().optInt("count");
}
});
}
// Converts the JSONArray that represents the results of a find command to an
// ArrayList<ParseObject>.
/* package */ <T extends ParseObject> List<T> convertFindResponse(ParseQuery.State<T> state,
JSONObject response) throws JSONException {
ArrayList<T> answer = new ArrayList<>();
JSONArray results = response.getJSONArray("results");
if (results == null) {
PLog.d(TAG, "null results in find response");
} else {
String resultClassName = response.optString("className", null);
if (resultClassName == null) {
resultClassName = state.className();
}
for (int i = 0; i < results.length(); ++i) {
JSONObject data = results.getJSONObject(i);
T object = ParseObject.fromJSON(data, resultClassName, ParseDecoder.get(), state.selectedKeys());
answer.add(object);
/*
* If there was a $relatedTo constraint on the query, then add any results to the list of
* known objects in the relation for offline caching
*/
ParseQuery.RelationConstraint relation =
(ParseQuery.RelationConstraint) state.constraints().get("$relatedTo");
if (relation != null) {
relation.getRelation().addKnownObject(object);
}
}
}
return answer;
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONObject;
import bolts.Continuation;
import bolts.Task;
/** package */ class NetworkSessionController implements ParseSessionController {
private final ParseHttpClient client;
private final ParseObjectCoder coder;
public NetworkSessionController(ParseHttpClient client) {
this.client = client;
this.coder = ParseObjectCoder.get(); // TODO(grantland): Inject
}
@Override
public Task<ParseObject.State> getSessionAsync(String sessionToken) {
ParseRESTSessionCommand command =
ParseRESTSessionCommand.getCurrentSessionCommand(sessionToken);
return command.executeAsync(client).onSuccess(new Continuation<JSONObject, ParseObject.State>() {
@Override
public ParseObject.State then(Task<JSONObject> task) throws Exception {
JSONObject result = task.getResult();
return coder.decode(new ParseObject.State.Builder("_Session"), result, ParseDecoder.get())
.isComplete(true)
.build();
}
});
}
@Override
public Task<Void> revokeAsync(String sessionToken) {
return ParseRESTSessionCommand.revoke(sessionToken)
.executeAsync(client)
.makeVoid();
}
@Override
public Task<ParseObject.State> upgradeToRevocable(String sessionToken) {
ParseRESTSessionCommand command =
ParseRESTSessionCommand.upgradeToRevocableSessionCommand(sessionToken);
return command.executeAsync(client).onSuccess(new Continuation<JSONObject, ParseObject.State>() {
@Override
public ParseObject.State then(Task<JSONObject> task) throws Exception {
JSONObject result = task.getResult();
return coder.decode(new ParseObject.State.Builder("_Session"), result, ParseDecoder.get())
.isComplete(true)
.build();
}
});
}
}

View File

@ -0,0 +1,142 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONObject;
import java.util.Map;
import bolts.Continuation;
import bolts.Task;
/** package */ class NetworkUserController implements ParseUserController {
private static final int STATUS_CODE_CREATED = 201;
private final ParseHttpClient client;
private final ParseObjectCoder coder;
private final boolean revocableSession;
public NetworkUserController(ParseHttpClient client) {
this(client, false);
}
public NetworkUserController(ParseHttpClient client, boolean revocableSession) {
this.client = client;
this.coder = ParseObjectCoder.get(); // TODO(grantland): Inject
this.revocableSession = revocableSession;
}
@Override
public Task<ParseUser.State> signUpAsync(
final ParseObject.State state,
ParseOperationSet operations,
String sessionToken) {
JSONObject objectJSON = coder.encode(state, operations, PointerEncoder.get());
ParseRESTCommand command = ParseRESTUserCommand.signUpUserCommand(
objectJSON, sessionToken, revocableSession);
return command.executeAsync(client).onSuccess(new Continuation<JSONObject, ParseUser.State>() {
@Override
public ParseUser.State then(Task<JSONObject> task) throws Exception {
JSONObject result = task.getResult();
return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get())
.isComplete(false)
.isNew(true)
.build();
}
});
}
//region logInAsync
@Override
public Task<ParseUser.State> logInAsync(
String username, String password) {
ParseRESTCommand command = ParseRESTUserCommand.logInUserCommand(
username, password, revocableSession);
return command.executeAsync(client).onSuccess(new Continuation<JSONObject, ParseUser.State>() {
@Override
public ParseUser.State then(Task<JSONObject> task) throws Exception {
JSONObject result = task.getResult();
return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get())
.isComplete(true)
.build();
}
});
}
@Override
public Task<ParseUser.State> logInAsync(
ParseUser.State state, ParseOperationSet operations) {
JSONObject objectJSON = coder.encode(state, operations, PointerEncoder.get());
final ParseRESTUserCommand command = ParseRESTUserCommand.serviceLogInUserCommand(
objectJSON, state.sessionToken(), revocableSession);
return command.executeAsync(client).onSuccess(new Continuation<JSONObject, ParseUser.State>() {
@Override
public ParseUser.State then(Task<JSONObject> task) throws Exception {
JSONObject result = task.getResult();
// TODO(grantland): Does the server really respond back with complete object data if the
// object isn't new?
boolean isNew = command.getStatusCode() == STATUS_CODE_CREATED;
boolean isComplete = !isNew;
return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get())
.isComplete(isComplete)
.isNew(isNew)
.build();
}
});
}
@Override
public Task<ParseUser.State> logInAsync(
final String authType, final Map<String, String> authData) {
final ParseRESTUserCommand command = ParseRESTUserCommand.serviceLogInUserCommand(
authType, authData, revocableSession);
return command.executeAsync(client).onSuccess(new Continuation<JSONObject, ParseUser.State>() {
@Override
public ParseUser.State then(Task<JSONObject> task) throws Exception {
JSONObject result = task.getResult();
return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get())
.isComplete(true)
.isNew(command.getStatusCode() == STATUS_CODE_CREATED)
.putAuthData(authType, authData)
.build();
}
});
}
//endregion
@Override
public Task<ParseUser.State> getUserAsync(String sessionToken) {
ParseRESTCommand command = ParseRESTUserCommand.getCurrentUserCommand(sessionToken);
return command.executeAsync(client).onSuccess(new Continuation<JSONObject, ParseUser.State>() {
@Override
public ParseUser.State then(Task<JSONObject> task) throws Exception {
JSONObject result = task.getResult();
return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get())
.isComplete(true)
.build();
}
});
}
@Override
public Task<Void> requestPasswordResetAsync(String email) {
ParseRESTCommand command = ParseRESTUserCommand.resetPasswordResetCommand(email);
return command.executeAsync(client).makeVoid();
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONObject;
/**
* Throws an exception if someone attemps to encode a {@code ParseObject}.
*/
/** package */ class NoObjectsEncoder extends ParseEncoder {
// This class isn't really a Singleton, but since it has no state, it's more efficient to get the
// default instance.
private static final NoObjectsEncoder INSTANCE = new NoObjectsEncoder();
public static NoObjectsEncoder get() {
return INSTANCE;
}
@Override
public JSONObject encodeRelatedObject(ParseObject object) {
throw new IllegalArgumentException("ParseObjects not allowed here");
}
}

View File

@ -0,0 +1,440 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.parse;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Build;
import android.widget.RemoteViews;
/**
* A simple implementation of the NotificationCompat class from android-support-v4
* It only differentiates between devices before and after JellyBean because the only extra
* feature that we currently support between the two device types is BigTextStyle notifications.
* This class takes advantage of lazy class loading to eliminate warnings of the type
* 'Could not find class...'
*/
/** package */ class NotificationCompat {
/**
* Obsolete flag indicating high-priority notifications; use the priority field instead.
*
* @deprecated Use {@link NotificationCompat.Builder#setPriority(int)} with a positive value.
*/
public static final int FLAG_HIGH_PRIORITY = 0x00000080;
/**
* Default notification priority for {@link NotificationCompat.Builder#setPriority(int)}.
* If your application does not prioritize its own notifications,
* use this value for all notifications.
*/
public static final int PRIORITY_DEFAULT = 0;
private static final NotificationCompatImpl IMPL;
interface NotificationCompatImpl {
Notification build(Builder b);
}
static class NotificationCompatImplBase implements NotificationCompatImpl {
@Override
public Notification build(Builder builder) {
Notification result = builder.mNotification;
NotificationCompat.Builder newBuilder = new NotificationCompat.Builder(builder.mContext);
newBuilder.setContentTitle(builder.mContentTitle);
newBuilder.setContentText(builder.mContentText);
newBuilder.setContentIntent(builder.mContentIntent);
// translate high priority requests into legacy flag
if (builder.mPriority > PRIORITY_DEFAULT) {
result.flags |= FLAG_HIGH_PRIORITY;
}
return newBuilder.build();
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
static class NotificationCompatPostJellyBean implements NotificationCompatImpl {
private Notification.Builder postJellyBeanBuilder;
@Override
public Notification build(Builder b) {
postJellyBeanBuilder = new Notification.Builder(b.mContext);
postJellyBeanBuilder.setContentTitle(b.mContentTitle)
.setContentText(b.mContentText)
.setTicker(b.mNotification.tickerText)
.setSmallIcon(b.mNotification.icon, b.mNotification.iconLevel)
.setContentIntent(b.mContentIntent)
.setDeleteIntent(b.mNotification.deleteIntent)
.setAutoCancel((b.mNotification.flags & Notification.FLAG_AUTO_CANCEL) != 0)
.setLargeIcon(b.mLargeIcon)
.setDefaults(b.mNotification.defaults);
if (b.mStyle != null) {
if (b.mStyle instanceof Builder.BigTextStyle) {
Builder.BigTextStyle staticStyle = (Builder.BigTextStyle) b.mStyle;
Notification.BigTextStyle style = new Notification.BigTextStyle(postJellyBeanBuilder)
.setBigContentTitle(staticStyle.mBigContentTitle)
.bigText(staticStyle.mBigText);
if (staticStyle.mSummaryTextSet) {
style.setSummaryText(staticStyle.mSummaryText);
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
postJellyBeanBuilder.setChannelId(b.mNotificationChannelId);
}
return postJellyBeanBuilder.build();
}
}
static {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
IMPL = new NotificationCompatPostJellyBean();
} else {
IMPL = new NotificationCompatImplBase();
}
}
public static class Builder {
/**
* Maximum length of CharSequences accepted by Builder and friends.
*
* <p>
* Avoids spamming the system with overly large strings such as full e-mails.
*/
private static final int MAX_CHARSEQUENCE_LENGTH = 5 * 1024;
Context mContext;
CharSequence mContentTitle;
CharSequence mContentText;
PendingIntent mContentIntent;
Bitmap mLargeIcon;
int mPriority;
Style mStyle;
String mNotificationChannelId;
Notification mNotification = new Notification();
/**
* Constructor.
*
* Automatically sets the when field to {@link System#currentTimeMillis()
* System.currentTimeMillis()} and the audio stream to the
* {@link Notification#STREAM_DEFAULT}.
*
* @param context A {@link Context} that will be used to construct the
* RemoteViews. The Context will not be held past the lifetime of this
* Builder object.
*/
public Builder(Context context) {
mContext = context;
// Set defaults to match the defaults of a Notification
mNotification.when = System.currentTimeMillis();
mNotification.audioStreamType = Notification.STREAM_DEFAULT;
mPriority = PRIORITY_DEFAULT;
}
public Builder setWhen(long when) {
mNotification.when = when;
return this;
}
/**
* Set the small icon to use in the notification layouts. Different classes of devices
* may return different sizes. See the UX guidelines for more information on how to
* design these icons.
*
* @param icon A resource ID in the application's package of the drawble to use.
*/
public Builder setSmallIcon(int icon) {
mNotification.icon = icon;
return this;
}
/**
* A variant of {@link #setSmallIcon(int) setSmallIcon(int)} that takes an additional
* level parameter for when the icon is a {@link android.graphics.drawable.LevelListDrawable
* LevelListDrawable}.
*
* @param icon A resource ID in the application's package of the drawble to use.
* @param level The level to use for the icon.
*
* @see android.graphics.drawable.LevelListDrawable
*/
public Builder setSmallIcon(int icon, int level) {
mNotification.icon = icon;
mNotification.iconLevel = level;
return this;
}
/**
* Set the title (first row) of the notification, in a standard notification.
*/
public Builder setContentTitle(CharSequence title) {
mContentTitle = limitCharSequenceLength(title);
return this;
}
/**
* Set the notification channel of the notification, in a standard notification.
*/
public Builder setNotificationChannel(String notificationChannelId) {
mNotificationChannelId = notificationChannelId;
return this;
}
/**
* Set the text (second row) of the notification, in a standard notification.
*/
public Builder setContentText(CharSequence text) {
mContentText = limitCharSequenceLength(text);
return this;
}
/**
* Supply a {@link PendingIntent} to send when the notification is clicked.
* If you do not supply an intent, you can now add PendingIntents to individual
* views to be launched when clicked by calling {@link RemoteViews#setOnClickPendingIntent
* RemoteViews.setOnClickPendingIntent(int,PendingIntent)}. Be sure to
* read {@link Notification#contentIntent Notification.contentIntent} for
* how to correctly use this.
*/
public Builder setContentIntent(PendingIntent intent) {
mContentIntent = intent;
return this;
}
/**
* Supply a {@link PendingIntent} to send when the notification is cleared by the user
* directly from the notification panel. For example, this intent is sent when the user
* clicks the "Clear all" button, or the individual "X" buttons on notifications. This
* intent is not sent when the application calls {@link NotificationManager#cancel
* NotificationManager.cancel(int)}.
*/
public Builder setDeleteIntent(PendingIntent intent) {
mNotification.deleteIntent = intent;
return this;
}
/**
* Set the text that is displayed in the status bar when the notification first
* arrives.
*/
public Builder setTicker(CharSequence tickerText) {
mNotification.tickerText = limitCharSequenceLength(tickerText);
return this;
}
/**
* Set the large icon that is shown in the ticker and notification.
*/
public Builder setLargeIcon(Bitmap icon) {
mLargeIcon = icon;
return this;
}
/**
* Setting this flag will make it so the notification is automatically
* canceled when the user clicks it in the panel. The PendingIntent
* set with {@link #setDeleteIntent} will be broadcast when the notification
* is canceled.
*/
public Builder setAutoCancel(boolean autoCancel) {
setFlag(Notification.FLAG_AUTO_CANCEL, autoCancel);
return this;
}
/**
* Set the default notification options that will be used.
* <p>
* The value should be one or more of the following fields combined with
* bitwise-or:
* {@link Notification#DEFAULT_SOUND}, {@link Notification#DEFAULT_VIBRATE},
* {@link Notification#DEFAULT_LIGHTS}.
* <p>
* For all default values, use {@link Notification#DEFAULT_ALL}.
*/
public Builder setDefaults(int defaults) {
mNotification.defaults = defaults;
if ((defaults & Notification.DEFAULT_LIGHTS) != 0) {
mNotification.flags |= Notification.FLAG_SHOW_LIGHTS;
}
return this;
}
private void setFlag(int mask, boolean value) {
if (value) {
mNotification.flags |= mask;
} else {
mNotification.flags &= ~mask;
}
}
/**
* Set the relative priority for this notification.
*
* Priority is an indication of how much of the user's
* valuable attention should be consumed by this
* notification. Low-priority notifications may be hidden from
* the user in certain situations, while the user might be
* interrupted for a higher-priority notification.
* The system sets a notification's priority based on various factors including the
* setPriority value. The effect may differ slightly on different platforms.
*/
public Builder setPriority(int pri) {
mPriority = pri;
return this;
}
/**
* Add a rich notification style to be applied at build time.
* <br>
* If the platform does not provide rich notification styles, this method has no effect. The
* user will always see the normal notification style.
*
* @param style Object responsible for modifying the notification style.
*/
public Builder setStyle(Style style) {
if (mStyle != style) {
mStyle = style;
if (mStyle != null) {
mStyle.setBuilder(this);
}
}
return this;
}
/**
* @deprecated Use {@link #build()} instead.
*/
@Deprecated
public Notification getNotification() {
return IMPL.build(this);
}
/**
* Combine all of the options that have been set and return a new {@link Notification}
* object.
*/
public Notification build() {
return IMPL.build(this);
}
protected static CharSequence limitCharSequenceLength(CharSequence cs) {
if (cs == null) return cs;
if (cs.length() > MAX_CHARSEQUENCE_LENGTH) {
cs = cs.subSequence(0, MAX_CHARSEQUENCE_LENGTH);
}
return cs;
}
/**
* An object that can apply a rich notification style to a {@link Notification.Builder}
* object.
* <br>
* If the platform does not provide rich notification styles, methods in this class have no
* effect.
*/
public static abstract class Style
{
Builder mBuilder;
CharSequence mBigContentTitle;
CharSequence mSummaryText;
boolean mSummaryTextSet = false;
public void setBuilder(Builder builder) {
if (mBuilder != builder) {
mBuilder = builder;
if (mBuilder != null) {
mBuilder.setStyle(this);
}
}
}
public Notification build() {
Notification notification = null;
if (mBuilder != null) {
notification = mBuilder.build();
}
return notification;
}
}
/**
* Helper class for generating large-format notifications that include a lot of text.
*
* <br>
* If the platform does not provide large-format notifications, this method has no effect. The
* user will always see the normal notification view.
* <br>
* This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so:
* <pre class="prettyprint">
* Notification noti = new Notification.Builder()
* .setContentTitle(&quot;New mail from &quot; + sender.toString())
* .setContentText(subject)
* .setSmallIcon(R.drawable.new_mail)
* .setLargeIcon(aBitmap)
* .setStyle(new Notification.BigTextStyle()
* .bigText(aVeryLongString))
* .build();
* </pre>
*
* @see Notification#bigContentView
*/
public static class BigTextStyle extends Style {
CharSequence mBigText;
public BigTextStyle() {
}
public BigTextStyle(Builder builder) {
setBuilder(builder);
}
/**
* Overrides ContentTitle in the big form of the template.
* This defaults to the value passed to setContentTitle().
*/
public BigTextStyle setBigContentTitle(CharSequence title) {
mBigContentTitle = title;
return this;
}
/**
* Set the first line of text after the detail section in the big form of the template.
*/
public BigTextStyle setSummaryText(CharSequence cs) {
mSummaryText = cs;
mSummaryTextSet = true;
return this;
}
/**
* Provide the longer text to be displayed in the big form of the
* template in place of the content text.
*/
public BigTextStyle bigText(CharSequence cs) {
mBigText = cs;
return this;
}
}
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/**
* Static utility methods pertaining to {@link Number} instances.
*/
/** package */ class Numbers {
/**
* Add two {@link Number} instances.
*/
/* package */ static Number add(Number first, Number second) {
if (first instanceof Double || second instanceof Double) {
return first.doubleValue() + second.doubleValue();
} else if (first instanceof Float || second instanceof Float) {
return first.floatValue() + second.floatValue();
} else if (first instanceof Long || second instanceof Long) {
return first.longValue() + second.longValue();
} else if (first instanceof Integer || second instanceof Integer) {
return first.intValue() + second.intValue();
} else if (first instanceof Short || second instanceof Short) {
return first.shortValue() + second.shortValue();
} else if (first instanceof Byte || second instanceof Byte) {
return first.byteValue() + second.byteValue();
} else {
throw new RuntimeException("Unknown number type.");
}
}
/**
* Subtract two {@link Number} instances.
*/
/* package */ static Number subtract(Number first, Number second) {
if (first instanceof Double || second instanceof Double) {
return first.doubleValue() - second.doubleValue();
} else if (first instanceof Float || second instanceof Float) {
return first.floatValue() - second.floatValue();
} else if (first instanceof Long || second instanceof Long) {
return first.longValue() - second.longValue();
} else if (first instanceof Integer || second instanceof Integer) {
return first.intValue() - second.intValue();
} else if (first instanceof Short || second instanceof Short) {
return first.shortValue() - second.shortValue();
} else if (first instanceof Byte || second instanceof Byte) {
return first.byteValue() - second.byteValue();
} else {
throw new RuntimeException("Unknown number type.");
}
}
/**
* Compare two {@link Number} instances.
*/
/* package */ static int compare(Number first, Number second) {
if (first instanceof Double || second instanceof Double) {
return (int) Math.signum(first.doubleValue() - second.doubleValue());
} else if (first instanceof Float || second instanceof Float) {
return (int) Math.signum(first.floatValue() - second.floatValue());
} else if (first instanceof Long || second instanceof Long) {
long diff = first.longValue() - second.longValue();
return (diff < 0) ? -1 : ((diff > 0) ? 1 : 0);
} else if (first instanceof Integer || second instanceof Integer) {
return first.intValue() - second.intValue();
} else if (first instanceof Short || second instanceof Short) {
return first.shortValue() - second.shortValue();
} else if (first instanceof Byte || second instanceof Byte) {
return first.byteValue() - second.byteValue();
} else {
throw new RuntimeException("Unknown number type.");
}
}
}

View File

@ -0,0 +1,134 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.Arrays;
import java.util.List;
import bolts.Continuation;
import bolts.Task;
/** package */ class OfflineObjectStore<T extends ParseObject> implements ParseObjectStore<T> {
private static ParseObjectSubclassingController getSubclassingController() {
return ParseCorePlugins.getInstance().getSubclassingController();
}
private static <T extends ParseObject> Task<T> migrate(
final ParseObjectStore<T> from, final ParseObjectStore<T> to) {
return from.getAsync().onSuccessTask(new Continuation<T, Task<T>>() {
@Override
public Task<T> then(Task<T> task) throws Exception {
final T object = task.getResult();
if (object == null) {
return task;
}
return Task.whenAll(Arrays.asList(
from.deleteAsync(),
to.setAsync(object)
)).continueWith(new Continuation<Void, T>() {
@Override
public T then(Task<Void> task) throws Exception {
return object;
}
});
}
});
}
private final String className;
private final String pinName;
private final ParseObjectStore<T> legacy;
public OfflineObjectStore(Class<T> clazz, String pinName, ParseObjectStore<T> legacy) {
this(getSubclassingController().getClassName(clazz), pinName, legacy);
}
public OfflineObjectStore(String className, String pinName, ParseObjectStore<T> legacy) {
this.className = className;
this.pinName = pinName;
this.legacy = legacy;
}
@Override
public Task<Void> setAsync(final T object) {
return ParseObject.unpinAllInBackground(pinName).continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return object.pinInBackground(pinName, false);
}
});
}
@Override
public Task<T> getAsync() {
// We need to set `ignoreACLs` since we can't use ACLs without the current user.
ParseQuery<T> query = ParseQuery.<T>getQuery(className)
.fromPin(pinName)
.ignoreACLs();
return query.findInBackground().onSuccessTask(new Continuation<List<T>, Task<T>>() {
@Override
public Task<T> then(Task<List<T>> task) throws Exception {
List<T> results = task.getResult();
if (results != null) {
if (results.size() == 1) {
return Task.forResult(results.get(0));
} else {
return ParseObject.unpinAllInBackground(pinName).cast();
}
}
return Task.forResult(null);
}
}).onSuccessTask(new Continuation<T, Task<T>>() {
@Override
public Task<T> then(Task<T> task) throws Exception {
T ldsObject = task.getResult();
if (ldsObject != null) {
return task;
}
return migrate(legacy, OfflineObjectStore.this).cast();
}
});
}
@Override
public Task<Boolean> existsAsync() {
// We need to set `ignoreACLs` since we can't use ACLs without the current user.
ParseQuery<T> query = ParseQuery.<T>getQuery(className)
.fromPin(pinName)
.ignoreACLs();
return query.countInBackground().onSuccessTask(new Continuation<Integer, Task<Boolean>>() {
@Override
public Task<Boolean> then(Task<Integer> task) throws Exception {
boolean exists = task.getResult() == 1;
if (exists) {
return Task.forResult(true);
}
return legacy.existsAsync();
}
});
}
@Override
public Task<Void> deleteAsync() {
final Task<Void> ldsTask = ParseObject.unpinAllInBackground(pinName);
return Task.whenAll(Arrays.asList(
legacy.deleteAsync(),
ldsTask
)).continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
// We only really care about the result of unpinning.
return ldsTask;
}
});
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.List;
import bolts.Continuation;
import bolts.Task;
/** package */ class OfflineQueryController extends AbstractQueryController {
private final OfflineStore offlineStore;
private final ParseQueryController networkController;
public OfflineQueryController(OfflineStore store, ParseQueryController network) {
offlineStore = store;
networkController = network;
}
@Override
public <T extends ParseObject> Task<List<T>> findAsync(
ParseQuery.State<T> state,
ParseUser user,
Task<Void> cancellationToken) {
if (state.isFromLocalDatastore()) {
return offlineStore.findFromPinAsync(state.pinName(), state, user);
} else {
return networkController.findAsync(state, user, cancellationToken);
}
}
@Override
public <T extends ParseObject> Task<Integer> countAsync(
ParseQuery.State<T> state,
ParseUser user,
Task<Void> cancellationToken) {
if (state.isFromLocalDatastore()) {
return offlineStore.countFromPinAsync(state.pinName(), state, user);
} else {
return networkController.countAsync(state, user, cancellationToken);
}
}
}

View File

@ -0,0 +1,109 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
/**
* This class just wraps a SQLiteDatabase with a better API. SQLite has a few limitations that this
* class works around. The primary problem is that if you call getWritableDatabase from multiple
* places, they all return the same instance, so you can't call "close" until you are done with all
* of them. SQLite also doesn't allow multiple transactions at the same time. We don't need
* transactions yet, but when we do, they will be part of this class. For convenience, this class
* also wraps database methods with methods that run them on a background thread and return a task.
*/
/** package */ class OfflineSQLiteOpenHelper extends ParseSQLiteOpenHelper {
/**
* The table that stores all ParseObjects.
*/
/* package */ static final String TABLE_OBJECTS = "ParseObjects";
/**
* Various keys in the table of ParseObjects.
*/
/* package */ /* package */ static final String KEY_UUID = "uuid";
/* package */ static final String KEY_CLASS_NAME = "className";
/* package */ static final String KEY_OBJECT_ID = "objectId";
/* package */ static final String KEY_JSON = "json";
/* package */ static final String KEY_IS_DELETING_EVENTUALLY = "isDeletingEventually";
/**
* The table that stores all Dependencies.
*/
/* package */ static final String TABLE_DEPENDENCIES = "Dependencies";
/**
* Various keys in the table of Dependencies.
*/
//TODO (grantland): rename this since we use UUIDs as keys now. root_uuid?
/* package */ static final String KEY_KEY = "key";
// static final String KEY_UUID = "uuid";
/**
* The SQLite Database name.
*/
private static final String DATABASE_NAME = "ParseOfflineStore";
private static final int DATABASE_VERSION = 4;
/**
* Creates a new helper for the database.
*/
public OfflineSQLiteOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
/**
* Initializes the schema for the database.
*/
private void createSchema(SQLiteDatabase db) {
String sql;
sql = "CREATE TABLE " + TABLE_OBJECTS + " (" +
KEY_UUID + " TEXT PRIMARY KEY, " +
KEY_CLASS_NAME + " TEXT NOT NULL, " +
KEY_OBJECT_ID + " TEXT, " +
KEY_JSON + " TEXT, " +
KEY_IS_DELETING_EVENTUALLY + " INTEGER DEFAULT 0, " +
"UNIQUE(" + KEY_CLASS_NAME + ", " + KEY_OBJECT_ID + ")" +
");";
db.execSQL(sql);
sql = "CREATE TABLE " + TABLE_DEPENDENCIES + " (" +
KEY_KEY + " TEXT NOT NULL, " +
KEY_UUID + " TEXT NOT NULL, " +
"PRIMARY KEY(" + KEY_KEY + ", " + KEY_UUID + ")" +
");";
db.execSQL(sql);
}
/**
* Called when the database is first created.
*/
@Override
public void onCreate(SQLiteDatabase db) {
createSchema(db);
}
/**
* Called when the version number in code doesn't match the one on disk.
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// do nothing
}
/**
* Drops all tables and then recreates the schema.
*/
public void clearDatabase(Context context) {
context.deleteDatabase(DATABASE_NAME);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,95 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.util.Log;
/** package */ class PLog {
public static final int LOG_LEVEL_NONE = Integer.MAX_VALUE;
private static int logLevel = Integer.MAX_VALUE;
/**
* Sets the level of logging to display, where each level includes all those below it. The default
* level is {@link #LOG_LEVEL_NONE}. Please ensure this is set to {@link Log#ERROR}
* or {@link #LOG_LEVEL_NONE} before deploying your app to ensure no sensitive information is
* logged. The levels are:
* <ul>
* <li>{@link Log#VERBOSE}</li>
* <li>{@link Log#DEBUG}</li>
* <li>{@link Log#INFO}</li>
* <li>{@link Log#WARN}</li>
* <li>{@link Log#ERROR}</li>
* <li>{@link #LOG_LEVEL_NONE}</li>
* </ul>
*
* @param logLevel
* The level of logcat logging that Parse should do.
*/
public static void setLogLevel(int logLevel) {
PLog.logLevel = logLevel;
}
/**
* Returns the level of logging that will be displayed.
*/
public static int getLogLevel() {
return logLevel;
}
private static void log(int messageLogLevel, String tag, String message, Throwable tr) {
if (messageLogLevel >= logLevel) {
if (tr == null) {
Log.println(logLevel, tag, message);
} else {
Log.println(logLevel, tag, message + '\n' + Log.getStackTraceString(tr));
}
}
}
/* package */ static void v(String tag, String message, Throwable tr) {
log(Log.VERBOSE, tag, message, tr);
}
/* package */ static void v(String tag, String message) {
v(tag, message, null);
}
/* package */ static void d(String tag, String message, Throwable tr) {
log(Log.DEBUG, tag, message, tr);
}
/* package */ static void d(String tag, String message) {
d(tag, message, null);
}
/* package */ static void i(String tag, String message, Throwable tr) {
log(Log.INFO, tag, message, tr);
}
/* package */ static void i(String tag, String message) {
i(tag, message, null);
}
/* package */ static void w(String tag, String message, Throwable tr) {
log(Log.WARN, tag, message, tr);
}
/* package */ static void w(String tag, String message) {
w(tag, message, null);
}
/* package */ static void e(String tag, String message, Throwable tr) {
log(Log.ERROR, tag, message, tr);
}
/* package */ static void e(String tag, String message) {
e(tag, message, null);
}
}

View File

@ -0,0 +1,770 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import bolts.Continuation;
import bolts.Task;
import okhttp3.OkHttpClient;
/**
* The {@code Parse} class contains static functions that handle global configuration for the Parse
* library.
*/
public class Parse {
private static final String TAG = "com.parse.Parse";
private static final int DEFAULT_MAX_RETRIES = ParseRequest.DEFAULT_MAX_RETRIES;
/**
* Represents an opaque configuration for the {@code Parse} SDK configuration.
*/
public static final class Configuration {
/**
* Allows for simple constructing of a {@code Configuration} object.
*/
public static final class Builder {
private Context context;
private String applicationId;
private String clientKey;
private String server;
private boolean localDataStoreEnabled;
private OkHttpClient.Builder clientBuilder;
private int maxRetries = DEFAULT_MAX_RETRIES;
/**
* Initialize a bulider with a given context.
* <p>
* This context will then be passed through to the rest of the Parse SDK for use during
* initialization.
* <p>
* <p/>
* You may define {@code com.parse.SERVER_URL}, {@code com.parse.APPLICATION_ID} and (optional) {@code com.parse.CLIENT_KEY}
* {@code meta-data} in your {@code AndroidManifest.xml}:
* <pre>
* &lt;manifest ...&gt;
*
* ...
*
* &lt;application ...&gt;
* &lt;meta-data
* android:name="com.parse.SERVER_URL"
* android:value="@string/parse_server_url" /&gt;
* &lt;meta-data
* android:name="com.parse.APPLICATION_ID"
* android:value="@string/parse_app_id" /&gt;
* &lt;meta-data
* android:name="com.parse.CLIENT_KEY"
* android:value="@string/parse_client_key" /&gt;
*
* ...
*
* &lt;/application&gt;
* &lt;/manifest&gt;
* </pre>
* <p/>
* <p>
* This will cause the values for {@code server}, {@code applicationId} and {@code clientKey} to be set to
* those defined in your manifest.
*
* @param context The active {@link Context} for your application. Cannot be null.
*/
public Builder(Context context) {
this.context = context;
// Yes, our public API states we cannot be null. But for unit tests, it's easier just to
// support null here.
if (context != null) {
Context applicationContext = context.getApplicationContext();
Bundle metaData = ManifestInfo.getApplicationMetadata(applicationContext);
if (metaData != null) {
server(metaData.getString(PARSE_SERVER_URL));
applicationId = metaData.getString(PARSE_APPLICATION_ID);
clientKey = metaData.getString(PARSE_CLIENT_KEY);
}
}
}
/**
* Set the application id to be used by Parse.
* <p>
* This method is only required if you intend to use a different {@code applicationId} than
* is defined by {@code com.parse.APPLICATION_ID} in your {@code AndroidManifest.xml}.
*
* @param applicationId The application id to set.
* @return The same builder, for easy chaining.
*/
public Builder applicationId(String applicationId) {
this.applicationId = applicationId;
return this;
}
/**
* Set the client key to be used by Parse.
* <p>
* This method is only required if you intend to use a different {@code clientKey} than
* is defined by {@code com.parse.CLIENT_KEY} in your {@code AndroidManifest.xml}.
*
* @param clientKey The client key to set.
* @return The same builder, for easy chaining.
*/
public Builder clientKey(String clientKey) {
this.clientKey = clientKey;
return this;
}
/**
* Set the server URL to be used by Parse.
*
* @param server The server URL to set.
* @return The same builder, for easy chaining.
*/
public Builder server(String server) {
// Add an extra trailing slash so that Parse REST commands include
// the path as part of the server URL (i.e. http://api.myhost.com/parse)
if (server != null && !server.endsWith("/")) {
server = server + "/";
}
this.server = server;
return this;
}
/**
* Enable pinning in your application. This must be called before your application can use
* pinning.
*
* @return The same builder, for easy chaining.
*/
public Builder enableLocalDataStore() {
localDataStoreEnabled = true;
return this;
}
private Builder setLocalDatastoreEnabled(boolean enabled) {
localDataStoreEnabled = enabled;
return this;
}
/**
* Set the {@link okhttp3.OkHttpClient.Builder} to use when communicating with the Parse
* REST API
* <p>
*
* @param builder The client builder, which will be modified for compatibility
* @return The same builder, for easy chaining.
*/
public Builder clientBuilder(OkHttpClient.Builder builder) {
clientBuilder = builder;
return this;
}
/**
* Set the max number of times to retry Parse operations before deeming them a failure
* <p>
*
* @param maxRetries The maximum number of times to retry. <=0 to never retry commands
* @return The same builder, for easy chaining.
*/
public Builder maxRetries(int maxRetries) {
this.maxRetries = maxRetries;
return this;
}
/**
* Construct this builder into a concrete {@code Configuration} instance.
*
* @return A constructed {@code Configuration} object.
*/
public Configuration build() {
return new Configuration(this);
}
}
final Context context;
final String applicationId;
final String clientKey;
final String server;
final boolean localDataStoreEnabled;
final OkHttpClient.Builder clientBuilder;
final int maxRetries;
private Configuration(Builder builder) {
this.context = builder.context;
this.applicationId = builder.applicationId;
this.clientKey = builder.clientKey;
this.server = builder.server;
this.localDataStoreEnabled = builder.localDataStoreEnabled;
this.clientBuilder = builder.clientBuilder;
this.maxRetries = builder.maxRetries;
}
}
private static final String PARSE_SERVER_URL = "com.parse.SERVER_URL";
private static final String PARSE_APPLICATION_ID = "com.parse.APPLICATION_ID";
private static final String PARSE_CLIENT_KEY = "com.parse.CLIENT_KEY";
private static final Object MUTEX = new Object();
static ParseEventuallyQueue eventuallyQueue = null;
//region LDS
private static boolean isLocalDatastoreEnabled;
private static OfflineStore offlineStore;
/**
* Enable pinning in your application. This must be called before your application can use
* pinning. You must invoke {@code enableLocalDatastore(Context)} before
* {@link #initialize(Context)} :
* <p/>
* <pre>
* public class MyApplication extends Application {
* public void onCreate() {
* Parse.enableLocalDatastore(this);
* Parse.initialize(this);
* }
* }
* </pre>
*
* @param context The active {@link Context} for your application.
*/
public static void enableLocalDatastore(Context context) {
if (isInitialized()) {
throw new IllegalStateException("`Parse#enableLocalDatastore(Context)` must be invoked " +
"before `Parse#initialize(Context)`");
}
isLocalDatastoreEnabled = true;
}
static void disableLocalDatastore() {
setLocalDatastore(null);
// We need to re-register ParseCurrentInstallationController otherwise it is still offline
// controller
ParseCorePlugins.getInstance().reset();
}
static OfflineStore getLocalDatastore() {
return offlineStore;
}
static void setLocalDatastore(OfflineStore offlineStore) {
Parse.isLocalDatastoreEnabled = offlineStore != null;
Parse.offlineStore = offlineStore;
}
public static boolean isLocalDatastoreEnabled() {
return isLocalDatastoreEnabled;
}
//endregion
/**
* Authenticates this client as belonging to your application.
* <p/>
* You may define {@code com.parse.SERVER_URL}, {@code com.parse.APPLICATION_ID} and (optional) {@code com.parse.CLIENT_KEY}
* {@code meta-data} in your {@code AndroidManifest.xml}:
* <pre>
* &lt;manifest ...&gt;
*
* ...
*
* &lt;application ...&gt;
* &lt;meta-data
* android:name="com.parse.SERVER_URL"
* android:value="@string/parse_server_url" /&gt;
* &lt;meta-data
* android:name="com.parse.APPLICATION_ID"
* android:value="@string/parse_app_id" /&gt;
* &lt;meta-data
* android:name="com.parse.CLIENT_KEY"
* android:value="@string/parse_client_key" /&gt;
*
* ...
*
* &lt;/application&gt;
* &lt;/manifest&gt;
* </pre>
* <p/>
* This must be called before your application can use the Parse library.
* The recommended way is to put a call to {@code Parse.initialize}
* in your {@code Application}'s {@code onCreate} method:
* <p/>
* <pre>
* public class MyApplication extends Application {
* public void onCreate() {
* Parse.initialize(this);
* }
* }
* </pre>
*
* @param context The active {@link Context} for your application.
*/
public static void initialize(Context context) {
Configuration.Builder builder = new Configuration.Builder(context);
if (builder.server == null) {
throw new RuntimeException("ServerUrl not defined. " +
"You must provide ServerUrl in AndroidManifest.xml.\n" +
"<meta-data\n" +
" android:name=\"com.parse.SERVER_URL\"\n" +
" android:value=\"<Your Server Url>\" />");
}
if (builder.applicationId == null) {
throw new RuntimeException("ApplicationId not defined. " +
"You must provide ApplicationId in AndroidManifest.xml.\n" +
"<meta-data\n" +
" android:name=\"com.parse.APPLICATION_ID\"\n" +
" android:value=\"<Your Application Id>\" />");
}
initialize(builder
.setLocalDatastoreEnabled(isLocalDatastoreEnabled)
.build()
);
}
/**
* Authenticates this client as belonging to your application.
* <p/>
* This method is only required if you intend to use a different {@code applicationId} or
* {@code clientKey} than is defined by {@code com.parse.APPLICATION_ID} or
* {@code com.parse.CLIENT_KEY} in your {@code AndroidManifest.xml}.
* <p/>
* This must be called before your
* application can use the Parse library. The recommended way is to put a call to
* {@code Parse.initialize} in your {@code Application}'s {@code onCreate} method:
* <p/>
* <pre>
* public class MyApplication extends Application {
* public void onCreate() {
* Parse.initialize(this, &quot;your application id&quot;, &quot;your client key&quot;);
* }
* }
* </pre>
*
* @param context The active {@link Context} for your application.
* @param applicationId The application id provided in the Parse dashboard.
* @param clientKey The client key provided in the Parse dashboard.
*/
public static void initialize(Context context, String applicationId, String clientKey) {
initialize(new Configuration.Builder(context)
.applicationId(applicationId)
.clientKey(clientKey)
.setLocalDatastoreEnabled(isLocalDatastoreEnabled)
.build()
);
}
public static void initialize(Configuration configuration) {
if (isInitialized()) {
PLog.w(TAG, "Parse is already initialized");
return;
}
// NOTE (richardross): We will need this here, as ParsePlugins uses the return value of
// isLocalDataStoreEnabled() to perform additional behavior.
isLocalDatastoreEnabled = configuration.localDataStoreEnabled;
ParsePlugins.initialize(configuration.context, configuration);
try {
ParseRESTCommand.server = new URL(configuration.server);
} catch (MalformedURLException ex) {
throw new RuntimeException(ex);
}
ParseObject.registerParseSubclasses();
if (configuration.localDataStoreEnabled) {
offlineStore = new OfflineStore(configuration.context);
} else {
ParseKeyValueCache.initialize(configuration.context);
}
// Make sure the data on disk for Parse is for the current
// application.
checkCacheApplicationId();
final Context context = configuration.context;
Task.callInBackground(new Callable<Void>() {
@Override
public Void call() throws Exception {
getEventuallyQueue(context);
return null;
}
});
ParseFieldOperations.registerDefaultDecoders();
if (!allParsePushIntentReceiversInternal()) {
throw new SecurityException("To prevent external tampering to your app's notifications, " +
"all receivers registered to handle the following actions must have " +
"their exported attributes set to false: com.parse.push.intent.RECEIVE, " +
"com.parse.push.intent.OPEN, com.parse.push.intent.DELETE");
}
// May need to update GCM registration ID if app version has changed.
// This also primes current installation.
PushServiceUtils.initialize().continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
// Prime current user in the background
return ParseUser.getCurrentUserAsync().makeVoid();
}
}).continueWith(new Continuation<Void, Void>() {
@Override
public Void then(Task<Void> task) throws Exception {
// Prime config in the background
ParseConfig.getCurrentConfig();
return null;
}
}, Task.BACKGROUND_EXECUTOR);
dispatchOnParseInitialized();
// FYI we probably don't want to do this if we ever add other callbacks.
synchronized (MUTEX_CALLBACKS) {
Parse.callbacks = null;
}
}
static void destroy() {
ParseEventuallyQueue queue;
synchronized (MUTEX) {
queue = eventuallyQueue;
eventuallyQueue = null;
}
if (queue != null) {
queue.onDestroy();
}
ParseCorePlugins.getInstance().reset();
ParsePlugins.reset();
}
/**
* @return {@code True} if {@link #initialize} has been called, otherwise {@code false}.
*/
static boolean isInitialized() {
return ParsePlugins.get() != null;
}
static Context getApplicationContext() {
checkContext();
return ParsePlugins.get().applicationContext();
}
/**
* Checks that each of the receivers associated with the three actions defined in
* ParsePushBroadcastReceiver (ACTION_PUSH_RECEIVE, ACTION_PUSH_OPEN, ACTION_PUSH_DELETE) has
* their exported attributes set to false. If this is the case for each of the receivers
* registered in the AndroidManifest.xml or if no receivers are registered (because we will be registering
* the default implementation of ParsePushBroadcastReceiver in PushService) then true is returned.
* Note: the reason for iterating through lists, is because you can define different receivers
* in the manifest that respond to the same intents and both all of the receivers will be triggered.
* So we want to make sure all them have the exported attribute set to false.
*/
private static boolean allParsePushIntentReceiversInternal() {
List<ResolveInfo> intentReceivers = ManifestInfo.getIntentReceivers(
ParsePushBroadcastReceiver.ACTION_PUSH_RECEIVE,
ParsePushBroadcastReceiver.ACTION_PUSH_DELETE,
ParsePushBroadcastReceiver.ACTION_PUSH_OPEN);
for (ResolveInfo resolveInfo : intentReceivers) {
if (resolveInfo.activityInfo.exported) {
return false;
}
}
return true;
}
/**
* @deprecated Please use {@link #getParseCacheDir(String)} or {@link #getParseFilesDir(String)}
* instead.
*/
@Deprecated
static File getParseDir() {
return ParsePlugins.get().getParseDir();
}
static File getParseCacheDir() {
return ParsePlugins.get().getCacheDir();
}
static File getParseCacheDir(String subDir) {
synchronized (MUTEX) {
File dir = new File(getParseCacheDir(), subDir);
if (!dir.exists()) {
dir.mkdirs();
}
return dir;
}
}
static File getParseFilesDir() {
return ParsePlugins.get().getFilesDir();
}
static File getParseFilesDir(String subDir) {
synchronized (MUTEX) {
File dir = new File(getParseFilesDir(), subDir);
if (!dir.exists()) {
dir.mkdirs();
}
return dir;
}
}
/**
* Verifies that the data stored on disk for Parse was generated using the same application that
* is running now.
*/
static void checkCacheApplicationId() {
synchronized (MUTEX) {
String applicationId = ParsePlugins.get().applicationId();
if (applicationId != null) {
File dir = Parse.getParseCacheDir();
// Make sure the current version of the cache is for this application id.
File applicationIdFile = new File(dir, "applicationId");
if (applicationIdFile.exists()) {
// Read the file
boolean matches = false;
try {
RandomAccessFile f = new RandomAccessFile(applicationIdFile, "r");
byte[] bytes = new byte[(int) f.length()];
f.readFully(bytes);
f.close();
String diskApplicationId = new String(bytes, "UTF-8");
matches = diskApplicationId.equals(applicationId);
} catch (IOException e) {
// Hmm, the applicationId file was malformed or something. Assume it
// doesn't match.
}
// The application id has changed, so everything on disk is invalid.
if (!matches) {
try {
ParseFileUtils.deleteDirectory(dir);
} catch (IOException e) {
// We're unable to delete the directy...
}
}
}
// Create the version file if needed.
applicationIdFile = new File(dir, "applicationId");
try {
FileOutputStream out = new FileOutputStream(applicationIdFile);
out.write(applicationId.getBytes("UTF-8"));
out.close();
} catch (IOException e) {
// Nothing we can really do about it.
}
}
}
}
/**
* Gets the shared command cache object for all ParseObjects. This command cache is used to
* locally store save commands created by the ParseObject.saveEventually(). When a new
* ParseCommandCache is instantiated, it will begin running its run loop, which will start by
* processing any commands already stored in the on-disk queue.
*/
static ParseEventuallyQueue getEventuallyQueue() {
Context context = ParsePlugins.get().applicationContext();
return getEventuallyQueue(context);
}
private static ParseEventuallyQueue getEventuallyQueue(Context context) {
synchronized (MUTEX) {
boolean isLocalDatastoreEnabled = Parse.isLocalDatastoreEnabled();
if (eventuallyQueue == null
|| (isLocalDatastoreEnabled && eventuallyQueue instanceof ParseCommandCache)
|| (!isLocalDatastoreEnabled && eventuallyQueue instanceof ParsePinningEventuallyQueue)) {
checkContext();
ParseHttpClient httpClient = ParsePlugins.get().restClient();
eventuallyQueue = isLocalDatastoreEnabled
? new ParsePinningEventuallyQueue(context, httpClient)
: new ParseCommandCache(context, httpClient);
// We still need to clear out the old command cache even if we're using Pinning in case
// anything is left over when the user upgraded. Checking number of pending and then
// initializing should be enough.
if (isLocalDatastoreEnabled && ParseCommandCache.getPendingCount() > 0) {
new ParseCommandCache(context, httpClient);
}
}
return eventuallyQueue;
}
}
static void checkInit() {
if (ParsePlugins.get() == null) {
throw new RuntimeException("You must call Parse.initialize(Context)"
+ " before using the Parse library.");
}
if (ParsePlugins.get().applicationId() == null) {
throw new RuntimeException("applicationId is null. "
+ "You must call Parse.initialize(Context)"
+ " before using the Parse library.");
}
}
static void checkContext() {
if (ParsePlugins.get().applicationContext() == null) {
throw new RuntimeException("applicationContext is null. "
+ "You must call Parse.initialize(Context)"
+ " before using the Parse library.");
}
}
static boolean hasPermission(String permission) {
return (getApplicationContext().checkCallingOrSelfPermission(permission) ==
PackageManager.PERMISSION_GRANTED);
}
static void requirePermission(String permission) {
if (!hasPermission(permission)) {
throw new IllegalStateException(
"To use this functionality, add this to your AndroidManifest.xml:\n"
+ "<uses-permission android:name=\"" + permission + "\" />");
}
}
//region ParseCallbacks
private static final Object MUTEX_CALLBACKS = new Object();
private static Set<ParseCallbacks> callbacks = new HashSet<>();
/**
* Registers a listener to be called at the completion of {@link #initialize}.
* <p>
* Throws {@link java.lang.IllegalStateException} if called after {@link #initialize}.
*
* @param listener the listener to register
*/
static void registerParseCallbacks(ParseCallbacks listener) {
if (isInitialized()) {
throw new IllegalStateException(
"You must register callbacks before Parse.initialize(Context)");
}
synchronized (MUTEX_CALLBACKS) {
if (callbacks == null) {
return;
}
callbacks.add(listener);
}
}
/**
* Unregisters a listener previously registered with {@link #registerParseCallbacks}.
*
* @param listener the listener to register
*/
static void unregisterParseCallbacks(ParseCallbacks listener) {
synchronized (MUTEX_CALLBACKS) {
if (callbacks == null) {
return;
}
callbacks.remove(listener);
}
}
private static void dispatchOnParseInitialized() {
ParseCallbacks[] callbacks = collectParseCallbacks();
if (callbacks != null) {
for (ParseCallbacks callback : callbacks) {
callback.onParseInitialized();
}
}
}
private static ParseCallbacks[] collectParseCallbacks() {
ParseCallbacks[] callbacks;
synchronized (MUTEX_CALLBACKS) {
if (Parse.callbacks == null) {
return null;
}
callbacks = new ParseCallbacks[Parse.callbacks.size()];
if (Parse.callbacks.size() > 0) {
callbacks = Parse.callbacks.toArray(callbacks);
}
}
return callbacks;
}
interface ParseCallbacks {
void onParseInitialized();
}
//endregion
//region Logging
public static final int LOG_LEVEL_VERBOSE = Log.VERBOSE;
public static final int LOG_LEVEL_DEBUG = Log.DEBUG;
public static final int LOG_LEVEL_INFO = Log.INFO;
public static final int LOG_LEVEL_WARNING = Log.WARN;
public static final int LOG_LEVEL_ERROR = Log.ERROR;
public static final int LOG_LEVEL_NONE = Integer.MAX_VALUE;
/**
* Sets the level of logging to display, where each level includes all those below it. The default
* level is {@link #LOG_LEVEL_NONE}. Please ensure this is set to {@link #LOG_LEVEL_ERROR}
* or {@link #LOG_LEVEL_NONE} before deploying your app to ensure no sensitive information is
* logged. The levels are:
* <ul>
* <li>{@link #LOG_LEVEL_VERBOSE}</li>
* <li>{@link #LOG_LEVEL_DEBUG}</li>
* <li>{@link #LOG_LEVEL_INFO}</li>
* <li>{@link #LOG_LEVEL_WARNING}</li>
* <li>{@link #LOG_LEVEL_ERROR}</li>
* <li>{@link #LOG_LEVEL_NONE}</li>
* </ul>
*
* @param logLevel The level of logcat logging that Parse should do.
*/
public static void setLogLevel(int logLevel) {
PLog.setLogLevel(logLevel);
}
/**
* Returns the level of logging that will be displayed.
*/
public static int getLogLevel() {
return PLog.getLogLevel();
}
//endregion
// Suppress constructor to prevent subclassing
private Parse() {
throw new AssertionError();
}
static String externalVersionName() {
return "a" + ParseObject.VERSION_NAME;
}
}

View File

@ -0,0 +1,611 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.os.Parcel;
import android.os.Parcelable;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* A {@code ParseACL} is used to control which users can access or modify a particular object. Each
* {@link ParseObject} can have its own {@code ParseACL}. You can grant read and write permissions
* separately to specific users, to groups of users that belong to roles, or you can grant
* permissions to "the public" so that, for example, any user could read a particular object but
* only a particular set of users could write to that object.
*/
public class ParseACL implements Parcelable {
private static final String PUBLIC_KEY = "*";
private final static String UNRESOLVED_KEY = "*unresolved";
private static final String KEY_ROLE_PREFIX = "role:";
private static final String UNRESOLVED_USER_JSON_KEY = "unresolvedUser";
private static class Permissions {
private static final String READ_PERMISSION = "read";
private static final String WRITE_PERMISSION = "write";
private final boolean readPermission;
private final boolean writePermission;
/* package */ Permissions(boolean readPermission, boolean write) {
this.readPermission = readPermission;
this.writePermission = write;
}
/* package */ Permissions(Permissions permissions) {
this.readPermission = permissions.readPermission;
this.writePermission = permissions.writePermission;
}
/* package */ JSONObject toJSONObject() {
JSONObject json = new JSONObject();
try {
if (readPermission) {
json.put(READ_PERMISSION, true);
}
if (writePermission) {
json.put(WRITE_PERMISSION, true);
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
return json;
}
/* package */ void toParcel(Parcel parcel) {
parcel.writeByte(readPermission ? (byte) 1 : 0);
parcel.writeByte(writePermission ? (byte) 1 : 0);
}
/* package */ boolean getReadPermission() {
return readPermission;
}
/* package */ boolean getWritePermission() {
return writePermission;
}
/* package */ static Permissions createPermissionsFromJSONObject(JSONObject object) {
boolean read = object.optBoolean(READ_PERMISSION, false);
boolean write = object.optBoolean(WRITE_PERMISSION, false);
return new Permissions(read, write);
}
/* package */ static Permissions createPermissionsFromParcel(Parcel parcel) {
return new Permissions(parcel.readByte() == 1, parcel.readByte() == 1);
}
}
private static ParseDefaultACLController getDefaultACLController() {
return ParseCorePlugins.getInstance().getDefaultACLController();
}
/**
* Sets a default ACL that will be applied to all {@link ParseObject}s when they are created.
*
* @param acl
* The ACL to use as a template for all {@link ParseObject}s created after setDefaultACL
* has been called. This value will be copied and used as a template for the creation of
* new ACLs, so changes to the instance after {@code setDefaultACL(ParseACL, boolean)}
* has been called will not be reflected in new {@link ParseObject}s.
* @param withAccessForCurrentUser
* If {@code true}, the {@code ParseACL} that is applied to newly-created
* {@link ParseObject}s will provide read and write access to the
* {@link ParseUser#getCurrentUser()} at the time of creation. If {@code false}, the
* provided ACL will be used without modification. If acl is {@code null}, this value is
* ignored.
*/
public static void setDefaultACL(ParseACL acl, boolean withAccessForCurrentUser) {
getDefaultACLController().set(acl, withAccessForCurrentUser);
}
/* package */ static ParseACL getDefaultACL() {
return getDefaultACLController().get();
}
// State
private final Map<String, Permissions> permissionsById = new HashMap<>();
private boolean shared;
/**
* A lazy user that hasn't been saved to Parse.
*/
//TODO (grantland): This should be a list for multiple lazy users with read/write permissions.
private ParseUser unresolvedUser;
/**
* Creates an ACL with no permissions granted.
*/
public ParseACL() {
// do nothing
}
/**
* Creates a copy of {@code acl}.
*
* @param acl
* The acl to copy.
*/
public ParseACL(ParseACL acl) {
for (String id : acl.permissionsById.keySet()) {
permissionsById.put(id, new Permissions(acl.permissionsById.get(id)));
}
unresolvedUser = acl.unresolvedUser;
if (unresolvedUser != null) {
unresolvedUser.registerSaveListener(new UserResolutionListener(this));
}
}
/* package for tests */ ParseACL copy() {
return new ParseACL(this);
}
boolean isShared() {
return shared;
}
void setShared(boolean shared) {
this.shared = shared;
}
// Internally we expose the json object this wraps
/* package */ JSONObject toJSONObject(ParseEncoder objectEncoder) {
JSONObject json = new JSONObject();
try {
for (String id: permissionsById.keySet()) {
json.put(id, permissionsById.get(id).toJSONObject());
}
if (unresolvedUser != null) {
Object encoded = objectEncoder.encode(unresolvedUser);
json.put(UNRESOLVED_USER_JSON_KEY, encoded);
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
return json;
}
// A helper for creating a ParseACL from the wire.
// We iterate over it rather than just copying to permissionsById so that we
// can ensure it's the right format.
/* package */ static ParseACL createACLFromJSONObject(JSONObject object, ParseDecoder decoder) {
ParseACL acl = new ParseACL();
for (String key : ParseJSONUtils.keys(object)) {
if (key.equals(UNRESOLVED_USER_JSON_KEY)) {
JSONObject unresolvedUser;
try {
unresolvedUser = object.getJSONObject(key);
} catch (JSONException e) {
throw new RuntimeException(e);
}
acl.unresolvedUser = (ParseUser) decoder.decode(unresolvedUser);
} else {
try {
Permissions permissions = Permissions.createPermissionsFromJSONObject(object.getJSONObject(key));
acl.permissionsById.put(key, permissions);
} catch (JSONException e) {
throw new RuntimeException("could not decode ACL: " + e.getMessage());
}
}
}
return acl;
}
/**
* Creates an ACL where only the provided user has access.
*
* @param owner
* The only user that can read or write objects governed by this ACL.
*/
public ParseACL(ParseUser owner) {
this();
setReadAccess(owner, true);
setWriteAccess(owner, true);
}
/* package for tests */ void resolveUser(ParseUser user) {
if (!isUnresolvedUser(user)) {
return;
}
if (permissionsById.containsKey(UNRESOLVED_KEY)) {
permissionsById.put(user.getObjectId(), permissionsById.get(UNRESOLVED_KEY));
permissionsById.remove(UNRESOLVED_KEY);
}
unresolvedUser = null;
}
/* package */ boolean hasUnresolvedUser() {
return unresolvedUser != null;
}
/* package */ ParseUser getUnresolvedUser() {
return unresolvedUser;
}
// Helper for setting stuff
private void setPermissionsIfNonEmpty(String userId, boolean readPermission, boolean writePermission) {
if (!(readPermission || writePermission)) {
permissionsById.remove(userId);
}
else {
permissionsById.put(userId, new Permissions(readPermission, writePermission));
}
}
/**
* Set whether the public is allowed to read this object.
*/
public void setPublicReadAccess(boolean allowed) {
setReadAccess(PUBLIC_KEY, allowed);
}
/**
* Get whether the public is allowed to read this object.
*/
public boolean getPublicReadAccess() {
return getReadAccess(PUBLIC_KEY);
}
/**
* Set whether the public is allowed to write this object.
*/
public void setPublicWriteAccess(boolean allowed) {
setWriteAccess(PUBLIC_KEY, allowed);
}
/**
* Set whether the public is allowed to write this object.
*/
public boolean getPublicWriteAccess() {
return getWriteAccess(PUBLIC_KEY);
}
/**
* Set whether the given user id is allowed to read this object.
*/
public void setReadAccess(String userId, boolean allowed) {
if (userId == null) {
throw new IllegalArgumentException("cannot setReadAccess for null userId");
}
boolean writePermission = getWriteAccess(userId);
setPermissionsIfNonEmpty(userId, allowed, writePermission);
}
/**
* Get whether the given user id is *explicitly* allowed to read this object. Even if this returns
* {@code false}, the user may still be able to access it if getPublicReadAccess returns
* {@code true} or a role that the user belongs to has read access.
*/
public boolean getReadAccess(String userId) {
if (userId == null) {
throw new IllegalArgumentException("cannot getReadAccess for null userId");
}
Permissions permissions = permissionsById.get(userId);
return permissions != null && permissions.getReadPermission();
}
/**
* Set whether the given user id is allowed to write this object.
*/
public void setWriteAccess(String userId, boolean allowed) {
if (userId == null) {
throw new IllegalArgumentException("cannot setWriteAccess for null userId");
}
boolean readPermission = getReadAccess(userId);
setPermissionsIfNonEmpty(userId, readPermission, allowed);
}
/**
* Get whether the given user id is *explicitly* allowed to write this object. Even if this
* returns {@code false}, the user may still be able to write it if getPublicWriteAccess returns
* {@code true} or a role that the user belongs to has write access.
*/
public boolean getWriteAccess(String userId) {
if (userId == null) {
throw new IllegalArgumentException("cannot getWriteAccess for null userId");
}
Permissions permissions = permissionsById.get(userId);
return permissions != null && permissions.getWritePermission();
}
/**
* Set whether the given user is allowed to read this object.
*/
public void setReadAccess(ParseUser user, boolean allowed) {
if (user.getObjectId() == null) {
if (user.isLazy()) {
setUnresolvedReadAccess(user, allowed);
return;
}
throw new IllegalArgumentException("cannot setReadAccess for a user with null id");
}
setReadAccess(user.getObjectId(), allowed);
}
private void setUnresolvedReadAccess(ParseUser user, boolean allowed) {
prepareUnresolvedUser(user);
setReadAccess(UNRESOLVED_KEY, allowed);
}
private void setUnresolvedWriteAccess(ParseUser user, boolean allowed) {
prepareUnresolvedUser(user);
setWriteAccess(UNRESOLVED_KEY, allowed);
}
private void prepareUnresolvedUser(ParseUser user) {
// Registers a listener for the user so that when it is saved, the
// unresolved ACL will be resolved.
if (!isUnresolvedUser(user)) {
permissionsById.remove(UNRESOLVED_KEY);
unresolvedUser = user;
unresolvedUser.registerSaveListener(new UserResolutionListener(this));
}
}
private boolean isUnresolvedUser(ParseUser other) {
// This might be a different instance, but if they have the same local id, assume it's correct.
if (other == null || unresolvedUser == null) return false;
return other == unresolvedUser || (other.getObjectId() == null &&
other.getOrCreateLocalId().equals(unresolvedUser.getOrCreateLocalId()));
}
/**
* Get whether the given user id is *explicitly* allowed to read this object. Even if this returns
* {@code false}, the user may still be able to access it if getPublicReadAccess returns
* {@code true} or a role that the user belongs to has read access.
*/
public boolean getReadAccess(ParseUser user) {
if (isUnresolvedUser(user)) {
return getReadAccess(UNRESOLVED_KEY);
}
if (user.isLazy()) {
return false;
}
if (user.getObjectId() == null) {
throw new IllegalArgumentException("cannot getReadAccess for a user with null id");
}
return getReadAccess(user.getObjectId());
}
/**
* Set whether the given user is allowed to write this object.
*/
public void setWriteAccess(ParseUser user, boolean allowed) {
if (user.getObjectId() == null) {
if (user.isLazy()) {
setUnresolvedWriteAccess(user, allowed);
return;
}
throw new IllegalArgumentException("cannot setWriteAccess for a user with null id");
}
setWriteAccess(user.getObjectId(), allowed);
}
/**
* Get whether the given user id is *explicitly* allowed to write this object. Even if this
* returns {@code false}, the user may still be able to write it if getPublicWriteAccess returns
* {@code true} or a role that the user belongs to has write access.
*/
public boolean getWriteAccess(ParseUser user) {
if (isUnresolvedUser(user)) {
return getWriteAccess(UNRESOLVED_KEY);
}
if (user.isLazy()) {
return false;
}
if (user.getObjectId() == null) {
throw new IllegalArgumentException("cannot getWriteAccess for a user with null id");
}
return getWriteAccess(user.getObjectId());
}
/**
* Get whether users belonging to the role with the given roleName are allowed to read this
* object. Even if this returns {@code false}, the role may still be able to read it if a parent
* role has read access.
*
* @param roleName
* The name of the role.
* @return {@code true} if the role has read access. {@code false} otherwise.
*/
public boolean getRoleReadAccess(String roleName) {
return getReadAccess(KEY_ROLE_PREFIX + roleName);
}
/**
* Set whether users belonging to the role with the given roleName are allowed to read this
* object.
*
* @param roleName
* The name of the role.
* @param allowed
* Whether the given role can read this object.
*/
public void setRoleReadAccess(String roleName, boolean allowed) {
setReadAccess(KEY_ROLE_PREFIX + roleName, allowed);
}
/**
* Get whether users belonging to the role with the given roleName are allowed to write this
* object. Even if this returns {@code false}, the role may still be able to write it if a parent
* role has write access.
*
* @param roleName
* The name of the role.
* @return {@code true} if the role has write access. {@code false} otherwise.
*/
public boolean getRoleWriteAccess(String roleName) {
return getWriteAccess(KEY_ROLE_PREFIX + roleName);
}
/**
* Set whether users belonging to the role with the given roleName are allowed to write this
* object.
*
* @param roleName
* The name of the role.
* @param allowed
* Whether the given role can write this object.
*/
public void setRoleWriteAccess(String roleName, boolean allowed) {
setWriteAccess(KEY_ROLE_PREFIX + roleName, allowed);
}
private static void validateRoleState(ParseRole role) {
if (role == null || role.getObjectId() == null) {
throw new IllegalArgumentException(
"Roles must be saved to the server before they can be used in an ACL.");
}
}
/**
* Get whether users belonging to the given role are allowed to read this object. Even if this
* returns {@code false}, the role may still be able to read it if a parent role has read access.
* The role must already be saved on the server and its data must have been fetched in order to
* use this method.
*
* @param role
* The role to check for access.
* @return {@code true} if the role has read access. {@code false} otherwise.
*/
public boolean getRoleReadAccess(ParseRole role) {
validateRoleState(role);
return getRoleReadAccess(role.getName());
}
/**
* Set whether users belonging to the given role are allowed to read this object. The role must
* already be saved on the server and its data must have been fetched in order to use this method.
*
* @param role
* The role to assign access.
* @param allowed
* Whether the given role can read this object.
*/
public void setRoleReadAccess(ParseRole role, boolean allowed) {
validateRoleState(role);
setRoleReadAccess(role.getName(), allowed);
}
/**
* Get whether users belonging to the given role are allowed to write this object. Even if this
* returns {@code false}, the role may still be able to write it if a parent role has write
* access. The role must already be saved on the server and its data must have been fetched in
* order to use this method.
*
* @param role
* The role to check for access.
* @return {@code true} if the role has write access. {@code false} otherwise.
*/
public boolean getRoleWriteAccess(ParseRole role) {
validateRoleState(role);
return getRoleWriteAccess(role.getName());
}
/**
* Set whether users belonging to the given role are allowed to write this object. The role must
* already be saved on the server and its data must have been fetched in order to use this method.
*
* @param role
* The role to assign access.
* @param allowed
* Whether the given role can write this object.
*/
public void setRoleWriteAccess(ParseRole role, boolean allowed) {
validateRoleState(role);
setRoleWriteAccess(role.getName(), allowed);
}
private static class UserResolutionListener implements GetCallback<ParseObject> {
private final WeakReference<ParseACL> parent;
public UserResolutionListener(ParseACL parent) {
this.parent = new WeakReference<>(parent);
}
@Override
public void done(ParseObject object, ParseException e) {
// A callback that will resolve the user when it is saved for any
// ACL that is listening to it.
try {
ParseACL parent = this.parent.get();
if (parent != null) {
parent.resolveUser((ParseUser) object);
}
} finally {
object.unregisterSaveListener(this);
}
}
}
/* package for tests */ Map<String, Permissions> getPermissionsById() {
return permissionsById;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
writeToParcel(dest, new ParseObjectParcelEncoder());
}
/* package */ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) {
dest.writeByte(shared ? (byte) 1 : 0);
dest.writeInt(permissionsById.size());
Set<String> keys = permissionsById.keySet();
for (String key : keys) {
dest.writeString(key);
Permissions permissions = permissionsById.get(key);
permissions.toParcel(dest);
}
dest.writeByte(unresolvedUser != null ? (byte) 1 : 0);
if (unresolvedUser != null) {
// Encoder will create a local id for unresolvedUser, so we recognize it after unparcel.
encoder.encode(unresolvedUser, dest);
}
}
public final static Creator<ParseACL> CREATOR = new Creator<ParseACL>() {
@Override
public ParseACL createFromParcel(Parcel source) {
return new ParseACL(source, new ParseObjectParcelDecoder());
}
@Override
public ParseACL[] newArray(int size) {
return new ParseACL[size];
}
};
/* package */ ParseACL(Parcel source, ParseParcelDecoder decoder) {
shared = source.readByte() == 1;
int size = source.readInt();
for (int i = 0; i < size; i++) {
String key = source.readString();
Permissions permissions = Permissions.createPermissionsFromParcel(source);
permissionsById.put(key, permissions);
}
if (source.readByte() == 1) {
unresolvedUser = (ParseUser) decoder.decode(source);
unresolvedUser.registerSaveListener(new UserResolutionListener(this));
}
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.os.Parcel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* An operation that adds a new element to an array field.
*/
/** package */ class ParseAddOperation implements ParseFieldOperation {
/* package */ final static String OP_NAME = "Add";
protected final ArrayList<Object> objects = new ArrayList<>();
public ParseAddOperation(Collection<?> coll) {
objects.addAll(coll);
}
@Override
public JSONObject encode(ParseEncoder objectEncoder) throws JSONException {
JSONObject output = new JSONObject();
output.put("__op", OP_NAME);
output.put("objects", objectEncoder.encode(objects));
return output;
}
@Override
public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) {
dest.writeString(OP_NAME);
dest.writeInt(objects.size());
for (Object object : objects) {
parcelableEncoder.encode(object, dest);
}
}
@Override
public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) {
if (previous == null) {
return this;
} else if (previous instanceof ParseDeleteOperation) {
return new ParseSetOperation(objects);
} else if (previous instanceof ParseSetOperation) {
Object value = ((ParseSetOperation) previous).getValue();
if (value instanceof JSONArray) {
ArrayList<Object> result = ParseFieldOperations.jsonArrayAsArrayList((JSONArray) value);
result.addAll(objects);
return new ParseSetOperation(new JSONArray(result));
} else if (value instanceof List) {
ArrayList<Object> result = new ArrayList<>((List<?>) value);
result.addAll(objects);
return new ParseSetOperation(result);
} else {
throw new IllegalArgumentException("You can only add an item to a List or JSONArray.");
}
} else if (previous instanceof ParseAddOperation) {
ArrayList<Object> result = new ArrayList<>(((ParseAddOperation) previous).objects);
result.addAll(objects);
return new ParseAddOperation(result);
} else {
throw new IllegalArgumentException("Operation is invalid after previous operation.");
}
}
@Override
public Object apply(Object oldValue, String key) {
if (oldValue == null) {
return objects;
} else if (oldValue instanceof JSONArray) {
ArrayList<Object> old = ParseFieldOperations.jsonArrayAsArrayList((JSONArray) oldValue);
@SuppressWarnings("unchecked")
ArrayList<Object> newValue = (ArrayList<Object>) this.apply(old, key);
return new JSONArray(newValue);
} else if (oldValue instanceof List) {
ArrayList<Object> result = new ArrayList<>((List<?>) oldValue);
result.addAll(objects);
return result;
} else {
throw new IllegalArgumentException("Operation is invalid after previous operation.");
}
}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.os.Parcel;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
/**
* An operation that adds a new element to an array field, only if it wasn't already present.
*/
/** package */ class ParseAddUniqueOperation implements ParseFieldOperation {
/* package */ final static String OP_NAME = "AddUnique";
protected final LinkedHashSet<Object> objects = new LinkedHashSet<>();
public ParseAddUniqueOperation(Collection<?> col) {
objects.addAll(col);
}
@Override
public JSONObject encode(ParseEncoder objectEncoder) throws JSONException {
JSONObject output = new JSONObject();
output.put("__op", OP_NAME);
output.put("objects", objectEncoder.encode(new ArrayList<>(objects)));
return output;
}
@Override
public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) {
dest.writeString(OP_NAME);
dest.writeInt(objects.size());
for (Object object : objects) {
parcelableEncoder.encode(object, dest);
}
}
@Override
@SuppressWarnings("unchecked")
public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) {
if (previous == null) {
return this;
} else if (previous instanceof ParseDeleteOperation) {
return new ParseSetOperation(objects);
} else if (previous instanceof ParseSetOperation) {
Object value = ((ParseSetOperation) previous).getValue();
if (value instanceof JSONArray || value instanceof List) {
return new ParseSetOperation(this.apply(value, null));
} else {
throw new IllegalArgumentException("You can only add an item to a List or JSONArray.");
}
} else if (previous instanceof ParseAddUniqueOperation) {
List<Object> previousResult =
new ArrayList<>(((ParseAddUniqueOperation) previous).objects);
return new ParseAddUniqueOperation((List<Object>) this.apply(previousResult, null));
} else {
throw new IllegalArgumentException("Operation is invalid after previous operation.");
}
}
@Override
public Object apply(Object oldValue, String key) {
if (oldValue == null) {
return new ArrayList<>(objects);
} else if (oldValue instanceof JSONArray) {
ArrayList<Object> old = ParseFieldOperations.jsonArrayAsArrayList((JSONArray) oldValue);
@SuppressWarnings("unchecked")
ArrayList<Object> newValue = (ArrayList<Object>) this.apply(old, key);
return new JSONArray(newValue);
} else if (oldValue instanceof List) {
ArrayList<Object> result = new ArrayList<>((List<?>) oldValue);
// Build up a Map of objectIds of the existing ParseObjects in this field.
HashMap<String, Integer> existingObjectIds = new HashMap<>();
for (int i = 0; i < result.size(); i++) {
if (result.get(i) instanceof ParseObject) {
existingObjectIds.put(((ParseObject) result.get(i)).getObjectId(), i);
}
}
// Iterate over the objects to add. If it already exists in the field,
// remove the old one and add the new one. Otherwise, just add normally.
for (Object obj : objects) {
if (obj instanceof ParseObject) {
String objectId = ((ParseObject) obj).getObjectId();
if (objectId != null && existingObjectIds.containsKey(objectId)) {
result.set(existingObjectIds.get(objectId), obj);
} else if (!result.contains(obj)) {
result.add(obj);
}
} else {
if (!result.contains(obj)) {
result.add(obj);
}
}
}
return result;
} else {
throw new IllegalArgumentException("Operation is invalid after previous operation.");
}
}
}

View File

@ -0,0 +1,243 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.content.Intent;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import bolts.Capture;
import bolts.Continuation;
import bolts.Task;
/**
* The {@code ParseAnalytics} class provides an interface to Parse's logging and analytics backend.
* Methods will return immediately and cache requests (+ timestamps) to be handled "eventually."
* That is, the request will be sent immediately if possible or the next time a network connection
* is available otherwise.
*/
public class ParseAnalytics {
private static final String TAG = "com.parse.ParseAnalytics";
/* package for test */ static ParseAnalyticsController getAnalyticsController() {
return ParseCorePlugins.getInstance().getAnalyticsController();
}
/**
* Tracks this application being launched (and if this happened as the result of the user opening
* a push notification, this method sends along information to correlate this open with that
* push).
*
* @param intent
* The {@code Intent} that started an {@code Activity}, if any. Can be null.
* @return A Task that is resolved when the event has been tracked by Parse.
*/
public static Task<Void> trackAppOpenedInBackground(Intent intent) {
String pushHashStr = getPushHashFromIntent(intent);
final Capture<String> pushHash = new Capture<>();
if (pushHashStr != null && pushHashStr.length() > 0) {
synchronized (lruSeenPushes) {
if (lruSeenPushes.containsKey(pushHashStr)) {
return Task.forResult(null);
} else {
lruSeenPushes.put(pushHashStr, true);
pushHash.set(pushHashStr);
}
}
}
return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation<String, Task<Void>>() {
@Override
public Task<Void> then(Task<String> task) throws Exception {
String sessionToken = task.getResult();
return getAnalyticsController().trackAppOpenedInBackground(pushHash.get(), sessionToken);
}
});
}
/**
* @deprecated Please use {@link #trackAppOpenedInBackground(android.content.Intent)} instead.
*/
@Deprecated
public static void trackAppOpened(Intent intent) {
trackAppOpenedInBackground(intent);
}
/**
* Tracks this application being launched (and if this happened as the result of the user opening
* a push notification, this method sends along information to correlate this open with that
* push).
*
* @param intent
* The {@code Intent} that started an {@code Activity}, if any. Can be null.
* @param callback
* callback.done(e) is called when the event has been tracked by Parse.
*/
public static void trackAppOpenedInBackground(Intent intent, SaveCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(trackAppOpenedInBackground(intent), callback);
}
/**
* @deprecated Please use {@link #trackEventInBackground(String)} instead.
*/
@Deprecated
public static void trackEvent(String name) {
trackEventInBackground(name);
}
/**
* Tracks the occurrence of a custom event. Parse will store a data point at the time of
* invocation with the given event name.
*
* @param name
* The name of the custom event to report to Parse as having happened.
* @param callback
* callback.done(e) is called when the event has been tracked by Parse.
*/
public static void trackEventInBackground(String name, SaveCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(trackEventInBackground(name), callback);
}
/**
* @deprecated Please use {@link #trackEventInBackground(String, java.util.Map)} instead.
*/
@Deprecated
public static void trackEvent(String name, Map<String, String> dimensions) {
trackEventInBackground(name, dimensions);
}
/**
* Tracks the occurrence of a custom event with additional dimensions. Parse will store a data
* point at the time of invocation with the given event name. Dimensions will allow segmentation
* of the occurrences of this custom event.
* <p>
* To track a user signup along with additional metadata, consider the following:
* <pre>
* Map<String, String> dimensions = new HashMap<String, String>();
* dimensions.put("gender", "m");
* dimensions.put("source", "web");
* dimensions.put("dayType", "weekend");
* ParseAnalytics.trackEvent("signup", dimensions);
* </pre>
* There is a default limit of 8 dimensions per event tracked.
*
* @param name
* The name of the custom event to report to Parse as having happened.
* @param dimensions
* The dictionary of information by which to segment this event.
* @param callback
* callback.done(e) is called when the event has been tracked by Parse.
*/
public static void trackEventInBackground(String name, Map<String, String> dimensions, SaveCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(trackEventInBackground(name, dimensions), callback);
}
/**
* Tracks the occurrence of a custom event with additional dimensions. Parse will store a data
* point at the time of invocation with the given event name. Dimensions will allow segmentation
* of the occurrences of this custom event.
* <p>
* To track a user signup along with additional metadata, consider the following:
* <pre>
* Map<String, String> dimensions = new HashMap<String, String>();
* dimensions.put("gender", "m");
* dimensions.put("source", "web");
* dimensions.put("dayType", "weekend");
* ParseAnalytics.trackEvent("signup", dimensions);
* </pre>
* There is a default limit of 8 dimensions per event tracked.
*
* @param name
* The name of the custom event to report to Parse as having happened.
*
* @return A Task that is resolved when the event has been tracked by Parse.
*/
public static Task<Void> trackEventInBackground(String name) {
return trackEventInBackground(name, (Map<String, String>) null);
}
/**
* Tracks the occurrence of a custom event with additional dimensions. Parse will store a data
* point at the time of invocation with the given event name. Dimensions will allow segmentation
* of the occurrences of this custom event.
* <p>
* To track a user signup along with additional metadata, consider the following:
* <pre>
* Map<String, String> dimensions = new HashMap<String, String>();
* dimensions.put("gender", "m");
* dimensions.put("source", "web");
* dimensions.put("dayType", "weekend");
* ParseAnalytics.trackEvent("signup", dimensions);
* </pre>
* There is a default limit of 8 dimensions per event tracked.
*
* @param name
* The name of the custom event to report to Parse as having happened.
* @param dimensions
* The dictionary of information by which to segment this event.
*
* @return A Task that is resolved when the event has been tracked by Parse.
*/
public static Task<Void> trackEventInBackground(final String name,
Map<String, String> dimensions) {
if (name == null || name.trim().length() == 0) {
throw new IllegalArgumentException("A name for the custom event must be provided.");
}
final Map<String, String> dimensionsCopy = dimensions != null
? Collections.unmodifiableMap(new HashMap<>(dimensions))
: null;
return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation<String, Task<Void>>() {
@Override
public Task<Void> then(Task<String> task) throws Exception {
String sessionToken = task.getResult();
return getAnalyticsController().trackEventInBackground(name, dimensionsCopy, sessionToken);
}
});
}
// Developers have the option to manually track push opens or the app open event can be tracked
// automatically by the ParsePushBroadcastReceiver. To avoid double-counting a push open, we track
// the pushes we've seen locally. We don't need to worry about doing this in any sort of durable
// way because a push can only launch the app once.
private static final Map<String, Boolean> lruSeenPushes = new LinkedHashMap<String, Boolean>() {
protected boolean removeEldestEntry(Map.Entry<String, Boolean> eldest) {
return size() > 10;
}
};
/* package */ static void clear() {
synchronized (lruSeenPushes) {
lruSeenPushes.clear();
}
}
/* package for test */ static String getPushHashFromIntent(Intent intent) {
String pushData = null;
if (intent != null && intent.getExtras() != null) {
pushData = intent.getExtras().getString(ParsePushBroadcastReceiver.KEY_PUSH_DATA);
}
if (pushData == null) {
return null;
}
String pushHash = null;
try {
JSONObject payload = new JSONObject(pushData);
pushHash = payload.optString("push_hash");
} catch (JSONException e) {
PLog.e(TAG, "Failed to parse push data: " + e.getMessage());
}
return pushHash;
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONObject;
import java.util.Map;
import bolts.Task;
/** package */ class ParseAnalyticsController {
/* package for test */ ParseEventuallyQueue eventuallyQueue;
public ParseAnalyticsController(ParseEventuallyQueue eventuallyQueue) {
this.eventuallyQueue = eventuallyQueue;
}
public Task<Void> trackEventInBackground(final String name,
Map<String, String> dimensions, String sessionToken) {
ParseRESTCommand command = ParseRESTAnalyticsCommand.trackEventCommand(name, dimensions,
sessionToken);
Task<JSONObject> eventuallyTask = eventuallyQueue.enqueueEventuallyAsync(command, null);
return eventuallyTask.makeVoid();
}
public Task<Void> trackAppOpenedInBackground(String pushHash, String sessionToken) {
ParseRESTCommand command = ParseRESTAnalyticsCommand.trackAppOpenedCommand(pushHash,
sessionToken);
Task<JSONObject> eventuallyTask = eventuallyQueue.enqueueEventuallyAsync(command, null);
return eventuallyTask.makeVoid();
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import bolts.Continuation;
import bolts.Task;
/**
* Provides utility functions for working with Anonymously logged-in users. Anonymous users have
* some unique characteristics:
* <ul>
* <li>Anonymous users don't need a user name or password.</li>
* <li>Once logged out, an anonymous user cannot be recovered.</li>
* <li>When the current user is anonymous, the following methods can be used to switch to a
* different user or convert the anonymous user into a regular one:
* <ul>
* <li>signUp converts an anonymous user to a standard user with the given username and password.
* Data associated with the anonymous user is retained.</li>
* <li>logIn switches users without converting the anonymous user. Data associated with the
* anonymous user will be lost.</li>
* <li>Service logIn (e.g. Facebook, Twitter) will attempt to convert the anonymous user into a
* standard user by linking it to the service. If a user already exists that is linked to the
* service, it will instead switch to the existing user.</li>
* <li>Service linking (e.g. Facebook, Twitter) will convert the anonymous user into a standard user
* by linking it to the service.</li>
* </ul>
* </ul>
*/
public final class ParseAnonymousUtils {
/* package */ static final String AUTH_TYPE = "anonymous";
/**
* Whether the user is logged in anonymously.
*
* @param user
* User to check for anonymity. The user must be logged in on this device.
* @return True if the user is anonymous. False if the user is not the current user or is not
* anonymous.
*/
public static boolean isLinked(ParseUser user) {
return user.isLinked(AUTH_TYPE);
}
/**
* Creates an anonymous user in the background.
*
* @return A Task that will be resolved when logging in is completed.
*/
public static Task<ParseUser> logInInBackground() {
return ParseUser.logInWithInBackground(AUTH_TYPE, getAuthData());
}
/**
* Creates an anonymous user in the background.
*
* @param callback
* The callback to execute when anonymous user creation is complete.
*/
public static void logIn(LogInCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(logInInBackground(), callback);
}
/* package */ static Map<String, String> getAuthData() {
Map<String, String> authData = new HashMap<>();
authData.put("id", UUID.randomUUID().toString());
return authData;
}
private ParseAnonymousUtils() {
// do nothing
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import bolts.Continuation;
import bolts.Task;
/** package */ class ParseAuthenticationManager {
private final Object lock = new Object();
private final Map<String, AuthenticationCallback> callbacks = new HashMap<>();
private final ParseCurrentUserController controller;
public ParseAuthenticationManager(ParseCurrentUserController controller) {
this.controller = controller;
}
public void register(final String authType, AuthenticationCallback callback) {
if (authType == null) {
throw new IllegalArgumentException("Invalid authType: " + null);
}
synchronized (lock) {
if (this.callbacks.containsKey(authType)) {
throw new IllegalStateException("Callback already registered for <" + authType + ">: "
+ this.callbacks.get(authType));
}
this.callbacks.put(authType, callback);
}
if (ParseAnonymousUtils.AUTH_TYPE.equals(authType)) {
// There's nothing to synchronize
return;
}
// Synchronize the current user with the auth callback.
controller.getAsync(false).onSuccessTask(new Continuation<ParseUser, Task<Void>>() {
@Override
public Task<Void> then(Task<ParseUser> task) throws Exception {
ParseUser user = task.getResult();
if (user != null) {
return user.synchronizeAuthDataAsync(authType);
}
return null;
}
});
}
public Task<Boolean> restoreAuthenticationAsync(String authType, final Map<String, String> authData) {
final AuthenticationCallback callback;
synchronized (lock) {
callback = this.callbacks.get(authType);
}
if (callback == null) {
return Task.forResult(true);
}
return Task.call(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return callback.onRestore(authData);
}
}, ParseExecutors.io());
}
public Task<Void> deauthenticateAsync(String authType) {
final AuthenticationCallback callback;
synchronized (lock) {
callback = this.callbacks.get(authType);
}
if (callback != null) {
return Task.call(new Callable<Void>() {
@Override
public Void call() throws Exception {
callback.onRestore(null);
return null;
}
}, ParseExecutors.io());
}
return Task.forResult(null);
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import com.parse.http.ParseHttpBody;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
/** package */ class ParseByteArrayHttpBody extends ParseHttpBody {
/* package */ final byte[] content;
/* package */ final InputStream contentInputStream;
public ParseByteArrayHttpBody(String content, String contentType)
throws UnsupportedEncodingException {
this(content.getBytes("UTF-8"), contentType);
}
public ParseByteArrayHttpBody(byte[] content, String contentType) {
super(contentType, content.length);
this.content = content;
this.contentInputStream = new ByteArrayInputStream(content);
}
@Override
public InputStream getContent() {
return contentInputStream;
}
@Override
public void writeTo(OutputStream out) throws IOException {
if (out == null) {
throw new IllegalArgumentException("Output stream may not be null");
}
out.write(content);
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/**
* A {@code ParseCallback} is used to do something after a background task completes. End users will
* use a specific subclass of {@code ParseCallback}.
*/
/** package */ interface ParseCallback1<T extends Throwable> {
/**
* {@code done(t)} must be overridden when you are doing a background operation. It is called
* when the background operation completes.
* <p/>
* If the operation is successful, {@code t} will be {@code null}.
* <p/>
* If the operation was unsuccessful, {@code t} will contain information about the operation
* failure.
*
* @param t
* Generally an {@link Throwable} that was thrown by the operation, if there was any.
*/
void done(T t);
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/**
* A {@code ParseCallback} is used to do something after a background task completes. End users will
* use a specific subclass of {@code ParseCallback}.
*/
/** package */ interface ParseCallback2<T1, T2 extends Throwable> {
/**
* {@code done(t1, t2)} must be overridden when you are doing a background operation. It is called
* when the background operation completes.
* <p/>
* If the operation is successful, {@code t1} will contain the results and {@code t2} will be
* {@code null}.
* <p/>
* If the operation was unsuccessful, {@code t1} will be {@code null} and {@code t2} will contain
* information about the operation failure.
*
* @param t1
* Generally the results of the operation.
* @param t2
* Generally an {@link Throwable} that was thrown by the operation, if there was any.
*/
void done(T1 t1, T2 t2);
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Associates a class name for a subclass of ParseObject.
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ParseClassName {
/**
* @return The Parse class name associated with the ParseObject subclass.
*/
String value();
}

View File

@ -0,0 +1,107 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.List;
import java.util.Map;
import bolts.Continuation;
import bolts.Task;
/**
* The ParseCloud class defines provides methods for interacting with Parse Cloud Functions. A Cloud
* Function can be called with {@link #callFunctionInBackground(String, Map, FunctionCallback)}
* using a {@link FunctionCallback}. For example, this sample code calls the "validateGame" Cloud
* Function and calls processResponse if the call succeeded and handleError if it failed.
*
* <pre>
* ParseCloud.callFunctionInBackground("validateGame", parameters, new FunctionCallback<Object>() {
* public void done(Object object, ParseException e) {
* if (e == null) {
* processResponse(object);
* } else {
* handleError();
* }
* }
* }
* </pre>
*
* Using the callback methods is usually preferred because the network operation will not block the
* calling thread. However, in some cases it may be easier to use the
* {@link #callFunction(String, Map)} call which do block the calling thread. For example, if your
* application has already spawned a background task to perform work, that background task could use
* the blocking calls and avoid the code complexity of callbacks.
*/
public final class ParseCloud {
/* package for test */ static ParseCloudCodeController getCloudCodeController() {
return ParseCorePlugins.getInstance().getCloudCodeController();
}
/**
* Calls a cloud function in the background.
*
* @param name
* The cloud function to call.
* @param params
* The parameters to send to the cloud function. This map can contain anything that could
* be placed in a ParseObject except for ParseObjects themselves.
*
* @return A Task that will be resolved when the cloud function has returned.
*/
public static <T> Task<T> callFunctionInBackground(final String name,
final Map<String, ?> params) {
return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation<String, Task<T>>() {
@Override
public Task<T> then(Task<String> task) throws Exception {
String sessionToken = task.getResult();
return getCloudCodeController().callFunctionInBackground(name, params, sessionToken);
}
});
}
/**
* Calls a cloud function.
*
* @param name
* The cloud function to call.
* @param params
* The parameters to send to the cloud function. This map can contain anything that could
* be placed in a ParseObject except for ParseObjects themselves.
* @return The result of the cloud call. Result may be a @{link Map}&lt; {@link String}, ?&gt;,
* {@link ParseObject}, {@link List}&lt;?&gt;, or any type that can be set as a field in a
* ParseObject.
* @throws ParseException
*/
public static <T> T callFunction(String name, Map<String, ?> params) throws ParseException {
return ParseTaskUtils.wait(ParseCloud.<T>callFunctionInBackground(name, params));
}
/**
* Calls a cloud function in the background.
*
* @param name
* The cloud function to call.
* @param params
* The parameters to send to the cloud function. This map can contain anything that could
* be placed in a ParseObject except for ParseObjects themselves.
* @param callback
* The callback that will be called when the cloud function has returned.
*/
public static <T> void callFunctionInBackground(String name, Map<String, ?> params,
FunctionCallback<T> callback) {
ParseTaskUtils.callbackOnMainThreadAsync(
ParseCloud.<T>callFunctionInBackground(name, params),
callback);
}
private ParseCloud() {
// do nothing
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONObject;
import java.util.Map;
import bolts.Continuation;
import bolts.Task;
/** package */ class ParseCloudCodeController {
/* package for test */ final ParseHttpClient restClient;
public ParseCloudCodeController(ParseHttpClient restClient) {
this.restClient = restClient;
}
public <T> Task<T> callFunctionInBackground(final String name,
final Map<String, ?> params, String sessionToken) {
ParseRESTCommand command = ParseRESTCloudCommand.callFunctionCommand(
name,
params,
sessionToken);
return command.executeAsync(restClient).onSuccess(new Continuation<JSONObject, T>() {
@Override
public T then(Task<JSONObject> task) throws Exception {
@SuppressWarnings("unchecked")
T result = (T) convertCloudResponse(task.getResult());
return result;
}
});
}
/*
* Decodes any Parse data types in the result of the cloud function call.
*/
/* package for test */ Object convertCloudResponse(Object result) {
if (result instanceof JSONObject) {
JSONObject jsonResult = (JSONObject)result;
result = jsonResult.opt("result");
}
ParseDecoder decoder = ParseDecoder.get();
Object finalResult = decoder.decode(result);
if (finalResult != null) {
return finalResult;
}
return result;
}
}

View File

@ -0,0 +1,688 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
import bolts.Capture;
import bolts.Continuation;
import bolts.Task;
import bolts.TaskCompletionSource;
/**
* ParseCommandCache manages an on-disk cache of commands to be executed, and a thread with a
* standard run loop that executes the commands. There should only ever be one instance of this
* class, because multiple instances would be running separate threads trying to read and execute
* the same commands.
*/
/** package */ class ParseCommandCache extends ParseEventuallyQueue {
private static final String TAG = "com.parse.ParseCommandCache";
private static int filenameCounter = 0; // Appended to temp file names so we know their creation
// order.
// Lock guards access to the file system and all of the instance variables above. It is static so
// that if somehow there are two instances of ParseCommandCache, they won't step on each others'
// toes while using the file system. A thread with lock should *not* try to get runningLock, under
// penalty of deadlock. Only the run loop (runLoop) thread should ever wait on this lock. Other
// threads should notify on this lock whenever the run loop should wake up and try to execute more
// commands.
private static final Object lock = new Object();
private static File getCacheDir() {
// Construct the path to the cache directory.
File cacheDir = new File(Parse.getParseDir(), "CommandCache");
cacheDir.mkdirs();
return cacheDir;
}
public static int getPendingCount() {
synchronized (lock) {
String[] files = getCacheDir().list();
return files == null ? 0 : files.length;
}
}
private File cachePath; // Where the cache is stored on disk.
private int timeoutMaxRetries = 5; // Don't retry more than 5 times before assuming disconnection.
private double timeoutRetryWaitSeconds = 600.0f; // Wait 10 minutes before retrying after network
// timeout.
private int maxCacheSizeBytes = 10 * 1024 * 1024; // Don't consume more than N bytes of storage.
private boolean shouldStop; // Should the run loop thread processing the disk cache continue?
private boolean unprocessedCommandsExist; // Has a command been added which hasn't yet been
// processed by the run loop?
// Map of filename to TaskCompletionSource, for all commands that are in the queue from this run
// of the program. This is necessary so that the original objects can be notified after their
// saves complete.
private HashMap<File, TaskCompletionSource<JSONObject>> pendingTasks = new HashMap<>();
private boolean running; // Is the run loop executing commands from the disk cache running?
// Guards access to running. Gets a broadcast whenever running changes. A thread should only wait
// on runningLock if it's sure the value of running is going to change. Only the run loop
// (runLoop) thread should ever notify on runningLock. It's perfectly fine for a thread that has
// runningLock to then also try to acquire the other lock.
private final Object runningLock;
private Logger log; // Why is there a custom logger? To prevent Mockito deadlock!
private final ParseHttpClient httpClient;
ConnectivityNotifier notifier;
ConnectivityNotifier.ConnectivityListener listener = new ConnectivityNotifier.ConnectivityListener() {
@Override
public void networkConnectivityStatusChanged(Context context, Intent intent) {
final boolean connectionLost =
intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
final boolean isConnected = ConnectivityNotifier.isConnected(context);
/*
Hack to avoid blocking the UI thread with disk I/O
setConnected uses the same lock we use for synchronizing disk I/O, so there's a possibility
that we can block the UI thread on disk I/O, so we're going to bump the lock usage to a
different thread.
TODO(grantland): Convert to TaskQueue, similar to ParsePinningEventuallyQueue
*/
Task.call(new Callable<Void>() {
@Override
public Void call() throws Exception {
if (connectionLost) {
setConnected(false);
} else {
setConnected(isConnected);
}
return null;
}
}, ParseExecutors.io());
}
};
public ParseCommandCache(Context context, ParseHttpClient client) {
setConnected(false);
shouldStop = false;
running = false;
runningLock = new Object();
httpClient = client;
log = Logger.getLogger(TAG);
cachePath = getCacheDir();
if (!Parse.hasPermission(Manifest.permission.ACCESS_NETWORK_STATE)) {
// The command cache only works if the user has granted us permission to monitor the network.
return;
}
setConnected(ConnectivityNotifier.isConnected(context));
notifier = ConnectivityNotifier.getNotifier(context);
notifier.addListener(listener);
resume();
}
@Override
public void onDestroy() {
//TODO (grantland): pause #6484855
notifier.removeListener(listener);
}
// Set the maximum number of times to retry before assuming disconnection.
@SuppressWarnings("unused")
public void setTimeoutMaxRetries(int tries) {
synchronized (lock) {
timeoutMaxRetries = tries;
}
}
// Sets the amount of time to wait before retrying after network timeout.
public void setTimeoutRetryWaitSeconds(double seconds) {
synchronized (lock) {
timeoutRetryWaitSeconds = seconds;
}
}
// Sets the maximum amount of storage space this cache can consume.
public void setMaxCacheSizeBytes(int bytes) {
synchronized (lock) {
maxCacheSizeBytes = bytes;
}
}
// Starts the run loop thread running.
public void resume() {
synchronized (runningLock) {
if (!running) {
new Thread("ParseCommandCache.runLoop()") {
@Override
public void run() {
runLoop();
}
}.start();
try {
runningLock.wait();
} catch (InterruptedException e) {
// Someone told this thread to stop.
synchronized (lock) {
shouldStop = true;
lock.notifyAll();
}
}
}
}
}
// Stops the run loop thread from processing commands until resume is called.
// When this function returns, the run loop has stopped.
public void pause() {
synchronized (runningLock) {
if (running) {
synchronized (lock) {
shouldStop = true;
lock.notifyAll();
}
}
while (running) {
try {
runningLock.wait();
} catch (InterruptedException e) {
// Someone told this thread to stop while it was already waiting to
// finish...
// Ignore them and continue waiting.
}
}
}
}
/**
* Removes a file from the file system and any internal caches.
*/
private void removeFile(File file) {
synchronized (lock) {
// Remove the data in memory for this command.
pendingTasks.remove(file);
// Release all the localIds referenced by the command.
// Read one command from the cache.
JSONObject json;
try {
json = ParseFileUtils.readFileToJSONObject(file);
ParseRESTCommand command = commandFromJSON(json);
command.releaseLocalIds();
} catch (Exception e) {
// Well, we did our best. We'll just have to leak a localId.
}
// Delete the command file itself.
ParseFileUtils.deleteQuietly(file);
}
}
/**
* Makes this command cache forget all the state it keeps during a single run of the app. This is
* only for testing purposes.
*/
void simulateReboot() {
synchronized (lock) {
pendingTasks.clear();
}
}
/**
* Fakes an object update notification for use in tests. This is used by saveEventually to make it
* look like test code has updated an object through the command cache even if it actually
* avoided executing update by determining the object wasn't dirty.
*/
void fakeObjectUpdate() {
notifyTestHelper(TestHelper.COMMAND_ENQUEUED);
notifyTestHelper(TestHelper.COMMAND_SUCCESSFUL);
notifyTestHelper(TestHelper.OBJECT_UPDATED);
}
@Override
public Task<JSONObject> enqueueEventuallyAsync(ParseRESTCommand command,
ParseObject object) {
return enqueueEventuallyAsync(command, false, object);
}
/**
* Attempts to run the given command and any pending commands. Adds the command to the pending set
* if it can't be run yet.
*
* @param command
* - The command to run.
* @param preferOldest
* - When the disk is full, if preferOldest, drop new commands. Otherwise, the oldest
* commands will be deleted to make room.
* @param object
* - See runEventually.
*/
private Task<JSONObject> enqueueEventuallyAsync(ParseRESTCommand command, boolean preferOldest,
ParseObject object) {
Parse.requirePermission(Manifest.permission.ACCESS_NETWORK_STATE);
TaskCompletionSource<JSONObject> tcs = new TaskCompletionSource<>();
byte[] json;
try {
// If this object doesn't have an objectId yet, store the localId so we can remap it to the
// objectId after the save completes.
if (object != null && object.getObjectId() == null) {
command.setLocalId(object.getOrCreateLocalId());
}
JSONObject jsonObject = command.toJSONObject();
json = jsonObject.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) {
log.log(Level.WARNING, "UTF-8 isn't supported. This shouldn't happen.", e);
}
notifyTestHelper(TestHelper.COMMAND_NOT_ENQUEUED);
return Task.forResult(null);
}
// If this object by itself is larger than the full disk cache, then don't
// even bother trying.
if (json.length > maxCacheSizeBytes) {
if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) {
log.warning("Unable to save command for later because it's too big.");
}
notifyTestHelper(TestHelper.COMMAND_NOT_ENQUEUED);
return Task.forResult(null);
}
synchronized (lock) {
try {
// Is there enough free storage space?
String[] fileNames = cachePath.list();
if (fileNames != null) {
Arrays.sort(fileNames);
int size = 0;
for (String fileName : fileNames) {
File file = new File(cachePath, fileName);
// Should be safe to convert long to int, because we don't allow
// files larger than 2GB.
size += (int) file.length();
}
size += json.length;
if (size > maxCacheSizeBytes) {
if (preferOldest) {
if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) {
log.warning("Unable to save command for later because storage is full.");
}
return Task.forResult(null);
} else {
if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) {
log.warning("Deleting old commands to make room in command cache.");
}
int indexToDelete = 0;
while (size > maxCacheSizeBytes && indexToDelete < fileNames.length) {
File file = new File(cachePath, fileNames[indexToDelete++]);
size -= (int) file.length();
removeFile(file);
}
}
}
}
// Get the current time to store in the filename, so that we process them in order.
String prefix1 = Long.toHexString(System.currentTimeMillis());
if (prefix1.length() < 16) {
char[] zeroes = new char[16 - prefix1.length()];
Arrays.fill(zeroes, '0');
prefix1 = new String(zeroes) + prefix1;
}
// Then add another incrementing number in case we enqueue items faster than the system's
// time granularity.
String prefix2 = Integer.toHexString(filenameCounter++);
if (prefix2.length() < 8) {
char[] zeroes = new char[8 - prefix2.length()];
Arrays.fill(zeroes, '0');
prefix2 = new String(zeroes) + prefix2;
}
String prefix = "CachedCommand_" + prefix1 + "_" + prefix2 + "_";
// Get a unique filename to store this command in.
File path = File.createTempFile(prefix, "", cachePath);
// Write the command to that file.
pendingTasks.put(path, tcs);
command.retainLocalIds();
ParseFileUtils.writeByteArrayToFile(path, json);
notifyTestHelper(TestHelper.COMMAND_ENQUEUED);
unprocessedCommandsExist = true;
} catch (IOException e) {
if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) {
log.log(Level.WARNING, "Unable to save command for later.", e);
}
} finally {
lock.notifyAll();
}
}
return tcs.getTask();
}
/**
* Returns the number of commands currently in the set waiting to be run.
*/
@Override
public int pendingCount() {
return getPendingCount();
}
/**
* Gets rid of all pending commands.
*/
public void clear() {
synchronized (lock) {
File[] files = cachePath.listFiles();
if (files == null) {
return;
}
for (File file : files) {
removeFile(file);
}
pendingTasks.clear();
}
}
/**
* Manually sets the network connection status.
*/
public void setConnected(boolean connected) {
synchronized (lock) {
if (isConnected() != connected) {
if (connected) {
lock.notifyAll();
}
}
super.setConnected(connected);
}
}
/**
* This is kind of like ParseTaskUtils.wait(), except that it gives up the CommandCache's lock
* while the task is running, and reclaims it before returning.
*/
private <T> T waitForTaskWithoutLock(Task<T> task) throws ParseException {
synchronized (lock) {
final Capture<Boolean> finished = new Capture<>(false);
task.continueWith(new Continuation<T, Void>() {
@Override
public Void then(Task<T> task) throws Exception {
finished.set(true);
synchronized(lock) {
lock.notifyAll();
}
return null;
}
}, Task.BACKGROUND_EXECUTOR);
while (!finished.get()) {
try {
lock.wait();
} catch (InterruptedException ie) {
shouldStop = true;
}
}
return ParseTaskUtils.wait(task); // Just to get the return value and maybe rethrow.
}
}
/**
* Attempts to run every command in the disk queue in order, synchronously. If there is no network
* connection, returns immediately without doing anything. If there is supposedly a connection,
* but parse can't be reached, waits timeoutRetryWaitSeconds before retrying up to
* retriesRemaining times. Blocks until either there's a connection, or the retries are exhausted.
* If any command fails, just deletes it and moves on to the next one.
*/
private void maybeRunAllCommandsNow(int retriesRemaining) {
synchronized (lock) {
unprocessedCommandsExist = false;
if (!isConnected()) {
// There's no way to do work when there's no network connection.
return;
}
String[] fileNames = cachePath.list();
if (fileNames == null || fileNames.length == 0) {
return;
}
Arrays.sort(fileNames);
for (String fileName : fileNames) {
final File file = new File(cachePath, fileName);
// Read one command from the cache.
JSONObject json;
try {
json = ParseFileUtils.readFileToJSONObject(file);
} catch (FileNotFoundException e) {
// This shouldn't really be possible.
if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) {
log.log(Level.SEVERE, "File disappeared from cache while being read.", e);
}
continue;
} catch (IOException e) {
if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) {
log.log(Level.SEVERE, "Unable to read contents of file in cache.", e);
}
removeFile(file);
continue;
} catch (JSONException e) {
if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) {
log.log(Level.SEVERE, "Error parsing JSON found in cache.", e);
}
removeFile(file);
continue;
}
// Convert the command from a string.
final ParseRESTCommand command;
final TaskCompletionSource<JSONObject> tcs =
pendingTasks.containsKey(file) ? pendingTasks.get(file) : null;
try {
command = commandFromJSON(json);
} catch (JSONException e) {
if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) {
log.log(Level.SEVERE, "Unable to create ParseCommand from JSON.", e);
}
removeFile(file);
continue;
}
try {
Task<JSONObject> commandTask;
if (command == null) {
commandTask = Task.forResult(null);
if (tcs != null) {
tcs.setResult(null);
}
notifyTestHelper(TestHelper.COMMAND_OLD_FORMAT_DISCARDED);
} else {
commandTask = command.executeAsync(httpClient).continueWithTask(new Continuation<JSONObject, Task<JSONObject>>() {
@Override
public Task<JSONObject> then(Task<JSONObject> task) throws Exception {
String localId = command.getLocalId();
Exception error = task.getError();
if (error != null) {
if (error instanceof ParseException
&& ((ParseException) error).getCode() == ParseException.CONNECTION_FAILED) {
// do nothing
} else {
if (tcs != null) {
tcs.setError(error);
}
}
return task;
}
JSONObject json = task.getResult();
if (tcs != null) {
tcs.setResult(json);
} else if (localId != null) {
// If this command created a new objectId, add it to the map.
String objectId = json.optString("objectId", null);
if (objectId != null) {
ParseCorePlugins.getInstance()
.getLocalIdManager().setObjectId(localId, objectId);
}
}
return task;
}
});
}
waitForTaskWithoutLock(commandTask);
if (tcs != null) {
waitForTaskWithoutLock(tcs.getTask());
}
// The command succeeded. Remove it from the cache.
removeFile(file);
notifyTestHelper(TestHelper.COMMAND_SUCCESSFUL);
} catch (ParseException e) {
if (e.getCode() == ParseException.CONNECTION_FAILED) {
if (retriesRemaining > 0) {
// Reachability says we have a network connection, but we can't actually contact
// Parse. Wait N minutes, or until we get signaled again before doing anything else.
if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) {
log.info("Network timeout in command cache. Waiting for " + timeoutRetryWaitSeconds
+ " seconds and then retrying " + retriesRemaining + " times.");
}
long currentTime = System.currentTimeMillis();
long waitUntil = currentTime + (long) (timeoutRetryWaitSeconds * 1000);
while (currentTime < waitUntil) {
// We haven't waited long enough, but if we lost the connection,
// or should stop, just quit.
if (!isConnected() || shouldStop) {
if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) {
log.info("Aborting wait because runEventually thread should stop.");
}
return;
}
try {
lock.wait(waitUntil - currentTime);
} catch (InterruptedException ie) {
shouldStop = true;
}
currentTime = System.currentTimeMillis();
if (currentTime < (waitUntil - (long) (timeoutRetryWaitSeconds * 1000))) {
// This situation should be impossible, so it must mean the clock changed.
currentTime = (waitUntil - (long) (timeoutRetryWaitSeconds * 1000));
}
}
maybeRunAllCommandsNow(retriesRemaining - 1);
} else {
setConnected(false);
notifyTestHelper(TestHelper.NETWORK_DOWN);
}
} else {
if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) {
log.log(Level.SEVERE, "Failed to run command.", e);
}
// Delete the command from the cache, even though it failed.
// Otherwise, we'll just keep trying it forever.
removeFile(file);
notifyTestHelper(TestHelper.COMMAND_FAILED, e);
}
}
}
}
}
/**
* The main function of the run loop thread. This function basically loops forever (unless pause
* is called). On each iteration, if it hasn't been told to stop, it calls maybeRunAllCommandsNow
* to try to execute everything queued up on disk. Then it waits until it gets signaled again by
* lock.notify(). Usually that happens as a result of either (1) Parse being initialized, (2)
* runEventually being called, or (3) the OS notifying that the network connection has been
* re-established.
*/
private void runLoop() {
if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) {
log.info("Parse command cache has started processing queued commands.");
}
// Make sure we marked as running.
synchronized (runningLock) {
if (running) {
// Don't run this thread more than once.
return;
} else {
running = true;
runningLock.notifyAll();
}
}
boolean shouldRun;
synchronized (lock) {
shouldRun = !(shouldStop || Thread.interrupted());
}
while (shouldRun) {
synchronized (lock) {
try {
maybeRunAllCommandsNow(timeoutMaxRetries);
if (!shouldStop) {
try {
/*
* If an unprocessed command was added, avoid waiting because we want
* maybeRunAllCommandsNow to run at least once to potentially process that command.
*/
if (!unprocessedCommandsExist) {
lock.wait();
}
} catch (InterruptedException e) {
shouldStop = true;
}
}
} catch (Exception e) {
if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) {
log.log(Level.SEVERE, "saveEventually thread had an error.", e);
}
} finally {
shouldRun = !shouldStop;
}
}
}
synchronized (runningLock) {
running = false;
runningLock.notifyAll();
}
if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) {
log.info("saveEventually thread has stopped processing commands.");
}
}
}

View File

@ -0,0 +1,563 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import bolts.Continuation;
import bolts.Task;
/**
* The {@code ParseConfig} is a local representation of configuration data that can be set from the
* Parse dashboard.
*/
public class ParseConfig {
/* package for tests */ static final TaskQueue taskQueue = new TaskQueue();
/* package for tests */ final Map<String, Object> params;
/* package for tests */ static ParseConfigController getConfigController() {
return ParseCorePlugins.getInstance().getConfigController();
}
/**
* Retrieves the most recently-fetched configuration object, either from memory or
* disk if necessary.
*
* @return The most recently-fetched {@code ParseConfig} if it exists, else an empty
* {@code ParseConfig}
*/
public static ParseConfig getCurrentConfig() {
try {
return ParseTaskUtils.wait(getConfigController().getCurrentConfigController()
.getCurrentConfigAsync()
);
} catch (ParseException e) {
// In order to have backward compatibility, we swallow the exception silently.
return new ParseConfig();
}
}
/**
* Fetches a new configuration object from the server.
*
* @throws ParseException
* Throws an exception if the server is inaccessible.
* @return The {@code ParseConfig} that was fetched.
*/
public static ParseConfig get() throws ParseException {
return ParseTaskUtils.wait(getInBackground());
}
/**
* Fetches a new configuration object from the server in a background thread. This is preferable
* to using {@link #get()}, unless your code is already running from a background thread.
*
* @param callback
* callback.done(config, e) is called when the fetch completes.
*/
public static void getInBackground(ConfigCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(getInBackground(), callback);
}
/**
* Fetches a new configuration object from the server in a background thread. This is preferable
* to using {@link #get()}, unless your code is already running from a background thread.
*
* @return A Task that is resolved when the fetch completes.
*/
public static Task<ParseConfig> getInBackground() {
return taskQueue.enqueue(new Continuation<Void, Task<ParseConfig>>() {
@Override
public Task<ParseConfig> then(Task<Void> toAwait) throws Exception {
return getAsync(toAwait);
}
});
}
private static Task<ParseConfig> getAsync(final Task<Void> toAwait) {
return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation<String, Task<ParseConfig>>() {
@Override
public Task<ParseConfig> then(Task<String> task) throws Exception {
final String sessionToken = task.getResult();
return toAwait.continueWithTask(new Continuation<Void, Task<ParseConfig>>() {
@Override
public Task<ParseConfig> then(Task<Void> task) throws Exception {
return getConfigController().getAsync(sessionToken);
}
});
}
});
}
@SuppressWarnings("unchecked")
/* package */ static ParseConfig decode(JSONObject json, ParseDecoder decoder) {
Map<String, Object> decodedObject = (Map<String, Object>) decoder.decode(json);
Map<String, Object> decodedParams = (Map<String, Object>) decodedObject.get("params");
if (decodedParams == null) {
throw new RuntimeException("Object did not contain the 'params' key.");
}
return new ParseConfig(decodedParams);
}
/* package */ ParseConfig(Map<String, Object> params) {
this.params = Collections.unmodifiableMap(params);
}
/* package */ ParseConfig() {
params = Collections.unmodifiableMap(new HashMap<String, Object>());
}
/* package */ Map<String, Object> getParams() {
return Collections.unmodifiableMap(new HashMap<>(params));
}
/**
* Access a value. In most cases it is more convenient to use a helper function such as
* {@link #getString} or {@link #getInt}.
*
* @param key
* The key to access the value for.
* @return Returns {@code null} if there is no such key.
*/
public Object get(String key) {
return get(key, null);
}
/**
* Access a value, returning a default value if the key doesn't exist. In most cases it is more
* convenient to use a helper function such as {@link #getString} or {@link #getInt}.
*
* @param key
* The key to access the value for.
* @param defaultValue
* The value to return if the key is not present in the configuration object.
* @return The default value if there is no such key.
*/
public Object get(String key, Object defaultValue) {
if (!params.containsKey(key)) {
return defaultValue;
}
Object value = params.get(key);
if (value == JSONObject.NULL) {
return null;
}
return params.get(key);
}
/**
* Access a {@code boolean} value.
*
* @param key
* The key to access the value for.
* @return Returns false if there is no such key or if it is not a {@code boolean}.
*/
public boolean getBoolean(String key) {
return getBoolean(key, false);
}
/**
* Access a {@code boolean} value, returning a default value if it doesn't exist.
*
* @param key
* The key to access the value for.
* @param defaultValue
* The value to return if the key is not present or has the wrong type.
* @return The default value if there is no such key or if it is not a {@code boolean}.
*/
public boolean getBoolean(String key, boolean defaultValue) {
if (!params.containsKey(key)) {
return defaultValue;
}
Object value = params.get(key);
return (value instanceof Boolean) ? (Boolean) value : defaultValue;
}
/**
* Access a {@link Date} value.
*
* @param key
* The key to access the value for.
* @return Returns {@code null} if there is no such key or if it is not a {@link Date}.
*/
public Date getDate(String key) {
return getDate(key, null);
}
/**
* Access a {@link Date} value, returning a default value if it doesn't exist.
*
* @param key
* The key to access the value for.
* @param defaultValue
* The value to return if the key is not present or has the wrong type.
* @return The default value if there is no such key or if it is not a {@link Date}.
*/
public Date getDate(String key, Date defaultValue) {
if (!params.containsKey(key)) {
return defaultValue;
}
Object value = params.get(key);
if (value == null || value == JSONObject.NULL) {
return null;
}
return (value instanceof Date) ? (Date) value : defaultValue;
}
/**
* Access a {@code double} value.
*
* @param key
* The key to access the value for.
* @return Returns 0 if there is no such key or if it is not a number.
*/
public double getDouble(String key) {
return getDouble(key, 0.0);
}
/**
* Access a {@code double} value, returning a default value if it doesn't exist.
*
* @param key
* The key to access the value for.
* @param defaultValue
* The value to return if the key is not present or has the wrong type.
* @return The default value if there is no such key or if it is not a number.
*/
public double getDouble(String key, double defaultValue) {
Number number = getNumber(key);
return number != null ? number.doubleValue() : defaultValue;
}
/**
* Access an {@code int} value.
*
* @param key
* The key to access the value for.
* @return Returns 0 if there is no such key or if it is not a number.
*/
public int getInt(String key) {
return getInt(key, 0);
}
/**
* Access an {@code int} value, returning a default value if it doesn't exist.
*
* @param key
* The key to access the value for.
* @param defaultValue
* The value to return if the key is not present or has the wrong type.
* @return The default value if there is no such key or if it is not a number.
*/
public int getInt(String key, int defaultValue) {
Number number = getNumber(key);
return number != null ? number.intValue() : defaultValue;
}
/**
* Access a {@link JSONArray} value.
*
* @param key
* The key to access the value for.
* @return Returns {@code null} if there is no such key or if it is not a {@link JSONArray}.
*/
public JSONArray getJSONArray(String key) {
return getJSONArray(key, null);
}
/**
* Access a {@link JSONArray} value, returning a default value if it doesn't exist.
*
* @param key
* The key to access the value for.
* @param defaultValue
* The value to return if the key is not present or has the wrong type.
* @return The default value if there is no such key or if it is not a {@link JSONArray}.
*/
public JSONArray getJSONArray(String key, JSONArray defaultValue) {
List<Object> list = getList(key);
Object encoded = (list != null) ? PointerEncoder.get().encode(list) : null;
//TODO(mengyan) There are actually two cases, getList(key) will return null
// case 1: key not exist, in this situation, we should return JSONArray defaultValue
// case 2: key exist but value is Json.NULL, in this situation, we should return null
// The following line we only cover case 2. We can not revise it since it may break some
// existing app, but we should do it someday.
return (encoded == null || encoded instanceof JSONArray) ? (JSONArray) encoded : defaultValue;
}
/**
* Access a {@link JSONObject} value.
*
* @param key
* The key to access the value for.
* @return Returns {@code null} if there is no such key or if it is not a {@link JSONObject}.
*/
public JSONObject getJSONObject(String key) {
return getJSONObject(key, null);
}
/**
* Access a {@link JSONObject} value, returning a default value if it doesn't exist.
*
* @param key
* The key to access the value for.
* @param defaultValue
* The value to return if the key is not present or has the wrong type.
* @return The default value if there is no such key or if it is not a {@link JSONObject}.
*/
public JSONObject getJSONObject(String key, JSONObject defaultValue) {
Map<String, Object> map = getMap(key);
Object encoded = (map != null) ? PointerEncoder.get().encode(map) : null;
//TODO(mengyan) There are actually two cases, getList(key) will return null
// case 1: key not exist, in this situation, we should return JSONArray defaultValue
// case 2: key exist but value is Json.NULL, in this situation, we should return null
// The following line we only cover case 2. We can not revise it since it may break some
// existing app, but we should do it someday.
return (encoded == null || encoded instanceof JSONObject) ? (JSONObject) encoded : defaultValue;
}
/**
* Access a {@link List} value.
*
* @param key
* The key to access the value for.
* @return Returns {@code null} if there is no such key or if it cannot be converted to a
* {@link List}.
*/
public <T> List<T> getList(String key) {
return getList(key, null);
}
/**
* Access a {@link List} value, returning a default value if it doesn't exist.
*
* @param key
* The key to access the value for.
* @param defaultValue
* The value to return if the key is not present or has the wrong type.
* @return The default value if there is no such key or if it cannot be
* converted to a {@link List}.
*/
public <T> List<T> getList(String key, List<T> defaultValue) {
if (!params.containsKey(key)) {
return defaultValue;
}
Object value = params.get(key);
if (value == null || value == JSONObject.NULL) {
return null;
}
@SuppressWarnings("unchecked")
List<T> returnValue = (value instanceof List) ? (List<T>) value : defaultValue;
return returnValue;
}
/**
* Access a {@code long} value.
*
* @param key
* The key to access the value for.
* @return Returns 0 if there is no such key or if it is not a number.
*/
public long getLong(String key) {
return getLong(key, 0);
}
/**
* Access a {@code long} value, returning a default value if it doesn't exist.
*
* @param key
* The key to access the value for.
* @param defaultValue
* The value to return if the key is not present or has the wrong type.
* @return The default value if there is no such key or if it is not a number.
*/
public long getLong(String key, long defaultValue) {
Number number = getNumber(key);
return number != null ? number.longValue() : defaultValue;
}
/**
* Access a {@link Map} value.
*
* @param key
* The key to access the value for.
* @return {@code null} if there is no such key or if it cannot be converted to a
* {@link Map}.
*/
public <V> Map<String, V> getMap(String key) {
return getMap(key, null);
}
/**
* Access a {@link Map} value, returning a default value if it doesn't exist.
*
* @param key
* The key to access the value for.
* @param defaultValue
* The value to return if the key is not present or has the wrong type.
* @return The default value if there is no such key or if it cannot be converted
* to a {@link Map}.
*/
public <V> Map<String, V> getMap(String key, Map<String, V> defaultValue) {
if (!params.containsKey(key)) {
return defaultValue;
}
Object value = params.get(key);
if (value == null || value == JSONObject.NULL) {
return null;
}
@SuppressWarnings("unchecked")
Map<String, V> returnValue = (value instanceof Map) ? (Map<String, V>) value : defaultValue;
return returnValue;
}
/**
* Access a numerical value.
*
* @param key
* The key to access the value for.
* @return Returns {@code null} if there is no such key or if it is not a {@link Number}.
*/
public Number getNumber(String key) {
return getNumber(key, null);
}
/**
* Access a numerical value, returning a default value if it doesn't exist.
*
* @param key
* The key to access the value for.
* @param defaultValue
* The value to return if the key is not present or has the wrong type.
* @return The default value if there is no such key or if it is not a {@link Number}.
*/
public Number getNumber(String key, Number defaultValue) {
if (!params.containsKey(key)) {
return defaultValue;
}
Object value = params.get(key);
if (value == null || value == JSONObject.NULL) {
return null;
}
return (value instanceof Number) ? (Number) value : defaultValue;
}
/**
* Access a {@link ParseFile} value. This function will not perform a network request. Unless the
* {@link ParseFile} has been downloaded (e.g. by calling {@link ParseFile#getData()}),
* {@link ParseFile#isDataAvailable()} will return false.
*
* @param key
* The key to access the value for.
* @return {@code null} if there is no such key or if it is not a {@link ParseFile}.
*/
public ParseFile getParseFile(String key) {
return getParseFile(key, null);
}
/**
* Access a {@link ParseFile} value, returning a default value if it doesn't exist. This function
* will not perform a network request. Unless the {@link ParseFile} has been downloaded
* (e.g. by calling {@link ParseFile#getData()}), {@link ParseFile#isDataAvailable()} will return
* false.
*
* @param key
* The key to access the value for.
* @param defaultValue
* The value to return if the key is not present or has the wrong type.
* @return The default value if there is no such key or if it is not a {@link ParseFile}.
*/
public ParseFile getParseFile(String key, ParseFile defaultValue) {
if (!params.containsKey(key)) {
return defaultValue;
}
Object value = params.get(key);
if (value == null || value == JSONObject.NULL) {
return null;
}
return (value instanceof ParseFile) ? (ParseFile) value : defaultValue;
}
/**
* Access a {@link ParseGeoPoint} value.
*
* @param key
* The key to access the value for
* @return {@code null} if there is no such key or if it is not a {@link ParseGeoPoint}.
*/
public ParseGeoPoint getParseGeoPoint(String key) {
return getParseGeoPoint(key, null);
}
/**
* Access a {@link ParseGeoPoint} value, returning a default value if it doesn't exist.
*
* @param key
* The key to access the value for
* @param defaultValue
* The value to return if the key is not present or has the wrong type.
* @return The default value if there is no such key or if it is not a {@link ParseGeoPoint}.
*/
public ParseGeoPoint getParseGeoPoint(String key, ParseGeoPoint defaultValue) {
if (!params.containsKey(key)) {
return defaultValue;
}
Object value = params.get(key);
if (value == null || value == JSONObject.NULL) {
return null;
}
return (value instanceof ParseGeoPoint) ? (ParseGeoPoint) value : defaultValue;
}
/**
* Access a {@link String} value.
*
* @param key
* The key to access the value for.
* @return Returns {@code null} if there is no such key or if it is not a {@link String}.
*/
public String getString(String key) {
return getString(key, null);
}
/**
* Access a {@link String} value, returning a default value if it doesn't exist.
*
* @param key
* The key to access the value for.
* @param defaultValue
* The value to return if the key is not present or has the wrong type.
* @return The default value if there is no such key or if it is not a {@link String}.
*/
public String getString(String key, String defaultValue) {
if (!params.containsKey(key)) {
return defaultValue;
}
Object value = params.get(key);
if (value == null || value == JSONObject.NULL) {
return null;
}
return (value instanceof String) ? (String) value : defaultValue;
}
@Override
public String toString() {
return "ParseConfig[" + params.toString() + "]";
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONObject;
import bolts.Continuation;
import bolts.Task;
/** package */ class ParseConfigController {
private ParseCurrentConfigController currentConfigController;
private final ParseHttpClient restClient;
public ParseConfigController(ParseHttpClient restClient,
ParseCurrentConfigController currentConfigController) {
this.restClient = restClient;
this.currentConfigController = currentConfigController;
}
/* package */ ParseCurrentConfigController getCurrentConfigController() {
return currentConfigController;
}
public Task<ParseConfig> getAsync(String sessionToken) {
final ParseRESTCommand command = ParseRESTConfigCommand.fetchConfigCommand(sessionToken);
return command.executeAsync(restClient).onSuccessTask(new Continuation<JSONObject, Task<ParseConfig>>() {
@Override
public Task<ParseConfig> then(Task<JSONObject> task) throws Exception {
JSONObject result = task.getResult();
final ParseConfig config = ParseConfig.decode(result, ParseDecoder.get());
return currentConfigController.setCurrentConfigAsync(config).continueWith(new Continuation<Void, ParseConfig>() {
@Override
public ParseConfig then(Task<Void> task) throws Exception {
return config;
}
});
}
});
}
}

View File

@ -0,0 +1,356 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.io.File;
import java.util.concurrent.atomic.AtomicReference;
/** package */ class ParseCorePlugins {
private static final ParseCorePlugins INSTANCE = new ParseCorePlugins();
public static ParseCorePlugins getInstance() {
return INSTANCE;
}
/* package */ static final String FILENAME_CURRENT_USER = "currentUser";
/* package */ static final String PIN_CURRENT_USER = "_currentUser";
/* package */ static final String FILENAME_CURRENT_INSTALLATION = "currentInstallation";
/* package */ static final String PIN_CURRENT_INSTALLATION = "_currentInstallation";
/* package */ static final String FILENAME_CURRENT_CONFIG = "currentConfig";
private AtomicReference<ParseObjectController> objectController = new AtomicReference<>();
private AtomicReference<ParseUserController> userController = new AtomicReference<>();
private AtomicReference<ParseSessionController> sessionController = new AtomicReference<>();
// TODO(mengyan): Inject into ParseUserInstanceController
private AtomicReference<ParseCurrentUserController> currentUserController =
new AtomicReference<>();
// TODO(mengyan): Inject into ParseInstallationInstanceController
private AtomicReference<ParseCurrentInstallationController> currentInstallationController =
new AtomicReference<>();
private AtomicReference<ParseAuthenticationManager> authenticationController =
new AtomicReference<>();
private AtomicReference<ParseQueryController> queryController = new AtomicReference<>();
private AtomicReference<ParseFileController> fileController = new AtomicReference<>();
private AtomicReference<ParseAnalyticsController> analyticsController = new AtomicReference<>();
private AtomicReference<ParseCloudCodeController> cloudCodeController = new AtomicReference<>();
private AtomicReference<ParseConfigController> configController = new AtomicReference<>();
private AtomicReference<ParsePushController> pushController = new AtomicReference<>();
private AtomicReference<ParsePushChannelsController> pushChannelsController =
new AtomicReference<>();
private AtomicReference<ParseDefaultACLController> defaultACLController = new AtomicReference<>();
private AtomicReference<LocalIdManager> localIdManager = new AtomicReference<>();
private AtomicReference<ParseObjectSubclassingController> subclassingController = new AtomicReference<>();
private ParseCorePlugins() {
// do nothing
}
/* package for tests */ void reset() {
objectController.set(null);
userController.set(null);
sessionController.set(null);
currentUserController.set(null);
currentInstallationController.set(null);
authenticationController.set(null);
queryController.set(null);
fileController.set(null);
analyticsController.set(null);
cloudCodeController.set(null);
configController.set(null);
pushController.set(null);
pushChannelsController.set(null);
defaultACLController.set(null);
localIdManager.set(null);
}
public ParseObjectController getObjectController() {
if (objectController.get() == null) {
// TODO(grantland): Do not rely on Parse global
objectController.compareAndSet(
null, new NetworkObjectController(ParsePlugins.get().restClient()));
}
return objectController.get();
}
public void registerObjectController(ParseObjectController controller) {
if (!objectController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another object controller was already registered: " + objectController.get());
}
}
public ParseUserController getUserController() {
if (userController.get() == null) {
// TODO(grantland): Do not rely on Parse global
userController.compareAndSet(
null, new NetworkUserController(ParsePlugins.get().restClient()));
}
return userController.get();
}
public void registerUserController(ParseUserController controller) {
if (!userController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another user controller was already registered: " + userController.get());
}
}
public ParseSessionController getSessionController() {
if (sessionController.get() == null) {
// TODO(grantland): Do not rely on Parse global
sessionController.compareAndSet(
null, new NetworkSessionController(ParsePlugins.get().restClient()));
}
return sessionController.get();
}
public void registerSessionController(ParseSessionController controller) {
if (!sessionController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another session controller was already registered: " + sessionController.get());
}
}
public ParseCurrentUserController getCurrentUserController() {
if (currentUserController.get() == null) {
File file = new File(Parse.getParseDir(), FILENAME_CURRENT_USER);
FileObjectStore<ParseUser> fileStore =
new FileObjectStore<>(ParseUser.class, file, ParseUserCurrentCoder.get());
ParseObjectStore<ParseUser> store = Parse.isLocalDatastoreEnabled()
? new OfflineObjectStore<>(ParseUser.class, PIN_CURRENT_USER, fileStore)
: fileStore;
ParseCurrentUserController controller = new CachedCurrentUserController(store);
currentUserController.compareAndSet(null, controller);
}
return currentUserController.get();
}
public void registerCurrentUserController(ParseCurrentUserController controller) {
if (!currentUserController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another currentUser controller was already registered: " +
currentUserController.get());
}
}
public ParseQueryController getQueryController() {
if (queryController.get() == null) {
NetworkQueryController networkController = new NetworkQueryController(
ParsePlugins.get().restClient());
ParseQueryController controller;
// TODO(grantland): Do not rely on Parse global
if (Parse.isLocalDatastoreEnabled()) {
controller = new OfflineQueryController(
Parse.getLocalDatastore(),
networkController);
} else {
controller = new CacheQueryController(networkController);
}
queryController.compareAndSet(null, controller);
}
return queryController.get();
}
public void registerQueryController(ParseQueryController controller) {
if (!queryController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another query controller was already registered: " + queryController.get());
}
}
public ParseFileController getFileController() {
if (fileController.get() == null) {
// TODO(grantland): Do not rely on Parse global
fileController.compareAndSet(null, new ParseFileController(
ParsePlugins.get().restClient(),
Parse.getParseCacheDir("files")));
}
return fileController.get();
}
public void registerFileController(ParseFileController controller) {
if (!fileController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another file controller was already registered: " + fileController.get());
}
}
public ParseAnalyticsController getAnalyticsController() {
if (analyticsController.get() == null) {
// TODO(mengyan): Do not rely on Parse global
analyticsController.compareAndSet(null,
new ParseAnalyticsController(Parse.getEventuallyQueue()));
}
return analyticsController.get();
}
public void registerAnalyticsController(ParseAnalyticsController controller) {
if (!analyticsController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another analytics controller was already registered: " + analyticsController.get());
}
}
public ParseCloudCodeController getCloudCodeController() {
if (cloudCodeController.get() == null) {
cloudCodeController.compareAndSet(null, new ParseCloudCodeController(
ParsePlugins.get().restClient()));
}
return cloudCodeController.get();
}
public void registerCloudCodeController(ParseCloudCodeController controller) {
if (!cloudCodeController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another cloud code controller was already registered: " + cloudCodeController.get());
}
}
public ParseConfigController getConfigController() {
if (configController.get() == null) {
// TODO(mengyan): Do not rely on Parse global
File file = new File(ParsePlugins.get().getParseDir(), FILENAME_CURRENT_CONFIG);
ParseCurrentConfigController currentConfigController =
new ParseCurrentConfigController(file);
configController.compareAndSet(null, new ParseConfigController(
ParsePlugins.get().restClient(), currentConfigController));
}
return configController.get();
}
public void registerConfigController(ParseConfigController controller) {
if (!configController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another config controller was already registered: " + configController.get());
}
}
public ParsePushController getPushController() {
if (pushController.get() == null) {
pushController.compareAndSet(null, new ParsePushController(ParsePlugins.get().restClient()));
}
return pushController.get();
}
public void registerPushController(ParsePushController controller) {
if (!pushController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another push controller was already registered: " + pushController.get());
}
}
public ParsePushChannelsController getPushChannelsController() {
if (pushChannelsController.get() == null) {
pushChannelsController.compareAndSet(null, new ParsePushChannelsController());
}
return pushChannelsController.get();
}
public void registerPushChannelsController(ParsePushChannelsController controller) {
if (!pushChannelsController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another pushChannels controller was already registered: " +
pushChannelsController.get());
}
}
public ParseCurrentInstallationController getCurrentInstallationController() {
if (currentInstallationController.get() == null) {
File file = new File(ParsePlugins.get().getParseDir(), FILENAME_CURRENT_INSTALLATION);
FileObjectStore<ParseInstallation> fileStore =
new FileObjectStore<>(ParseInstallation.class, file, ParseObjectCurrentCoder.get());
ParseObjectStore<ParseInstallation> store = Parse.isLocalDatastoreEnabled()
? new OfflineObjectStore<>(ParseInstallation.class, PIN_CURRENT_INSTALLATION, fileStore)
: fileStore;
CachedCurrentInstallationController controller =
new CachedCurrentInstallationController(store, ParsePlugins.get().installationId());
currentInstallationController.compareAndSet(null, controller);
}
return currentInstallationController.get();
}
public void registerCurrentInstallationController(ParseCurrentInstallationController controller) {
if (!currentInstallationController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another currentInstallation controller was already registered: " +
currentInstallationController.get());
}
}
public ParseAuthenticationManager getAuthenticationManager() {
if (authenticationController.get() == null) {
ParseAuthenticationManager controller =
new ParseAuthenticationManager(getCurrentUserController());
authenticationController.compareAndSet(null, controller);
}
return authenticationController.get();
}
public void registerAuthenticationManager(ParseAuthenticationManager manager) {
if (!authenticationController.compareAndSet(null, manager)) {
throw new IllegalStateException(
"Another authentication manager was already registered: " +
authenticationController.get());
}
}
public ParseDefaultACLController getDefaultACLController() {
if (defaultACLController.get() == null) {
ParseDefaultACLController controller = new ParseDefaultACLController();
defaultACLController.compareAndSet(null, controller);
}
return defaultACLController.get();
}
public void registerDefaultACLController(ParseDefaultACLController controller) {
if (!defaultACLController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another defaultACL controller was already registered: " + defaultACLController.get());
}
}
public LocalIdManager getLocalIdManager() {
if (localIdManager.get() == null) {
LocalIdManager manager = new LocalIdManager(Parse.getParseDir());
localIdManager.compareAndSet(null, manager);
}
return localIdManager.get();
}
public void registerLocalIdManager(LocalIdManager manager) {
if (!localIdManager.compareAndSet(null, manager)) {
throw new IllegalStateException(
"Another localId manager was already registered: " + localIdManager.get());
}
}
public ParseObjectSubclassingController getSubclassingController() {
if (subclassingController.get() == null) {
ParseObjectSubclassingController controller = new ParseObjectSubclassingController();
subclassingController.compareAndSet(null, controller);
}
return subclassingController.get();
}
public void registerSubclassingController(ParseObjectSubclassingController controller) {
if (!subclassingController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another subclassing controller was already registered: " + subclassingController.get());
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.Callable;
import bolts.Task;
import static java.lang.Math.min;
/** package */ class ParseCountingByteArrayHttpBody extends ParseByteArrayHttpBody {
private static final int DEFAULT_CHUNK_SIZE = 4096;
private final ProgressCallback progressCallback;
public ParseCountingByteArrayHttpBody(byte[] content, String contentType,
final ProgressCallback progressCallback) {
super(content, contentType);
this.progressCallback = progressCallback;
}
@Override
public void writeTo(OutputStream out) throws IOException {
if (out == null) {
throw new IllegalArgumentException("Output stream may not be null");
}
int position = 0;
int totalLength = content.length;
while (position < totalLength) {
int length = min(totalLength - position, DEFAULT_CHUNK_SIZE);
out.write(content, position, length);
out.flush();
if (progressCallback != null) {
position += length;
int progress = 100 * position / totalLength;
progressCallback.done(progress);
}
}
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
/** package */ class ParseCountingFileHttpBody extends ParseFileHttpBody {
private static final int DEFAULT_CHUNK_SIZE = 4096;
private static final int EOF = -1;
private final ProgressCallback progressCallback;
public ParseCountingFileHttpBody(File file, ProgressCallback progressCallback) {
this(file, null, progressCallback);
}
public ParseCountingFileHttpBody(
File file, String contentType, ProgressCallback progressCallback) {
super(file, contentType);
this.progressCallback = progressCallback;
}
@Override
public void writeTo(OutputStream output) throws IOException {
if (output == null) {
throw new IllegalArgumentException("Output stream may not be null");
}
final FileInputStream fileInput = new FileInputStream(file);
try {
byte[] buffer = new byte[DEFAULT_CHUNK_SIZE];
int n;
long totalLength = file.length();
long position = 0;
while (EOF != (n = fileInput.read(buffer))) {
output.write(buffer, 0, n);
position += n;
if (progressCallback != null) {
int progress = (int) (100 * position / totalLength);
progressCallback.done(progress);
}
}
} finally {
ParseIOUtils.closeQuietly(fileInput);
}
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.Callable;
import bolts.Task;
/** package */ class ParseCurrentConfigController {
private final Object currentConfigMutex = new Object();
/* package for test */ ParseConfig currentConfig;
private File currentConfigFile;
public ParseCurrentConfigController(File currentConfigFile) {
this.currentConfigFile = currentConfigFile;
}
public Task<Void> setCurrentConfigAsync(final ParseConfig config) {
return Task.call(new Callable<Void>() {
@Override
public Void call() throws Exception {
synchronized (currentConfigMutex) {
currentConfig = config;
saveToDisk(config);
}
return null;
}
}, ParseExecutors.io());
}
public Task<ParseConfig> getCurrentConfigAsync() {
return Task.call(new Callable<ParseConfig>() {
@Override
public ParseConfig call() throws Exception {
synchronized (currentConfigMutex) {
if (currentConfig == null) {
ParseConfig config = getFromDisk();
currentConfig = (config != null) ? config : new ParseConfig();
}
}
return currentConfig;
}
}, ParseExecutors.io());
}
/**
* Retrieves a {@code ParseConfig} from a file on disk.
*
* @return The {@code ParseConfig} that was retrieved. If the file wasn't found, or the contents
* of the file is an invalid {@code ParseConfig}, returns null.
*/
/* package for test */ ParseConfig getFromDisk() {
JSONObject json;
try {
json = ParseFileUtils.readFileToJSONObject(currentConfigFile);
} catch (IOException | JSONException e) {
return null;
}
return ParseConfig.decode(json, ParseDecoder.get());
}
/* package */ void clearCurrentConfigForTesting() {
synchronized (currentConfigMutex) {
currentConfig = null;
}
}
/**
* Saves the {@code ParseConfig} to the a file on disk as JSON.
*
* @param config
* The ParseConfig which needs to be saved.
*/
/* package for test */ void saveToDisk(ParseConfig config) {
JSONObject object = new JSONObject();
try {
JSONObject jsonParams = (JSONObject) NoObjectsEncoder.get().encode(config.getParams());
object.put("params", jsonParams);
} catch (JSONException e) {
throw new RuntimeException("could not serialize config to JSON");
}
try {
ParseFileUtils.writeJSONObjectToFile(currentConfigFile, object);
} catch (IOException e) {
//TODO (grantland): We should do something if this fails...
}
}
}

View File

@ -0,0 +1,13 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/** package */ interface ParseCurrentInstallationController
extends ParseObjectCurrentController<ParseInstallation> {
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import bolts.Task;
/** package */ interface ParseCurrentUserController
extends ParseObjectCurrentController<ParseUser> {
/**
* Gets the persisted current ParseUser.
* @param shouldAutoCreateUser
* @return
*/
Task<ParseUser> getAsync(boolean shouldAutoCreateUser);
/**
* Sets the persisted current ParseUser only if it's current or we're not synced with disk.
* @param user
* @return
*/
Task<Void> setIfNeededAsync(ParseUser user);
/**
* Gets the session token of the persisted current ParseUser.
* @return
*/
Task<String> getCurrentSessionTokenAsync();
/**
* Logs out the current ParseUser.
* @return
*/
Task<Void> logOutAsync();
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.SimpleTimeZone;
/**
* This is the currently used date format. It is precise to the millisecond.
*/
/* package */ class ParseDateFormat {
private static final String TAG = "ParseDateFormat";
private static final ParseDateFormat INSTANCE = new ParseDateFormat();
public static ParseDateFormat getInstance() {
return INSTANCE;
}
// SimpleDateFormat isn't inherently thread-safe
private final Object lock = new Object();
private final DateFormat dateFormat;
private ParseDateFormat() {
DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
format.setTimeZone(new SimpleTimeZone(0, "GMT"));
dateFormat = format;
}
/* package */ Date parse(String dateString) {
synchronized (lock) {
try {
return dateFormat.parse(dateString);
} catch (java.text.ParseException e) {
// Should never happen
PLog.e(TAG, "could not parse date: " + dateString, e);
return null;
}
}
}
/* package */ String format(Date date) {
synchronized (lock) {
return dateFormat.format(date);
}
}
}

View File

@ -0,0 +1,156 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.util.Base64;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* A {@code ParseDecoder} can be used to transform JSON data structures into actual objects, such as
* {@link ParseObjects}.
*
* @see com.parse.ParseEncoder
*/
/** package */ class ParseDecoder {
// This class isn't really a Singleton, but since it has no state, it's more efficient to get the
// default instance.
private static final ParseDecoder INSTANCE = new ParseDecoder();
public static ParseDecoder get() {
return INSTANCE;
}
protected ParseDecoder() {
// do nothing
}
/* package */ List<Object> convertJSONArrayToList(JSONArray array) {
List<Object> list = new ArrayList<>();
for (int i = 0; i < array.length(); ++i) {
list.add(decode(array.opt(i)));
}
return list;
}
/* package */ Map<String, Object> convertJSONObjectToMap(JSONObject object) {
Map<String, Object> outputMap = new HashMap<>();
Iterator<String> it = object.keys();
while (it.hasNext()) {
String key = it.next();
Object value = object.opt(key);
outputMap.put(key, decode(value));
}
return outputMap;
}
/**
* Gets the <code>ParseObject</code> another object points to. By default a new
* object will be created.
*/
protected ParseObject decodePointer(String className, String objectId) {
return ParseObject.createWithoutData(className, objectId);
}
public Object decode(Object object) {
if (object instanceof JSONArray) {
return convertJSONArrayToList((JSONArray) object);
}
if (object == JSONObject.NULL) {
return null;
}
if (!(object instanceof JSONObject)) {
return object;
}
JSONObject jsonObject = (JSONObject) object;
String opString = jsonObject.optString("__op", null);
if (opString != null) {
try {
return ParseFieldOperations.decode(jsonObject, this);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
String typeString = jsonObject.optString("__type", null);
if (typeString == null) {
return convertJSONObjectToMap(jsonObject);
}
if (typeString.equals("Date")) {
String iso = jsonObject.optString("iso");
return ParseDateFormat.getInstance().parse(iso);
}
if (typeString.equals("Bytes")) {
String base64 = jsonObject.optString("base64");
return Base64.decode(base64, Base64.NO_WRAP);
}
if (typeString.equals("Pointer")) {
return decodePointer(jsonObject.optString("className"),
jsonObject.optString("objectId"));
}
if (typeString.equals("File")) {
return new ParseFile(jsonObject, this);
}
if (typeString.equals("GeoPoint")) {
double latitude, longitude;
try {
latitude = jsonObject.getDouble("latitude");
longitude = jsonObject.getDouble("longitude");
} catch (JSONException e) {
throw new RuntimeException(e);
}
return new ParseGeoPoint(latitude, longitude);
}
if (typeString.equals("Polygon")) {
List<ParseGeoPoint> coordinates = new ArrayList<ParseGeoPoint>();
try {
JSONArray array = jsonObject.getJSONArray("coordinates");
for (int i = 0; i < array.length(); ++i) {
JSONArray point = array.getJSONArray(i);
coordinates.add(new ParseGeoPoint(point.getDouble(0), point.getDouble(1)));
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
return new ParsePolygon(coordinates);
}
if (typeString.equals("Object")) {
return ParseObject.fromJSON(jsonObject, null, this);
}
if (typeString.equals("Relation")) {
return new ParseRelation<>(jsonObject, this);
}
if (typeString.equals("OfflineObject")) {
throw new RuntimeException("An unexpected offline pointer was encountered.");
}
return null;
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.lang.ref.WeakReference;
/** package */ class ParseDefaultACLController {
/* package for tests */ ParseACL defaultACL;
/* package for tests */ boolean defaultACLUsesCurrentUser;
/* package for tests */ WeakReference<ParseUser> lastCurrentUser;
/* package for tests */ ParseACL defaultACLWithCurrentUser;
/**
* Sets a default ACL that will be applied to all {@link ParseObject}s when they are created.
*
* @param acl
* The ACL to use as a template for all {@link ParseObject}s created after set
* has been called. This value will be copied and used as a template for the creation of
* new ACLs, so changes to the instance after {@code set(ParseACL, boolean)}
* has been called will not be reflected in new {@link ParseObject}s.
* @param withAccessForCurrentUser
* If {@code true}, the {@code ParseACL} that is applied to newly-created
* {@link ParseObject}s will provide read and write access to the
* {@link ParseUser#getCurrentUser()} at the time of creation. If {@code false}, the
* provided ACL will be used without modification. If acl is {@code null}, this value is
* ignored.
*/
public void set(ParseACL acl, boolean withAccessForCurrentUser) {
defaultACLWithCurrentUser = null;
lastCurrentUser = null;
if (acl != null) {
ParseACL newDefaultACL = acl.copy();
newDefaultACL.setShared(true);
defaultACL = newDefaultACL;
defaultACLUsesCurrentUser = withAccessForCurrentUser;
} else {
defaultACL = null;
}
}
public ParseACL get() {
if (defaultACLUsesCurrentUser && defaultACL != null) {
ParseUser currentUser = ParseUser.getCurrentUser();
if (currentUser != null) {
// If the currentUser has changed, generate a new ACL from the defaultACL.
ParseUser last = lastCurrentUser != null ? lastCurrentUser.get() : null;
if (last != currentUser) {
ParseACL newDefaultACLWithCurrentUser = defaultACL.copy();
newDefaultACLWithCurrentUser.setShared(true);
newDefaultACLWithCurrentUser.setReadAccess(currentUser, true);
newDefaultACLWithCurrentUser.setWriteAccess(currentUser, true);
defaultACLWithCurrentUser = newDefaultACLWithCurrentUser;
lastCurrentUser = new WeakReference<>(currentUser);
}
return defaultACLWithCurrentUser;
}
}
return defaultACL;
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.os.Parcel;
import org.json.JSONException;
import org.json.JSONObject;
/**
* An operation where a field is deleted from the object.
*/
/** package */ class ParseDeleteOperation implements ParseFieldOperation {
/* package */ final static String OP_NAME = "Delete";
private static final ParseDeleteOperation defaultInstance = new ParseDeleteOperation();
public static ParseDeleteOperation getInstance() {
return defaultInstance;
}
private ParseDeleteOperation() {
}
@Override
public JSONObject encode(ParseEncoder objectEncoder) throws JSONException {
JSONObject output = new JSONObject();
output.put("__op", OP_NAME);
return output;
}
@Override
public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) {
dest.writeString(OP_NAME);
}
@Override
public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) {
return this;
}
@Override
public Object apply(Object oldValue, String key) {
return null;
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Static utility helpers to compute {@link MessageDigest}s.
*/
/* package */ class ParseDigestUtils {
private static final char[] hexArray = "0123456789abcdef".toCharArray();
private ParseDigestUtils() {
// no instances allowed
}
public static String md5(String string) {
MessageDigest digester;
try {
digester = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
digester.update(string.getBytes());
byte[] digest = digester.digest();
return toHex(digest);
}
private static String toHex(byte[] bytes) {
// The returned string will be double the length of the passed array, as it takes two
// characters to represent any given byte.
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
}

View File

@ -0,0 +1,163 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.util.Base64;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* A {@code ParseEncoder} can be used to transform objects such as {@link ParseObjects} into JSON
* data structures.
*
* @see com.parse.ParseDecoder
*/
/** package */ abstract class ParseEncoder {
/* package */ static boolean isValidType(Object value) {
return value instanceof String
|| value instanceof Number
|| value instanceof Boolean
|| value instanceof Date
|| value instanceof List
|| value instanceof Map
|| value instanceof byte[]
|| value == JSONObject.NULL
|| value instanceof ParseObject
|| value instanceof ParseACL
|| value instanceof ParseFile
|| value instanceof ParseGeoPoint
|| value instanceof ParsePolygon
|| value instanceof ParseRelation;
}
public Object encode(Object object) {
try {
if (object instanceof ParseObject) {
return encodeRelatedObject((ParseObject) object);
}
// TODO(grantland): Remove once we disallow mutable nested queries t6941155
if (object instanceof ParseQuery.State.Builder<?>) {
ParseQuery.State.Builder<?> builder = (ParseQuery.State.Builder<?>) object;
return encode(builder.build());
}
if (object instanceof ParseQuery.State<?>) {
ParseQuery.State<?> state = (ParseQuery.State<?>) object;
return state.toJSON(this);
}
if (object instanceof Date) {
return encodeDate((Date) object);
}
if (object instanceof byte[]) {
JSONObject json = new JSONObject();
json.put("__type", "Bytes");
json.put("base64", Base64.encodeToString((byte[]) object, Base64.NO_WRAP));
return json;
}
if (object instanceof ParseFile) {
return ((ParseFile) object).encode();
}
if (object instanceof ParseGeoPoint) {
ParseGeoPoint point = (ParseGeoPoint) object;
JSONObject json = new JSONObject();
json.put("__type", "GeoPoint");
json.put("latitude", point.getLatitude());
json.put("longitude", point.getLongitude());
return json;
}
if (object instanceof ParsePolygon) {
ParsePolygon polygon = (ParsePolygon) object;
JSONObject json = new JSONObject();
json.put("__type", "Polygon");
json.put("coordinates", polygon.coordinatesToJSONArray());
return json;
}
if (object instanceof ParseACL) {
ParseACL acl = (ParseACL) object;
return acl.toJSONObject(this);
}
if (object instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) object;
JSONObject json = new JSONObject();
for (Map.Entry<String, Object> pair : map.entrySet()) {
json.put(pair.getKey(), encode(pair.getValue()));
}
return json;
}
if (object instanceof Collection) {
JSONArray array = new JSONArray();
for (Object item : (Collection<?>) object) {
array.put(encode(item));
}
return array;
}
if (object instanceof ParseRelation) {
ParseRelation<?> relation = (ParseRelation<?>) object;
return relation.encodeToJSON(this);
}
if (object instanceof ParseFieldOperation) {
return ((ParseFieldOperation) object).encode(this);
}
if (object instanceof ParseQuery.RelationConstraint) {
return ((ParseQuery.RelationConstraint) object).encode(this);
}
if (object == null) {
return JSONObject.NULL;
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
// String, Number, Boolean,
if (isValidType(object)) {
return object;
}
throw new IllegalArgumentException("invalid type for ParseObject: "
+ object.getClass().toString());
}
protected abstract JSONObject encodeRelatedObject(ParseObject object);
protected JSONObject encodeDate(Date date) {
JSONObject object = new JSONObject();
String iso = ParseDateFormat.getInstance().format(date);
try {
object.put("__type", "Date");
object.put("iso", iso);
} catch (JSONException e) {
// This should not happen
throw new RuntimeException(e);
}
return object;
}
}

View File

@ -0,0 +1,230 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.util.SparseArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import bolts.Task;
/* package */ abstract class ParseEventuallyQueue {
private boolean isConnected;
/**
* Gets notifications of various events happening in the command cache, so that tests can be
* synchronized.
*/
private TestHelper testHelper;
public abstract void onDestroy();
public void setConnected(boolean connected) {
isConnected = connected;
}
public boolean isConnected() {
return isConnected;
}
public abstract int pendingCount();
public void setTimeoutRetryWaitSeconds(double seconds) {
// do nothing
}
public void setMaxCacheSizeBytes(int bytes) {
// do nothing
}
/** See class TestHelper below. */
public TestHelper getTestHelper() {
if (testHelper == null) {
testHelper = new TestHelper();
}
return testHelper;
}
protected void notifyTestHelper(int event) {
notifyTestHelper(event, null);
}
protected void notifyTestHelper(int event, Throwable t) {
if (testHelper != null) {
testHelper.notify(event, t);
}
}
public abstract void pause();
public abstract void resume();
/**
* Attempts to run the given command and any pending commands. Adds the command to the pending set
* if it can't be run yet.
*
* @param command
* - The command to run.
* @param object
* - If this command references an unsaved object, we need to remove any previous command
* referencing that unsaved object. Otherwise, it will get created over and over again.
* So object is a reference to the object, if it has no objectId. Otherwise, it can be
* null.
*/
public abstract Task<JSONObject> enqueueEventuallyAsync(ParseRESTCommand command,
ParseObject object);
protected ParseRESTCommand commandFromJSON(JSONObject json)
throws JSONException {
ParseRESTCommand command = null;
if (ParseRESTCommand.isValidCommandJSONObject(json)) {
command = ParseRESTCommand.fromJSONObject(json);
} else if (ParseRESTCommand.isValidOldFormatCommandJSONObject(json)) {
// do nothing
} else {
throw new JSONException("Failed to load command from JSON.");
}
return command;
}
/* package */ Task<JSONObject> waitForOperationSetAndEventuallyPin(ParseOperationSet operationSet,
EventuallyPin eventuallyPin) {
return Task.forResult(null);
}
/* package */ abstract void simulateReboot();
/**
* Gets rid of all pending commands.
*/
public abstract void clear();
/**
* Fakes an object update notification for use in tests. This is used by saveEventually to make it
* look like test code has updated an object through the command cache even if it actually
* avoided executing update by determining the object wasn't dirty.
*/
void fakeObjectUpdate() {
if (testHelper != null) {
testHelper.notify(TestHelper.COMMAND_ENQUEUED);
testHelper.notify(TestHelper.COMMAND_SUCCESSFUL);
testHelper.notify(TestHelper.OBJECT_UPDATED);
}
}
/**
* Gets notifications of various events happening in the command cache, so that tests can be
* synchronized. See ParseCommandCacheTest for examples of how to use this.
*/
public static class TestHelper {
private static final int MAX_EVENTS = 1000;
public static final int COMMAND_SUCCESSFUL = 1;
public static final int COMMAND_FAILED = 2;
public static final int COMMAND_ENQUEUED = 3;
public static final int COMMAND_NOT_ENQUEUED = 4;
public static final int OBJECT_UPDATED = 5;
public static final int OBJECT_REMOVED = 6;
public static final int NETWORK_DOWN = 7;
public static final int COMMAND_OLD_FORMAT_DISCARDED = 8;
public static String getEventString(int event) {
switch (event) {
case COMMAND_SUCCESSFUL:
return "COMMAND_SUCCESSFUL";
case COMMAND_FAILED:
return "COMMAND_FAILED";
case COMMAND_ENQUEUED:
return "COMMAND_ENQUEUED";
case COMMAND_NOT_ENQUEUED:
return "COMMAND_NOT_ENQUEUED";
case OBJECT_UPDATED:
return "OBJECT_UPDATED";
case OBJECT_REMOVED:
return "OBJECT_REMOVED";
case NETWORK_DOWN:
return "NETWORK_DOWN";
case COMMAND_OLD_FORMAT_DISCARDED:
return "COMMAND_OLD_FORMAT_DISCARDED";
default:
throw new IllegalStateException("Encountered unknown event: " + event);
}
}
private SparseArray<Semaphore> events = new SparseArray<>();
private TestHelper() {
clear();
}
public void clear() {
events.clear();
events.put(COMMAND_SUCCESSFUL, new Semaphore(MAX_EVENTS));
events.put(COMMAND_FAILED, new Semaphore(MAX_EVENTS));
events.put(COMMAND_ENQUEUED, new Semaphore(MAX_EVENTS));
events.put(COMMAND_NOT_ENQUEUED, new Semaphore(MAX_EVENTS));
events.put(OBJECT_UPDATED, new Semaphore(MAX_EVENTS));
events.put(OBJECT_REMOVED, new Semaphore(MAX_EVENTS));
events.put(NETWORK_DOWN, new Semaphore(MAX_EVENTS));
events.put(COMMAND_OLD_FORMAT_DISCARDED, new Semaphore(MAX_EVENTS));
for (int i = 0; i < events.size(); i++) {
int event = events.keyAt(i);
events.get(event).acquireUninterruptibly(MAX_EVENTS);
}
}
public int unexpectedEvents() {
int sum = 0;
for (int i = 0; i < events.size(); i++) {
int event = events.keyAt(i);
sum += events.get(event).availablePermits();
}
return sum;
}
public List<String> getUnexpectedEvents() {
List<String> unexpectedEvents = new ArrayList<>();
for (int i = 0; i < events.size(); i++) {
int event = events.keyAt(i);
if (events.get(event).availablePermits() > 0) {
unexpectedEvents.add(getEventString(event));
}
}
return unexpectedEvents;
}
public void notify(int event) {
notify(event, null);
}
public void notify(int event, Throwable t) {
events.get(event).release();
}
public boolean waitFor(int event) {
return waitFor(event, 1);
}
public boolean waitFor(int event, int permits) {
try {
return events.get(event).tryAcquire(permits, 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
return false;
}
}
}
}

View File

@ -0,0 +1,291 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
/**
* A ParseException gets raised whenever a {@link ParseObject} issues an invalid request, such as
* deleting or editing an object that no longer exists on the server, or when there is a network
* failure preventing communication with the Parse server.
*/
public class ParseException extends Exception {
private static final long serialVersionUID = 1;
private int code;
public static final int OTHER_CAUSE = -1;
/**
* Error code indicating the connection to the Parse servers failed.
*/
public static final int CONNECTION_FAILED = 100;
/**
* Error code indicating the specified object doesn't exist.
*/
public static final int OBJECT_NOT_FOUND = 101;
/**
* Error code indicating you tried to query with a datatype that doesn't support it, like exact
* matching an array or object.
*/
public static final int INVALID_QUERY = 102;
/**
* Error code indicating a missing or invalid classname. Classnames are case-sensitive. They must
* start with a letter, and a-zA-Z0-9_ are the only valid characters.
*/
public static final int INVALID_CLASS_NAME = 103;
/**
* Error code indicating an unspecified object id.
*/
public static final int MISSING_OBJECT_ID = 104;
/**
* Error code indicating an invalid key name. Keys are case-sensitive. They must start with a
* letter, and a-zA-Z0-9_ are the only valid characters.
*/
public static final int INVALID_KEY_NAME = 105;
/**
* Error code indicating a malformed pointer. You should not see this unless you have been mucking
* about changing internal Parse code.
*/
public static final int INVALID_POINTER = 106;
/**
* Error code indicating that badly formed JSON was received upstream. This either indicates you
* have done something unusual with modifying how things encode to JSON, or the network is failing
* badly.
*/
public static final int INVALID_JSON = 107;
/**
* Error code indicating that the feature you tried to access is only available internally for
* testing purposes.
*/
public static final int COMMAND_UNAVAILABLE = 108;
/**
* You must call Parse.initialize before using the Parse library.
*/
public static final int NOT_INITIALIZED = 109;
/**
* Error code indicating that a field was set to an inconsistent type.
*/
public static final int INCORRECT_TYPE = 111;
/**
* Error code indicating an invalid channel name. A channel name is either an empty string (the
* broadcast channel) or contains only a-zA-Z0-9_ characters and starts with a letter.
*/
public static final int INVALID_CHANNEL_NAME = 112;
/**
* Error code indicating that push is misconfigured.
*/
public static final int PUSH_MISCONFIGURED = 115;
/**
* Error code indicating that the object is too large.
*/
public static final int OBJECT_TOO_LARGE = 116;
/**
* Error code indicating that the operation isn't allowed for clients.
*/
public static final int OPERATION_FORBIDDEN = 119;
/**
* Error code indicating the result was not found in the cache.
*/
public static final int CACHE_MISS = 120;
/**
* Error code indicating that an invalid key was used in a nested JSONObject.
*/
public static final int INVALID_NESTED_KEY = 121;
/**
* Error code indicating that an invalid filename was used for ParseFile. A valid file name
* contains only a-zA-Z0-9_. characters and is between 1 and 128 characters.
*/
public static final int INVALID_FILE_NAME = 122;
/**
* Error code indicating an invalid ACL was provided.
*/
public static final int INVALID_ACL = 123;
/**
* Error code indicating that the request timed out on the server. Typically this indicates that
* the request is too expensive to run.
*/
public static final int TIMEOUT = 124;
/**
* Error code indicating that the email address was invalid.
*/
public static final int INVALID_EMAIL_ADDRESS = 125;
/**
* Error code indicating that required field is missing.
*/
public static final int MISSING_REQUIRED_FIELD_ERROR = 135;
/**
* Error code indicating that a unique field was given a value that is already taken.
*/
public static final int DUPLICATE_VALUE = 137;
/**
* Error code indicating that a role's name is invalid.
*/
public static final int INVALID_ROLE_NAME = 139;
/**
* Error code indicating that an application quota was exceeded. Upgrade to resolve.
*/
public static final int EXCEEDED_QUOTA = 140;
/**
* Error code indicating that a Cloud Code script failed.
*/
public static final int SCRIPT_ERROR = 141;
/**
* Error code indicating that cloud code validation failed.
*/
public static final int VALIDATION_ERROR = 142;
/**
* Error code indicating that deleting a file failed.
*/
public static final int FILE_DELETE_ERROR = 153;
/**
* Error code indicating that the application has exceeded its request limit.
*/
public static final int REQUEST_LIMIT_EXCEEDED = 155;
/**
* Error code indicating that the provided event name is invalid.
*/
public static final int INVALID_EVENT_NAME = 160;
/**
* Error code indicating that the username is missing or empty.
*/
public static final int USERNAME_MISSING = 200;
/**
* Error code indicating that the password is missing or empty.
*/
public static final int PASSWORD_MISSING = 201;
/**
* Error code indicating that the username has already been taken.
*/
public static final int USERNAME_TAKEN = 202;
/**
* Error code indicating that the email has already been taken.
*/
public static final int EMAIL_TAKEN = 203;
/**
* Error code indicating that the email is missing, but must be specified.
*/
public static final int EMAIL_MISSING = 204;
/**
* Error code indicating that a user with the specified email was not found.
*/
public static final int EMAIL_NOT_FOUND = 205;
/**
* Error code indicating that a user object without a valid session could not be altered.
*/
public static final int SESSION_MISSING = 206;
/**
* Error code indicating that a user can only be created through signup.
*/
public static final int MUST_CREATE_USER_THROUGH_SIGNUP = 207;
/**
* Error code indicating that an an account being linked is already linked to another user.
*/
public static final int ACCOUNT_ALREADY_LINKED = 208;
/**
* Error code indicating that the current session token is invalid.
*/
public static final int INVALID_SESSION_TOKEN = 209;
/**
* Error code indicating that a user cannot be linked to an account because that account's id
* could not be found.
*/
public static final int LINKED_ID_MISSING = 250;
/**
* Error code indicating that a user with a linked (e.g. Facebook) account has an invalid session.
*/
public static final int INVALID_LINKED_SESSION = 251;
/**
* Error code indicating that a service being linked (e.g. Facebook or Twitter) is unsupported.
*/
public static final int UNSUPPORTED_SERVICE = 252;
/**
* Construct a new ParseException with a particular error code.
*
* @param theCode
* The error code to identify the type of exception.
* @param theMessage
* A message describing the error in more detail.
*/
public ParseException(int theCode, String theMessage) {
super(theMessage);
code = theCode;
}
/**
* Construct a new ParseException with an external cause.
*
* @param message
* A message describing the error in more detail.
* @param cause
* The cause of the error.
*/
public ParseException(int theCode, String message, Throwable cause) {
super(message, cause);
code = theCode;
}
/**
* Construct a new ParseException with an external cause.
*
* @param cause
* The cause of the error.
*/
public ParseException(Throwable cause) {
super(cause);
code = OTHER_CAUSE;
}
/**
* Access the code for this error.
*
* @return The numerical code for this error.
*/
public int getCode() {
return code;
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import bolts.Task;
/** package */ class ParseExecutors {
private static ScheduledExecutorService scheduledExecutor;
private static final Object SCHEDULED_EXECUTOR_LOCK = new Object();
/**
* Long running operations should NOT be put onto SCHEDULED_EXECUTOR.
*/
/* package */ static ScheduledExecutorService scheduled() {
synchronized (SCHEDULED_EXECUTOR_LOCK) {
if (scheduledExecutor == null) {
scheduledExecutor = java.util.concurrent.Executors.newScheduledThreadPool(1);
}
}
return scheduledExecutor;
}
/* package */ static Executor main() {
return Task.UI_THREAD_EXECUTOR;
}
/* package */ static Executor io() {
return Task.BACKGROUND_EXECUTOR;
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.os.Parcel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* A ParseFieldOperation represents a modification to a value in a ParseObject. For example,
* setting, deleting, or incrementing a value are all different kinds of ParseFieldOperations.
* ParseFieldOperations themselves can be considered to be immutable.
*/
/** package */ interface ParseFieldOperation {
/**
* Converts the ParseFieldOperation to a data structure (typically a JSONObject) that can be
* converted to JSON and sent to Parse as part of a save operation.
*
* @param objectEncoder
* An object responsible for serializing ParseObjects.
* @return An object to be jsonified.
*/
Object encode(ParseEncoder objectEncoder) throws JSONException;
/**
* Writes the ParseFieldOperation to the given Parcel using the given encoder.
*
* @param dest
* The destination Parcel.
* @param parcelableEncoder
* A ParseParcelableEncoder.
*/
void encode(Parcel dest, ParseParcelEncoder parcelableEncoder);
/**
* Returns a field operation that is composed of a previous operation followed by this operation.
* This will not mutate either operation. However, it may return self if the current operation is
* not affected by previous changes. For example:
*
* <pre>
* {increment by 2}.mergeWithPrevious({set to 5}) -> {set to 7}
* {set to 5}.mergeWithPrevious({increment by 2}) -> {set to 5}
* {add "foo"}.mergeWithPrevious({delete}) -> {set to ["foo"]}
* {delete}.mergeWithPrevious({add "foo"}) -> {delete}
* </pre>
*
* @param previous
* The most recent operation on the field, or null if none.
* @return A new ParseFieldOperation or this.
*/
ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous);
/**
* Returns a new estimated value based on a previous value and this operation. This value is not
* intended to be sent to Parse, but it used locally on the client to inspect the most likely
* current value for a field. The key and object are used solely for ParseRelation to be able to
* construct objects that refer back to its parent.
*
* @param oldValue
* The previous value for the field.
* @param key
* The key that this value is for.
* @return The new value for the field.
*/
Object apply(Object oldValue, String key);
}

View File

@ -0,0 +1,253 @@
package com.parse;
import android.os.Parcel;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Utility methods to deal with {@link ParseFieldOperation} decoding, both from JSON objects and
* from {@link Parcel}s.
*/
/* package */ final class ParseFieldOperations {
private ParseFieldOperations() {
}
/**
* A function that creates a ParseFieldOperation from a JSONObject or a Parcel.
*/
private interface ParseFieldOperationFactory {
ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException;
ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder);
}
// A map of all known decoders.
private static Map<String, ParseFieldOperationFactory> opDecoderMap = new HashMap<>();
/**
* Registers a single factory for a given __op field value.
*/
private static void registerDecoder(String opName, ParseFieldOperationFactory factory) {
opDecoderMap.put(opName, factory);
}
/**
* Registers a list of default decoder functions that convert a JSONObject with an __op field,
* or a Parcel with a op name string, into a ParseFieldOperation.
*/
static void registerDefaultDecoders() {
registerDecoder(ParseRelationOperation.OP_NAME_BATCH, new ParseFieldOperationFactory() {
@Override
public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder)
throws JSONException {
ParseFieldOperation op = null;
JSONArray ops = object.getJSONArray("ops");
for (int i = 0; i < ops.length(); ++i) {
ParseFieldOperation nextOp = ParseFieldOperations.decode(ops.getJSONObject(i), decoder);
op = nextOp.mergeWithPrevious(op);
}
return op;
}
@Override
public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) {
// Decode AddRelation and then RemoveRelation
ParseFieldOperation add = ParseFieldOperations.decode(source, decoder);
ParseFieldOperation remove = ParseFieldOperations.decode(source, decoder);
return remove.mergeWithPrevious(add);
}
});
registerDecoder(ParseDeleteOperation.OP_NAME, new ParseFieldOperationFactory() {
@Override
public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder)
throws JSONException {
return ParseDeleteOperation.getInstance();
}
@Override
public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) {
return ParseDeleteOperation.getInstance();
}
});
registerDecoder(ParseIncrementOperation.OP_NAME, new ParseFieldOperationFactory() {
@Override
public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder)
throws JSONException {
return new ParseIncrementOperation((Number) decoder.decode(object.opt("amount")));
}
@Override
public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) {
return new ParseIncrementOperation((Number) decoder.decode(source));
}
});
registerDecoder(ParseAddOperation.OP_NAME, new ParseFieldOperationFactory() {
@Override
public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder)
throws JSONException {
return new ParseAddOperation((Collection) decoder.decode(object.opt("objects")));
}
@Override
public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) {
int size = source.readInt();
List<Object> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
list.add(i, decoder.decode(source));
}
return new ParseAddOperation(list);
}
});
registerDecoder(ParseAddUniqueOperation.OP_NAME, new ParseFieldOperationFactory() {
@Override
public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder)
throws JSONException {
return new ParseAddUniqueOperation((Collection) decoder.decode(object.opt("objects")));
}
@Override
public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) {
int size = source.readInt();
List<Object> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
list.add(i, decoder.decode(source));
}
return new ParseAddUniqueOperation(list);
}
});
registerDecoder(ParseRemoveOperation.OP_NAME, new ParseFieldOperationFactory() {
@Override
public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder)
throws JSONException {
return new ParseRemoveOperation((Collection) decoder.decode(object.opt("objects")));
}
@Override
public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) {
int size = source.readInt();
List<Object> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
list.add(i, decoder.decode(source));
}
return new ParseRemoveOperation(list);
}
});
registerDecoder(ParseRelationOperation.OP_NAME_ADD, new ParseFieldOperationFactory() {
@Override
public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder)
throws JSONException {
JSONArray objectsArray = object.optJSONArray("objects");
List<ParseObject> objectsList = (List<ParseObject>) decoder.decode(objectsArray);
return new ParseRelationOperation<>(new HashSet<>(objectsList), null);
}
@Override
public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) {
int size = source.readInt();
Set<ParseObject> set = new HashSet<>(size);
for (int i = 0; i < size; i++) {
set.add((ParseObject) decoder.decode(source));
}
return new ParseRelationOperation<>(set, null);
}
});
registerDecoder(ParseRelationOperation.OP_NAME_REMOVE, new ParseFieldOperationFactory() {
@Override
public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder)
throws JSONException {
JSONArray objectsArray = object.optJSONArray("objects");
List<ParseObject> objectsList = (List<ParseObject>) decoder.decode(objectsArray);
return new ParseRelationOperation<>(null, new HashSet<>(objectsList));
}
@Override
public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) {
int size = source.readInt();
Set<ParseObject> set = new HashSet<>(size);
for (int i = 0; i < size; i++) {
set.add((ParseObject) decoder.decode(source));
}
return new ParseRelationOperation<>(null, set);
}
});
registerDecoder(ParseSetOperation.OP_NAME, new ParseFieldOperationFactory() {
@Override
public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException {
return null; // Not called.
}
@Override
public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) {
return new ParseSetOperation(decoder.decode(source));
}
});
}
/**
* Converts a parsed JSON object into a ParseFieldOperation.
*
* @param encoded
* A JSONObject containing an __op field.
* @return A ParseFieldOperation.
*/
static ParseFieldOperation decode(JSONObject encoded, ParseDecoder decoder) throws JSONException {
String op = encoded.optString("__op");
ParseFieldOperationFactory factory = opDecoderMap.get(op);
if (factory == null) {
throw new RuntimeException("Unable to decode operation of type " + op);
}
return factory.decode(encoded, decoder);
}
/**
* Reads a ParseFieldOperation out of the given Parcel.
*
* @param source
* The source Parcel.
* @param decoder
* The given ParseParcelableDecoder.
*
* @return A ParseFieldOperation.
*/
static ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) {
String op = source.readString();
ParseFieldOperationFactory factory = opDecoderMap.get(op);
if (factory == null) {
throw new RuntimeException("Unable to decode operation of type " + op);
}
return factory.decode(source, decoder);
}
/**
* Converts a JSONArray into an ArrayList.
*/
static ArrayList<Object> jsonArrayAsArrayList(JSONArray array) {
ArrayList<Object> result = new ArrayList<>(array.length());
for (int i = 0; i < array.length(); ++i) {
try {
result.add(array.get(i));
} catch (JSONException e) {
// This can't actually happen.
throw new RuntimeException(e);
}
}
return result;
}
}

View File

@ -0,0 +1,774 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.os.Parcel;
import android.os.Parcelable;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Callable;
import bolts.Continuation;
import bolts.Task;
import bolts.TaskCompletionSource;
/**
* {@code ParseFile} is a local representation of a file that is saved to the Parse cloud.
* <p/>
* The workflow is to construct a {@code ParseFile} with data and optionally a filename. Then save
* it and set it as a field on a {@link ParseObject}.
* <p/>
* Example:
* <pre>
* ParseFile file = new ParseFile("hello".getBytes());
* file.save();
*
* ParseObject object = new ParseObject("TestObject");
* object.put("file", file);
* object.save();
* </pre>
*/
public class ParseFile implements Parcelable {
/* package for tests */ static ParseFileController getFileController() {
return ParseCorePlugins.getInstance().getFileController();
}
private static ProgressCallback progressCallbackOnMainThread(
final ProgressCallback progressCallback) {
if (progressCallback == null) {
return null;
}
return new ProgressCallback() {
@Override
public void done(final Integer percentDone) {
Task.call(new Callable<Void>() {
@Override
public Void call() throws Exception {
progressCallback.done(percentDone);
return null;
}
}, ParseExecutors.main());
}
};
}
/* package */ static class State {
/* package */ static class Builder {
private String name;
private String mimeType;
private String url;
public Builder() {
// do nothing
}
public Builder(State state) {
name = state.name();
mimeType = state.mimeType();
url = state.url();
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder mimeType(String mimeType) {
this.mimeType = mimeType;
return this;
}
public Builder url(String url) {
this.url = url;
return this;
}
public State build() {
return new State(this);
}
}
private final String name;
private final String contentType;
private final String url;
private State(Builder builder) {
name = builder.name != null ? builder.name : "file";
contentType = builder.mimeType;
url = builder.url;
}
public String name() {
return name;
}
public String mimeType() {
return contentType;
}
public String url() {
return url;
}
}
private State state;
/**
* Staging of {@code ParseFile}'s data is stored in memory until the {@code ParseFile} has been
* successfully synced with the server.
*/
/* package for tests */ byte[] data;
/* package for tests */ File file;
/* package for tests */ final TaskQueue taskQueue = new TaskQueue();
private Set<TaskCompletionSource<?>> currentTasks = Collections.synchronizedSet(
new HashSet<TaskCompletionSource<?>>());
/**
* Creates a new file from a file pointer.
*
* @param file
* The file.
*/
public ParseFile(File file) {
this(file, null);
}
/**
* Creates a new file from a file pointer, and content type. Content type will be used instead of
* auto-detection by file extension.
*
* @param file
* The file.
* @param contentType
* The file's content type.
*/
public ParseFile(File file, String contentType) {
this(new State.Builder().name(file.getName()).mimeType(contentType).build());
this.file = file;
}
/**
* Creates a new file from a byte array, file name, and content type. Content type will be used
* instead of auto-detection by file extension.
*
* @param name
* The file's name, ideally with extension. The file name must begin with an alphanumeric
* character, and consist of alphanumeric characters, periods, spaces, underscores, or
* dashes.
* @param data
* The file's data.
* @param contentType
* The file's content type.
*/
public ParseFile(String name, byte[] data, String contentType) {
this(new State.Builder().name(name).mimeType(contentType).build());
this.data = data;
}
/**
* Creates a new file from a byte array.
*
* @param data
* The file's data.
*/
public ParseFile(byte[] data) {
this(null, data, null);
}
/**
* Creates a new file from a byte array and a name. Giving a name with a proper file extension
* (e.g. ".png") is ideal because it allows Parse to deduce the content type of the file and set
* appropriate HTTP headers when it is fetched.
*
* @param name
* The file's name, ideally with extension. The file name must begin with an alphanumeric
* character, and consist of alphanumeric characters, periods, spaces, underscores, or
* dashes.
* @param data
* The file's data.
*/
public ParseFile(String name, byte[] data) {
this(name, data, null);
}
/**
* Creates a new file from a byte array, and content type. Content type will be used instead of
* auto-detection by file extension.
*
* @param data
* The file's data.
* @param contentType
* The file's content type.
*/
public ParseFile(byte[] data, String contentType) {
this(null, data, contentType);
}
/**
* Creates a new file instance from a {@link Parcel} source. This is used when unparceling
* a non-dirty ParseFile. Subclasses that need Parcelable behavior should provide their own
* {@link android.os.Parcelable.Creator} and override this constructor.
*
* @param source
* the source Parcel
*/
protected ParseFile(Parcel source) {
this(source, ParseParcelDecoder.get());
}
/**
* Creates a new file instance from a {@link Parcel} using the given {@link ParseParcelDecoder}.
* The decoder is currently unused, but it might be in the future, plus this is the pattern we
* are using in parcelable classes.
*
* @param source the parcel
* @param decoder the decoder
*/
ParseFile(Parcel source, ParseParcelDecoder decoder) {
this(new State.Builder()
.url(source.readString())
.name(source.readString())
.mimeType(source.readByte() == 1 ? source.readString() : null)
.build());
}
/* package for tests */ ParseFile(State state) {
this.state = state;
}
/* package for tests */ State getState() {
return state;
}
/**
* The filename. Before save is called, this is just the filename given by the user (if any).
* After save is called, that name gets prefixed with a unique identifier.
*
* @return The file's name.
*/
public String getName() {
return state.name();
}
/**
* Whether the file still needs to be saved.
*
* @return Whether the file needs to be saved.
*/
public boolean isDirty() {
return state.url() == null;
}
/**
* Whether the file has available data.
*/
public boolean isDataAvailable() {
return data != null || getFileController().isDataAvailable(state);
}
/**
* This returns the url of the file. It's only available after you save or after you get the file
* from a ParseObject.
*
* @return The url of the file.
*/
public String getUrl() {
return state.url();
}
/**
* Saves the file to the Parse cloud synchronously.
*/
public void save() throws ParseException {
ParseTaskUtils.wait(saveInBackground());
}
private Task<Void> saveAsync(final String sessionToken,
final ProgressCallback uploadProgressCallback,
Task<Void> toAwait, final Task<Void> cancellationToken) {
// If the file isn't dirty, just return immediately.
if (!isDirty()) {
return Task.forResult(null);
}
if (cancellationToken != null && cancellationToken.isCancelled()) {
return Task.cancelled();
}
// Wait for our turn in the queue, then check state to decide whether to no-op.
return toAwait.continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
if (!isDirty()) {
return Task.forResult(null);
}
if (cancellationToken != null && cancellationToken.isCancelled()) {
return Task.cancelled();
}
Task<ParseFile.State> saveTask;
if (data != null) {
saveTask = getFileController().saveAsync(
state,
data,
sessionToken,
progressCallbackOnMainThread(uploadProgressCallback),
cancellationToken);
} else {
saveTask = getFileController().saveAsync(
state,
file,
sessionToken,
progressCallbackOnMainThread(uploadProgressCallback),
cancellationToken);
}
return saveTask.onSuccessTask(new Continuation<State, Task<Void>>() {
@Override
public Task<Void> then(Task<State> task) throws Exception {
state = task.getResult();
// Since we have successfully uploaded the file, we do not need to hold the file pointer
// anymore.
data = null;
file = null;
return task.makeVoid();
}
});
}
});
}
/**
* Saves the file to the Parse cloud in a background thread.
* `progressCallback` is guaranteed to be called with 100 before saveCallback is called.
*
* @param uploadProgressCallback
* A ProgressCallback that is called periodically with progress updates.
* @return A Task that will be resolved when the save completes.
*/
public Task<Void> saveInBackground(final ProgressCallback uploadProgressCallback) {
final TaskCompletionSource<Void> cts = new TaskCompletionSource<>();
currentTasks.add(cts);
return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation<String, Task<Void>>() {
@Override
public Task<Void> then(Task<String> task) throws Exception {
final String sessionToken = task.getResult();
return saveAsync(sessionToken, uploadProgressCallback, cts.getTask());
}
}).continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
cts.trySetResult(null); // release
currentTasks.remove(cts);
return task;
}
});
}
/* package */ Task<Void> saveAsync(final String sessionToken,
final ProgressCallback uploadProgressCallback, final Task<Void> cancellationToken) {
return taskQueue.enqueue(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> toAwait) throws Exception {
return saveAsync(sessionToken, uploadProgressCallback, toAwait, cancellationToken);
}
});
}
/**
* Saves the file to the Parse cloud in a background thread.
*
* @return A Task that will be resolved when the save completes.
*/
public Task<Void> saveInBackground() {
return saveInBackground((ProgressCallback) null);
}
/**
* Saves the file to the Parse cloud in a background thread.
* `progressCallback` is guaranteed to be called with 100 before saveCallback is called.
*
* @param saveCallback
* A SaveCallback that gets called when the save completes.
* @param progressCallback
* A ProgressCallback that is called periodically with progress updates.
*/
public void saveInBackground(final SaveCallback saveCallback,
ProgressCallback progressCallback) {
ParseTaskUtils.callbackOnMainThreadAsync(saveInBackground(progressCallback), saveCallback);
}
/**
* Saves the file to the Parse cloud in a background thread.
*
* @param callback
* A SaveCallback that gets called when the save completes.
*/
public void saveInBackground(SaveCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(saveInBackground(), callback);
}
/**
* Synchronously gets the data from cache if available or fetches its content from the network.
* You probably want to use {@link #getDataInBackground()} instead unless you're already in a
* background thread.
*/
public byte[] getData() throws ParseException {
return ParseTaskUtils.wait(getDataInBackground());
}
/**
* Asynchronously gets the data from cache if available or fetches its content from the network.
* A {@code ProgressCallback} will be called periodically with progress updates.
*
* @param progressCallback
* A {@code ProgressCallback} that is called periodically with progress updates.
* @return A Task that is resolved when the data has been fetched.
*/
public Task<byte[]> getDataInBackground(final ProgressCallback progressCallback) {
final TaskCompletionSource<Void> cts = new TaskCompletionSource<>();
currentTasks.add(cts);
return taskQueue.enqueue(new Continuation<Void, Task<byte[]>>() {
@Override
public Task<byte[]> then(Task<Void> toAwait) throws Exception {
return fetchInBackground(progressCallback, toAwait, cts.getTask()).onSuccess(new Continuation<File, byte[]>() {
@Override
public byte[] then(Task<File> task) throws Exception {
File file = task.getResult();
try {
return ParseFileUtils.readFileToByteArray(file);
} catch (IOException e) {
// do nothing
}
return null;
}
});
}
}).continueWithTask(new Continuation<byte[], Task<byte[]>>() {
@Override
public Task<byte[]> then(Task<byte[]> task) throws Exception {
cts.trySetResult(null); // release
currentTasks.remove(cts);
return task;
}
});
}
/**
* Asynchronously gets the data from cache if available or fetches its content from the network.
*
* @return A Task that is resolved when the data has been fetched.
*/
public Task<byte[]> getDataInBackground() {
return getDataInBackground((ProgressCallback) null);
}
/**
* Asynchronously gets the data from cache if available or fetches its content from the network.
* A {@code ProgressCallback} will be called periodically with progress updates.
* A {@code GetDataCallback} will be called when the get completes.
*
* @param dataCallback
* A {@code GetDataCallback} that is called when the get completes.
* @param progressCallback
* A {@code ProgressCallback} that is called periodically with progress updates.
*/
public void getDataInBackground(GetDataCallback dataCallback,
final ProgressCallback progressCallback) {
ParseTaskUtils.callbackOnMainThreadAsync(getDataInBackground(progressCallback), dataCallback);
}
/**
* Asynchronously gets the data from cache if available or fetches its content from the network.
* A {@code GetDataCallback} will be called when the get completes.
*
* @param dataCallback
* A {@code GetDataCallback} that is called when the get completes.
*/
public void getDataInBackground(GetDataCallback dataCallback) {
ParseTaskUtils.callbackOnMainThreadAsync(getDataInBackground(), dataCallback);
}
/**
* Synchronously gets the file pointer from cache if available or fetches its content from the
* network. You probably want to use {@link #getFileInBackground()} instead unless you're already
* in a background thread.
* <strong>Note: </strong> The {@link File} location may change without notice and should not be
* stored to be accessed later.
*/
public File getFile() throws ParseException {
return ParseTaskUtils.wait(getFileInBackground());
}
/**
* Asynchronously gets the file pointer from cache if available or fetches its content from the
* network. The {@code ProgressCallback} will be called periodically with progress updates.
* <strong>Note: </strong> The {@link File} location may change without notice and should not be
* stored to be accessed later.
*
* @param progressCallback
* A {@code ProgressCallback} that is called periodically with progress updates.
* @return A Task that is resolved when the file pointer of this object has been fetched.
*/
public Task<File> getFileInBackground(final ProgressCallback progressCallback) {
final TaskCompletionSource<Void> cts = new TaskCompletionSource<>();
currentTasks.add(cts);
return taskQueue.enqueue(new Continuation<Void, Task<File>>() {
@Override
public Task<File> then(Task<Void> toAwait) throws Exception {
return fetchInBackground(progressCallback, toAwait, cts.getTask());
}
}).continueWithTask(new Continuation<File, Task<File>>() {
@Override
public Task<File> then(Task<File> task) throws Exception {
cts.trySetResult(null); // release
currentTasks.remove(cts);
return task;
}
});
}
/**
* Asynchronously gets the file pointer from cache if available or fetches its content from the
* network.
* <strong>Note: </strong> The {@link File} location may change without notice and should not be
* stored to be accessed later.
*
* @return A Task that is resolved when the data has been fetched.
*/
public Task<File> getFileInBackground() {
return getFileInBackground((ProgressCallback) null);
}
/**
* Asynchronously gets the file pointer from cache if available or fetches its content from the
* network. The {@code GetFileCallback} will be called when the get completes.
* The {@code ProgressCallback} will be called periodically with progress updates.
* The {@code ProgressCallback} is guaranteed to be called with 100 before the
* {@code GetFileCallback} is called.
* <strong>Note: </strong> The {@link File} location may change without notice and should not be
* stored to be accessed later.
*
* @param fileCallback
* A {@code GetFileCallback} that is called when the get completes.
* @param progressCallback
* A {@code ProgressCallback} that is called periodically with progress updates.
*/
public void getFileInBackground(GetFileCallback fileCallback,
final ProgressCallback progressCallback) {
ParseTaskUtils.callbackOnMainThreadAsync(getFileInBackground(progressCallback), fileCallback);
}
/**
* Asynchronously gets the file pointer from cache if available or fetches its content from the
* network. The {@code GetFileCallback} will be called when the get completes.
* <strong>Note: </strong> The {@link File} location may change without notice and should not be
* stored to be accessed later.
*
* @param fileCallback
* A {@code GetFileCallback} that is called when the get completes.
*/
public void getFileInBackground(GetFileCallback fileCallback) {
ParseTaskUtils.callbackOnMainThreadAsync(getFileInBackground(), fileCallback);
}
/**
* Synchronously gets the data stream from cached file if available or fetches its content from
* the network, saves the content as cached file and returns the data stream of the cached file.
* You probably want to use {@link #getDataStreamInBackground} instead unless you're already in a
* background thread.
*/
public InputStream getDataStream() throws ParseException {
return ParseTaskUtils.wait(getDataStreamInBackground());
}
/**
* Asynchronously gets the data stream from cached file if available or fetches its content from
* the network, saves the content as cached file and returns the data stream of the cached file.
* The {@code ProgressCallback} will be called periodically with progress updates.
*
* @param progressCallback
* A {@code ProgressCallback} that is called periodically with progress updates.
* @return A Task that is resolved when the data stream of this object has been fetched.
*/
public Task<InputStream> getDataStreamInBackground(final ProgressCallback progressCallback) {
final TaskCompletionSource<Void> cts = new TaskCompletionSource<>();
currentTasks.add(cts);
return taskQueue.enqueue(new Continuation<Void, Task<InputStream>>() {
@Override
public Task<InputStream> then(Task<Void> toAwait) throws Exception {
return fetchInBackground(progressCallback, toAwait, cts.getTask()).onSuccess(new Continuation<File, InputStream>() {
@Override
public InputStream then(Task<File> task) throws Exception {
return new FileInputStream(task.getResult());
}
});
}
}).continueWithTask(new Continuation<InputStream, Task<InputStream>>() {
@Override
public Task<InputStream> then(Task<InputStream> task) throws Exception {
cts.trySetResult(null); // release
currentTasks.remove(cts);
return task;
}
});
}
/**
* Asynchronously gets the data stream from cached file if available or fetches its content from
* the network, saves the content as cached file and returns the data stream of the cached file.
*
* @return A Task that is resolved when the data stream has been fetched.
*/
public Task<InputStream> getDataStreamInBackground() {
return getDataStreamInBackground((ProgressCallback) null);
}
/**
* Asynchronously gets the data stream from cached file if available or fetches its content from
* the network, saves the content as cached file and returns the data stream of the cached file.
* The {@code GetDataStreamCallback} will be called when the get completes. The
* {@code ProgressCallback} will be called periodically with progress updates. The
* {@code ProgressCallback} is guaranteed to be called with 100 before
* {@code GetDataStreamCallback} is called.
*
* @param dataStreamCallback
* A {@code GetDataStreamCallback} that is called when the get completes.
* @param progressCallback
* A {@code ProgressCallback} that is called periodically with progress updates.
*/
public void getDataStreamInBackground(GetDataStreamCallback dataStreamCallback,
final ProgressCallback progressCallback) {
ParseTaskUtils.callbackOnMainThreadAsync(
getDataStreamInBackground(progressCallback), dataStreamCallback);
}
/**
* Asynchronously gets the data stream from cached file if available or fetches its content from
* the network, saves the content as cached file and returns the data stream of the cached file.
* The {@code GetDataStreamCallback} will be called when the get completes.
*
* @param dataStreamCallback
* A {@code GetDataStreamCallback} that is called when the get completes.
*/
public void getDataStreamInBackground(GetDataStreamCallback dataStreamCallback) {
ParseTaskUtils.callbackOnMainThreadAsync(getDataStreamInBackground(), dataStreamCallback);
}
private Task<File> fetchInBackground(
final ProgressCallback progressCallback,
Task<Void> toAwait,
final Task<Void> cancellationToken) {
if (cancellationToken != null && cancellationToken.isCancelled()) {
return Task.cancelled();
}
return toAwait.continueWithTask(new Continuation<Void, Task<File>>() {
@Override
public Task<File> then(Task<Void> task) throws Exception {
if (cancellationToken != null && cancellationToken.isCancelled()) {
return Task.cancelled();
}
return getFileController().fetchAsync(
state,
null,
progressCallbackOnMainThread(progressCallback),
cancellationToken);
}
});
}
/**
* Cancels the operations for this {@code ParseFile} if they are still in the task queue. However,
* if a network request has already been started for an operation, the network request will not
* be canceled.
*/
//TODO (grantland): Deprecate and replace with CancellationToken
public void cancel() {
Set<TaskCompletionSource<?>> tasks = new HashSet<>(currentTasks);
for (TaskCompletionSource<?> tcs : tasks) {
tcs.trySetCancelled();
}
currentTasks.removeAll(tasks);
}
/*
* Encode/Decode
*/
@SuppressWarnings("unused")
/* package */ ParseFile(JSONObject json, ParseDecoder decoder) {
this(new State.Builder().name(json.optString("name")).url(json.optString("url")).build());
}
/* package */ JSONObject encode() throws JSONException {
JSONObject json = new JSONObject();
json.put("__type", "File");
json.put("name", getName());
String url = getUrl();
if (url == null) {
throw new IllegalStateException("Unable to encode an unsaved ParseFile.");
}
json.put("url", getUrl());
return json;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
writeToParcel(dest, ParseParcelEncoder.get());
}
void writeToParcel(Parcel dest, ParseParcelEncoder encoder) {
if (isDirty()) {
throw new RuntimeException("Unable to parcel an unsaved ParseFile.");
}
dest.writeString(getUrl()); // Not null
dest.writeString(getName()); // Not null
String type = state.mimeType(); // Nullable
dest.writeByte(type != null ? (byte) 1 : 0);
if (type != null) {
dest.writeString(type);
}
}
public final static Creator<ParseFile> CREATOR = new Creator<ParseFile>() {
@Override
public ParseFile createFromParcel(Parcel source) {
return new ParseFile(source);
}
@Override
public ParseFile[] newArray(int size) {
return new ParseFile[size];
}
};
}

View File

@ -0,0 +1,238 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import com.parse.http.ParseHttpRequest;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import bolts.Continuation;
import bolts.Task;
// TODO(grantland): Create ParseFileController interface
/** package */ class ParseFileController {
private final Object lock = new Object();
private final ParseHttpClient restClient;
private final File cachePath;
private ParseHttpClient fileClient;
public ParseFileController(ParseHttpClient restClient, File cachePath) {
this.restClient = restClient;
this.cachePath = cachePath;
}
/**
* Gets the file http client if exists, otherwise lazily creates since developers might not always
* use our download mechanism.
*/
/* package */ ParseHttpClient fileClient() {
synchronized (lock) {
if (fileClient == null) {
fileClient = ParsePlugins.get().fileClient();
}
return fileClient;
}
}
/* package for tests */ ParseFileController fileClient(ParseHttpClient fileClient) {
synchronized (lock) {
this.fileClient = fileClient;
}
return this;
}
public File getCacheFile(ParseFile.State state) {
return new File(cachePath, state.name());
}
/* package for tests */ File getTempFile(ParseFile.State state) {
if (state.url() == null) {
return null;
}
return new File(cachePath, state.url() + ".tmp");
}
public boolean isDataAvailable(ParseFile.State state) {
return getCacheFile(state).exists();
}
public void clearCache() {
File[] files = cachePath.listFiles();
if (files == null) {
return;
}
for (File file : files) {
ParseFileUtils.deleteQuietly(file);
}
}
public Task<ParseFile.State> saveAsync(
final ParseFile.State state,
final byte[] data,
String sessionToken,
ProgressCallback uploadProgressCallback,
Task<Void> cancellationToken) {
if (state.url() != null) { // !isDirty
return Task.forResult(state);
}
if (cancellationToken != null && cancellationToken.isCancelled()) {
return Task.cancelled();
}
final ParseRESTCommand command = new ParseRESTFileCommand.Builder()
.fileName(state.name())
.data(data)
.contentType(state.mimeType())
.sessionToken(sessionToken)
.build();
return command.executeAsync(
restClient,
uploadProgressCallback,
null,
cancellationToken
).onSuccess(new Continuation<JSONObject, ParseFile.State>() {
@Override
public ParseFile.State then(Task<JSONObject> task) throws Exception {
JSONObject result = task.getResult();
ParseFile.State newState = new ParseFile.State.Builder(state)
.name(result.getString("name"))
.url(result.getString("url"))
.build();
// Write data to cache
try {
ParseFileUtils.writeByteArrayToFile(getCacheFile(newState), data);
} catch (IOException e) {
// do nothing
}
return newState;
}
}, ParseExecutors.io());
}
public Task<ParseFile.State> saveAsync(
final ParseFile.State state,
final File file,
String sessionToken,
ProgressCallback uploadProgressCallback,
Task<Void> cancellationToken) {
if (state.url() != null) { // !isDirty
return Task.forResult(state);
}
if (cancellationToken != null && cancellationToken.isCancelled()) {
return Task.cancelled();
}
final ParseRESTCommand command = new ParseRESTFileCommand.Builder()
.fileName(state.name())
.file(file)
.contentType(state.mimeType())
.sessionToken(sessionToken)
.build();
return command.executeAsync(
restClient,
uploadProgressCallback,
null,
cancellationToken
).onSuccess(new Continuation<JSONObject, ParseFile.State>() {
@Override
public ParseFile.State then(Task<JSONObject> task) throws Exception {
JSONObject result = task.getResult();
ParseFile.State newState = new ParseFile.State.Builder(state)
.name(result.getString("name"))
.url(result.getString("url"))
.build();
// Write data to cache
try {
ParseFileUtils.copyFile(file, getCacheFile(newState));
} catch (IOException e) {
// do nothing
}
return newState;
}
}, ParseExecutors.io());
}
public Task<File> fetchAsync(
final ParseFile.State state,
@SuppressWarnings("UnusedParameters") String sessionToken,
final ProgressCallback downloadProgressCallback,
final Task<Void> cancellationToken) {
if (cancellationToken != null && cancellationToken.isCancelled()) {
return Task.cancelled();
}
final File cacheFile = getCacheFile(state);
return Task.call(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return cacheFile.exists();
}
}, ParseExecutors.io()).continueWithTask(new Continuation<Boolean, Task<File>>() {
@Override
public Task<File> then(Task<Boolean> task) throws Exception {
boolean result = task.getResult();
if (result) {
return Task.forResult(cacheFile);
}
if (cancellationToken != null && cancellationToken.isCancelled()) {
return Task.cancelled();
}
// Generate the temp file path for caching ParseFile content based on ParseFile's url
// The reason we do not write to the cacheFile directly is because there is no way we can
// verify if a cacheFile is complete or not. If download is interrupted in the middle, next
// time when we download the ParseFile, since cacheFile has already existed, we will return
// this incomplete cacheFile
final File tempFile = getTempFile(state);
// network
final ParseFileRequest request =
new ParseFileRequest(ParseHttpRequest.Method.GET, state.url(), tempFile);
// We do not need to delete the temp file since we always try to overwrite it
return request.executeAsync(
fileClient(),
null,
downloadProgressCallback,
cancellationToken).continueWithTask(new Continuation<Void, Task<File>>() {
@Override
public Task<File> then(Task<Void> task) throws Exception {
// If the top-level task was cancelled, don't actually set the data -- just move on.
if (cancellationToken != null && cancellationToken.isCancelled()) {
throw new CancellationException();
}
if (task.isFaulted()) {
ParseFileUtils.deleteQuietly(tempFile);
return task.cast();
}
// Since we give the cacheFile pointer to developers, it is not safe to guarantee
// cacheFile always does not exist here, so it is better to delete it manually,
// otherwise moveFile may throw an exception.
ParseFileUtils.deleteQuietly(cacheFile);
ParseFileUtils.moveFile(tempFile, cacheFile);
return Task.forResult(cacheFile);
}
}, ParseExecutors.io());
}
});
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import com.parse.http.ParseHttpBody;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/** package */ class ParseFileHttpBody extends ParseHttpBody {
/* package */ final File file;
public ParseFileHttpBody(File file) {
this(file, null);
}
public ParseFileHttpBody(File file, String contentType) {
super(contentType, file.length());
this.file = file;
}
@Override
public InputStream getContent() throws IOException {
return new FileInputStream(file);
}
@Override
public void writeTo(OutputStream out) throws IOException {
if (out == null) {
throw new IllegalArgumentException("Output stream can not be null");
}
final FileInputStream fileInput = new FileInputStream(file);
try {
ParseIOUtils.copy(fileInput, out);
} finally {
ParseIOUtils.closeQuietly(fileInput);
}
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import com.parse.http.ParseHttpRequest;
import com.parse.http.ParseHttpResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.concurrent.Callable;
import bolts.Task;
/**
* Request returns a byte array of the response and provides a callback the progress of the data
* read from the network.
*/
/** package */ class ParseFileRequest extends ParseRequest<Void> {
// The temp file is used to save the ParseFile content when we fetch it from server
private final File tempFile;
public ParseFileRequest(ParseHttpRequest.Method method, String url, File tempFile) {
super(method, url);
this.tempFile = tempFile;
}
@Override
protected Task<Void> onResponseAsync(final ParseHttpResponse response,
final ProgressCallback downloadProgressCallback) {
int statusCode = response.getStatusCode();
if (statusCode >= 200 && statusCode < 300 || statusCode == 304) {
// OK
} else {
String action = method == ParseHttpRequest.Method.GET ? "Download from" : "Upload to";
return Task.forError(new ParseException(ParseException.CONNECTION_FAILED, String.format(
"%s file server failed. %s", action, response.getReasonPhrase())));
}
if (method != ParseHttpRequest.Method.GET) {
return null;
}
return Task.call(new Callable<Void>() {
@Override
public Void call() throws Exception {
long totalSize = response.getTotalSize();
long downloadedSize = 0;
InputStream responseStream = null;
FileOutputStream tempFileStream = null;
try {
responseStream = response.getContent();
tempFileStream = ParseFileUtils.openOutputStream(tempFile);
int nRead;
byte[] data = new byte[32 << 10]; // 32KB
while ((nRead = responseStream.read(data, 0, data.length)) != -1) {
tempFileStream.write(data, 0, nRead);
downloadedSize += nRead;
if (downloadProgressCallback != null && totalSize != -1) {
int progressToReport =
Math.round((float) downloadedSize / (float) totalSize * 100.0f);
downloadProgressCallback.done(progressToReport);
}
}
return null;
} finally {
ParseIOUtils.closeQuietly(responseStream);
ParseIOUtils.closeQuietly(tempFileStream);
}
}
}, ParseExecutors.io());
}
}

View File

@ -0,0 +1,546 @@
/*
* 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.
*/
package com.parse;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
/**
* General file manipulation utilities.
*/
/** package */ class ParseFileUtils {
/**
* The number of bytes in a kilobyte.
*/
public static final long ONE_KB = 1024;
/**
* The number of bytes in a megabyte.
*/
public static final long ONE_MB = ONE_KB * ONE_KB;
/**
* The file copy buffer size (30 MB)
*/
private static final long FILE_COPY_BUFFER_SIZE = ONE_MB * 30;
/**
* Reads the contents of a file into a byte array.
* The file is always closed.
*
* @param file the file to read, must not be <code>null</code>
* @return the file contents, never <code>null</code>
* @throws IOException in case of an I/O error
* @since Commons IO 1.1
*/
public static byte[] readFileToByteArray(File file) throws IOException {
InputStream in = null;
try {
in = openInputStream(file);
return ParseIOUtils.toByteArray(in);
} finally {
ParseIOUtils.closeQuietly(in);
}
}
//-----------------------------------------------------------------------
/**
* Opens a {@link FileInputStream} for the specified file, providing better
* error messages than simply calling <code>new FileInputStream(file)</code>.
* <p>
* At the end of the method either the stream will be successfully opened,
* or an exception will have been thrown.
* <p>
* An exception is thrown if the file does not exist.
* An exception is thrown if the file object exists but is a directory.
* An exception is thrown if the file exists but cannot be read.
*
* @param file the file to open for input, must not be <code>null</code>
* @return a new {@link FileInputStream} for the specified file
* @throws FileNotFoundException if the file does not exist
* @throws IOException if the file object is a directory
* @throws IOException if the file cannot be read
* @since Commons IO 1.3
*/
public static FileInputStream openInputStream(File file) throws IOException {
if (file.exists()) {
if (file.isDirectory()) {
throw new IOException("File '" + file + "' exists but is a directory");
}
if (!file.canRead()) {
throw new IOException("File '" + file + "' cannot be read");
}
} else {
throw new FileNotFoundException("File '" + file + "' does not exist");
}
return new FileInputStream(file);
}
/**
* Writes a byte array to a file creating the file if it does not exist.
* <p>
* NOTE: As from v1.3, the parent directories of the file will be created
* if they do not exist.
*
* @param file the file to write to
* @param data the content to write to the file
* @throws IOException in case of an I/O error
* @since Commons IO 1.1
*/
public static void writeByteArrayToFile(File file, byte[] data) throws IOException {
OutputStream out = null;
try {
out = openOutputStream(file);
out.write(data);
} finally {
ParseIOUtils.closeQuietly(out);
}
}
//-----------------------------------------------------------------------
/**
* Opens a {@link FileOutputStream} for the specified file, checking and
* creating the parent directory if it does not exist.
* <p>
* At the end of the method either the stream will be successfully opened,
* or an exception will have been thrown.
* <p>
* The parent directory will be created if it does not exist.
* The file will be created if it does not exist.
* An exception is thrown if the file object exists but is a directory.
* An exception is thrown if the file exists but cannot be written to.
* An exception is thrown if the parent directory cannot be created.
*
* @param file the file to open for output, must not be <code>null</code>
* @return a new {@link FileOutputStream} for the specified file
* @throws IOException if the file object is a directory
* @throws IOException if the file cannot be written to
* @throws IOException if a parent directory needs creating but that fails
* @since Commons IO 1.3
*/
public static FileOutputStream openOutputStream(File file) throws IOException {
if (file.exists()) {
if (file.isDirectory()) {
throw new IOException("File '" + file + "' exists but is a directory");
}
if (!file.canWrite()) {
throw new IOException("File '" + file + "' cannot be written to");
}
} else {
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
if (!parent.mkdirs()) {
throw new IOException("File '" + file + "' could not be created");
}
}
}
return new FileOutputStream(file);
}
/**
* Moves a file.
* <p>
* When the destination file is on another file system, do a "copy and delete".
*
* @param srcFile the file to be moved
* @param destFile the destination file
* @throws NullPointerException if source or destination is {@code null}
* @throws IOException if source or destination is invalid
* @throws IOException if an IO error occurs moving the file
* @since 1.4
*/
public static void moveFile(final File srcFile, final File destFile) throws IOException {
if (srcFile == null) {
throw new NullPointerException("Source must not be null");
}
if (destFile == null) {
throw new NullPointerException("Destination must not be null");
}
if (!srcFile.exists()) {
throw new FileNotFoundException("Source '" + srcFile + "' does not exist");
}
if (srcFile.isDirectory()) {
throw new IOException("Source '" + srcFile + "' is a directory");
}
if (destFile.exists()) {
throw new IOException("Destination '" + destFile + "' already exists");
}
if (destFile.isDirectory()) {
throw new IOException("Destination '" + destFile + "' is a directory");
}
final boolean rename = srcFile.renameTo(destFile);
if (!rename) {
copyFile( srcFile, destFile );
if (!srcFile.delete()) {
ParseFileUtils.deleteQuietly(destFile);
throw new IOException("Failed to delete original file '" + srcFile +
"' after copy to '" + destFile + "'");
}
}
}
/**
* Copies a file to a new location preserving the file date.
* <p>
* This method copies the contents of the specified source file to the
* specified destination file. The directory holding the destination file is
* created if it does not exist. If the destination file exists, then this
* method will overwrite it.
* <p>
* <strong>Note:</strong> This method tries to preserve the file's last
* modified date/times using {@link File#setLastModified(long)}, however
* it is not guaranteed that the operation will succeed.
* If the modification operation fails, no indication is provided.
*
* @param srcFile an existing file to copy, must not be {@code null}
* @param destFile the new file, must not be {@code null}
*
* @throws NullPointerException if source or destination is {@code null}
* @throws IOException if source or destination is invalid
* @throws IOException if an IO error occurs during copying
* @throws IOException if the output file length is not the same as the input file length after the copy completes
* @see #copyFile(File, File, boolean)
*/
public static void copyFile(final File srcFile, final File destFile) throws IOException {
copyFile(srcFile, destFile, true);
}
/**
* Copies a file to a new location.
* <p>
* This method copies the contents of the specified source file
* to the specified destination file.
* The directory holding the destination file is created if it does not exist.
* If the destination file exists, then this method will overwrite it.
* <p>
* <strong>Note:</strong> Setting <code>preserveFileDate</code> to
* {@code true} tries to preserve the file's last modified
* date/times using {@link File#setLastModified(long)}, however it is
* not guaranteed that the operation will succeed.
* If the modification operation fails, no indication is provided.
*
* @param srcFile an existing file to copy, must not be {@code null}
* @param destFile the new file, must not be {@code null}
* @param preserveFileDate true if the file date of the copy
* should be the same as the original
*
* @throws NullPointerException if source or destination is {@code null}
* @throws IOException if source or destination is invalid
* @throws IOException if an IO error occurs during copying
* @throws IOException if the output file length is not the same as the input file length after the copy completes
* @see #doCopyFile(File, File, boolean)
*/
public static void copyFile(final File srcFile, final File destFile,
final boolean preserveFileDate) throws IOException {
if (srcFile == null) {
throw new NullPointerException("Source must not be null");
}
if (destFile == null) {
throw new NullPointerException("Destination must not be null");
}
if (!srcFile.exists()) {
throw new FileNotFoundException("Source '" + srcFile + "' does not exist");
}
if (srcFile.isDirectory()) {
throw new IOException("Source '" + srcFile + "' exists but is a directory");
}
if (srcFile.getCanonicalPath().equals(destFile.getCanonicalPath())) {
throw new IOException("Source '" + srcFile + "' and destination '" + destFile + "' are the same");
}
final File parentFile = destFile.getParentFile();
if (parentFile != null) {
if (!parentFile.mkdirs() && !parentFile.isDirectory()) {
throw new IOException("Destination '" + parentFile + "' directory cannot be created");
}
}
if (destFile.exists() && !destFile.canWrite()) {
throw new IOException("Destination '" + destFile + "' exists but is read-only");
}
doCopyFile(srcFile, destFile, preserveFileDate);
}
/**
* Internal copy file method.
* This caches the original file length, and throws an IOException
* if the output file length is different from the current input file length.
* So it may fail if the file changes size.
* It may also fail with "IllegalArgumentException: Negative size" if the input file is truncated part way
* through copying the data and the new file size is less than the current position.
*
* @param srcFile the validated source file, must not be {@code null}
* @param destFile the validated destination file, must not be {@code null}
* @param preserveFileDate whether to preserve the file date
* @throws IOException if an error occurs
* @throws IOException if the output file length is not the same as the input file length after the copy completes
* @throws IllegalArgumentException "Negative size" if the file is truncated so that the size is less than the position
*/
private static void doCopyFile(final File srcFile, final File destFile, final boolean preserveFileDate) throws IOException {
if (destFile.exists() && destFile.isDirectory()) {
throw new IOException("Destination '" + destFile + "' exists but is a directory");
}
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel input = null;
FileChannel output = null;
try {
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
input = fis.getChannel();
output = fos.getChannel();
final long size = input.size(); // TODO See IO-386
long pos = 0;
long count = 0;
while (pos < size) {
final long remain = size - pos;
count = remain > FILE_COPY_BUFFER_SIZE ? FILE_COPY_BUFFER_SIZE : remain;
final long bytesCopied = output.transferFrom(input, pos, count);
if (bytesCopied == 0) { // IO-385 - can happen if file is truncated after caching the size
break; // ensure we don't loop forever
}
pos += bytesCopied;
}
} finally {
ParseIOUtils.closeQuietly(output);
ParseIOUtils.closeQuietly(fos);
ParseIOUtils.closeQuietly(input);
ParseIOUtils.closeQuietly(fis);
}
final long srcLen = srcFile.length(); // TODO See IO-386
final long dstLen = destFile.length(); // TODO See IO-386
if (srcLen != dstLen) {
throw new IOException("Failed to copy full contents from '" +
srcFile + "' to '" + destFile + "' Expected length: " + srcLen +" Actual: " + dstLen);
}
if (preserveFileDate) {
destFile.setLastModified(srcFile.lastModified());
}
}
//-----------------------------------------------------------------------
/**
* Deletes a directory recursively.
*
* @param directory directory to delete
* @throws IOException in case deletion is unsuccessful
*/
public static void deleteDirectory(final File directory) throws IOException {
if (!directory.exists()) {
return;
}
if (!isSymlink(directory)) {
cleanDirectory(directory);
}
if (!directory.delete()) {
final String message =
"Unable to delete directory " + directory + ".";
throw new IOException(message);
}
}
/**
* Deletes a file, never throwing an exception. If file is a directory, delete it and all sub-directories.
* <p>
* The difference between File.delete() and this method are:
* <ul>
* <li>A directory to be deleted does not have to be empty.</li>
* <li>No exceptions are thrown when a file or directory cannot be deleted.</li>
* </ul>
*
* @param file file or directory to delete, can be {@code null}
* @return {@code true} if the file or directory was deleted, otherwise
* {@code false}
*
* @since 1.4
*/
public static boolean deleteQuietly(final File file) {
if (file == null) {
return false;
}
try {
if (file.isDirectory()) {
cleanDirectory(file);
}
} catch (final Exception ignored) {
}
try {
return file.delete();
} catch (final Exception ignored) {
return false;
}
}
/**
* Cleans a directory without deleting it.
*
* @param directory directory to clean
* @throws IOException in case cleaning is unsuccessful
*/
public static void cleanDirectory(final File directory) throws IOException {
if (!directory.exists()) {
final String message = directory + " does not exist";
throw new IllegalArgumentException(message);
}
if (!directory.isDirectory()) {
final String message = directory + " is not a directory";
throw new IllegalArgumentException(message);
}
final File[] files = directory.listFiles();
if (files == null) { // null if security restricted
throw new IOException("Failed to list contents of " + directory);
}
IOException exception = null;
for (final File file : files) {
try {
forceDelete(file);
} catch (final IOException ioe) {
exception = ioe;
}
}
if (null != exception) {
throw exception;
}
}
//-----------------------------------------------------------------------
/**
* Deletes a file. If file is a directory, delete it and all sub-directories.
* <p>
* The difference between File.delete() and this method are:
* <ul>
* <li>A directory to be deleted does not have to be empty.</li>
* <li>You get exceptions when a file or directory cannot be deleted.
* (java.io.File methods returns a boolean)</li>
* </ul>
*
* @param file file or directory to delete, must not be {@code null}
* @throws NullPointerException if the directory is {@code null}
* @throws FileNotFoundException if the file was not found
* @throws IOException in case deletion is unsuccessful
*/
public static void forceDelete(final File file) throws IOException {
if (file.isDirectory()) {
deleteDirectory(file);
} else {
final boolean filePresent = file.exists();
if (!file.delete()) {
if (!filePresent){
throw new FileNotFoundException("File does not exist: " + file);
}
final String message =
"Unable to delete file: " + file;
throw new IOException(message);
}
}
}
/**
* Determines whether the specified file is a Symbolic Link rather than an actual file.
* <p>
* Will not return true if there is a Symbolic Link anywhere in the path,
* only if the specific file is.
* <p>
* For code that runs on Java 1.7 or later, use the following method instead:
* <br>
* {@code boolean java.nio.file.Files.isSymbolicLink(Path path)}
* @param file the file to check
* @return true if the file is a Symbolic Link
* @throws IOException if an IO error occurs while checking the file
* @since 2.0
*/
public static boolean isSymlink(final File file) throws IOException {
if (file == null) {
throw new NullPointerException("File must not be null");
}
// if (FilenameUtils.isSystemWindows()) {
// return false;
// }
File fileInCanonicalDir = null;
if (file.getParent() == null) {
fileInCanonicalDir = file;
} else {
final File canonicalDir = file.getParentFile().getCanonicalFile();
fileInCanonicalDir = new File(canonicalDir, file.getName());
}
if (fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile())) {
return false;
} else {
return true;
}
}
//region String
public static String readFileToString(File file, Charset encoding) throws IOException {
return new String(readFileToByteArray(file), encoding);
}
public static String readFileToString(File file, String encoding) throws IOException {
return readFileToString(file, Charset.forName(encoding));
}
public static void writeStringToFile(File file, String string, Charset encoding)
throws IOException {
writeByteArrayToFile(file, string.getBytes(encoding));
}
public static void writeStringToFile(File file, String string, String encoding)
throws IOException {
writeStringToFile(file, string, Charset.forName(encoding));
}
//endregion
//region JSONObject
/**
* Reads the contents of a file into a {@link JSONObject}. The file is always closed.
*/
public static JSONObject readFileToJSONObject(File file) throws IOException, JSONException {
String content = readFileToString(file, "UTF-8");
return new JSONObject(content);
}
/**
* Writes a {@link JSONObject} to a file creating the file if it does not exist.
*/
public static void writeJSONObjectToFile(File file, JSONObject json) throws IOException {
ParseFileUtils.writeByteArrayToFile(file, json.toString().getBytes(Charset.forName("UTF-8")));
}
//endregion
}

View File

@ -0,0 +1,336 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.location.Criteria;
import android.location.Location;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Locale;
import bolts.Continuation;
import bolts.Task;
/**
* {@code ParseGeoPoint} represents a latitude / longitude point that may be associated with a key
* in a {@link ParseObject} or used as a reference point for geo queries. This allows proximity
* based queries on the key.
* <p/>
* Only one key in a class may contain a {@code ParseGeoPoint}.
* <p/>
* Example:
* <pre>
* ParseGeoPoint point = new ParseGeoPoint(30.0, -20.0);
* ParseObject object = new ParseObject("PlaceObject");
* object.put("location", point);
* object.save();
* </pre>
*/
public class ParseGeoPoint implements Parcelable {
static double EARTH_MEAN_RADIUS_KM = 6371.0;
static double EARTH_MEAN_RADIUS_MILE = 3958.8;
private double latitude = 0.0;
private double longitude = 0.0;
/**
* Creates a new default point with latitude and longitude set to 0.0.
*/
public ParseGeoPoint() {
}
/**
* Creates a new point with the specified latitude and longitude.
*
* @param latitude
* The point's latitude.
* @param longitude
* The point's longitude.
*/
public ParseGeoPoint(double latitude, double longitude) {
setLatitude(latitude);
setLongitude(longitude);
}
/**
* Creates a copy of {@code point};
*
* @param point
* The point to copy.
*/
public ParseGeoPoint(ParseGeoPoint point) {
this(point.getLatitude(), point.getLongitude());
}
/**
* Creates a new point instance from a {@link Parcel} source. This is used when unparceling a
* ParseGeoPoint. Subclasses that need Parcelable behavior should provide their own
* {@link android.os.Parcelable.Creator} and override this constructor.
*
* @param source The recovered parcel.
*/
protected ParseGeoPoint(Parcel source) {
this(source, ParseParcelDecoder.get());
}
/**
* Creates a new point instance from a {@link Parcel} using the given {@link ParseParcelDecoder}.
* The decoder is currently unused, but it might be in the future, plus this is the pattern we
* are using in parcelable classes.
*
* @param source the parcel
* @param decoder the decoder
*/
ParseGeoPoint(Parcel source, ParseParcelDecoder decoder) {
setLatitude(source.readDouble());
setLongitude(source.readDouble());
}
/**
* Set latitude. Valid range is (-90.0, 90.0). Extremes should not be used.
*
* @param latitude
* The point's latitude.
*/
public void setLatitude(double latitude) {
if (latitude > 90.0 || latitude < -90.0) {
throw new IllegalArgumentException("Latitude must be within the range (-90.0, 90.0).");
}
this.latitude = latitude;
}
/**
* Get latitude.
*/
public double getLatitude() {
return latitude;
}
/**
* Set longitude. Valid range is (-180.0, 180.0). Extremes should not be used.
*
* @param longitude
* The point's longitude.
*/
public void setLongitude(double longitude) {
if (longitude > 180.0 || longitude < -180.0) {
throw new IllegalArgumentException("Longitude must be within the range (-180.0, 180.0).");
}
this.longitude = longitude;
}
/**
* Get longitude.
*/
public double getLongitude() {
return longitude;
}
/**
* Get distance in radians between this point and another {@code ParseGeoPoint}. This is the
* smallest angular distance between the two points.
*
* @param point
* {@code ParseGeoPoint} describing the other point being measured against.
*/
public double distanceInRadiansTo(ParseGeoPoint point) {
double d2r = Math.PI / 180.0; // radian conversion factor
double lat1rad = latitude * d2r;
double long1rad = longitude * d2r;
double lat2rad = point.getLatitude() * d2r;
double long2rad = point.getLongitude() * d2r;
double deltaLat = lat1rad - lat2rad;
double deltaLong = long1rad - long2rad;
double sinDeltaLatDiv2 = Math.sin(deltaLat / 2.);
double sinDeltaLongDiv2 = Math.sin(deltaLong / 2.);
// Square of half the straight line chord distance between both points.
// [0.0, 1.0]
double a =
sinDeltaLatDiv2 * sinDeltaLatDiv2 + Math.cos(lat1rad) * Math.cos(lat2rad)
* sinDeltaLongDiv2 * sinDeltaLongDiv2;
a = Math.min(1.0, a);
return 2. * Math.asin(Math.sqrt(a));
}
/**
* Get distance between this point and another {@code ParseGeoPoint} in kilometers.
*
* @param point
* {@code ParseGeoPoint} describing the other point being measured against.
*/
public double distanceInKilometersTo(ParseGeoPoint point) {
return distanceInRadiansTo(point) * EARTH_MEAN_RADIUS_KM;
}
/**
* Get distance between this point and another {@code ParseGeoPoint} in kilometers.
*
* @param point
* {@code ParseGeoPoint} describing the other point being measured against.
*/
public double distanceInMilesTo(ParseGeoPoint point) {
return distanceInRadiansTo(point) * EARTH_MEAN_RADIUS_MILE;
}
/**
* Asynchronously fetches the current location of the device.
*
* This will use a default {@link Criteria} with no accuracy or power requirements, which will
* generally result in slower, but more accurate location fixes.
* <p/>
* <strong>Note:</strong> If GPS is the best provider, it might not be able to locate the device
* at all and timeout.
*
* @param timeout
* The number of milliseconds to allow before timing out.
* @return A Task that is resolved when a location is found.
*
* @see android.location.LocationManager#getBestProvider(android.location.Criteria, boolean)
* @see android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener)
*/
public static Task<ParseGeoPoint> getCurrentLocationInBackground(long timeout) {
Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.NO_REQUIREMENT);
criteria.setPowerRequirement(Criteria.NO_REQUIREMENT);
return LocationNotifier.getCurrentLocationAsync(Parse.getApplicationContext(), timeout, criteria)
.onSuccess(new Continuation<Location, ParseGeoPoint>() {
@Override
public ParseGeoPoint then(Task<Location> task) throws Exception {
Location location = task.getResult();
return new ParseGeoPoint(location.getLatitude(), location.getLongitude());
}
});
}
/**
* Asynchronously fetches the current location of the device.
*
* This will use a default {@link Criteria} with no accuracy or power requirements, which will
* generally result in slower, but more accurate location fixes.
* <p/>
* <strong>Note:</strong> If GPS is the best provider, it might not be able to locate the device
* at all and timeout.
*
* @param timeout
* The number of milliseconds to allow before timing out.
* @param callback
* callback.done(geoPoint, error) is called when a location is found.
*
* @see android.location.LocationManager#getBestProvider(android.location.Criteria, boolean)
* @see android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener)
*/
public static void getCurrentLocationInBackground(long timeout, LocationCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(getCurrentLocationInBackground(timeout), callback);
}
/**
* Asynchronously fetches the current location of the device.
*
* This will request location updates from the best provider that match the given criteria
* and return the first location received.
*
* You can customize the criteria to meet your specific needs.
* * For higher accuracy, you can set {@link Criteria#setAccuracy(int)}, however result in longer
* times for a fix.
* * For better battery efficiency and faster location fixes, you can set
* {@link Criteria#setPowerRequirement(int)}, however, this will result in lower accuracy.
*
* @param timeout
* The number of milliseconds to allow before timing out.
* @param criteria
* The application criteria for selecting a location provider.
* @return A Task that is resolved when a location is found.
*
* @see android.location.LocationManager#getBestProvider(android.location.Criteria, boolean)
* @see android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener)
*/
public static Task<ParseGeoPoint> getCurrentLocationInBackground(long timeout, Criteria criteria) {
return LocationNotifier.getCurrentLocationAsync(Parse.getApplicationContext(), timeout, criteria)
.onSuccess(new Continuation<Location, ParseGeoPoint>() {
@Override
public ParseGeoPoint then(Task<Location> task) throws Exception {
Location location = task.getResult();
return new ParseGeoPoint(location.getLatitude(), location.getLongitude());
}
});
}
/**
* Asynchronously fetches the current location of the device.
*
* This will request location updates from the best provider that match the given criteria
* and return the first location received.
*
* You can customize the criteria to meet your specific needs.
* * For higher accuracy, you can set {@link Criteria#setAccuracy(int)}, however result in longer
* times for a fix.
* * For better battery efficiency and faster location fixes, you can set
* {@link Criteria#setPowerRequirement(int)}, however, this will result in lower accuracy.
*
* @param timeout
* The number of milliseconds to allow before timing out.
* @param criteria
* The application criteria for selecting a location provider.
* @param callback
* callback.done(geoPoint, error) is called when a location is found.
*
* @see android.location.LocationManager#getBestProvider(android.location.Criteria, boolean)
* @see android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener)
*/
public static void getCurrentLocationInBackground(long timeout, Criteria criteria,
LocationCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(getCurrentLocationInBackground(timeout, criteria), callback);
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof ParseGeoPoint)) {
return false;
}
if (obj == this) {
return true;
}
return ((ParseGeoPoint) obj).getLatitude() == latitude &&
((ParseGeoPoint) obj).getLongitude() == longitude;
}
@Override
public String toString() {
return String.format(Locale.US, "ParseGeoPoint[%.6f,%.6f]", latitude, longitude);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
writeToParcel(dest, ParseParcelEncoder.get());
}
void writeToParcel(Parcel dest, ParseParcelEncoder encoder) {
dest.writeDouble(latitude);
dest.writeDouble(longitude);
}
public final static Creator<ParseGeoPoint> CREATOR = new Creator<ParseGeoPoint>() {
@Override
public ParseGeoPoint createFromParcel(Parcel source) {
return new ParseGeoPoint(source, ParseParcelDecoder.get());
}
@Override
public ParseGeoPoint[] newArray(int size) {
return new ParseGeoPoint[size];
}
};
}

View File

@ -0,0 +1,186 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.support.annotation.Nullable;
import com.parse.http.ParseHttpBody;
import com.parse.http.ParseHttpRequest;
import com.parse.http.ParseHttpResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import okhttp3.Call;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.BufferedSink;
/**
* Internal http client which wraps an {@link OkHttpClient}
*/
class ParseHttpClient {
static ParseHttpClient createClient(@Nullable OkHttpClient.Builder builder) {
return new ParseHttpClient(builder);
}
private OkHttpClient okHttpClient;
private boolean hasExecuted;
ParseHttpClient(@Nullable OkHttpClient.Builder builder) {
if (builder == null) {
builder = new OkHttpClient.Builder();
}
okHttpClient = builder.build();
}
public final ParseHttpResponse execute(ParseHttpRequest request) throws IOException {
if (!hasExecuted) {
hasExecuted = true;
}
return executeInternal(request);
}
/**
* Execute internal. Keep default protection for tests
* @param parseRequest request
* @return response
* @throws IOException exception
*/
ParseHttpResponse executeInternal(ParseHttpRequest parseRequest) throws IOException {
Request okHttpRequest = getRequest(parseRequest);
Call okHttpCall = okHttpClient.newCall(okHttpRequest);
Response okHttpResponse = okHttpCall.execute();
return getResponse(okHttpResponse);
}
ParseHttpResponse getResponse(Response okHttpResponse)
throws IOException {
// Status code
int statusCode = okHttpResponse.code();
// Content
InputStream content = okHttpResponse.body().byteStream();
// Total size
int totalSize = (int) okHttpResponse.body().contentLength();
// Reason phrase
String reasonPhrase = okHttpResponse.message();
// Headers
Map<String, String> headers = new HashMap<>();
for (String name : okHttpResponse.headers().names()) {
headers.put(name, okHttpResponse.header(name));
}
// Content type
String contentType = null;
ResponseBody body = okHttpResponse.body();
if (body != null && body.contentType() != null) {
contentType = body.contentType().toString();
}
return new ParseHttpResponse.Builder()
.setStatusCode(statusCode)
.setContent(content)
.setTotalSize(totalSize)
.setReasonPhrase(reasonPhrase)
.setHeaders(headers)
.setContentType(contentType)
.build();
}
Request getRequest(ParseHttpRequest parseRequest) throws IOException {
Request.Builder okHttpRequestBuilder = new Request.Builder();
ParseHttpRequest.Method method = parseRequest.getMethod();
// Set method
switch (method) {
case GET:
okHttpRequestBuilder.get();
break;
case DELETE:
case POST:
case PUT:
// Since we need to set body and method at the same time for DELETE, POST, PUT, we will do it in
// the following.
break;
default:
// This case will never be reached since we have already handled this case in
// ParseRequest.newRequest().
throw new IllegalStateException("Unsupported http method " + method.toString());
}
// Set url
okHttpRequestBuilder.url(parseRequest.getUrl());
// Set Header
Headers.Builder okHttpHeadersBuilder = new Headers.Builder();
for (Map.Entry<String, String> entry : parseRequest.getAllHeaders().entrySet()) {
okHttpHeadersBuilder.add(entry.getKey(), entry.getValue());
}
// OkHttp automatically add gzip header so we do not need to deal with it
Headers okHttpHeaders = okHttpHeadersBuilder.build();
okHttpRequestBuilder.headers(okHttpHeaders);
// Set Body
ParseHttpBody parseBody = parseRequest.getBody();
ParseOkHttpRequestBody okHttpRequestBody = null;
if (parseBody != null) {
okHttpRequestBody = new ParseOkHttpRequestBody(parseBody);
}
switch (method) {
case PUT:
okHttpRequestBuilder.put(okHttpRequestBody);
break;
case POST:
okHttpRequestBuilder.post(okHttpRequestBody);
break;
case DELETE:
okHttpRequestBuilder.delete(okHttpRequestBody);
}
return okHttpRequestBuilder.build();
}
private static class ParseOkHttpRequestBody extends RequestBody {
private ParseHttpBody parseBody;
ParseOkHttpRequestBody(ParseHttpBody parseBody) {
this.parseBody = parseBody;
}
@Override
public long contentLength() throws IOException {
return parseBody.getContentLength();
}
@Override
public MediaType contentType() {
String contentType = parseBody.getContentType();
return contentType == null ? null : MediaType.parse(parseBody.getContentType());
}
@Override
public void writeTo(BufferedSink bufferedSink) throws IOException {
parseBody.writeTo(bufferedSink.outputStream());
}
}
}

View File

@ -0,0 +1,352 @@
/*
* 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.
*/
package com.parse;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* General IO stream manipulation utilities.
*/
/** package */ class ParseIOUtils {
private static final int EOF = -1;
/**
* The default buffer size ({@value}) to use for
* {@link #copyLarge(InputStream, OutputStream)}
*/
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
/**
* The default buffer size to use for the skip() methods.
*/
private static final int SKIP_BUFFER_SIZE = 2048;
// Allocated in the relevant skip method if necessary.
/*
* N.B. no need to synchronize these because:
* - we don't care if the buffer is created multiple times (the data is ignored)
* - we always use the same size buffer, so if it it is recreated it will still be OK
* (if the buffer size were variable, we would need to synch. to ensure some other thread
* did not create a smaller one)
*/
private static byte[] SKIP_BYTE_BUFFER;
// read toByteArray
//-----------------------------------------------------------------------
/**
* Get the contents of an <code>InputStream</code> as a <code>byte[]</code>.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedInputStream</code>.
*
* @param input the <code>InputStream</code> to read from
* @return the requested byte array
* @throws NullPointerException if the input is null
* @throws IOException if an I/O error occurs
*/
public static byte[] toByteArray(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
copy(input, output);
return output.toByteArray();
}
// copy from InputStream
//-----------------------------------------------------------------------
/**
* Copy bytes from an <code>InputStream</code> to an
* <code>OutputStream</code>.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedInputStream</code>.
* <p>
* Large streams (over 2GB) will return a bytes copied value of
* <code>-1</code> after the copy has completed since the correct
* number of bytes cannot be returned as an int. For large streams
* use the <code>copyLarge(InputStream, OutputStream)</code> method.
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @return the number of bytes copied, or -1 if &gt; Integer.MAX_VALUE
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 1.1
*/
public static int copy(InputStream input, OutputStream output) throws IOException {
long count = copyLarge(input, output);
if (count > Integer.MAX_VALUE) {
return -1;
}
return (int) count;
}
/**
* Copy bytes from a large (over 2GB) <code>InputStream</code> to an
* <code>OutputStream</code>.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedInputStream</code>.
* <p>
* The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @return the number of bytes copied
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 1.3
*/
public static long copyLarge(InputStream input, OutputStream output)
throws IOException {
return copyLarge(input, output, new byte[DEFAULT_BUFFER_SIZE]);
}
/**
* Copy bytes from a large (over 2GB) <code>InputStream</code> to an
* <code>OutputStream</code>.
* <p>
* This method uses the provided buffer, so there is no need to use a
* <code>BufferedInputStream</code>.
* <p>
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @param buffer the buffer to use for the copy
* @return the number of bytes copied
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 2.2
*/
public static long copyLarge(InputStream input, OutputStream output, byte[] buffer)
throws IOException {
long count = 0;
int n = 0;
while (EOF != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
/**
* Copy some or all bytes from a large (over 2GB) <code>InputStream</code> to an
* <code>OutputStream</code>, optionally skipping input bytes.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedInputStream</code>.
* <p>
* The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @param inputOffset : number of bytes to skip from input before copying
* -ve values are ignored
* @param length : number of bytes to copy. -ve means all
* @return the number of bytes copied
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 2.2
*/
public static long copyLarge(InputStream input, OutputStream output, long inputOffset, long length)
throws IOException {
return copyLarge(input, output, inputOffset, length, new byte[DEFAULT_BUFFER_SIZE]);
}
/**
* Skip bytes from an input byte stream.
* This implementation guarantees that it will read as many bytes
* as possible before giving up; this may not always be the case for
* subclasses of {@link java.io.Reader}.
*
* @param input byte stream to skip
* @param toSkip number of bytes to skip.
* @return number of bytes actually skipped.
*
* @see InputStream#skip(long)
*
* @throws IOException if there is a problem reading the file
* @throws IllegalArgumentException if toSkip is negative
* @since 2.0
*/
public static long skip(InputStream input, long toSkip) throws IOException {
if (toSkip < 0) {
throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip);
}
/*
* N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data
* is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer
* size were variable, we would need to synch. to ensure some other thread did not create a smaller one)
*/
if (SKIP_BYTE_BUFFER == null) {
SKIP_BYTE_BUFFER = new byte[SKIP_BUFFER_SIZE];
}
long remain = toSkip;
while (remain > 0) {
long n = input.read(SKIP_BYTE_BUFFER, 0, (int) Math.min(remain, SKIP_BUFFER_SIZE));
if (n < 0) { // EOF
break;
}
remain -= n;
}
return toSkip - remain;
}
/**
* Copy some or all bytes from a large (over 2GB) <code>InputStream</code> to an
* <code>OutputStream</code>, optionally skipping input bytes.
* <p>
* This method uses the provided buffer, so there is no need to use a
* <code>BufferedInputStream</code>.
* <p>
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @param inputOffset : number of bytes to skip from input before copying
* -ve values are ignored
* @param length : number of bytes to copy. -ve means all
* @param buffer the buffer to use for the copy
*
* @return the number of bytes copied
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 2.2
*/
public static long copyLarge(InputStream input, OutputStream output,
final long inputOffset, final long length, byte[] buffer) throws IOException {
if (inputOffset > 0) {
skipFully(input, inputOffset);
}
if (length == 0) {
return 0;
}
final int bufferLength = buffer.length;
int bytesToRead = bufferLength;
if (length > 0 && length < bufferLength) {
bytesToRead = (int) length;
}
int read;
long totalRead = 0;
while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) {
output.write(buffer, 0, read);
totalRead += read;
if (length > 0) { // only adjust length if not reading to the end
// Note the cast must work because buffer.length is an integer
bytesToRead = (int) Math.min(length - totalRead, bufferLength);
}
}
return totalRead;
}
/**
* Skip the requested number of bytes or fail if there are not enough left.
* <p>
* This allows for the possibility that {@link InputStream#skip(long)} may
* not skip as many bytes as requested (most likely because of reaching EOF).
*
* @param input stream to skip
* @param toSkip the number of bytes to skip
* @see InputStream#skip(long)
*
* @throws IOException if there is a problem reading the file
* @throws IllegalArgumentException if toSkip is negative
* @throws EOFException if the number of bytes skipped was incorrect
* @since 2.0
*/
public static void skipFully(InputStream input, long toSkip) throws IOException {
if (toSkip < 0) {
throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip);
}
long skipped = skip(input, toSkip);
if (skipped != toSkip) {
throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped);
}
}
/**
* Unconditionally close an <code>InputStream</code>.
* <p>
* Equivalent to {@link InputStream#close()}, except any exceptions will be ignored.
* This is typically used in finally blocks.
*
* @param input the InputStream to close, may be null or already closed
*/
public static void closeQuietly(InputStream input) {
try {
if (input != null) {
input.close();
}
} catch (IOException ioe) {
// ignore
}
}
/**
* Unconditionally close an <code>OutputStream</code>.
* <p>
* Equivalent to {@link OutputStream#close()}, except any exceptions will be ignored.
* This is typically used in finally blocks.
*
* @param output the OutputStream to close, may be null or already closed
*/
public static void closeQuietly(OutputStream output) {
try {
if (output != null) {
output.close();
}
} catch (IOException ioe) {
// ignore
}
}
/**
* Closes a <code>Closeable</code> unconditionally.
* <p>
* Equivalent to {@link Closeable#close()}, except any exceptions will be ignored.
* This is typically used in finally blocks.
* <p>
* Example code:
* <pre>
* Closeable closeable = null;
* try {
* closeable = new FileReader("foo.txt");
* // process closeable
* closeable.close();
* } catch (Exception e) {
* // error handling
* } finally {
* IOUtils.closeQuietly(closeable);
* }
* </pre>
*
* @param closeable the object to close, may be null or already closed
* @since 2.0
*/
public static void closeQuietly(final Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (final IOException ioe) {
// ignore
}
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.SimpleTimeZone;
/**
* Used to parse legacy created_at and updated_at from disk. It is only precise to the second.
*/
/** package */ class ParseImpreciseDateFormat {
private static final String TAG = "ParseDateFormat";
private static final ParseImpreciseDateFormat INSTANCE = new ParseImpreciseDateFormat();
public static ParseImpreciseDateFormat getInstance() {
return INSTANCE;
}
// SimpleDateFormat isn't inherently thread-safe
private final Object lock = new Object();
private final DateFormat dateFormat;
private ParseImpreciseDateFormat() {
DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
format.setTimeZone(new SimpleTimeZone(0, "GMT"));
dateFormat = format;
}
/* package */ Date parse(String dateString) {
synchronized (lock) {
try {
return dateFormat.parse(dateString);
} catch (java.text.ParseException e) {
// Should never happen
PLog.e(TAG, "could not parse date: " + dateString, e);
return null;
}
}
}
/* package */ String format(Date date) {
synchronized (lock) {
return dateFormat.format(date);
}
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.os.Parcel;
import org.json.JSONException;
import org.json.JSONObject;
/**
* An operation that increases a numeric field's value by a given amount.
*/
/** package */ class ParseIncrementOperation implements ParseFieldOperation {
/* package */ final static String OP_NAME = "Increment";
private final Number amount;
public ParseIncrementOperation(Number amount) {
this.amount = amount;
}
@Override
public JSONObject encode(ParseEncoder objectEncoder) throws JSONException {
JSONObject output = new JSONObject();
output.put("__op", OP_NAME);
output.put("amount", amount);
return output;
}
@Override
public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) {
dest.writeString(OP_NAME);
parcelableEncoder.encode(amount, dest); // Let encoder figure out how to parcel Number
}
@Override
public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) {
if (previous == null) {
return this;
} else if (previous instanceof ParseDeleteOperation) {
return new ParseSetOperation(amount);
} else if (previous instanceof ParseSetOperation) {
Object oldValue = ((ParseSetOperation) previous).getValue();
if (oldValue instanceof Number) {
return new ParseSetOperation(Numbers.add((Number) oldValue, amount));
} else {
throw new IllegalArgumentException("You cannot increment a non-number.");
}
} else if (previous instanceof ParseIncrementOperation) {
Number oldAmount = ((ParseIncrementOperation) previous).amount;
return new ParseIncrementOperation(Numbers.add(oldAmount, amount));
} else {
throw new IllegalArgumentException("Operation is invalid after previous operation.");
}
}
@Override
public Object apply(Object oldValue, String key) {
if (oldValue == null) {
return amount;
} else if (oldValue instanceof Number) {
return Numbers.add((Number) oldValue, amount);
} else {
throw new IllegalArgumentException("You cannot increment a non-number.");
}
}
}

View File

@ -0,0 +1,321 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import bolts.Continuation;
import bolts.Task;
/**
* The {@code ParseInstallation} is a local representation of installation data that can be saved
* and retrieved from the Parse cloud.
*/
@ParseClassName("_Installation")
public class ParseInstallation extends ParseObject {
private static final String TAG = "com.parse.ParseInstallation";
private static final String KEY_OBJECT_ID = "objectId";
private static final String KEY_INSTALLATION_ID = "installationId";
private static final String KEY_DEVICE_TYPE = "deviceType";
private static final String KEY_APP_NAME = "appName";
private static final String KEY_APP_IDENTIFIER = "appIdentifier";
private static final String KEY_PARSE_VERSION = "parseVersion";
private static final String KEY_DEVICE_TOKEN = "deviceToken";
private static final String KEY_PUSH_TYPE = "pushType";
private static final String KEY_TIME_ZONE = "timeZone";
private static final String KEY_LOCALE = "localeIdentifier";
private static final String KEY_APP_VERSION = "appVersion";
/* package */ static final String KEY_CHANNELS = "channels";
private static final List<String> READ_ONLY_FIELDS = Collections.unmodifiableList(
Arrays.asList(KEY_DEVICE_TYPE, KEY_INSTALLATION_ID, KEY_DEVICE_TOKEN, KEY_PUSH_TYPE,
KEY_TIME_ZONE, KEY_LOCALE, KEY_APP_VERSION, KEY_APP_NAME, KEY_PARSE_VERSION,
KEY_APP_IDENTIFIER, KEY_OBJECT_ID));
// TODO(mengyan): Inject into ParseInstallationInstanceController
/* package */ static ParseCurrentInstallationController getCurrentInstallationController() {
return ParseCorePlugins.getInstance().getCurrentInstallationController();
}
public static ParseInstallation getCurrentInstallation() {
try {
return ParseTaskUtils.wait(
getCurrentInstallationController().getAsync());
} catch (ParseException e) {
// In order to have backward compatibility, we swallow the exception silently.
return null;
}
}
/**
* Constructs a query for {@code ParseInstallation}.
* <p/>
* <strong>Note:</strong> We only allow the following types of queries for installations:
* <pre>
* query.get(objectId)
* query.whereEqualTo("installationId", value)
* query.whereMatchesKeyInQuery("installationId", keyInQuery, query)
* </pre>
* <p/>
* You can add additional query clauses, but one of the above must appear as a top-level
* {@code AND} clause in the query.
*
* @see com.parse.ParseQuery#getQuery(Class)
*/
public static ParseQuery<ParseInstallation> getQuery() {
return ParseQuery.getQuery(ParseInstallation.class);
}
public ParseInstallation() {
// do nothing
}
/**
* Returns the unique ID of this installation.
*
* @return A UUID that represents this device.
*/
public String getInstallationId() {
return getString(KEY_INSTALLATION_ID);
}
@Override
public void setObjectId(String newObjectId) {
throw new RuntimeException("Installation's objectId cannot be changed");
}
@Override
/* package */ boolean needsDefaultACL() {
return false;
}
@Override
/* package */ boolean isKeyMutable(String key) {
return !READ_ONLY_FIELDS.contains(key);
}
@Override
/* package */ void updateBeforeSave() {
super.updateBeforeSave();
if (getCurrentInstallationController().isCurrent(ParseInstallation.this)) {
updateTimezone();
updateVersionInfo();
updateDeviceInfo();
updateLocaleIdentifier();
}
}
@Override
/* package */ <T extends ParseObject> Task<T> fetchAsync(
final String sessionToken, final Task<Void> toAwait) {
synchronized (mutex) {
// Because the Service and the global currentInstallation are different objects, we may not
// have the same ObjectID (we never will at bootstrap). The server has a special hack for
// _Installation where save with an existing InstallationID will merge Object IDs
Task<Void> result;
if (getObjectId() == null) {
result = saveAsync(sessionToken, toAwait);
} else {
result = Task.forResult(null);
}
return result.onSuccessTask(new Continuation<Void, Task<T>>() {
@Override
public Task<T> then(Task<Void> task) throws Exception {
return ParseInstallation.super.fetchAsync(sessionToken, toAwait);
}
});
}
}
@Override
/* package */ Task<Void> saveAsync(final String sessionToken, final Task<Void> toAwait) {
return super.saveAsync(sessionToken, toAwait).continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
// Retry the fetch as a save operation because this Installation was deleted on the server.
if (task.getError() != null
&& task.getError() instanceof ParseException) {
int errCode = ((ParseException) task.getError()).getCode();
if (errCode == ParseException.OBJECT_NOT_FOUND
|| (errCode == ParseException.MISSING_REQUIRED_FIELD_ERROR && getObjectId() == null)) {
synchronized (mutex) {
setState(new State.Builder(getState()).objectId(null).build());
markAllFieldsDirty();
return ParseInstallation.super.saveAsync(sessionToken, toAwait);
}
}
}
return task;
}
});
}
@Override
/* package */ Task<Void> handleSaveResultAsync(ParseObject.State result,
ParseOperationSet operationsBeforeSave) {
Task<Void> task = super.handleSaveResultAsync(result, operationsBeforeSave);
if (result == null) { // Failure
return task;
}
return task.onSuccessTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return getCurrentInstallationController().setAsync(ParseInstallation.this);
}
});
}
@Override
/* package */ Task<Void> handleFetchResultAsync(final ParseObject.State newState) {
return super.handleFetchResultAsync(newState).onSuccessTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return getCurrentInstallationController().setAsync(ParseInstallation.this);
}
});
}
// Android documentation states that getID may return one of many forms: America/LosAngeles,
// GMT-<offset>, or code. We only accept the first on the server, so for now we will not upload
// time zones from devices reporting other formats.
private void updateTimezone() {
String zone = TimeZone.getDefault().getID();
if ((zone.indexOf('/') > 0 || zone.equals("GMT")) && !zone.equals(get(KEY_TIME_ZONE))) {
performPut(KEY_TIME_ZONE, zone);
}
}
private void updateVersionInfo() {
synchronized (mutex) {
try {
Context context = Parse.getApplicationContext();
String packageName = context.getPackageName();
PackageManager pm = context.getPackageManager();
PackageInfo pkgInfo = pm.getPackageInfo(packageName, 0);
String appVersion = pkgInfo.versionName;
String appName = pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString();
if (packageName != null && !packageName.equals(get(KEY_APP_IDENTIFIER))) {
performPut(KEY_APP_IDENTIFIER, packageName);
}
if (appName != null && !appName.equals(get(KEY_APP_NAME))) {
performPut(KEY_APP_NAME, appName);
}
if (appVersion != null && !appVersion.equals(get(KEY_APP_VERSION))) {
performPut(KEY_APP_VERSION, appVersion);
}
} catch (PackageManager.NameNotFoundException e) {
PLog.w(TAG, "Cannot load package info; will not be saved to installation");
}
if (!VERSION_NAME.equals(get(KEY_PARSE_VERSION))) {
performPut(KEY_PARSE_VERSION, VERSION_NAME);
}
}
}
/*
* Save locale in the following format:
* [language code]-[country code]
*
* The language codes are two-letter lowercase ISO language codes (such as "en") as defined by
* <a href="http://en.wikipedia.org/wiki/ISO_639-1">ISO 639-1</a>.
* The country codes are two-letter uppercase ISO country codes (such as "US") as defined by
* <a href="http://en.wikipedia.org/wiki/ISO_3166-1_alpha-3">ISO 3166-1</a>.
*
* Note that Java uses several deprecated two-letter codes. The Hebrew ("he") language
* code is rewritten as "iw", Indonesian ("id") as "in", and Yiddish ("yi") as "ji". This
* rewriting happens even if you construct your own {@code Locale} object, not just for
* instances returned by the various lookup methods.
*/
private void updateLocaleIdentifier() {
final Locale locale = Locale.getDefault();
String language = locale.getLanguage();
String country = locale.getCountry();
if (TextUtils.isEmpty(language)) {
return;
}
// rewrite depreciated two-letter codes
if (language.equals("iw")) language = "he"; // Hebrew
if (language.equals("in")) language = "id"; // Indonesian
if (language.equals("ji")) language = "yi"; // Yiddish
String localeString = language;
if (!TextUtils.isEmpty(country)) {
localeString = String.format(Locale.US, "%s-%s", language, country);
}
if (!localeString.equals(get(KEY_LOCALE))) {
performPut(KEY_LOCALE, localeString);
}
}
// TODO(mengyan): Move to ParseInstallationInstanceController
/* package */ void updateDeviceInfo() {
updateDeviceInfo(ParsePlugins.get().installationId());
}
/* package */ void updateDeviceInfo(InstallationId installationId) {
/*
* If we don't have an installationId, use the one that comes from the installationId file on
* disk. This should be impossible since we set the installationId in setDefaultValues.
*/
if (!has(KEY_INSTALLATION_ID)) {
performPut(KEY_INSTALLATION_ID, installationId.get());
}
String deviceType = "android";
if (!deviceType.equals(get(KEY_DEVICE_TYPE))) {
performPut(KEY_DEVICE_TYPE, deviceType);
}
}
/* package */ PushType getPushType() {
return PushType.fromString(super.getString(KEY_PUSH_TYPE));
}
/* package */ void setPushType(PushType pushType) {
if (pushType != null) {
performPut(KEY_PUSH_TYPE, pushType.toString());
}
}
/* package */ void removePushType() {
performRemove(KEY_PUSH_TYPE);
}
/* package */ String getDeviceToken() {
return super.getString(KEY_DEVICE_TOKEN);
}
/* package */ void setDeviceToken(String deviceToken) {
if (deviceToken != null && deviceToken.length() > 0) {
performPut(KEY_DEVICE_TOKEN, deviceToken);
}
}
/* package */ void removeDeviceToken() {
performRemove(KEY_DEVICE_TOKEN);
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
/**
* Static utility methods pertaining to {@link JSONObject} and {@link JSONArray} instances.
*/
/** package */ class ParseJSONUtils {
/**
* Creates a copy of {@code copyFrom}, excluding the keys from {@code excludes}.
*/
public static JSONObject create(JSONObject copyFrom, Collection<String> excludes) {
JSONObject json = new JSONObject();
Iterator<String> iterator = copyFrom.keys();
while (iterator.hasNext()) {
String name = iterator.next();
if (excludes.contains(name)) {
continue;
}
try {
json.put(name, copyFrom.opt(name));
} catch (JSONException e) {
// This shouldn't ever happen since it'll only throw if `name` is null
throw new RuntimeException(e);
}
}
return json;
}
/**
* A helper for nonugly iterating over JSONObject keys.
*/
public static Iterable<String> keys(JSONObject object) {
final JSONObject finalObject = object;
return new Iterable<String>() {
@Override
public Iterator<String> iterator() {
return finalObject.keys();
}
};
}
/**
* A helper for returning the value mapped by a list of keys, ordered by priority.
*/
public static int getInt(JSONObject object, List<String> keys) throws JSONException {
for (String key : keys) {
try {
return object.getInt(key);
} catch (JSONException e) {
// do nothing
}
}
throw new JSONException("No value for " + keys);
}
}

View File

@ -0,0 +1,243 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.content.Context;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
/**
* Used for ParseQuery caching.
*/
/** package */ class ParseKeyValueCache {
private static final String TAG = "ParseKeyValueCache";
private static final String DIR_NAME = "ParseKeyValueCache";
// We limit the cache to 2MB because that's about what the default browser
// uses.
/* package */ static final int DEFAULT_MAX_KEY_VALUE_CACHE_BYTES = 2 * 1024 * 1024;
// We limit to 1000 cache files to avoid taking too long while scanning the
// cache
/* package */ static final int DEFAULT_MAX_KEY_VALUE_CACHE_FILES = 1000;
/**
* Prevent multiple threads from modifying the cache at the same time.
*/
private static final Object MUTEX_IO = new Object();
/* package */ static int maxKeyValueCacheBytes = DEFAULT_MAX_KEY_VALUE_CACHE_BYTES;
/* package */ static int maxKeyValueCacheFiles = DEFAULT_MAX_KEY_VALUE_CACHE_FILES;
private static File directory;
// Creates a directory to keep cache-type files in.
// The operating system will automatically clear out these files first
// when space gets low.
/* package */ static void initialize(Context context) {
initialize(new File(context.getCacheDir(), DIR_NAME));
}
/* package for tests */ static void initialize(File path) {
if (!path.isDirectory() && !path.mkdir()) {
throw new RuntimeException("Could not create ParseKeyValueCache directory");
}
directory = path;
}
private static File getKeyValueCacheDir() {
if (directory != null && !directory.exists()) {
directory.mkdir();
}
return directory;
}
/**
* How many files are in the key-value cache.
*/
/* package */ static int size() {
File[] files = getKeyValueCacheDir().listFiles();
if (files == null) {
return 0;
}
return files.length;
}
private static File getKeyValueCacheFile(String key) {
final String suffix = '.' + key;
File[] matches = getKeyValueCacheDir().listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
return filename.endsWith(suffix);
}
});
return (matches == null || matches.length == 0) ? null : matches[0];
}
// Badly formatted files return the epoch
private static long getKeyValueCacheAge(File cacheFile) {
// Format: <date>.<key>
String name = cacheFile.getName();
try {
return Long.parseLong(name.substring(0, name.indexOf('.')));
} catch (NumberFormatException e) {
return 0;
}
}
private static File createKeyValueCacheFile(String key) {
String filename = String.valueOf(new Date().getTime()) + '.' + key;
return new File(getKeyValueCacheDir(), filename);
}
// Removes all the cache entries.
/* package */ static void clearKeyValueCacheDir() {
synchronized (MUTEX_IO) {
File dir = getKeyValueCacheDir();
if (dir == null) {
return;
}
File[] entries = dir.listFiles();
if (entries == null) {
return;
}
for (File entry : entries) {
entry.delete();
}
}
}
// Saves a key-value pair to the cache
/* package */ static void saveToKeyValueCache(String key, String value) {
synchronized (MUTEX_IO) {
File prior = getKeyValueCacheFile(key);
if (prior != null) {
prior.delete();
}
File f = createKeyValueCacheFile(key);
try {
ParseFileUtils.writeByteArrayToFile(f, value.getBytes("UTF-8"));
} catch (IOException e) {
// do nothing
}
// Check if we should kick out old cache entries
File[] files = getKeyValueCacheDir().listFiles();
// We still need this check since dir.mkdir() may fail
if (files == null || files.length == 0) {
return;
}
int numFiles = files.length;
int numBytes = 0;
for (File file : files) {
numBytes += file.length();
}
// If we do not need to clear the cache, simply return
if (numFiles <= maxKeyValueCacheFiles && numBytes <= maxKeyValueCacheBytes) {
return;
}
// We need to kick out some cache entries.
// Sort oldest-first. We touch on read so mtime is really LRU.
// Sometimes (i.e. tests) the time of lastModified isn't granular enough,
// so we resort
// to sorting by the file name which is always prepended with time in ms
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
int dateCompare = Long.valueOf(f1.lastModified()).compareTo(f2.lastModified());
if (dateCompare != 0) {
return dateCompare;
} else {
return f1.getName().compareTo(f2.getName());
}
}
});
for (File file : files) {
numFiles--;
numBytes -= file.length();
file.delete();
if (numFiles <= maxKeyValueCacheFiles && numBytes <= maxKeyValueCacheBytes) {
break;
}
}
}
}
// Clears a key from the cache if it's there. If it's not there, this is a
// no-op.
/* package */ static void clearFromKeyValueCache(String key) {
synchronized (MUTEX_IO) {
File file = getKeyValueCacheFile(key);
if (file != null) {
file.delete();
}
}
}
// Loads a value from the key-value cache.
// Returns null if nothing is there.
/* package */ static String loadFromKeyValueCache(final String key, final long maxAgeMilliseconds) {
synchronized (MUTEX_IO) {
File file = getKeyValueCacheFile(key);
if (file == null) {
return null;
}
Date now = new Date();
long oldestAcceptableAge = Math.max(0, now.getTime() - maxAgeMilliseconds);
if (getKeyValueCacheAge(file) < oldestAcceptableAge) {
return null;
}
// Update mtime to make the LRU work
file.setLastModified(now.getTime());
try {
RandomAccessFile f = new RandomAccessFile(file, "r");
byte[] bytes = new byte[(int) f.length()];
f.readFully(bytes);
f.close();
return new String(bytes, "UTF-8");
} catch (IOException e) {
PLog.e(TAG, "error reading from cache", e);
return null;
}
}
}
// Returns null if the value does not exist or is not json
/* package */ static JSONObject jsonFromKeyValueCache(String key, long maxAgeMilliseconds) {
String raw = loadFromKeyValueCache(key, maxAgeMilliseconds);
if (raw == null) {
return null;
}
try {
return new JSONObject(raw);
} catch (JSONException e) {
PLog.e(TAG, "corrupted cache for " + key, e);
clearFromKeyValueCache(key);
return null;
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/** package */ class ParseMulticastDelegate<T> {
private final List<ParseCallback2<T, ParseException>> callbacks;
public ParseMulticastDelegate() {
callbacks = new LinkedList<>();
}
public void subscribe(ParseCallback2<T, ParseException> callback) {
callbacks.add(callback);
}
public void unsubscribe(ParseCallback2<T, ParseException> callback) {
callbacks.remove(callback);
}
public void invoke(T result, ParseException exception) {
for (ParseCallback2<T, ParseException> callback : new ArrayList<>(callbacks)) {
callback.done(result, exception);
}
}
public void clear() {
callbacks.clear();
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import java.util.concurrent.atomic.AtomicInteger;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.util.SparseIntArray;
/**
* A utility class for building and showing notifications.
*/
/** package */ class ParseNotificationManager {
public static final String TAG = "com.parse.ParseNotificationManager";
public static class Singleton {
private static final ParseNotificationManager INSTANCE = new ParseNotificationManager();
}
public static ParseNotificationManager getInstance() {
return Singleton.INSTANCE;
}
private final AtomicInteger notificationCount = new AtomicInteger(0);
private volatile boolean shouldShowNotifications = true;
public void setShouldShowNotifications(boolean show) {
shouldShowNotifications = show;
}
public int getNotificationCount() {
return notificationCount.get();
}
public void showNotification(Context context, Notification notification) {
if (context != null && notification != null) {
notificationCount.incrementAndGet();
if (shouldShowNotifications) {
// Fire off the notification
NotificationManager nm =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
// Pick an id that probably won't overlap anything
int notificationId = (int)System.currentTimeMillis();
try {
nm.notify(notificationId, notification);
} catch (SecurityException e) {
// Some phones throw an exception for unapproved vibration
notification.defaults = Notification.DEFAULT_LIGHTS | Notification.DEFAULT_SOUND;
nm.notify(notificationId, notification);
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,135 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Iterator;
/**
* Handles encoding/decoding ParseObjects to/from REST JSON.
*/
/** package */ class ParseObjectCoder {
private static final String KEY_OBJECT_ID = "objectId";
private static final String KEY_CLASS_NAME = "className";
private static final String KEY_ACL = "ACL";
private static final String KEY_CREATED_AT = "createdAt";
private static final String KEY_UPDATED_AT = "updatedAt";
private static final ParseObjectCoder INSTANCE = new ParseObjectCoder();
public static ParseObjectCoder get() {
return INSTANCE;
}
/* package */ ParseObjectCoder() {
// do nothing
}
/**
* Converts a {@code ParseObject.State} to REST JSON for saving.
*
* Only dirty keys from {@code operations} are represented in the data. Non-dirty keys such as
* {@code updatedAt}, {@code createdAt}, etc. are not included.
*
* @param state
* {@link ParseObject.State} of the type of {@link ParseObject} that will be returned.
* Properties are completely ignored.
* @param operations
* Dirty operations that are to be saved.
* @param encoder
* Encoder instance that will be used to encode the request.
* @return
* A REST formatted {@link JSONObject} that will be used for saving.
*/
public <T extends ParseObject.State> JSONObject encode(
T state, ParseOperationSet operations, ParseEncoder encoder) {
JSONObject objectJSON = new JSONObject();
try {
// Serialize the data
for (String key : operations.keySet()) {
ParseFieldOperation operation = operations.get(key);
objectJSON.put(key, encoder.encode(operation));
// TODO(grantland): Use cached value from hashedObjects if it's a set operation.
}
if (state.objectId() != null) {
objectJSON.put(KEY_OBJECT_ID, state.objectId());
}
} catch (JSONException e) {
throw new RuntimeException("could not serialize object to JSON");
}
return objectJSON;
}
/**
* Converts REST JSON response to {@link ParseObject.State.Init}.
*
* This returns Builder instead of a State since we'll probably want to set some additional
* properties on it after decoding such as {@link ParseObject.State.Init#isComplete()}, etc.
*
* @param builder
* A {@link ParseObject.State.Init} instance that will have the server JSON applied
* (mutated) to it. This will generally be a instance created by clearing a mutable
* copy of a {@link ParseObject.State} to ensure it's an instance of the correct
* subclass: {@code state.newBuilder().clear()}
* @param json
* JSON response in REST format from the server.
* @param decoder
* Decoder instance that will be used to decode the server response.
* @return
* The same Builder instance passed in after the JSON is applied.
*/
public <T extends ParseObject.State.Init<?>> T decode(
T builder, JSONObject json, ParseDecoder decoder) {
try {
Iterator<?> keys = json.keys();
while (keys.hasNext()) {
String key = (String) keys.next();
/*
__type: Returned by queries and cloud functions to designate body is a ParseObject
__className: Used by fromJSON, should be stripped out by the time it gets here...
*/
if (key.equals("__type") || key.equals(KEY_CLASS_NAME)) {
continue;
}
if (key.equals(KEY_OBJECT_ID)) {
String newObjectId = json.getString(key);
builder.objectId(newObjectId);
continue;
}
if (key.equals(KEY_CREATED_AT)) {
builder.createdAt(ParseDateFormat.getInstance().parse(json.getString(key)));
continue;
}
if (key.equals(KEY_UPDATED_AT)) {
builder.updatedAt(ParseDateFormat.getInstance().parse(json.getString(key)));
continue;
}
if (key.equals(KEY_ACL)) {
ParseACL acl = ParseACL.createACLFromJSONObject(json.getJSONObject(key), decoder);
builder.put(KEY_ACL, acl);
continue;
}
Object value = json.get(key);
Object decodedObject = decoder.decode(value);
builder.put(key, decodedObject);
}
return builder;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONObject;
import java.util.List;
import bolts.Task;
/** package */ interface ParseObjectController {
Task<ParseObject.State> fetchAsync(
ParseObject.State state, String sessionToken, ParseDecoder decoder);
Task<ParseObject.State> saveAsync(
ParseObject.State state,
ParseOperationSet operations,
String sessionToken,
ParseDecoder decoder);
List<Task<ParseObject.State>> saveAllAsync(
List<ParseObject.State> states,
List<ParseOperationSet> operationsList,
String sessionToken,
List<ParseDecoder> decoders);
Task<Void> deleteAsync(ParseObject.State state, String sessionToken);
List<Task<Void>> deleteAllAsync(List<ParseObject.State> states, String sessionToken);
}

View File

@ -0,0 +1,184 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Date;
import java.util.Iterator;
/**
* Handles encoding/decoding ParseObjects to/from /2 format JSON. /2 format json is only used for
* persisting current ParseObject(currentInstallation, currentUser) to disk when LDS is not enabled.
*/
/** package */ class ParseObjectCurrentCoder extends ParseObjectCoder {
/*
/2 format JSON Keys
*/
private static final String KEY_OBJECT_ID = "objectId";
private static final String KEY_CLASS_NAME = "classname";
private static final String KEY_CREATED_AT = "createdAt";
private static final String KEY_UPDATED_AT = "updatedAt";
private static final String KEY_DATA = "data";
/*
Old serialized JSON keys
*/
private static final String KEY_OLD_OBJECT_ID = "id";
private static final String KEY_OLD_CREATED_AT = "created_at";
private static final String KEY_OLD_UPDATED_AT = "updated_at";
private static final String KEY_OLD_POINTERS = "pointers";
private static final ParseObjectCurrentCoder INSTANCE =
new ParseObjectCurrentCoder();
public static ParseObjectCurrentCoder get() {
return INSTANCE;
}
/* package */ ParseObjectCurrentCoder() {
// do nothing
}
/**
* Converts a {@code ParseObject} to /2/ JSON representation suitable for saving to disk.
*
* <pre>
* {
* data: {
* // data fields, including objectId, createdAt, updatedAt
* },
* classname: class name for the object,
* operations: { } // operations per field
* }
* </pre>
*
* All keys are included, regardless of whether they are dirty.
*
* @see #decode(ParseObject.State.Init, JSONObject, ParseDecoder)
*/
@Override
public <T extends ParseObject.State> JSONObject encode(
T state, ParseOperationSet operations, ParseEncoder encoder) {
if (operations != null) {
throw new IllegalArgumentException("Parameter ParseOperationSet is not null");
}
// Public data goes in dataJSON; special fields go in objectJSON.
JSONObject objectJSON = new JSONObject();
JSONObject dataJSON = new JSONObject();
try {
// Serialize the data
for (String key : state.keySet()) {
Object object = state.get(key);
dataJSON.put(key, encoder.encode(object));
// TODO(grantland): Use cached value from hashedObjects, but only if we're not dirty.
}
if (state.createdAt() > 0) {
dataJSON.put(KEY_CREATED_AT,
ParseDateFormat.getInstance().format(new Date(state.createdAt())));
}
if (state.updatedAt() > 0) {
dataJSON.put(KEY_UPDATED_AT,
ParseDateFormat.getInstance().format(new Date(state.updatedAt())));
}
if (state.objectId() != null) {
dataJSON.put(KEY_OBJECT_ID, state.objectId());
}
objectJSON.put(KEY_DATA, dataJSON);
objectJSON.put(KEY_CLASS_NAME, state.className());
} catch (JSONException e) {
throw new RuntimeException("could not serialize object to JSON");
}
return objectJSON;
}
/**
* Decodes from /2/ JSON.
*
* This is only used to read ParseObjects stored on disk in JSON.
*
* @see #encode(ParseObject.State, ParseOperationSet, ParseEncoder)
*/
@Override
public <T extends ParseObject.State.Init<?>> T decode(
T builder, JSONObject json, ParseDecoder decoder) {
try {
// The handlers for id, created_at, updated_at, and pointers are for
// backward compatibility with old serialized users.
if (json.has(KEY_OLD_OBJECT_ID)) {
String newObjectId = json.getString(KEY_OLD_OBJECT_ID);
builder.objectId(newObjectId);
}
if (json.has(KEY_OLD_CREATED_AT)) {
String createdAtString =
json.getString(KEY_OLD_CREATED_AT);
if (createdAtString != null) {
builder.createdAt(ParseImpreciseDateFormat.getInstance().parse(createdAtString));
}
}
if (json.has(KEY_OLD_UPDATED_AT)) {
String updatedAtString =
json.getString(KEY_OLD_UPDATED_AT);
if (updatedAtString != null) {
builder.updatedAt(ParseImpreciseDateFormat.getInstance().parse(updatedAtString));
}
}
if (json.has(KEY_OLD_POINTERS)) {
JSONObject newPointers =
json.getJSONObject(KEY_OLD_POINTERS);
Iterator<?> keys = newPointers.keys();
while (keys.hasNext()) {
String key = (String) keys.next();
JSONArray pointerArray = newPointers.getJSONArray(key);
builder.put(key, ParseObject.createWithoutData(pointerArray.optString(0),
pointerArray.optString(1)));
}
}
JSONObject data = json.optJSONObject(KEY_DATA);
if (data != null) {
Iterator<?> keys = data.keys();
while (keys.hasNext()) {
String key = (String) keys.next();
if (key.equals(KEY_OBJECT_ID)) {
String newObjectId = data.getString(key);
builder.objectId(newObjectId);
continue;
}
if (key.equals(KEY_CREATED_AT)) {
builder.createdAt(ParseDateFormat.getInstance().parse(data.getString(key)));
continue;
}
if (key.equals(KEY_UPDATED_AT)) {
builder.updatedAt(ParseDateFormat.getInstance().parse(data.getString(key)));
continue;
}
Object value = data.get(key);
Object decodedObject = decoder.decode(value);
builder.put(key, decodedObject);
}
}
return builder;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import bolts.Task;
/** package */ interface ParseObjectCurrentController<T extends ParseObject> {
/**
* Persist the currentParseObject
* @param object
* @return
*/
Task<Void> setAsync(T object);
/**
* Get the persisted currentParseObject
* @return
*/
Task<T> getAsync();
/**
* Check whether the currentParseObject exists or not
* @return
*/
Task<Boolean> existsAsync();
/**
* Judge whether the given ParseObject is the currentParseObject
* @param object
* @return {@code true} if the give {@link ParseObject} is the currentParseObject
*/
boolean isCurrent(T object);
/**
* A test helper to reset the current ParseObject. This method nullifies the in memory
* currentParseObject
*/
void clearFromMemory();
/**
* A test helper to reset the current ParseObject. This method nullifies the in memory and in
* disk currentParseObject
*/
void clearFromDisk();
}

Some files were not shown because too many files have changed in this diff Show More