diff --git a/android/expoview/src/main/AndroidManifest.xml b/android/expoview/src/main/AndroidManifest.xml index 50be3c41656ee..9ebf0551156fa 100644 --- a/android/expoview/src/main/AndroidManifest.xml +++ b/android/expoview/src/main/AndroidManifest.xml @@ -106,6 +106,11 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" /> + + diff --git a/android/expoview/src/main/java/host/exp/exponent/ExpoApplication.java b/android/expoview/src/main/java/host/exp/exponent/ExpoApplication.java index 40035a7e60d52..9c1fd4dd1f823 100644 --- a/android/expoview/src/main/java/host/exp/exponent/ExpoApplication.java +++ b/android/expoview/src/main/java/host/exp/exponent/ExpoApplication.java @@ -53,8 +53,6 @@ public void onCreate() { if (!Constants.isStandaloneApp()) { KernelConstants.MAIN_ACTIVITY_CLASS = LauncherActivity.class; } - - AppLoaderProvider.registerLoader(this, "react-native-headless", ExpoHeadlessAppLoader.class); KernelProvider.setFactory(new KernelProvider.KernelFactory() { @Override public KernelInterface create() { @@ -62,7 +60,7 @@ public KernelInterface create() { } }); - ExponentKernelModuleProvider.setFactory(reactContext -> new ExponentKernelModule(reactContext)); + ExponentKernelModuleProvider.setFactory(ExponentKernelModule::new); Exponent.initialize(this, this); NativeModuleDepsProvider.getInstance().add(Kernel.class, KernelProvider.getInstance()); diff --git a/android/versioned-abis/expoview-abi37_0_0/src/main/java/abi37_0_0/org/unimodules/adapters/react/ReactAdapterPackage.java b/android/versioned-abis/expoview-abi37_0_0/src/main/java/abi37_0_0/org/unimodules/adapters/react/ReactAdapterPackage.java index b2ac8831b4941..93e9258d1d1f1 100644 --- a/android/versioned-abis/expoview-abi37_0_0/src/main/java/abi37_0_0/org/unimodules/adapters/react/ReactAdapterPackage.java +++ b/android/versioned-abis/expoview-abi37_0_0/src/main/java/abi37_0_0/org/unimodules/adapters/react/ReactAdapterPackage.java @@ -26,7 +26,6 @@ public class ReactAdapterPackage extends BasePackage { @Override public List createInternalModules(Context context) { - AppLoaderProvider.registerLoader(context, "react-native-headless", RNHeadlessAppLoader.class); // We can force-cast here, because this package will only be used in React Native context. ReactContext reactContext = (ReactContext) context; return Arrays.asList( diff --git a/packages/@unimodules/react-native-adapter/android/src/main/AndroidManifest.xml b/packages/@unimodules/react-native-adapter/android/src/main/AndroidManifest.xml index e63893dfe2b96..ad5d93b80b46d 100644 --- a/packages/@unimodules/react-native-adapter/android/src/main/AndroidManifest.xml +++ b/packages/@unimodules/react-native-adapter/android/src/main/AndroidManifest.xml @@ -1,5 +1,11 @@ + - + + + diff --git a/packages/@unimodules/react-native-adapter/android/src/main/java/org/unimodules/adapters/react/ReactAdapterPackage.java b/packages/@unimodules/react-native-adapter/android/src/main/java/org/unimodules/adapters/react/ReactAdapterPackage.java index 4613be46e7771..20fd42d8a2ec6 100644 --- a/packages/@unimodules/react-native-adapter/android/src/main/java/org/unimodules/adapters/react/ReactAdapterPackage.java +++ b/packages/@unimodules/react-native-adapter/android/src/main/java/org/unimodules/adapters/react/ReactAdapterPackage.java @@ -26,7 +26,6 @@ public class ReactAdapterPackage extends BasePackage { @Override public List createInternalModules(Context context) { - AppLoaderProvider.registerLoader(context, "react-native-headless", RNHeadlessAppLoader.class); // We can force-cast here, because this package will only be used in React Native context. ReactContext reactContext = (ReactContext) context; return Arrays.asList( diff --git a/packages/expo-background-fetch/CHANGELOG.md b/packages/expo-background-fetch/CHANGELOG.md index bea04155f08bc..a4a0bad684184 100644 --- a/packages/expo-background-fetch/CHANGELOG.md +++ b/packages/expo-background-fetch/CHANGELOG.md @@ -8,6 +8,4 @@ ### 🐛 Bug fixes -## 8.2.0 — 2020-05-27 - -*This version does not introduce any user-facing changes.* +- Upgrading an application does not cause `BackgroundFetch` tasks to unregister. ([#8348](https://github.com/expo/expo/pull/8438) by [@mczernek](https://github.com/mczernek)) diff --git a/packages/expo-background-fetch/android/src/main/java/expo/modules/backgroundfetch/BackgroundFetchTaskConsumer.java b/packages/expo-background-fetch/android/src/main/java/expo/modules/backgroundfetch/BackgroundFetchTaskConsumer.java index b97a7453fcb04..3fda3ea600958 100644 --- a/packages/expo-background-fetch/android/src/main/java/expo/modules/backgroundfetch/BackgroundFetchTaskConsumer.java +++ b/packages/expo-background-fetch/android/src/main/java/expo/modules/backgroundfetch/BackgroundFetchTaskConsumer.java @@ -39,8 +39,8 @@ public String taskType() { @Override public boolean canReceiveCustomBroadcast(String action) { // Let the TaskService know that we want to receive custom broadcasts - // having "android.intent.action.BOOT_COMPLETED" action. - return Intent.ACTION_BOOT_COMPLETED.equals(action); + // having "android.intent.action.BOOT_COMPLETED" or "Intent.ACTION_MY_PACKAGE_REPLACED" action. + return Intent.ACTION_BOOT_COMPLETED.equals(action) || Intent.ACTION_MY_PACKAGE_REPLACED.equals(action); } @Override @@ -65,13 +65,16 @@ public void didReceiveBroadcast(Intent intent) { String action = intent.getAction(); if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { - // Device has just been booted up - restore an alarm if "startOnBoot" option is enabled. + // Device has just been booted up, so we need restore an alarm if "startOnBoot" option is enabled. Map options = mTask.getOptions(); boolean startOnBoot = options.containsKey("startOnBoot") && (boolean) options.get("startOnBoot"); if (startOnBoot) { startAlarm(); } + } else if(Intent.ACTION_MY_PACKAGE_REPLACED.equals(action)) { + // App has just been reinstalled, so we need restore an alarm. + startAlarm(); } else { Context context = getContext(); TaskManagerUtilsInterface taskManagerUtils = getTaskManagerUtils(); diff --git a/packages/expo-task-manager/CHANGELOG.md b/packages/expo-task-manager/CHANGELOG.md index bea04155f08bc..a4a0bad684184 100644 --- a/packages/expo-task-manager/CHANGELOG.md +++ b/packages/expo-task-manager/CHANGELOG.md @@ -8,6 +8,4 @@ ### 🐛 Bug fixes -## 8.2.0 — 2020-05-27 - -*This version does not introduce any user-facing changes.* +- Upgrading an application does not cause `BackgroundFetch` tasks to unregister. ([#8348](https://github.com/expo/expo/pull/8438) by [@mczernek](https://github.com/mczernek)) diff --git a/packages/expo-task-manager/android/src/main/AndroidManifest.xml b/packages/expo-task-manager/android/src/main/AndroidManifest.xml index d008016078e2e..d86a93deb18ba 100644 --- a/packages/expo-task-manager/android/src/main/AndroidManifest.xml +++ b/packages/expo-task-manager/android/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + + + diff --git a/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskService.java b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskService.java index 3c8a33b99bbd0..5bac3b8341720 100644 --- a/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskService.java +++ b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskService.java @@ -11,8 +11,6 @@ import android.os.PersistableBundle; import android.util.Log; -import org.json.JSONArray; -import org.json.JSONException; import org.json.JSONObject; import org.unimodules.apploader.AppLoaderProvider; import org.unimodules.apploader.HeadlessAppLoader; @@ -26,10 +24,8 @@ import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.UUID; @@ -38,6 +34,11 @@ import expo.modules.taskManager.exceptions.InvalidConsumerClassException; import expo.modules.taskManager.exceptions.TaskNotFoundException; import expo.modules.taskManager.exceptions.TaskRegisteringFailedException; +import expo.modules.taskManager.repository.TasksAndEventsRepository; + +import static expo.modules.taskManager.Utils.getConsumerVersion; +import static expo.modules.taskManager.Utils.jsonToMap; +import static expo.modules.taskManager.Utils.unversionedClassForClass; // @tsapeta: TaskService is a funny kind of singleton module... because it's actually not a singleton :D // Since it would make too much troubles in order to get the singleton instance (from ModuleRegistryProvider) @@ -52,9 +53,6 @@ public class TaskService implements SingletonModule, TaskServiceInterface { private WeakReference mContextRef; private TaskManagerUtilsInterface mTaskManagerUtils; - // { "": { "": TaskInterface } } - private static Map> sTasksTable = null; - // Map with task managers of running (foregrounded) apps. { "": WeakReference(TaskManagerInterface) } private static final Map> sTaskManagers = new HashMap<>(); @@ -64,8 +62,7 @@ public class TaskService implements SingletonModule, TaskServiceInterface { // { "": List(eventIds...) } private static final Map> sEvents = new HashMap<>(); - // { "": List(eventBodies...) } - private static final Map> sEventsQueues = new HashMap<>(); + private TasksAndEventsRepository mTasksAndEventsRepository; // Map of callbacks for task execution events. Schema: { "": TaskExecutionCallback } private static final Map sTaskCallbacks = new HashMap<>(); @@ -73,9 +70,10 @@ public class TaskService implements SingletonModule, TaskServiceInterface { public TaskService(Context context) { super(); mContextRef = new WeakReference<>(context); + mTasksAndEventsRepository = TasksAndEventsRepository.create(context); - if (sTasksTable == null) { - sTasksTable = new HashMap<>(); + if (!mTasksAndEventsRepository.tasksExist()) { + mTasksAndEventsRepository.createTasks(); restoreTasks(); } } @@ -104,7 +102,7 @@ public void registerTask(String taskName, String appId, String appUrl, Class con } else { internalRegisterTask(taskName, appId, appUrl, consumerClass, options); } - saveTasksForAppWithId(appId); + mTasksAndEventsRepository.persistTasksForAppId(getSharedPreferences(), appId); } @Override @@ -122,7 +120,7 @@ public void unregisterTask(String taskName, String appId, Class consumerClass) t throw new InvalidConsumerClassException(taskName); } - Map appTasks = sTasksTable.get(appId); + Map appTasks = mTasksAndEventsRepository.getTasks(appId); if (appTasks != null) { appTasks.remove(taskName); @@ -131,12 +129,12 @@ public void unregisterTask(String taskName, String appId, Class consumerClass) t Log.i(TAG, "Unregistering task '" + taskName + "' for app '" + appId + "'."); task.getConsumer().didUnregister(); - saveTasksForAppWithId(appId); + mTasksAndEventsRepository.persistTasksForAppId(getSharedPreferences(), appId); } @Override public void unregisterAllTasksForAppId(String appId) { - Map appTasks = sTasksTable.get(appId); + Map appTasks = mTasksAndEventsRepository.getTasks(appId); if (appTasks != null) { Log.i(TAG, "Unregistering all tasks for app '" + appId + "'."); @@ -168,7 +166,7 @@ public Bundle getTaskOptions(String taskName, String appId) { @Override public List getTasksForAppId(String appId) { - Map appTasks = sTasksTable.get(appId); + Map appTasks = mTasksAndEventsRepository.getTasks(appId); List tasks = new ArrayList<>(); if (appTasks != null) { @@ -187,7 +185,7 @@ public List getTasksForAppId(String appId) { @Override public List getTaskConsumers(String appId) { - Map appTasks = sTasksTable.get(appId); + Map appTasks = mTasksAndEventsRepository.getTasks(appId); List taskConsumers = new ArrayList<>(); if (appTasks != null) { @@ -250,7 +248,7 @@ public void setTaskManager(TaskManagerInterface taskManager, String appId, Strin taskManagers.put(appId, new WeakReference<>(taskManager)); // Execute events waiting for the task manager. - List eventsQueue = sEventsQueues.get(appId); + List eventsQueue = mTasksAndEventsRepository.getEvents(appId); if (eventsQueue != null) { for (Bundle body : eventsQueue) { @@ -259,7 +257,7 @@ public void setTaskManager(TaskManagerInterface taskManager, String appId, Strin } // Remove events queue for that app. - sEventsQueues.remove(appId); + mTasksAndEventsRepository.removeEvents(appId); if (!isHeadless) { // Maybe update app url in user defaults. It might change only in non-headless mode. @@ -285,7 +283,7 @@ public void handleIntent(Intent intent) { Log.i(TAG, "Handling intent with action '" + action + "'."); - for (String appId : sTasksTable.keySet()) { + for (String appId : mTasksAndEventsRepository.allAppIdsWithTasks()) { List taskConsumers = getTaskConsumers(appId); for (TaskConsumerInterface consumer : taskConsumers) { @@ -405,17 +403,17 @@ public void executeTask(TaskInterface task, Bundle data, Error error, TaskExecut // The app is not fully loaded as its task manager is not there yet. // We need to add event's body to the queue from which events will be executed once the task manager is ready. - if (!sEventsQueues.containsKey(appId)) { - sEventsQueues.put(appId, new ArrayList<>()); + if (!mTasksAndEventsRepository.hasEvents(appId)) { + mTasksAndEventsRepository.putEvents(appId, new ArrayList<>()); } - sEventsQueues.get(appId).add(body); + mTasksAndEventsRepository.putEventForAppId(appId, body); try { getAppLoader().loadApp(mContextRef.get(), new HeadlessAppLoader.Params(appId, task.getAppUrl()), () -> { }, success -> { if (!success) { sEvents.remove(appId); - sEventsQueues.remove(appId); + mTasksAndEventsRepository.removeEvents(appId); // Host unreachable? Unregister all tasks for that app. unregisterAllTasksForAppId(appId); @@ -429,7 +427,7 @@ public void executeTask(TaskInterface task, Bundle data, Error error, TaskExecut } appEvents.remove(eventId); - sEventsQueues.remove(appId); + mTasksAndEventsRepository.removeEvents(appId); } } @@ -462,9 +460,9 @@ private void internalRegisterTask(String taskName, String appId, String appUrl, Task task = new Task(taskName, appId, appUrl, consumer, options, this); - Map appTasks = sTasksTable.containsKey(appId) ? sTasksTable.get(appId) : new HashMap(); + Map appTasks = mTasksAndEventsRepository.hasTasks(appId) ? mTasksAndEventsRepository.getTasks(appId) : new HashMap(); appTasks.put(taskName, task); - sTasksTable.put(appId, appTasks); + mTasksAndEventsRepository.putTasks(appId, appTasks); Log.i(TAG, "Registered task with name '" + taskName + "' for app with ID '" + appId + "'."); @@ -496,8 +494,8 @@ private Bundle errorBundleForError(Error error) { return errorBundle; } - private static TaskInterface getTask(String taskName, String appId) { - Map appTasks = sTasksTable.get(appId); + private TaskInterface getTask(String taskName, String appId) { + Map appTasks = mTasksAndEventsRepository.getTasks(appId); return appTasks != null ? appTasks.get(taskName) : null; } @@ -541,13 +539,12 @@ private void maybeUpdateAppUrlForAppId(String appUrl, String appId) { @SuppressWarnings("unchecked") private void restoreTasks() { - SharedPreferences preferences = getSharedPreferences(); - Map config = preferences.getAll(); + Map apps = mTasksAndEventsRepository.readPersistedTasks(getSharedPreferences()); - for (Map.Entry entry : config.entrySet()) { - Map appConfig = jsonToMap(entry.getValue().toString()); - Map tasksConfig = (HashMap) appConfig.get("tasks"); - String appUrl = (String) appConfig.get("appUrl"); + for (Map.Entry entry : apps.entrySet()) { + String appId = entry.getKey(); + String appUrl = entry.getValue().appUrl; + Map tasksConfig = entry.getValue().tasks; if (appUrl != null && tasksConfig != null && tasksConfig.size() > 0) { for (String taskName : tasksConfig.keySet()) { @@ -565,7 +562,7 @@ private void restoreTasks() { try { // register the task using internal method which doesn't change shared preferences. - internalRegisterTask(taskName, entry.getKey(), appUrl, consumerClass, options); + internalRegisterTask(taskName, appId, appUrl, consumerClass, options); } catch (TaskRegisteringFailedException e) { Log.e(TAG, e.getMessage()); } @@ -581,41 +578,10 @@ private void restoreTasks() { } // Update tasks for the app to unregister tasks that couldn't be restored. - saveTasksForAppWithId(entry.getKey()); + mTasksAndEventsRepository.persistTasksForAppId(getSharedPreferences(), entry.getKey()); } } - private void saveTasksForAppWithId(String appId) { - SharedPreferences preferences = getSharedPreferences(); - Map appRow = sTasksTable.get(appId); - - if (preferences == null) { - return; - } - if (appRow == null || appRow.size() == 0) { - preferences.edit().remove(appId).apply(); - return; - } - - Map appConfig = new HashMap<>(); - Map tasks = new HashMap<>(); - String appUrl = null; - - for (TaskInterface task : appRow.values()) { - Map taskConfig = exportTaskToMap(task); - tasks.put(task.getName(), taskConfig); - appUrl = task.getAppUrl(); - } - - appConfig.put("appUrl", appUrl); - appConfig.put("tasks", tasks); - - preferences - .edit() - .putString(appId, new JSONObject(appConfig).toString()) - .apply(); - } - private void removeAppFromConfig(String appId) { getSharedPreferences().edit().remove(appId).apply(); } @@ -634,19 +600,6 @@ private TaskManagerInterface getTaskManager(String appId) { return weakRef == null ? null : weakRef.get(); } - private Map exportTaskToMap(TaskInterface task) { - Map map = new HashMap<>(); - Class consumerClass = task.getConsumer().getClass(); - String consumerClassName = unversionedClassNameForClass(consumerClass); - - map.put("name", task.getName()); - map.put("consumerClass", consumerClassName); - map.put("consumerVersion", getConsumerVersion(consumerClass)); - map.put("options", task.getOptions()); - - return map; - } - private void invalidateAppRecord(String appId) { HeadlessAppLoader appLoader = getAppLoader(); if (appLoader != null) { @@ -666,100 +619,4 @@ public void run() { }, timeout); } - private static Map jsonToMap(String jsonStr) { - try { - return jsonToMap(new JSONObject(jsonStr)); - } catch (JSONException e) { - return new HashMap<>(); - } - } - - private static Map jsonToMap(JSONObject json) { - Map map = new HashMap<>(); - - try { - Iterator keys = json.keys(); - - while (keys.hasNext()) { - String key = (String) keys.next(); - Object value = jsonObjectToObject(json.get(key)); - - map.put(key, value); - } - } catch (JSONException e) { - e.printStackTrace(); - } - return map; - } - - private static List jsonToList(JSONArray json) { - List list = new ArrayList<>(); - - try { - for (int i = 0; i < json.length(); i++) { - Object value = json.get(i); - - if (value instanceof JSONArray) { - value = jsonToList((JSONArray) value); - } else if (value instanceof JSONObject) { - value = jsonToMap((JSONObject) value); - } - list.add(value); - } - } catch (JSONException e) { - e.printStackTrace(); - } - return list; - } - - private static Object jsonObjectToObject(Object json) { - if (json instanceof JSONObject) { - return jsonToMap((JSONObject) json); - } - if (json instanceof JSONArray) { - return jsonToList((JSONArray) json); - } - return json; - } - - /** - * Returns task consumer's version. Defaults to 0 if `VERSION` static field is not implemented. - */ - private static int getConsumerVersion(Class consumerClass) { - try { - Field versionField = consumerClass.getDeclaredField("VERSION"); - return (Integer) versionField.get(null); - } catch (NoSuchFieldException | IllegalAccessException e) { - return 0; - } - } - - /** - * Method that unversions class names, so we can always use unversioned task consumer classes. - */ - private static String unversionedClassNameForClass(Class versionedClass) { - String className = versionedClass.getName(); - return className.replaceFirst("\\^abi\\d+_\\d+_\\d+\\.", ""); - } - - /** - * Returns unversioned class from versioned one. - */ - private static Class unversionedClassForClass(Class versionedClass) { - if (versionedClass == null) { - return null; - } - - String unversionedClassName = unversionedClassNameForClass(versionedClass); - - try { - return Class.forName(unversionedClassName); - } catch (ClassNotFoundException e) { - Log.e(TAG, "Class with name '" + unversionedClassName + "' not found."); - e.printStackTrace(); - return null; - } - } - - //endregion } diff --git a/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/Utils.java b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/Utils.java new file mode 100644 index 0000000000000..b82d78c42d7fb --- /dev/null +++ b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/Utils.java @@ -0,0 +1,128 @@ +package expo.modules.taskManager; + +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.unimodules.interfaces.taskManager.TaskInterface; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class Utils { + private static final String TAG = "taskManager.Utils"; + + public static Map exportTaskToMap(TaskInterface task) { + Map map = new HashMap<>(); + Class consumerClass = task.getConsumer().getClass(); + String consumerClassName = unversionedClassNameForClass(consumerClass); + + map.put("name", task.getName()); + map.put("consumerClass", consumerClassName); + map.put("consumerVersion", getConsumerVersion(consumerClass)); + map.put("options", task.getOptions()); + + return map; + } + + /** + * Returns unversioned class from versioned one. + */ + public static Class unversionedClassForClass(Class versionedClass) { + if (versionedClass == null) { + return null; + } + + String unversionedClassName = unversionedClassNameForClass(versionedClass); + + try { + return Class.forName(unversionedClassName); + } catch (ClassNotFoundException e) { + Log.e(TAG, "Class with name '" + unversionedClassName + "' not found."); + e.printStackTrace(); + return null; + } + } + + /** + * Method that unversions class names, so we can always use unversioned task consumer classes. + */ + public static String unversionedClassNameForClass(Class versionedClass) { + String className = versionedClass.getName(); + return className.replaceFirst("\\^abi\\d+_\\d+_\\d+\\.", ""); + } + + /** + * Returns task consumer's version. Defaults to 0 if `VERSION` static field is not implemented. + */ + public static int getConsumerVersion(Class consumerClass) { + try { + Field versionField = consumerClass.getDeclaredField("VERSION"); + return (Integer) versionField.get(null); + } catch (NoSuchFieldException | IllegalAccessException e) { + return 0; + } + } + + public static Map jsonToMap(String jsonStr) { + try { + return jsonToMap(new JSONObject(jsonStr)); + } catch (JSONException e) { + return new HashMap<>(); + } + } + + public static Map jsonToMap(JSONObject json) { + Map map = new HashMap<>(); + + try { + Iterator keys = json.keys(); + + while (keys.hasNext()) { + String key = (String) keys.next(); + Object value = jsonObjectToObject(json.get(key)); + + map.put(key, value); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return map; + } + + public static List jsonToList(JSONArray json) { + List list = new ArrayList<>(); + + try { + for (int i = 0; i < json.length(); i++) { + Object value = json.get(i); + + if (value instanceof JSONArray) { + value = jsonToList((JSONArray) value); + } else if (value instanceof JSONObject) { + value = jsonToMap((JSONObject) value); + } + list.add(value); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return list; + } + + public static Object jsonObjectToObject(Object json) { + if (json instanceof JSONObject) { + return jsonToMap((JSONObject) json); + } + if (json instanceof JSONArray) { + return jsonToList((JSONArray) json); + } + return json; + } + +} diff --git a/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/repository/BareTasksAndEventsRepository.java b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/repository/BareTasksAndEventsRepository.java new file mode 100644 index 0000000000000..1266006145af0 --- /dev/null +++ b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/repository/BareTasksAndEventsRepository.java @@ -0,0 +1,119 @@ +package expo.modules.taskManager.repository; + +import android.content.SharedPreferences; +import android.os.Bundle; + +import org.unimodules.interfaces.taskManager.TaskInterface; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import androidx.annotation.NonNull; + +/** + * Implementation of {@link TasksAndEventsRepository} to be used in bare workflow. + * + * It stores tasks and events by in relation to appId. However, upon retrieval it ignores appId and returns all stored tasks and events. + * + * In bare workflow there is no reason to map appId to task, since there is only one appId. However, we maintain this structure for some reasons: + * 1. Maintaining as much similarity as possible, due to delegation of persistent storage functionality + * 2. It might happen, after migration from managed to bare workflow, that there are some tasks under different appIds stored already + * + * For the sake of simplicity and avoiding potential bugs, we change the behavior as minimally as possible. + * In very unlikely scenario of having more than one appId in bare, we merge all tasks into one list upon retrieval. + * While storing, we clear all info that is not associated with current appId to avoid future confusions. + */ +public class BareTasksAndEventsRepository implements TasksAndEventsRepository { + + private final TasksPersistence tasksPersistence; + + private static Map> sEvents = new HashMap<>(); + private static Map> sTasks; + + public BareTasksAndEventsRepository(TasksPersistence tasksPersistence) { + this.tasksPersistence = tasksPersistence; + } + + @Override + public void putEvents(String appId, List events) { + sEvents.put(appId, events); + } + + @Override + public void putEventForAppId(String appId, Bundle body) { + sEvents.get(appId).add(body); + } + + @Override + public boolean hasEvents(String appId) { + return sEvents.containsKey(appId); + } + + @Override + public void removeEvents(String appId) { + sEvents.remove(appId); + } + + @Override + public List getEvents(String appId) { + List allEvents = new LinkedList<>(); + for(List value: sEvents.values()) { + allEvents.addAll(value); + } + return allEvents; + } + + @Override + public boolean tasksExist() { + return sTasks != null; + } + + @Override + public void createTasks() { + sTasks = new HashMap<>(); + } + + @NonNull + @Override + public Set allAppIdsWithTasks() { + return sTasks.keySet(); + } + + @Override + public Map getTasks(String appId) { + Map allTasks = new HashMap<>(); + for(Map value: sTasks.values()) { + allTasks.putAll(value); + } + return allTasks; + } + + @Override + public boolean hasTasks(String appId) { + return sTasks.containsKey(appId); + } + + @Override + public void putTasks(String appId, Map tasks) { + sTasks.put(appId, tasks); + } + + @Override + public void removeTasks(String appId) { + sTasks.remove(appId); + } + + @Override + public void persistTasksForAppId(SharedPreferences preferences, String appId) { + tasksPersistence.clearTaskPersistence(preferences, appId); + tasksPersistence.persistTasksForAppId(preferences, appId, getTasks(appId)); + } + + @Override + public Map readPersistedTasks(SharedPreferences preferences) { + return tasksPersistence.readPersistedTasks(preferences); + } +} diff --git a/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/repository/ManagedTasksAndEventsRepository.java b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/repository/ManagedTasksAndEventsRepository.java new file mode 100644 index 0000000000000..ea587c6d32be4 --- /dev/null +++ b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/repository/ManagedTasksAndEventsRepository.java @@ -0,0 +1,101 @@ +package expo.modules.taskManager.repository; + +import android.content.SharedPreferences; +import android.os.Bundle; + +import org.unimodules.interfaces.taskManager.TaskInterface; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import androidx.annotation.NonNull; + +/** + * Standard implementation of {@link TasksAndEventsRepository} to be used in managed workflow. + * + * It maps appId to its tasks and events, and allows to retrieve them accordingly. + */ +public class ManagedTasksAndEventsRepository implements TasksAndEventsRepository { + + private final TasksPersistence tasksPersistence; + + public ManagedTasksAndEventsRepository(TasksPersistence tasksPersistence) { + this.tasksPersistence = tasksPersistence; + } + + private static Map> sEvents = new HashMap<>(); + private static Map> sTasks; + + @Override + public void putEvents(String appId, List events) { + sEvents.put(appId, events); + } + + @Override + public void putEventForAppId(String appId, Bundle body) { + sEvents.get(appId).add(body); + } + + @Override + public boolean hasEvents(String appId) { + return sEvents.containsKey(appId); + } + + @Override + public void removeEvents(String appId) { + sEvents.remove(appId); + } + + @Override + public List getEvents(String appId) { + return sEvents.get(appId); + } + + @Override + public boolean tasksExist() { + return sTasks != null; + } + + @Override + public void createTasks() { + sTasks = new HashMap<>(); + } + + @NonNull + @Override + public Set allAppIdsWithTasks() { + return sTasks.keySet(); + } + + @Override + public Map getTasks(String appId) { + return sTasks.get(appId); + } + + @Override + public boolean hasTasks(String appId) { + return sTasks.containsKey(appId); + } + + @Override + public void putTasks(String appId, Map tasks) { + sTasks.put(appId, tasks); + } + + @Override + public void removeTasks(String appId) { + sTasks.remove(appId); + } + + @Override + public void persistTasksForAppId(SharedPreferences preferences, String appId) { + tasksPersistence.persistTasksForAppId(preferences, appId, getTasks(appId)); + } + + @Override + public Map readPersistedTasks(SharedPreferences preferences) { + return tasksPersistence.readPersistedTasks(preferences); + } +} diff --git a/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/repository/TasksAndEventsRepository.java b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/repository/TasksAndEventsRepository.java new file mode 100644 index 0000000000000..97a1c5e26aeb1 --- /dev/null +++ b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/repository/TasksAndEventsRepository.java @@ -0,0 +1,69 @@ +package expo.modules.taskManager.repository; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Bundle; + +import org.unimodules.interfaces.taskManager.TaskInterface; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import androidx.annotation.NonNull; + +/** + * Class used to store Tasks and Events for TaskManager. + * + * Tasks and Events are stored in static collections, thanks to which they remain intact during whole application lifetime. + * Additionally, it provides means to persistently store and restore Tasks whenever necessary. + * + * Differences in behavior are dictated by different approach in managed and bare workflow. See {@link ManagedTasksAndEventsRepository} and {@link BareTasksAndEventsRepository} for details. + */ +public interface TasksAndEventsRepository { + + /** + * This factory methods tries to detect which strategy for storage and retrieving of stored tasks would be appropriate. + * + * Decision is based on value of metadata for *expo.modules.taskManager.oneAppId* which should be set to *true* in bare applications and to *false* in managed and in Client. + * This is due to the fact, that the value is originally set in *expo-task-manager's* AndroidManifest.xml and replaced in *expoview's* one. + * The latter is present only in managed workflow, the former is the only one in bare applications. + * + * @return Proper implementation of TasksAndEventsRepository + */ + static TasksAndEventsRepository create(@NonNull Context context) { + String oneAppIdMetadata = "expo.modules.taskManager.oneAppId"; + boolean oneAppId = false; + try { + oneAppId = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA).metaData.getBoolean(oneAppIdMetadata); + } catch (PackageManager.NameNotFoundException ignore) { + } + if(oneAppId) { + return new BareTasksAndEventsRepository(new TasksPersistence()); + } else { + return new ManagedTasksAndEventsRepository(new TasksPersistence()); + } + } + + class AppConfig { + public String appUrl; + public Map tasks; + } + + void putEvents(String appId, List events); + void putEventForAppId(String appId, Bundle body); + boolean hasEvents(String appId); + void removeEvents(String appId); + List getEvents(String appId); + + boolean tasksExist(); + void createTasks(); + @NonNull Set allAppIdsWithTasks(); + Map getTasks(String appId); + boolean hasTasks(String appId); + void putTasks(String appId, Map tasks); + void removeTasks(String appId); + void persistTasksForAppId(SharedPreferences preferences, String appId); + Map readPersistedTasks(SharedPreferences preferences); +} diff --git a/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/repository/TasksPersistence.java b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/repository/TasksPersistence.java new file mode 100644 index 0000000000000..5e4503eda1c1a --- /dev/null +++ b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/repository/TasksPersistence.java @@ -0,0 +1,81 @@ +package expo.modules.taskManager.repository; + +import android.content.SharedPreferences; +import android.os.Debug; + +import org.json.JSONObject; +import org.unimodules.interfaces.taskManager.TaskInterface; + +import java.util.HashMap; +import java.util.Map; + +import static expo.modules.taskManager.Utils.exportTaskToMap; +import static expo.modules.taskManager.Utils.jsonToMap; + +public class TasksPersistence { + + public void clearTaskPersistence(SharedPreferences preferences, String but) { + Map map = preferences.getAll(); + for(String key: map.keySet()) { + if(!but.equals(key)) { + preferences.edit().remove(key).apply(); + } + } + } + + public void persistTasksForAppId(SharedPreferences preferences, String appId, Map appRow) { + + if (preferences == null) { + return; + } + if (appRow == null || appRow.size() == 0) { + preferences.edit().remove(appId).apply(); + return; + } + + Map appConfig = new HashMap<>(); + Map tasks = new HashMap<>(); + String appUrl = null; + + for (TaskInterface task : appRow.values()) { + Map taskConfig = exportTaskToMap(task); + tasks.put(task.getName(), taskConfig); + appUrl = task.getAppUrl(); + } + + appConfig.put("appUrl", appUrl); + appConfig.put("tasks", tasks); + + preferences + .edit() + .putString(appId, new JSONObject(appConfig).toString()) + .apply(); + } + + public Map readPersistedTasks(SharedPreferences preferences) { + Map result = new HashMap<>(); + + Map appIdToAppConfigsMap = preferences.getAll(); + + for (Map.Entry appIdToConfig : appIdToAppConfigsMap.entrySet()) { + Map appConfig = jsonToMap(appIdToConfig.getValue().toString()); + String appUrl = (String) appConfig.get("appUrl"); + Map tasksConfig = (HashMap) appConfig.get("tasks"); + + if (appUrl != null && tasksConfig != null && tasksConfig.size() > 0) { + Map tasksForApp = new HashMap<>(); + for (String taskName : tasksConfig.keySet()) { + tasksForApp.put(taskName, tasksConfig.get(taskName)); + } + TasksAndEventsRepository.AppConfig app = new TasksAndEventsRepository.AppConfig(); + app.appUrl = appUrl; + app.tasks = tasksForApp; + result.put(appIdToConfig.getKey(), app); + } + } + + return result; + + } + +} diff --git a/packages/unimodules-app-loader/android/src/main/java/org/unimodules/apploader/AppLoaderProvider.java b/packages/unimodules-app-loader/android/src/main/java/org/unimodules/apploader/AppLoaderProvider.java index 3f8393ee4b581..ea5ac4cbf818b 100644 --- a/packages/unimodules-app-loader/android/src/main/java/org/unimodules/apploader/AppLoaderProvider.java +++ b/packages/unimodules-app-loader/android/src/main/java/org/unimodules/apploader/AppLoaderProvider.java @@ -1,7 +1,7 @@ package org.unimodules.apploader; import android.content.Context; -import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.util.Log; import java.util.HashMap; @@ -9,23 +9,8 @@ public class AppLoaderProvider { - private static Map loaderClasses = new HashMap<>(); private static Map loaders = new HashMap<>(); - public static void registerLoader(Context context, String name, Class loaderClass) { - registerLoader(context, name, loaderClass, false); - } - - public static void registerLoader(Context context, String name, Class loaderClass, boolean overload) { - if (!overload) { - if (appLoaderRegisteredForName(context, name, loaderClass)) return; - } - getSharedPreferences(context).edit() - .putString(appLoaderKey(name), loaderClass.getName()) - .apply(); - loaderClasses.put(name, loaderClass); - } - public static HeadlessAppLoader getLoader(String name, Context context) { if (!loaders.containsKey(name)) { try { @@ -39,32 +24,21 @@ public static HeadlessAppLoader getLoader(String name, Context context) { return loaders.get(name); } - private static boolean appLoaderRegisteredForName(Context context, String name, Class loaderClass) { - String cachedClassName = getSharedPreferences(context).getString(appLoaderKey(name), null); - return loaderClasses.containsKey(name) || loaderClass.getName().equals(cachedClassName); - } - + @SuppressWarnings("unchecked") private static void createLoader(String name, Context context) throws ClassNotFoundException, IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException, NoSuchMethodException { - Class loaderClass = loaderClasses.get(name); - if (loaderClass == null) { - String loaderClassName = getSharedPreferences(context) - .getString(appLoaderKey(name), null); + Class loaderClass; + try { + String loaderClassName = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA).metaData.getString("org.unimodules.core.AppLoader#" + name); if (loaderClassName != null) { - loaderClass = Class.forName(loaderClassName); + loaderClass = (Class)Class.forName(loaderClassName); + loaders.put(name, (HeadlessAppLoader) loaderClass.getDeclaredConstructor(Context.class).newInstance(context)); + } else { + throw new IllegalStateException("Unable to instantiate AppLoader!"); } + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException("Unable to instantiate AppLoader!", e); } - loaders.put(name, (HeadlessAppLoader) loaderClass.getDeclaredConstructor(Context.class).newInstance(context)); - } - - private static final String APP_LOADER_PREFERENCES_NAME = "appLoader_config"; - private static final String KEY_LOADER_PREFIX = "appLoader_"; - - private static String appLoaderKey(String appLoaderName) { - return KEY_LOADER_PREFIX + appLoaderName; - } - private static SharedPreferences getSharedPreferences(Context context) { - return context.getSharedPreferences(APP_LOADER_PREFERENCES_NAME, Context.MODE_PRIVATE); } public static abstract class Callback {