Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: users logged out after SDK upgrade due to different cache path #1168

Merged
merged 10 commits into from
May 26, 2022
127 changes: 74 additions & 53 deletions parse/src/main/java/com/parse/Parse.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.parse.boltsinternal.Continuation;
import com.parse.boltsinternal.Task;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
Expand All @@ -26,6 +29,7 @@
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;

import okhttp3.OkHttpClient;

/**
Expand Down Expand Up @@ -75,7 +79,7 @@ private Parse() {
* }
* }
* </pre>
*
* <p>
* See <a
* href="https://github.com/parse-community/Parse-SDK-Android/issues/279">https://github.com/parse-community/Parse-SDK-Android/issues/279</a>
* for a discussion on performance of local datastore, and if it is right for your project.
Expand All @@ -85,8 +89,8 @@ private Parse() {
public static void enableLocalDatastore(Context context) {
if (isInitialized()) {
throw new IllegalStateException(
"`Parse#enableLocalDatastore(Context)` must be invoked "
+ "before `Parse#initialize(Context)`");
"`Parse#enableLocalDatastore(Context)` must be invoked "
+ "before `Parse#initialize(Context)`");
}
isLocalDatastoreEnabled = true;
}
Expand All @@ -113,7 +117,7 @@ public static boolean isLocalDatastoreEnabled() {

/**
* @return {@code True} if {@link Configuration.Builder#allowCustomObjectId()} has been called,
* otherwise {@code false}.
* otherwise {@code false}.
*/
public static boolean isAllowCustomObjectId() {
return allowCustomObjectId;
Expand Down Expand Up @@ -145,6 +149,9 @@ static void initialize(Configuration configuration, ParsePlugins parsePlugins) {
PLog.w(TAG, "Parse is already initialized");
return;
}
// Perform old dir migration on initialize.
new ParseCacheDirMigrationUtils(configuration.context).runMigrations();

// NOTE (richardross): We will need this here, as ParsePlugins uses the return value of
// isLocalDataStoreEnabled() to perform additional behavior.
isLocalDatastoreEnabled = configuration.localDataStoreEnabled;
Expand Down Expand Up @@ -176,32 +183,32 @@ static void initialize(Configuration configuration, ParsePlugins parsePlugins) {
checkCacheApplicationId();
final Context context = configuration.context;
Task.callInBackground(
(Callable<Void>)
() -> {
getEventuallyQueue(context);
return null;
});
(Callable<Void>)
() -> {
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");
"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");
}

ParseUser.getCurrentUserAsync()
.makeVoid()
.continueWith(
(Continuation<Void, Void>)
task -> {
// Prime config in the background
ParseConfig.getCurrentConfig();
return null;
},
Task.BACKGROUND_EXECUTOR);
.makeVoid()
.continueWith(
(Continuation<Void, Void>)
task -> {
// Prime config in the background
ParseConfig.getCurrentConfig();
return null;
},
Task.BACKGROUND_EXECUTOR);

dispatchOnParseInitialized();

Expand All @@ -213,8 +220,11 @@ static void initialize(Configuration configuration, ParsePlugins parsePlugins) {

// region Server URL

/** Returns the current server URL. */
public static @Nullable String getServer() {
/**
* Returns the current server URL.
*/
public static @Nullable
String getServer() {
URL server = ParseRESTCommand.server;
return server == null ? null : server.toString();
}
Expand Down Expand Up @@ -248,7 +258,8 @@ public static void setServer(@NonNull String server) {
* @param server The server URL to validate.
* @return The validated server URL.
*/
private static @Nullable String validateServerUrl(@Nullable String server) {
private static @Nullable
String validateServerUrl(@Nullable 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)
Expand Down Expand Up @@ -285,7 +296,9 @@ public static void destroy() {
allowCustomObjectId = false;
}

/** @return {@code True} if {@link #initialize} has been called, otherwise {@code false}. */
/**
* @return {@code True} if {@link #initialize} has been called, otherwise {@code false}.
*/
static boolean isInitialized() {
return ParsePlugins.get() != null;
}
Expand All @@ -308,10 +321,10 @@ public static Context getApplicationContext() {
*/
private static boolean allParsePushIntentReceiversInternal() {
List<ResolveInfo> intentReceivers =
ManifestInfo.getIntentReceivers(
ParsePushBroadcastReceiver.ACTION_PUSH_RECEIVE,
ParsePushBroadcastReceiver.ACTION_PUSH_DELETE,
ParsePushBroadcastReceiver.ACTION_PUSH_OPEN);
ManifestInfo.getIntentReceivers(
ParsePushBroadcastReceiver.ACTION_PUSH_RECEIVE,
ParsePushBroadcastReceiver.ACTION_PUSH_DELETE,
ParsePushBroadcastReceiver.ACTION_PUSH_OPEN);

for (ResolveInfo resolveInfo : intentReceivers) {
if (resolveInfo.activityInfo.exported) {
Expand Down Expand Up @@ -414,15 +427,15 @@ private static ParseEventuallyQueue getEventuallyQueue(Context context) {
synchronized (MUTEX) {
boolean isLocalDatastoreEnabled = Parse.isLocalDatastoreEnabled();
if (eventuallyQueue == null
|| (isLocalDatastoreEnabled && eventuallyQueue instanceof ParseCommandCache)
|| (!isLocalDatastoreEnabled
&& eventuallyQueue instanceof ParsePinningEventuallyQueue)) {
|| (isLocalDatastoreEnabled && eventuallyQueue instanceof ParseCommandCache)
|| (!isLocalDatastoreEnabled
&& eventuallyQueue instanceof ParsePinningEventuallyQueue)) {
checkContext();
ParseHttpClient httpClient = ParsePlugins.get().restClient();
eventuallyQueue =
isLocalDatastoreEnabled
? new ParsePinningEventuallyQueue(context, httpClient)
: new ParseCommandCache(context, httpClient);
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
Expand All @@ -436,33 +449,35 @@ private static ParseEventuallyQueue getEventuallyQueue(Context context) {
}
}

/** Used by Parse LiveQuery */
/**
* Used by Parse LiveQuery
*/
public static void checkInit() {
if (ParsePlugins.get() == null) {
throw new RuntimeException(
"You must call Parse.initialize(Context)" + " before using the Parse library.");
"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.");
"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.");
"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);
== PackageManager.PERMISSION_GRANTED);
}

// endregion
Expand All @@ -472,10 +487,10 @@ static boolean hasPermission(String permission) {
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
+ "\" />");
"To use this functionality, add this to your AndroidManifest.xml:\n"
+ "<uses-permission android:name=\""
+ permission
+ "\" />");
}
}

Expand All @@ -489,7 +504,7 @@ static void requirePermission(String permission) {
static void registerParseCallbacks(ParseCallbacks listener) {
if (isInitialized()) {
throw new IllegalStateException(
"You must register callbacks before Parse.initialize(Context)");
"You must register callbacks before Parse.initialize(Context)");
}

synchronized (MUTEX_CALLBACKS) {
Expand Down Expand Up @@ -537,7 +552,9 @@ private static ParseCallbacks[] collectParseCallbacks() {
return callbacks;
}

/** Returns the level of logging that will be displayed. */
/**
* Returns the level of logging that will be displayed.
*/
public static int getLogLevel() {
return PLog.getLogLevel();
}
Expand Down Expand Up @@ -569,7 +586,9 @@ interface ParseCallbacks {
void onParseInitialized();
}

/** Represents an opaque configuration for the {@code Parse} SDK configuration. */
/**
* Represents an opaque configuration for the {@code Parse} SDK configuration.
*/
public static final class Configuration {
final Context context;
final String applicationId;
Expand All @@ -591,7 +610,9 @@ private Configuration(Builder builder) {
this.maxRetries = builder.maxRetries;
}

/** Allows for simple constructing of a {@code Configuration} object. */
/**
* Allows for simple constructing of a {@code Configuration} object.
*/
public static final class Builder {
private final Context context;
private String applicationId;
Expand Down
106 changes: 106 additions & 0 deletions parse/src/main/java/com/parse/ParseCacheDirMigrationUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.parse;

import android.content.Context;

import java.io.File;
import java.util.ArrayList;

/**
* The {@code ParseMigrationUtils} class perform caching dir migration operation for {@code Parse} SDK.
*/
public class ParseCacheDirMigrationUtils {
private final String TAG = this.getClass().getName();
private final Object lock = new Object();
private final Context context;

protected ParseCacheDirMigrationUtils(Context context) {
this.context = context;
}

/*Start old data migrations to new respective locations ("/files/com.parse/", "/cache/com.parse/")*/
protected void runMigrations() {
synchronized (lock) {
runSilentMigration(context);
}
}

private void runSilentMigration(Context context) {
ArrayList<File> filesToBeMigrated = new ArrayList<>();
ParseFileUtils.getAllNestedFiles(
getOldParseDir(context).getAbsolutePath(),
filesToBeMigrated
);
if (filesToBeMigrated.isEmpty()) {
return;
}
boolean useFilesDir = false;
//Hard coded config file names list.
String[] configNamesList = {"installationId", "currentUser", "currentConfig", "currentInstallation", "LocalId", "pushState"};
//Start migration for each files in `allFiles`.
for (File itemToMove : filesToBeMigrated) {
try {
for (String configName : configNamesList) {
if (itemToMove.getAbsolutePath().contains(configName)) {
useFilesDir = true;
break;
} else {
useFilesDir = false;
}
}
File fileToSave = new File(
(useFilesDir ? context.getFilesDir() : context.getCacheDir())
+ "/com.parse/" +
getFileOldDir(context, itemToMove),
itemToMove.getName());
//Perform copy operation if file doesn't exist in the new directory.
if (!fileToSave.exists()) {
ParseFileUtils.copyFile(itemToMove, fileToSave);
logMigrationStatus(itemToMove.getName(), itemToMove.getPath(), fileToSave.getAbsolutePath(), "Successful.");
} else {
logMigrationStatus(itemToMove.getName(), itemToMove.getPath(), fileToSave.getAbsolutePath(), "Already exist in new location.");
}
ParseFileUtils.deleteQuietly(itemToMove);
PLog.v(TAG, "File deleted: " + "{" + itemToMove.getName() + "}" + " successfully");
} catch (Exception e) {
e.printStackTrace();
}
}
//Check again, if all files has been resolved or not. If yes, delete the old dir "app_Parse".
filesToBeMigrated.clear();
ParseFileUtils.getAllNestedFiles(getOldParseDir(context).getAbsolutePath(), filesToBeMigrated);
if (filesToBeMigrated.isEmpty()) {
try {
ParseFileUtils.deleteDirectory(getOldParseDir(context));
} catch (Exception e) {
e.printStackTrace();
}
}
PLog.v(TAG, "Migration completed.");
}

private String getFileOldDir(Context context, File file) {
//Parse the old sub directory name where the file should be moved (new location) by following the old sub directory name.
String temp = file
.getAbsolutePath()
.replace(
getOldParseDir(context).getAbsolutePath(), "")
.replace("/" + file.getName(), "");
//Before returning the path, replace file name from the last, eg. dir name & file name could be same, as we want to get only dir name.
return replaceLast(temp, file.getName());
}

private void logMigrationStatus(String fileName, String oldPath, String newPath, String status) {
PLog.v(TAG, "Migration for file: " + "{" + fileName + "}" + " from {" + oldPath + "} to {" + newPath + "}, Status: " + status);
}

/*Replace a given string from the last*/
private String replaceLast(String text, String regex) {
return text.replaceFirst("(?s)" + regex + "(?!.*?" + regex + ")", "");
}

private File getOldParseDir(Context context) {
return context.getDir("Parse", Context.MODE_PRIVATE);
}


}
Loading