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

Commit a127fceb authored by Trung Lam's avatar Trung Lam
Browse files

Add persistence of ConversationInfo during device reboot.

Change-Id: I01ee08eb212a42b0c19d654241adb6afd718c14b
Test: Build and run on test device.
Bug: 147782721
parent 6a1ec23c
Loading
Loading
Loading
Loading
+264 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.android.server.people.data;

import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;

import com.android.internal.annotations.GuardedBy;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Base class for reading and writing protobufs on disk from a root directory. Callers should
 * ensure that the root directory is unlocked before doing I/O operations using this class.
 *
 * @param <T> is the data class representation of a protobuf.
 */
abstract class AbstractProtoDiskReadWriter<T> {

    private static final String TAG = AbstractProtoDiskReadWriter.class.getSimpleName();
    private static final long SHUTDOWN_DISK_WRITE_TIMEOUT = 5L * DateUtils.SECOND_IN_MILLIS;

    private final File mRootDir;
    private final ScheduledExecutorService mScheduledExecutorService;
    private final long mWriteDelayMs;

    @GuardedBy("this")
    private ScheduledFuture<?> mScheduledFuture;

    @GuardedBy("this")
    private Map<String, T> mScheduledFileDataMap = new ArrayMap<>();

    /**
     * Child class shall provide a {@link ProtoStreamWriter} to facilitate the writing of data as a
     * protobuf on disk.
     */
    abstract ProtoStreamWriter<T> protoStreamWriter();

    /**
     * Child class shall provide a {@link ProtoStreamReader} to facilitate the reading of protobuf
     * data on disk.
     */
    abstract ProtoStreamReader<T> protoStreamReader();

    AbstractProtoDiskReadWriter(@NonNull File rootDir, long writeDelayMs,
            @NonNull ScheduledExecutorService scheduledExecutorService) {
        mRootDir = rootDir;
        mWriteDelayMs = writeDelayMs;
        mScheduledExecutorService = scheduledExecutorService;
    }

    @WorkerThread
    void delete(@NonNull String fileName) {
        final File file = getFile(fileName);
        if (!file.exists()) {
            return;
        }
        if (!file.delete()) {
            Slog.e(TAG, "Failed to delete file: " + file.getPath());
        }
    }

    @WorkerThread
    void writeTo(@NonNull String fileName, @NonNull T data) {
        final File file = getFile(fileName);
        final AtomicFile atomicFile = new AtomicFile(file);

        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = atomicFile.startWrite();
        } catch (IOException e) {
            Slog.e(TAG, "Failed to write to protobuf file.", e);
            return;
        }

