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

Commit 9d0b195b authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add multiuser support for notification history"

parents cc865a5d a614c279
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -311,6 +311,14 @@ public final class NotificationHistory implements Parcelable {
        mHistoryCount++;
    }

    public void addNotificationsToWrite(@NonNull NotificationHistory notificationHistory) {
        for (HistoricalNotification hn : notificationHistory.getNotificationsToWrite()) {
            // TODO: consider merging by date
            addNotificationToWrite(hn);
        }
        poolStringsFromNotifications();
    }

    /**
     * Removes a package's historical notifications and regenerates the string pool
     */
+27 −0
Original line number Diff line number Diff line
@@ -113,6 +113,33 @@ public class NotificationHistoryTest {
        assertThat(history.getHistoryCount()).isEqualTo(2);
    }

    @Test
    public void testAddNotificationsToWrite() {
        NotificationHistory history = new NotificationHistory();
        HistoricalNotification n = getHistoricalNotification(0);
        HistoricalNotification n2 = getHistoricalNotification(1);
        history.addNotificationToWrite(n2);
        history.addNotificationToWrite(n);

        NotificationHistory secondHistory = new NotificationHistory();
        HistoricalNotification n3 = getHistoricalNotification(2);
        HistoricalNotification n4 = getHistoricalNotification(3);
        secondHistory.addNotificationToWrite(n4);
        secondHistory.addNotificationToWrite(n3);

        history.addNotificationsToWrite(secondHistory);

        assertThat(history.getNotificationsToWrite().size()).isEqualTo(4);
        assertThat(history.getNotificationsToWrite().get(0)).isSameAs(n2);
        assertThat(history.getNotificationsToWrite().get(1)).isSameAs(n);
        assertThat(history.getNotificationsToWrite().get(2)).isSameAs(n4);
        assertThat(history.getNotificationsToWrite().get(3)).isSameAs(n3);
        assertThat(history.getHistoryCount()).isEqualTo(4);

        assertThat(history.getPooledStringsToWrite()).asList().contains(n2.getChannelName());
        assertThat(history.getPooledStringsToWrite()).asList().contains(n4.getPackage());
    }

    @Test
    public void testPoolStringsFromNotifications() {
        NotificationHistory history = new NotificationHistory();
+8 −9
Original line number Diff line number Diff line
@@ -75,7 +75,7 @@ public class NotificationHistoryDatabase {
    private final Context mContext;
    private final AlarmManager mAlarmManager;
    private final Object mLock = new Object();
    private Handler mFileWriteHandler;
    private final Handler mFileWriteHandler;
    @VisibleForTesting
    // List of files holding history information, sorted newest to oldest
    final LinkedList<AtomicFile> mHistoryFiles;
@@ -90,11 +90,12 @@ public class NotificationHistoryDatabase {
    @VisibleForTesting
    NotificationHistory mBuffer;

    public NotificationHistoryDatabase(Context context, File dir,
    public NotificationHistoryDatabase(Context context, Handler fileWriteHandler, File dir,
            FileAttrProvider fileAttrProvider) {
        mContext = context;
        mAlarmManager = context.getSystemService(AlarmManager.class);
        mCurrentVersion = DEFAULT_CURRENT_VERSION;
        mFileWriteHandler = fileWriteHandler;
        mVersionFile = new File(dir, "version");
        mHistoryDir = new File(dir, "history");
        mHistoryFiles = new LinkedList<>();
@@ -107,10 +108,8 @@ public class NotificationHistoryDatabase {
        mContext.registerReceiver(mFileCleaupReceiver, deletionFilter);
    }

    public void init(Handler fileWriteHandler) {
    public void init() {
        synchronized (mLock) {
            mFileWriteHandler = fileWriteHandler;

            try {
                mHistoryDir.mkdir();
                mVersionFile.createNewFile();
@@ -160,13 +159,13 @@ public class NotificationHistoryDatabase {
        }
    }

    void forceWriteToDisk() {
    public void forceWriteToDisk() {
        if (!mFileWriteHandler.hasCallbacks(mWriteBufferRunnable)) {
            mFileWriteHandler.post(mWriteBufferRunnable);
        }
    }

    void onPackageRemoved(String packageName) {
    public void onPackageRemoved(String packageName) {
        RemovePackageRunnable rpr = new RemovePackageRunnable(packageName);
        mFileWriteHandler.post(rpr);
    }
@@ -227,7 +226,7 @@ public class NotificationHistoryDatabase {
    /**
     * Remove any files that are too old and schedule jobs to clean up the rest
     */
    public void prune(final int retentionDays, final long currentTimeMillis) {
    void prune(final int retentionDays, final long currentTimeMillis) {
        synchronized (mLock) {
            GregorianCalendar retentionBoundary = new GregorianCalendar();
            retentionBoundary.setTimeInMillis(currentTimeMillis);
@@ -252,7 +251,7 @@ public class NotificationHistoryDatabase {
        }
    }

    void scheduleDeletion(File file, long deletionTime) {
    private void scheduleDeletion(File file, long deletionTime) {
        if (DEBUG) {
            Slog.d(TAG, "Scheduling deletion for " + file.getName() + " at " + deletionTime);
        }
+41 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.notification;

import android.annotation.NonNull;
import android.content.Context;
import android.os.Handler;

import java.io.File;

public class NotificationHistoryDatabaseFactory {

    private static NotificationHistoryDatabase sTestingNotificationHistoryDb;

    public static void setTestingNotificationHistoryDatabase(NotificationHistoryDatabase db) {
        sTestingNotificationHistoryDb = db;
    }

    public static NotificationHistoryDatabase create(@NonNull Context context,
            @NonNull Handler handler, @NonNull File rootDir,
            @NonNull NotificationHistoryDatabase.FileAttrProvider fileAttrProvider) {
        if(sTestingNotificationHistoryDb != null) {
            return sTestingNotificationHistoryDb;
        }
        return new NotificationHistoryDatabase(context, handler, rootDir, fileAttrProvider);
    }
}
+245 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.notification;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.NotificationHistory;
import android.app.NotificationHistory.HistoricalNotification;
import android.content.Context;
import android.os.Environment;
import android.os.UserManager;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.IoThread;
import com.android.server.notification.NotificationHistoryDatabase.NotificationHistoryFileAttrProvider;

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

/**
 * Keeps track of per-user notification histories.
 */
public class NotificationHistoryManager {
    private static final String TAG = "NotificationHistory";
    private static final boolean DEBUG = NotificationManagerService.DBG;

    @VisibleForTesting
    static final String DIRECTORY_PER_USER = "notification_history";

    private final Context mContext;
    private final UserManager mUserManager;
    private final Object mLock = new Object();
    @GuardedBy("mLock")
    private final SparseArray<NotificationHistoryDatabase> mUserState = new SparseArray<>();
    @GuardedBy("mLock")
    private final SparseBooleanArray mUserUnlockedStates = new SparseBooleanArray();
    // TODO: does this need to be persisted across reboots?
    @GuardedBy("mLock")
    private final SparseArray<List<String>> mUserPendingPackageRemovals = new SparseArray<>();

    public NotificationHistoryManager(Context context) {
        mContext = context;
        mUserManager = context.getSystemService(UserManager.class);
    }

    public void onUserUnlocked(@UserIdInt int userId) {
        synchronized (mLock) {
            mUserUnlockedStates.put(userId, true);
            final NotificationHistoryDatabase userHistory =
                    getUserHistoryAndInitializeIfNeededLocked(userId);
            if (userHistory == null) {
                Slog.i(TAG, "Attempted to unlock stopped or removed user " + userId);
                return;
            }

            // remove any packages that were deleted while the user was locked
            final List<String> pendingPackageRemovals = mUserPendingPackageRemovals.get(userId);
            if (pendingPackageRemovals != null) {
                for (int i = 0; i < pendingPackageRemovals.size(); i++) {
                    userHistory.onPackageRemoved(pendingPackageRemovals.get(i));
                }
                mUserPendingPackageRemovals.put(userId, null);
            }
        }
    }

    public void onUserStopped(@UserIdInt int userId) {
        synchronized (mLock) {
            mUserUnlockedStates.put(userId, false);
            mUserState.put(userId, null); // release the service (mainly for GC)
        }
    }

    void onUserRemoved(@UserIdInt int userId) {
        synchronized (mLock) {
            // Actual data deletion is handled by other parts of the system (the entire directory is
            // removed) - we just need clean up our internal state for GC
            mUserPendingPackageRemovals.put(userId, null);
            onUserStopped(userId);
        }
    }

    void onPackageRemoved(int userId, String packageName) {
        synchronized (mLock) {
            if (!mUserUnlockedStates.get(userId, false)) {
                List<String> userPendingRemovals =
                        mUserPendingPackageRemovals.get(userId, new ArrayList<>());
                userPendingRemovals.add(packageName);
                mUserPendingPackageRemovals.put(userId, userPendingRemovals);
                return;
            }
            final NotificationHistoryDatabase userHistory = mUserState.get(userId);
            if (userHistory == null) {
                return;
            }

            userHistory.onPackageRemoved(packageName);
        }
    }

    void triggerWriteToDisk() {
        synchronized (mLock) {
            final int userCount = mUserState.size();
            for (int i = 0; i < userCount; i++) {
                final int userId = mUserState.keyAt(i);
                if (!mUserUnlockedStates.get(userId)) {
                    continue;
                }
                NotificationHistoryDatabase userHistory = mUserState.get(userId);
                if (userHistory != null) {
                    userHistory.forceWriteToDisk();
                }
            }
        }
    }

    public void addNotification(@NonNull final HistoricalNotification notification) {
        synchronized (mLock) {
            final NotificationHistoryDatabase userHistory =
                    getUserHistoryAndInitializeIfNeededLocked(notification.getUserId());
            if (userHistory == null) {
                Slog.w(TAG, "Attempted to add notif for locked/gone user "
                        + notification.getUserId());
                return;
            }
            userHistory.addNotification(notification);
        }
    }

    public @NonNull NotificationHistory readNotificationHistory(@UserIdInt int[] userIds) {
        synchronized (mLock) {
            NotificationHistory mergedHistory = new NotificationHistory();
            if (userIds == null) {
                return mergedHistory;
            }
            for (int userId : userIds) {
                final NotificationHistoryDatabase userHistory =
                        getUserHistoryAndInitializeIfNeededLocked(userId);
                if (userHistory == null) {
                    Slog.i(TAG, "Attempted to read history for locked/gone user " +userId);
                    continue;
                }
                mergedHistory.addNotificationsToWrite(userHistory.readNotificationHistory());
            }
            return mergedHistory;
        }
    }

    public @NonNull android.app.NotificationHistory readFilteredNotificationHistory(
            @UserIdInt int userId, String packageName, String channelId, int maxNotifications) {
        synchronized (mLock) {
            final NotificationHistoryDatabase userHistory =
                    getUserHistoryAndInitializeIfNeededLocked(userId);
            if (userHistory == null) {
                Slog.i(TAG, "Attempted to read history for locked/gone user " +userId);
                return new android.app.NotificationHistory();
            }

            return userHistory.readNotificationHistory(packageName, channelId, maxNotifications);
        }
    }

    @GuardedBy("mLock")
    private @Nullable NotificationHistoryDatabase getUserHistoryAndInitializeIfNeededLocked(
            int userId) {
        NotificationHistoryDatabase userHistory = mUserState.get(userId);
        if (userHistory == null) {
            final File historyDir = new File(Environment.getDataSystemCeDirectory(userId),
                    DIRECTORY_PER_USER);
            userHistory = NotificationHistoryDatabaseFactory.create(mContext, IoThread.getHandler(),
                    historyDir, new NotificationHistoryFileAttrProvider());
            if (mUserUnlockedStates.get(userId)) {
                try {
                    userHistory.init();
                } catch (Exception e) {
                    if (mUserManager.isUserUnlocked(userId)) {
                        throw e; // rethrow exception - user is unlocked
                    } else {
                        Slog.w(TAG, "Attempted to initialize service for "
                                + "stopped or removed user " + userId);
                        return null;
                    }
                }
            } else {
                // locked! data unavailable
                Slog.w(TAG, "Attempted to initialize service for "
                        + "stopped or removed user " + userId);
                return null;
            }
            mUserState.put(userId, userHistory);
        }
        return userHistory;
    }

    @VisibleForTesting
    boolean isUserUnlocked(@UserIdInt int userId) {
        synchronized (mLock) {
            return mUserUnlockedStates.get(userId);
        }
    }

    @VisibleForTesting
    boolean doesHistoryExistForUser(@UserIdInt int userId) {
        synchronized (mLock) {
            return mUserState.get(userId) != null;
        }
    }

    @VisibleForTesting
    void replaceNotificationHistoryDatabase(@UserIdInt int userId,
            NotificationHistoryDatabase replacement) {
        synchronized (mLock) {
            if (mUserState.get(userId) != null) {
                mUserState.put(userId, replacement);
            }
        }
    }

    @VisibleForTesting
    List<String> getPendingPackageRemovalsForUser(@UserIdInt int userId) {
        synchronized (mLock) {
            return mUserPendingPackageRemovals.get(userId);
        }
    }
}
Loading