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