        try {
            final ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
            protoStreamWriter().write(protoOutputStream, data);
            protoOutputStream.flush();
            atomicFile.finishWrite(fileOutputStream);
            fileOutputStream = null;
        } finally {
            // When fileInputStream is null (successful write), this will no-op.
            atomicFile.failWrite(fileOutputStream);
        }
    }

    @WorkerThread
    @Nullable
    T read(@NonNull String fileName) {
        File[] files = mRootDir.listFiles(
                pathname -> pathname.isFile() && pathname.getName().equals(fileName));
        if (files == null || files.length == 0) {
            return null;
        } else if (files.length > 1) {
            // This can't possibly happen, but sanity check.
            Slog.w(TAG, "Found multiple files with the same name: " + Arrays.toString(files));
        }
        return parseFile(files[0]);
    }

    /**
     * Reads all files in directory and returns a map with file names as keys and parsed file
     * contents as values.
     */
    @WorkerThread
    @Nullable
    Map<String, T> readAll() {
        File[] files = mRootDir.listFiles(File::isFile);
        if (files == null) {
            return null;
        }

        Map<String, T> results = new ArrayMap<>();
        for (File file : files) {
            T result = parseFile(file);
            if (result != null) {
                results.put(file.getName(), result);
            }
        }
        return results;
    }

    /**
     * Schedules the specified data to be flushed to a file in the future. Subsequent
     * calls for the same file before the flush occurs will replace the previous data but will not
     * reset when the flush will occur. All unique files will be flushed at the same time.
     */
    @MainThread
    synchronized void scheduleSave(@NonNull String fileName, @NonNull T data) {
        mScheduledFileDataMap.put(fileName, data);

        if (mScheduledExecutorService.isShutdown()) {
            Slog.e(TAG, "Worker is shutdown, failed to schedule data saving.");
            return;
        }

        // Skip scheduling another flush when one is pending.
        if (mScheduledFuture != null) {
            return;
        }

        mScheduledFuture = mScheduledExecutorService.schedule(this::flushScheduledData,
                mWriteDelayMs, TimeUnit.MILLISECONDS);
    }

    /**
     * Saves specified data immediately on a background thread, and blocks until its completed. This
     * is useful for when device is powering off.
     */
    @MainThread
    synchronized void saveImmediately(@NonNull String fileName, @NonNull T data) {
        if (mScheduledExecutorService.isShutdown()) {
            return;
        }
        // Cancel existing future.
        if (mScheduledFuture != null) {

            // We shouldn't need to interrupt as this method and threaded task
            // #flushScheduledData are both synchronized.
            mScheduledFuture.cancel(true);
        }

        mScheduledFileDataMap.put(fileName, data);
        // Submit flush and blocks until it completes. Blocking will prevent the device from
        // shutting down before flushing completes.
        Future<?> future = mScheduledExecutorService.submit(this::flushScheduledData);
        try {
            future.get(SHUTDOWN_DISK_WRITE_TIMEOUT, TimeUnit.MILLISECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            Slog.e(TAG, "Failed to save data immediately.", e);
        }
    }

    @WorkerThread
    private synchronized void flushScheduledData() {
        if (mScheduledFileDataMap.isEmpty()) {
            mScheduledFuture = null;
            return;
        }
        for (String fileName : mScheduledFileDataMap.keySet()) {
            T data = mScheduledFileDataMap.remove(fileName);
            writeTo(fileName, data);
        }
        mScheduledFuture = null;
    }

    @WorkerThread
    @Nullable
    private T parseFile(@NonNull File file) {
        final AtomicFile atomicFile = new AtomicFile(file);
        try (FileInputStream fileInputStream = atomicFile.openRead()) {
            final ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
            return protoStreamReader().read(protoInputStream);
        } catch (IOException e) {
            Slog.e(TAG, "Failed to parse protobuf file.", e);
        }
        return null;
    }

    @NonNull
    private File getFile(String fileName) {
        return new File(mRootDir, fileName);
    }

    /**
     * {@code ProtoStreamWriter} writes {@code T} fields to {@link ProtoOutputStream}.
     *
     * @param <T> is the data class representation of a protobuf.
     */
    interface ProtoStreamWriter<T> {

        /**
         * Writes {@code T} to {@link ProtoOutputStream}.
         */
        void write(@NonNull ProtoOutputStream protoOutputStream, @NonNull T data);
    }

    /**
     * {@code ProtoStreamReader} reads {@link ProtoInputStream} and translate it to {@code T}.
     *
     * @param <T> is the data class representation of a protobuf.
     */
    interface ProtoStreamReader<T> {
        /**
         * Reads {@link ProtoInputStream} and translates it to {@code T}.
         */
        @Nullable
        T read(@NonNull ProtoInputStream protoInputStream);
    }
}
+74 −0
Original line number Diff line number Diff line
@@ -20,12 +20,18 @@ import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.LocusId;
import android.content.LocusIdProto;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutInfo.ShortcutFlags;
import android.net.Uri;
import android.util.Slog;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;

import com.android.internal.util.Preconditions;
import com.android.server.people.ConversationInfoProto;

import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
@@ -35,6 +41,8 @@ import java.util.Objects;
 */
public class ConversationInfo {

    private static final String TAG = ConversationInfo.class.getSimpleName();

    private static final int FLAG_IMPORTANT = 1;

    private static final int FLAG_NOTIFICATION_SILENCED = 1 << 1;
@@ -241,6 +249,72 @@ public class ConversationInfo {
        return (mConversationFlags & flags) == flags;
    }

    /** Writes field members to {@link ProtoOutputStream}. */
    void writeToProto(@NonNull ProtoOutputStream protoOutputStream) {
        protoOutputStream.write(ConversationInfoProto.SHORTCUT_ID, mShortcutId);
        if (mLocusId != null) {
            long locusIdToken = protoOutputStream.start(ConversationInfoProto.LOCUS_ID_PROTO);
            protoOutputStream.write(LocusIdProto.LOCUS_ID, mLocusId.getId());
            protoOutputStream.end(locusIdToken);
        }
        if (mContactUri != null) {
            protoOutputStream.write(ConversationInfoProto.CONTACT_URI, mContactUri.toString());
        }
        if (mNotificationChannelId != null) {
            protoOutputStream.write(ConversationInfoProto.NOTIFICATION_CHANNEL_ID,
                    mNotificationChannelId);
        }
        protoOutputStream.write(ConversationInfoProto.SHORTCUT_FLAGS, mShortcutFlags);
        protoOutputStream.write(ConversationInfoProto.CONVERSATION_FLAGS, mConversationFlags);
    }

