Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1c9612e6 authored by Garfield Tan's avatar Garfield Tan
Browse files

Use a one-off thread to load launch params

It isn't possible to reuse LazyTaskWriterThread in PersisterQueue.
Code in that thread needs to obtain the WM lock, while it is possible
for getLaunchParams() to wait for the loading task with the WM lock held
in a different thread. That's a recipe for deadlocks.

It is also unsafe to use other shared background threads, because we
don't control what they do in them either.

That means we have to use a new thread to load launch params. There are
two options in front of us:

1. Create a thread every time we need to load launch params, and tear it
   down after the loading is done.
2. Introduce a long-standing thread to load launch params. In this case
   it is also optional to move all writing of launch params to this
   thread.

I chose #1 because I don't think loading launch params is frequent
enough to introduce a new long-standing thread in system_server.

Bug: 365983567
Test: atest LaunchParamsPersisterTests
Test: Settings launches at correct location after reboot.
Flag: EXEMPT bug fix
Change-Id: I613ba79b03db1b6ec25596d5a0cdbf03d65ce9d9
parent b28bcb6e
Loading
Loading
Loading
Loading
+163 −100
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.content.pm.ActivityInfo;
import android.content.pm.PackageManagerInternal;
import android.graphics.Rect;
import android.os.Environment;
import android.os.Process;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
@@ -50,6 +51,9 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.function.IntFunction;

