Base Configuration
This commit is contained in:
52
ExternalLibs/Parse-SDK-Android/Parse/build.gradle
Normal file
52
ExternalLibs/Parse-SDK-Android/Parse/build.gradle
Normal 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"
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
-keepnames class com.parse.** { *; }
|
||||
|
||||
# Required for Parse
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes Signature
|
||||
# https://github.com/square/okio#proguard
|
||||
-dontwarn okio.**
|
@ -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>
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ParseObject> query = ParseQuery.getQuery("MyClass");
|
||||
* 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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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<ParseObject> query = ParseQuery.getQuery("MyClass");
|
||||
* query.findInBackground(new FindCallback<ParseObject>() {
|
||||
* public void done(List<ParseObject> 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);
|
||||
}
|
@ -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("MyFunction"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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ParseObject> query = ParseQuery.getQuery("MyClass");
|
||||
* query.getInBackground(myId, new GetCallback<ParseObject>() {
|
||||
* 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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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("username", "password", new LogInCallback() {
|
||||
* public void done(ParseUser user, ParseException e) {
|
||||
* if (e == null && 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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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("New mail from " + 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
* <manifest ...>
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* <application ...>
|
||||
* <meta-data
|
||||
* android:name="com.parse.SERVER_URL"
|
||||
* android:value="@string/parse_server_url" />
|
||||
* <meta-data
|
||||
* android:name="com.parse.APPLICATION_ID"
|
||||
* android:value="@string/parse_app_id" />
|
||||
* <meta-data
|
||||
* android:name="com.parse.CLIENT_KEY"
|
||||
* android:value="@string/parse_client_key" />
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* </application>
|
||||
* </manifest>
|
||||
* </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>
|
||||
* <manifest ...>
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* <application ...>
|
||||
* <meta-data
|
||||
* android:name="com.parse.SERVER_URL"
|
||||
* android:value="@string/parse_server_url" />
|
||||
* <meta-data
|
||||
* android:name="com.parse.APPLICATION_ID"
|
||||
* android:value="@string/parse_app_id" />
|
||||
* <meta-data
|
||||
* android:name="com.parse.CLIENT_KEY"
|
||||
* android:value="@string/parse_client_key" />
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* </application>
|
||||
* </manifest>
|
||||
* </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, "your application id", "your client key");
|
||||
* }
|
||||
* }
|
||||
* </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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
@ -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}< {@link String}, ?>,
|
||||
* {@link ParseObject}, {@link List}<?>, 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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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() + "]";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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...
|
||||
}
|
||||
}
|
||||
}
|
@ -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> {
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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 > 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
Reference in New Issue
Block a user