    /** Reads from {@link ProtoInputStream} and constructs a {@link ConversationInfo}. */
    @NonNull
    static ConversationInfo readFromProto(@NonNull ProtoInputStream protoInputStream)
            throws IOException {
        ConversationInfo.Builder builder = new ConversationInfo.Builder();
        while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
            switch (protoInputStream.getFieldNumber()) {
                case (int) ConversationInfoProto.SHORTCUT_ID:
                    builder.setShortcutId(
                            protoInputStream.readString(ConversationInfoProto.SHORTCUT_ID));
                    break;
                case (int) ConversationInfoProto.LOCUS_ID_PROTO:
                    long locusIdToken = protoInputStream.start(
                            ConversationInfoProto.LOCUS_ID_PROTO);
                    while (protoInputStream.nextField()
                            != ProtoInputStream.NO_MORE_FIELDS) {
                        if (protoInputStream.getFieldNumber() == (int) LocusIdProto.LOCUS_ID) {
                            builder.setLocusId(new LocusId(
                                    protoInputStream.readString(LocusIdProto.LOCUS_ID)));
                        }
                    }
                    protoInputStream.end(locusIdToken);
                    break;
                case (int) ConversationInfoProto.CONTACT_URI:
                    builder.setContactUri(Uri.parse(protoInputStream.readString(
                            ConversationInfoProto.CONTACT_URI)));
                    break;
                case (int) ConversationInfoProto.NOTIFICATION_CHANNEL_ID:
                    builder.setNotificationChannelId(protoInputStream.readString(
                            ConversationInfoProto.NOTIFICATION_CHANNEL_ID));
                    break;
                case (int) ConversationInfoProto.SHORTCUT_FLAGS:
                    builder.setShortcutFlags(protoInputStream.readInt(
                            ConversationInfoProto.SHORTCUT_FLAGS));
                    break;
                case (int) ConversationInfoProto.CONVERSATION_FLAGS:
                    builder.setConversationFlags(protoInputStream.readInt(
                            ConversationInfoProto.CONVERSATION_FLAGS));
                    break;
                default:
                    Slog.w(TAG, "Could not read undefined field: "
                            + protoInputStream.getFieldNumber());
            }
        }
        return builder.build();
    }

    /**
     * Builder class for {@link ConversationInfo} objects.
     */
+227 −24

File changed.

Preview size limit exceeded, changes collapsed.

+16 −1
Original line number Diff line number Diff line
@@ -86,6 +86,7 @@ public class DataManager {
    private final Context mContext;
    private final Injector mInjector;
    private final ScheduledExecutorService mUsageStatsQueryExecutor;
    private final ScheduledExecutorService mDiskReadWriterExecutor;

    private final SparseArray<UserData> mUserDataArray = new SparseArray<>();
    private final SparseArray<BroadcastReceiver> mBroadcastReceivers = new SparseArray<>();
@@ -113,6 +114,7 @@ public class DataManager {
                BackgroundThread.getHandler());
        mMmsSmsContentObserver = new MmsSmsContentObserver(
                BackgroundThread.getHandler());
        mDiskReadWriterExecutor = mInjector.createScheduledExecutor();
    }

    /** Initialization. Called when the system services are up running. */
@@ -122,13 +124,18 @@ public class DataManager {
        mUserManager = mContext.getSystemService(UserManager.class);

        mShortcutServiceInternal.addListener(new ShortcutServiceListener());

        IntentFilter shutdownIntentFilter = new IntentFilter(Intent.ACTION_SHUTDOWN);
        BroadcastReceiver shutdownBroadcastReceiver = new ShutdownBroadcastReceiver();
        mContext.registerReceiver(shutdownBroadcastReceiver, shutdownIntentFilter);
    }

    /** This method is called when a user is unlocked. */
    public void onUserUnlocked(int userId) {
        UserData userData = mUserDataArray.get(userId);
        if (userData == null) {
            userData = new UserData(userId);
            userData = new UserData(userId, mDiskReadWriterExecutor,
                    mInjector.createContactsQueryHelper(mContext));
            mUserDataArray.put(userId, userData);
        }
        userData.setUserUnlocked();
@@ -662,6 +669,14 @@ public class DataManager {
        }
    }

    private class ShutdownBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            forAllPackages(PackageData::saveToDisk);
        }
    }

    @VisibleForTesting
    static class Injector {

+23 −2
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@ import android.annotation.UserIdInt;
import android.content.LocusId;
import android.text.TextUtils;

import java.io.File;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer;
import java.util.function.Predicate;

@@ -43,17 +45,36 @@ public class PackageData {

    private final Predicate<String> mIsDefaultSmsAppPredicate;

    private final File mPackageDataDir;

    PackageData(@NonNull String packageName, @UserIdInt int userId,
            @NonNull Predicate<String> isDefaultDialerPredicate,
            @NonNull Predicate<String> isDefaultSmsAppPredicate) {
            @NonNull Predicate<String> isDefaultSmsAppPredicate,
            @NonNull ScheduledExecutorService scheduledExecutorService,
            @NonNull File perUserPeopleDataDir,
            @NonNull ContactsQueryHelper helper) {
        mPackageName = packageName;
        mUserId = userId;
        mConversationStore = new ConversationStore();

        mPackageDataDir = new File(perUserPeopleDataDir, mPackageName);
        mConversationStore = new ConversationStore(mPackageDataDir, scheduledExecutorService,
                helper);
        mEventStore = new EventStore();
        mIsDefaultDialerPredicate = isDefaultDialerPredicate;
        mIsDefaultSmsAppPredicate = isDefaultSmsAppPredicate;
    }

    /** Called when user is unlocked. */
    void loadFromDisk() {
        mPackageDataDir.mkdirs();
        mConversationStore.loadConversationsFromDisk();
    }

    /** Called when device is shutting down. */
    void saveToDisk() {
        mConversationStore.saveConversationsToDisk();
    }

    @NonNull
    public String getPackageName() {
        return mPackageName;
Loading