/**
@@ -83,6 +87,12 @@ class LaunchParamsPersister {

    private PackageList mPackageList;

    /**
     * A map from user ID to the active {@link LoadingTask} when we're loading the launch params for
     * that user.
     */
    private final SparseArray<LoadingTask> mLoadingTaskMap = new SparseArray<>();

    /**
     * A dual layer map that first maps user ID to a secondary map, which maps component name (the
     * launching activity of tasks) to {@link PersistableLaunchParams} that stores launch metadata
@@ -117,113 +127,33 @@ class LaunchParamsPersister {
    }

    void onUnlockUser(int userId) {
        loadLaunchParams(userId);
    }

    void onCleanupUser(int userId) {
        mLaunchParamsMap.remove(userId);
    }

    private void loadLaunchParams(int userId) {
        final List<File> filesToDelete = new ArrayList<>();
        final File launchParamsFolder = getLaunchParamFolder(userId);
        if (!launchParamsFolder.isDirectory()) {
            Slog.i(TAG, "Didn't find launch param folder for user " + userId);
        if (mLoadingTaskMap.contains(userId)) {
            Slog.e(TAG, "Duplicated onUnlockUser " + userId);
            return;
        }

        final Set<String> packages = new ArraySet<>(mPackageList.getPackageNames());

        final File[] paramsFiles = launchParamsFolder.listFiles();
        final ArrayMap<ComponentName, PersistableLaunchParams> map =
                new ArrayMap<>(paramsFiles.length);
        mLaunchParamsMap.put(userId, map);

        for (File paramsFile : paramsFiles) {
            if (!paramsFile.isFile()) {
                Slog.w(TAG, paramsFile.getAbsolutePath() + " is not a file.");
                continue;
            }
            if (!paramsFile.getName().endsWith(LAUNCH_PARAMS_FILE_SUFFIX)) {
                Slog.w(TAG, "Unexpected params file name: " + paramsFile.getName());
                filesToDelete.add(paramsFile);
                continue;
            }
            String paramsFileName = paramsFile.getName();
            // Migrate all records from old separator to new separator.
            final int oldSeparatorIndex =
                    paramsFileName.indexOf(OLD_ESCAPED_COMPONENT_SEPARATOR);
            if (oldSeparatorIndex != -1) {
                if (paramsFileName.indexOf(
                        OLD_ESCAPED_COMPONENT_SEPARATOR, oldSeparatorIndex + 1) != -1) {
                    // Rare case. We have more than one old escaped component separator probably
                    // because this app uses underscore in their package name. We can't distinguish
                    // which one is the real separator so let's skip it.
                    filesToDelete.add(paramsFile);
                    continue;
                }
                paramsFileName = paramsFileName.replace(
                        OLD_ESCAPED_COMPONENT_SEPARATOR, ESCAPED_COMPONENT_SEPARATOR);
                final File newFile = new File(launchParamsFolder, paramsFileName);
                if (paramsFile.renameTo(newFile)) {
                    paramsFile = newFile;
                } else {
                    // Rare case. For some reason we can't rename the file. Let's drop this record
                    // instead.
                    filesToDelete.add(paramsFile);
                    continue;
                }
            }
            final String componentNameString = paramsFileName.substring(
                    0 /* beginIndex */,
                    paramsFileName.length() - LAUNCH_PARAMS_FILE_SUFFIX.length())
                    .replace(ESCAPED_COMPONENT_SEPARATOR, ORIGINAL_COMPONENT_SEPARATOR);
            final ComponentName name = ComponentName.unflattenFromString(
                    componentNameString);
            if (name == null) {
                Slog.w(TAG, "Unexpected file name: " + paramsFileName);
                filesToDelete.add(paramsFile);
                continue;
            }

            if (!packages.contains(name.getPackageName())) {
                // Rare case. PersisterQueue doesn't have a chance to remove files for removed
                // packages last time.
                filesToDelete.add(paramsFile);
                continue;
            }

            try (InputStream in = new FileInputStream(paramsFile)) {
                final PersistableLaunchParams params = new PersistableLaunchParams();
                final TypedXmlPullParser parser = Xml.resolvePullParser(in);
                int event;
                while ((event = parser.next()) != XmlPullParser.END_DOCUMENT
                        && event != XmlPullParser.END_TAG) {
                    if (event != XmlPullParser.START_TAG) {
                        continue;
                    }

                    final String tagName = parser.getName();
                    if (!TAG_LAUNCH_PARAMS.equals(tagName)) {
                        Slog.w(TAG, "Unexpected tag name: " + tagName);
                        continue;
        final LoadingTask task = new LoadingTask(userId);
        mLoadingTaskMap.put(userId, task);
        task.execute();
    }

                    params.restore(paramsFile, parser);
    void onCleanupUser(int userId) {
        // There is no need to abort the task itself. Just let the loading task finish silently
        // without modifying any state.
        mLoadingTaskMap.remove(userId);
        mLaunchParamsMap.remove(userId);
    }

                map.put(name, params);
                addComponentNameToLaunchParamAffinityMapIfNotNull(
                        name, params.mWindowLayoutAffinity);
            } catch (Exception e) {
                Slog.w(TAG, "Failed to restore launch params for " + name, e);
                filesToDelete.add(paramsFile);
            }
    private void waitAndMoveResultIfLoading(int userId) {
        final LoadingTask task = mLoadingTaskMap.removeReturnOld(userId);
        if (task == null) {
            return;
        }

        if (!filesToDelete.isEmpty()) {
            mPersisterQueue.addItem(new CleanUpComponentQueueItem(filesToDelete), true);
        final ArrayMap<ComponentName, PersistableLaunchParams> map = task.get();
        if (map == null) {
            return;
        }
        mLaunchParamsMap.put(userId, map);
    }

    void saveTask(Task task) {
@@ -236,6 +166,7 @@ class LaunchParamsPersister {
            return;
        }
        final int userId = task.mUserId;
        waitAndMoveResultIfLoading(userId);
        PersistableLaunchParams params;
        ArrayMap<ComponentName, PersistableLaunchParams> map = mLaunchParamsMap.get(userId);
        if (map == null) {
@@ -297,6 +228,7 @@ class LaunchParamsPersister {
    void getLaunchParams(Task task, ActivityRecord activity, LaunchParams outParams) {
        final ComponentName name = task != null ? task.realActivity : activity.mActivityComponent;
        final int userId = task != null ? task.mUserId : activity.mUserId;
        waitAndMoveResultIfLoading(userId);
        final String windowLayoutAffinity;
        if (task != null) {
            windowLayoutAffinity = task.mWindowLayoutAffinity;
@@ -394,6 +326,137 @@ class LaunchParamsPersister {
        }
    }

    private class LoadingTask
            implements Callable<ArrayMap<ComponentName, PersistableLaunchParams>> {
        private final int mUserId;
        private final FutureTask<ArrayMap<ComponentName, PersistableLaunchParams>> mFutureTask;

        private LoadingTask(int userId) {
            mUserId = userId;
            mFutureTask = new FutureTask<>(this);
        }

        private void execute() {
            new Thread(mFutureTask).start();
        }

        private ArrayMap<ComponentName, PersistableLaunchParams> get() {
            try {
                return mFutureTask.get();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (ExecutionException e) {
                Slog.e(TAG, "Failed to load launch params for user#" + mUserId, e);
                return null;
            }
        }

        @Override
        public ArrayMap<ComponentName, PersistableLaunchParams> call() {
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

            final List<File> filesToDelete = new ArrayList<>();
            final File launchParamsFolder = getLaunchParamFolder(mUserId);
            if (!launchParamsFolder.isDirectory()) {
                Slog.i(TAG, "Didn't find launch param folder for user " + mUserId);
                return null;
            }

            final Set<String> packages = new ArraySet<>(mPackageList.getPackageNames());

            final File[] paramsFiles = launchParamsFolder.listFiles();
            final ArrayMap<ComponentName, PersistableLaunchParams> map =
                    new ArrayMap<>(paramsFiles.length);

            for (File paramsFile : paramsFiles) {
                if (!paramsFile.isFile()) {
                    Slog.w(TAG, paramsFile.getAbsolutePath() + " is not a file.");
                    continue;
                }
                if (!paramsFile.getName().endsWith(LAUNCH_PARAMS_FILE_SUFFIX)) {
                    Slog.w(TAG, "Unexpected params file name: " + paramsFile.getName());
                    filesToDelete.add(paramsFile);
                    continue;
                }
                String paramsFileName = paramsFile.getName();
                // Migrate all records from old separator to new separator.
                final int oldSeparatorIndex =
                        paramsFileName.indexOf(OLD_ESCAPED_COMPONENT_SEPARATOR);
                if (oldSeparatorIndex != -1) {
                    if (paramsFileName.indexOf(
                            OLD_ESCAPED_COMPONENT_SEPARATOR, oldSeparatorIndex + 1) != -1) {
                        // Rare case. We have more than one old escaped component separator probably
                        // because this app uses underscore in their package name. We can't
                        // distinguish which one is the real separator so let's skip it.
                        filesToDelete.add(paramsFile);
                        continue;
                    }
                    paramsFileName = paramsFileName.replace(
                            OLD_ESCAPED_COMPONENT_SEPARATOR, ESCAPED_COMPONENT_SEPARATOR);
                    final File newFile = new File(launchParamsFolder, paramsFileName);
                    if (paramsFile.renameTo(newFile)) {
                        paramsFile = newFile;
                    } else {
                        // Rare case. For some reason we can't rename the file. Let's drop this
                        // record instead.
                        filesToDelete.add(paramsFile);
                        continue;
                    }
                }
                final String componentNameString = paramsFileName.substring(
                                0 /* beginIndex */,
                                paramsFileName.length() - LAUNCH_PARAMS_FILE_SUFFIX.length())
                        .replace(ESCAPED_COMPONENT_SEPARATOR, ORIGINAL_COMPONENT_SEPARATOR);
                final ComponentName name = ComponentName.unflattenFromString(
                        componentNameString);
                if (name == null) {
                    Slog.w(TAG, "Unexpected file name: " + paramsFileName);
                    filesToDelete.add(paramsFile);
                    continue;
                }

                if (!packages.contains(name.getPackageName())) {
                    // Rare case. PersisterQueue doesn't have a chance to remove files for removed
                    // packages last time.
                    filesToDelete.add(paramsFile);
                    continue;
                }

                try (InputStream in = new FileInputStream(paramsFile)) {
                    final PersistableLaunchParams params = new PersistableLaunchParams();
                    final TypedXmlPullParser parser = Xml.resolvePullParser(in);
                    int event;
                    while ((event = parser.next()) != XmlPullParser.END_DOCUMENT
                            && event != XmlPullParser.END_TAG) {
                        if (event != XmlPullParser.START_TAG) {
                            continue;
                        }

                        final String tagName = parser.getName();
                        if (!TAG_LAUNCH_PARAMS.equals(tagName)) {
                            Slog.w(TAG, "Unexpected tag name: " + tagName);
                            continue;
                        }

                        params.restore(paramsFile, parser);
                    }

                    map.put(name, params);
                    addComponentNameToLaunchParamAffinityMapIfNotNull(
                            name, params.mWindowLayoutAffinity);
                } catch (Exception e) {
                    Slog.w(TAG, "Failed to restore launch params for " + name, e);
                    filesToDelete.add(paramsFile);
                }
            }

            if (!filesToDelete.isEmpty()) {
                mPersisterQueue.addItem(new CleanUpComponentQueueItem(filesToDelete), true);
            }
            return map;
        }
    }

    private class LaunchParamsWriteQueueItem
            implements PersisterQueue.WriteQueueItem<LaunchParamsWriteQueueItem> {
        private final int mUserId;
@@ -466,7 +529,7 @@ class LaunchParamsPersister {
        }
    }

    private class CleanUpComponentQueueItem implements PersisterQueue.WriteQueueItem {
    private static class CleanUpComponentQueueItem implements PersisterQueue.WriteQueueItem {
        private final List<File> mComponentFiles;

        private CleanUpComponentQueueItem(List<File> componentFiles) {
@@ -483,7 +546,7 @@ class LaunchParamsPersister {
        }
    }

    private class PersistableLaunchParams {
    private static class PersistableLaunchParams {
        private static final String ATTR_WINDOWING_MODE = "windowing_mode";
        private static final String ATTR_DISPLAY_UNIQUE_ID = "display_unique_id";
        private static final String ATTR_BOUNDS = "bounds";
+15 −0
Original line number Diff line number Diff line
@@ -494,6 +494,21 @@ public class LaunchParamsPersisterTests extends WindowTestsBase {
        assertTrue("Result should be empty.", mResult.isEmpty());
    }

    @Test
    public void testAbortsLoadingWhenUserCleansUpBeforeLoadingFinishes() {
        mTarget.saveTask(mTestTask);
        mPersisterQueue.flush();

        final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor,
                mUserFolderGetter);
        target.onSystemReady();
        target.onUnlockUser(TEST_USER_ID);
        target.onCleanupUser(TEST_USER_ID);

        target.getLaunchParams(mTestTask, null, mResult);
        assertTrue("Result should be empty.", mResult.isEmpty());
    }

    private static boolean deleteRecursively(File file) {
        boolean result = true;
        if (file.isDirectory()) {