Base Configuration

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

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild

6
.gitmodules vendored Normal file
View File

@ -0,0 +1,6 @@
[submodule "app/libs/ParseLiveQuery-Android"]
path = app/libs/ParseLiveQuery-Android
url = https://github.com/parse-community/ParseLiveQuery-Android.git
[submodule "app/libs/Parse-SDK-Android"]
path = app/libs/Parse-SDK-Android
url = https://github.com/parse-community/Parse-SDK-Android.git

View File

@ -0,0 +1,3 @@
<component name="ProjectDictionaryState">
<dictionary name="Philippe-Adrien" />
</component>

20
.idea/gradle.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/ExternalLibs/Parse-SDK-Android/Parse" />
<option value="$PROJECT_DIR$/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

49
.idea/misc.xml Normal file
View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectInspectionProfilesVisibleTreeState">
<entry key="Project Default">
<profile-state>
<expanded-state>
<State>
<id />
</State>
</expanded-state>
<selected-state>
<State>
<id>Android</id>
</State>
</selected-state>
</profile-state>
</entry>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

11
.idea/modules.xml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/ExternalLibs/Parse-SDK-Android/Parse/Parse.iml" filepath="$PROJECT_DIR$/ExternalLibs/Parse-SDK-Android/Parse/Parse.iml" />
<module fileurl="file://$PROJECT_DIR$/ParseApplication.iml" filepath="$PROJECT_DIR$/ParseApplication.iml" />
<module fileurl="file://$PROJECT_DIR$/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/ParseLiveQuery.iml" filepath="$PROJECT_DIR$/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/ParseLiveQuery.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@ -0,0 +1,11 @@
coverage:
precision: 2
round: down
range: "45...100"
status:
project:
default:
target: 45%
patch: yes
changes: no

View File

