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

Commit 998c6ad1 authored by Trung Lam's avatar Trung Lam Committed by Android (Google) Code Review
Browse files

Merge "Add persistence of ConversationInfo during device reboot."

parents dd240c30 a127fceb
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