@ -0,0 +1,40 @@
# built application files
*.apk
*.ap_
# files for the dex VM
*.dex
# Java class files
*.class
# generated files
bin/
gen/
# Local configuration file (sdk path, etc)
local.properties
# Android Studio
.idea
*.iml
*.ipr
*.iws
classes
gen-external-apklibs
# Gradle
.gradle
build
# Other
.metadata
*/bin/*
*/gen/*
testData
testCache
server.config
# Jacoco
jacoco.exec

View File

@ -0,0 +1,35 @@
branches:
only:
- master
- /^\d+\.\d+\.\d+$/ # regex
language: android
jdk:
- oraclejdk8
before_install:
- pip install --user codecov
- mkdir "$ANDROID_HOME/licenses" || true
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" > "$ANDROID_HOME/licenses/android-sdk-license"
script:
- ./gradlew clean testDebugUnitTest jacocoTestReport
after_success:
- ./gradlew coveralls
- codecov
- ./scripts/publish_snapshot.sh
cache:
directories:
- $HOME/.gradle
- $HOME/.m2/repository
deploy:
provider: script
script: ./gradlew bintrayUpload
skip_cleanup: true
on:
branch: master
tags: true

View File

@ -0,0 +1,65 @@
# Contributing to Parse SDK for Android
We want to make contributing to this project as easy and transparent as possible.
## Code of Conduct
Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated.
## Our Development Process
Most of our work will be done in public directly on GitHub. There may be changes done through our internal source control, but it will be rare and only as needed.
### `master` is unsafe
Our goal is to keep `master` stable, but there may be changes that your application may not be compatible with. We'll do our best to publicize any breaking changes, but try to use our specific releases in any production environment.
### Pull Requests
We actively welcome your pull requests. When we get one, we'll run some Parse-specific integration tests on it first. From here, we'll need to get a core member to sign off on the changes and then merge the pull request. For API changes we may need to fix internal uses, which could cause some delay. We'll do our best to provide updates and feedback throughout the process.
1. Fork the repo and create your branch from `master`.
4. Add unit tests for any new code you add.
3. If you've changed APIs, update the documentation.
4. Ensure the test suite passes.
5. Make sure your code lints.
6. If you haven't already, complete the Contributor License Agreement ("CLA").
### Contributor License Agreement ("CLA")
In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects.
Complete your CLA here: <https://developers.facebook.com/opensource/cla>
## Bugs
Although we try to keep developing on Parse easy, you still may run into some issues. Technical questions should be asked on [Stack Overflow][stack-overflow], and for everything else we'll be using GitHub issues.
### Known Issues
We use GitHub issues to track public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new issue, try to make sure your problem doesn't already exist.
### Reporting New Issues
Not all issues are SDK issues. If you're unsure whether your bug is with the SDK or backend, you can test to see if it reproduces with our [REST API][rest-api] and [Parse API Console][parse-api-console]. If it does, you can report backend bugs [here][bug-reports].
To view the REST API network requests issued by the Parse SDK and responses from the Parse backend, please check out [OkHttp Interceptors][network-debugging-tool]. With this tool, you can either log network requests/responses to Android logcat, or log them to Chrome Debugger via Stetho.
Details are key. The more information you provide us the easier it'll be for us to debug and the faster you'll receive a fix. Some examples of useful tidbits:
* A description. What did you expect to happen and what actually happened? Why do you think that was wrong?
* A simple unit test that fails. Refer [here][tests-dir] for examples of existing unit tests. See our [README](README.md#usage) for how to run unit tests. You can submit a pull request with your failing unit test so that our CI verifies that the test fails.
* What version does this reproduce on? What version did it last work on?
* [Stacktrace or GTFO][stacktrace-or-gtfo]. In all honesty, full stacktraces with line numbers make a happy developer.
* Anything else you find relevant.
### Security Bugs
Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue.
## Style Guide
We're still working on providing a code style for your IDE and getting a linter on GitHub, but for now try to keep the following:
* Most importantly, match the existing code style as much as possible.
* Try to keep lines under 100 characters, if possible.
## License
By contributing to Parse Android SDK, you agree that your contributions will be licensed under its license.
[stack-overflow]: http://stackoverflow.com/tags/parse.com
[bug-reports]: https://github.com/parse-community/parse-server
[rest-api]: http://docs.parseplatform.org/rest/guide/
[network-debugging-tool]: https://github.com/square/okhttp/wiki/Interceptors
[parse-api-console]: http://blog.parse.com/announcements/introducing-the-parse-api-console/
[stacktrace-or-gtfo]: http://i.imgur.com/jacoj.jpg
[tests-dir]: /Parse/src/test/java/com/parse

View File

@ -0,0 +1,34 @@
BSD License
For Parse Android SDK software
Copyright (c) 2015-present, Parse, LLC. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name Parse nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-----
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.

View File

@ -0,0 +1,37 @@
Additional Grant of Patent Rights Version 2
"Software" means the Parse Android SDK software distributed by Parse, LLC.
Parse, LLC. ("Parse") hereby grants to each recipient of the Software
("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable
(subject to the termination provision below) license under any Necessary
Claims, to make, have made, use, sell, offer to sell, import, and otherwise
transfer the Software. For avoidance of doubt, no license is granted under
Parses rights in any patent claims that are infringed by (i) modifications
to the Software made by you or any third party or (ii) the Software in
combination with any software or other technology.
The license granted hereunder will terminate, automatically and without notice,
if you (or any of your subsidiaries, corporate affiliates or agents) initiate
directly or indirectly, or take a direct financial interest in, any Patent
Assertion: (i) against Parse or any of its subsidiaries or corporate
affiliates, (ii) against any party if such Patent Assertion arises in whole or
in part from any software, technology, product or service of Parse or any of
its subsidiaries or corporate affiliates, or (iii) against any party relating
to the Software. Notwithstanding the foregoing, if Parse or any of its
subsidiaries or corporate affiliates files a lawsuit alleging patent
infringement against you in the first instance, and you respond by filing a
patent infringement counterclaim in that lawsuit against that party that is
unrelated to the Software, the license granted hereunder will not terminate
under section (i) of this paragraph due to such counterclaim.
A "Necessary Claim" is a claim of a patent owned by Parse that is
necessarily infringed by the Software standing alone.
A "Patent Assertion" is any lawsuit or other action alleging direct, indirect,
or contributory infringement or inducement to infringe any patent, including a
cross-claim or counterclaim.
-----
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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