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

Commit 98546a0e authored by Julia Reynolds's avatar Julia Reynolds
Browse files

Clean up history files via job vs multiple alarms

Test: services/tests/uiservicestests/src/com/android/server/notification
Test: monitor history files over 24 hours
Fixes: 237039804
Change-Id: Ied5db4a63b366b5bfc33b415f041d06a369282ad
parent eec7bcb1
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -7061,6 +7061,10 @@
                 android:permission="android.permission.BIND_JOB_SERVICE">
        </service>

        <service android:name="com.android.server.notification.NotificationHistoryJobService"
                 android:permission="android.permission.BIND_JOB_SERVICE" >
        </service>

        <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader"
            android:exported="false">
            <intent-filter>
+12 −85
Original line number Diff line number Diff line
@@ -16,15 +16,8 @@

package com.android.server.notification;

import android.app.AlarmManager;
import android.app.NotificationHistory;
import android.app.NotificationHistory.HistoricalNotification;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Handler;
import android.util.AtomicFile;
import android.util.Slog;
@@ -60,18 +53,9 @@ public class NotificationHistoryDatabase {
    private static final String TAG = "NotiHistoryDatabase";
    private static final boolean DEBUG = NotificationManagerService.DBG;
    private static final int HISTORY_RETENTION_DAYS = 1;
    private static final int HISTORY_RETENTION_MS = 24 * 60 * 60 * 1000;
    private static final long WRITE_BUFFER_INTERVAL_MS = 1000 * 60 * 20;
    private static final long INVALID_FILE_TIME_MS = -1;

    private static final String ACTION_HISTORY_DELETION =
            NotificationHistoryDatabase.class.getSimpleName() + ".CLEANUP";
    private static final int REQUEST_CODE_DELETION = 1;
    private static final String SCHEME_DELETION = "delete";
    private static final String EXTRA_KEY = "key";

    private final Context mContext;
    private final AlarmManager mAlarmManager;
    private final Object mLock = new Object();
    private final Handler mFileWriteHandler;
    @VisibleForTesting
@@ -87,9 +71,7 @@ public class NotificationHistoryDatabase {
    @VisibleForTesting
    NotificationHistory mBuffer;

    public NotificationHistoryDatabase(Context context, Handler fileWriteHandler, File dir) {
        mContext = context;
        mAlarmManager = context.getSystemService(AlarmManager.class);
    public NotificationHistoryDatabase(Handler fileWriteHandler, File dir) {
        mCurrentVersion = DEFAULT_CURRENT_VERSION;
        mFileWriteHandler = fileWriteHandler;
        mVersionFile = new File(dir, "version");
@@ -97,11 +79,6 @@ public class NotificationHistoryDatabase {
        mHistoryFiles = new LinkedList<>();
        mBuffer = new NotificationHistory();
        mWriteBufferRunnable = new WriteBufferRunnable();

        IntentFilter deletionFilter = new IntentFilter(ACTION_HISTORY_DELETION);
        deletionFilter.addDataScheme(SCHEME_DELETION);
        mContext.registerReceiver(mFileCleanupReceiver, deletionFilter,
                Context.RECEIVER_EXPORTED_UNAUDITED);
    }

    public void init() {
@@ -117,7 +94,7 @@ public class NotificationHistoryDatabase {

            checkVersionAndBuildLocked();
            indexFilesLocked();
            prune(HISTORY_RETENTION_DAYS, System.currentTimeMillis());
            prune();
        }
    }

@@ -246,7 +223,14 @@ public class NotificationHistoryDatabase {
    }

    /**
     * Remove any files that are too old and schedule jobs to clean up the rest
     * Remove any files that are too old.
     */
    void prune() {
        prune(HISTORY_RETENTION_DAYS, System.currentTimeMillis());
    }

    /**
     * Remove any files that are too old.
     */
    void prune(final int retentionDays, final long currentTimeMillis) {
        synchronized (mLock) {
@@ -265,10 +249,6 @@ public class NotificationHistoryDatabase {

                if (creationTime <= retentionBoundary.getTimeInMillis()) {
                    deleteFile(currentOldestFile);
                } else {
                    // all remaining files are newer than the cut off; schedule jobs to delete
                    scheduleDeletion(
                            currentOldestFile.getBaseFile(), creationTime, retentionDays);
                }
            }
        }
@@ -306,26 +286,6 @@ public class NotificationHistoryDatabase {
        removeFilePathFromHistory(file.getBaseFile().getAbsolutePath());
    }

    private void scheduleDeletion(File file, long creationTime, int retentionDays) {
        final long deletionTime = creationTime + (retentionDays * HISTORY_RETENTION_MS);
        scheduleDeletion(file, deletionTime);
    }

    private void scheduleDeletion(File file, long deletionTime) {
        if (DEBUG) {
            Slog.d(TAG, "Scheduling deletion for " + file.getName() + " at " + deletionTime);
        }
        final PendingIntent pi = PendingIntent.getBroadcast(mContext,
                REQUEST_CODE_DELETION,
                new Intent(ACTION_HISTORY_DELETION)
                        .setData(new Uri.Builder().scheme(SCHEME_DELETION)
                                .appendPath(file.getAbsolutePath()).build())
                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
                        .putExtra(EXTRA_KEY, file.getAbsolutePath()),
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
        mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, deletionTime, pi);
    }

    private void writeLocked(AtomicFile file, NotificationHistory notifications)
            throws IOException {
        FileOutputStream fos = file.startWrite();
@@ -355,12 +315,6 @@ public class NotificationHistoryDatabase {
        }
    }

    public void unregisterFileCleanupReceiver() {
        if(mContext != null) {
            mContext.unregisterReceiver(mFileCleanupReceiver);
        }
    }

    private static long safeParseLong(String fileName) {
        // AtomicFile will create copies of the numeric files with ".new" and ".bak"
        // over the course of its processing. If these files still exist on boot we need to clean
@@ -372,40 +326,15 @@ public class NotificationHistoryDatabase {
        }
    }

    private final BroadcastReceiver mFileCleanupReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action == null) {
                return;
            }
            if (ACTION_HISTORY_DELETION.equals(action)) {
                try {
                    synchronized (mLock) {
                        final String filePath = intent.getStringExtra(EXTRA_KEY);
                        AtomicFile fileToDelete = new AtomicFile(new File(filePath));
                        if (DEBUG) {
                            Slog.d(TAG, "Removed " + fileToDelete.getBaseFile().getName());
                        }
                        fileToDelete.delete();
                        removeFilePathFromHistory(filePath);
                    }
                } catch (Exception e) {
                    Slog.e(TAG, "Failed to delete notification history file", e);
                }
            }
        }
    };

    final class WriteBufferRunnable implements Runnable {

        @Override
        public void run() {
            long time = System.currentTimeMillis();
            run(time, new AtomicFile(new File(mHistoryDir, String.valueOf(time))));
            run(new AtomicFile(new File(mHistoryDir, String.valueOf(time))));
        }

        void run(long time, AtomicFile file) {
        void run(AtomicFile file) {
            synchronized (mLock) {
                if (DEBUG) Slog.d(TAG, "WriteBufferRunnable "
                        + file.getBaseFile().getAbsolutePath());
@@ -413,8 +342,6 @@ public class NotificationHistoryDatabase {
                    writeLocked(file, mBuffer);
                    mHistoryFiles.addFirst(file);
                    mBuffer = new NotificationHistory();

                    scheduleDeletion(file.getBaseFile(), time, HISTORY_RETENTION_DAYS);
                } catch (IOException e) {
                    Slog.e(TAG, "Failed to write buffer to disk. not flushing buffer", e);
                }
+1 −1
Original line number Diff line number Diff line
@@ -35,6 +35,6 @@ public class NotificationHistoryDatabaseFactory {
        if(sTestingNotificationHistoryDb != null) {
            return sTestingNotificationHistoryDb;
        }
        return new NotificationHistoryDatabase(context, handler, rootDir);
        return new NotificationHistoryDatabase(handler, rootDir);
    }
}
+81 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 static android.app.job.JobScheduler.RESULT_SUCCESS;

import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.os.CancellationSignal;
import android.util.Slog;

import com.android.server.LocalServices;

import java.util.concurrent.TimeUnit;

/**
 * This service runs every twenty minutes to ensure the retention policy for notification history
 * data.
 */
public class NotificationHistoryJobService extends JobService {
    private final static String TAG = "NotificationHistoryJob";
    private static final long JOB_RUN_INTERVAL = TimeUnit.MINUTES.toMillis(20);

    static final int BASE_JOB_ID = 237039804;

    static void scheduleJob(Context context) {
        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
        if (jobScheduler.getPendingJob(BASE_JOB_ID) == null) {
            ComponentName component =
                    new ComponentName(context, NotificationHistoryJobService.class);
            JobInfo newJob = new JobInfo.Builder(BASE_JOB_ID, component)
                    .setRequiresDeviceIdle(false)
                    .setPeriodic(JOB_RUN_INTERVAL)
                    .build();
            if (jobScheduler.schedule(newJob) != RESULT_SUCCESS) {
                Slog.w(TAG, "Failed to schedule history cleanup job");
            }
        }
    }

    private CancellationSignal mSignal;

    @Override
    public boolean onStartJob(JobParameters params) {
        mSignal = new CancellationSignal();
        new Thread(() -> {
            NotificationManagerInternal nmInternal =
                    LocalServices.getService(NotificationManagerInternal.class);
            nmInternal.cleanupHistoryFiles();
            jobFinished(params, mSignal.isCanceled());
        }).start();
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        if (mSignal != null) {
            mSignal.cancel();
        }
        return false;
    }
}
+24 −1
Original line number Diff line number Diff line
@@ -84,6 +84,12 @@ public class NotificationHistoryManager {
    }

    void onBootPhaseAppsCanStart() {
        try {
            Slog.d("julia", "trying to schedule job");
            NotificationHistoryJobService.scheduleJob(mContext);
        } catch (Throwable e) {
            Slog.e(TAG, "Failed to schedule cleanup job", e);
        }
        mSettingsObserver.observe();
    }

@@ -151,6 +157,24 @@ public class NotificationHistoryManager {
        }
    }

    public void cleanupHistoryFiles() {
        synchronized (mLock) {
            int n = mUserUnlockedStates.size();
            for (int i = 0;  i < n; i++) {
                // cleanup old files for currently unlocked users. User are additionally cleaned
                // on unlock in NotificationHistoryDatabase.init().
                if (mUserUnlockedStates.valueAt(i)) {
                    final NotificationHistoryDatabase userHistory =
                            mUserState.get(mUserUnlockedStates.keyAt(i));
                    if (userHistory == null) {
                        continue;
                    }
                    userHistory.prune();
                }
            }
        }
    }

    public void deleteNotificationHistoryItem(String pkg, int uid, long postedTime) {
        synchronized (mLock) {
            int userId = UserHandle.getUserId(uid);
@@ -288,7 +312,6 @@ public class NotificationHistoryManager {

    private void disableHistory(NotificationHistoryDatabase userHistory, @UserIdInt int userId) {
        userHistory.disableHistory();
        userHistory.unregisterFileCleanupReceiver();

        mUserPendingHistoryDisables.put(userId, false);
        mHistoryEnabled.put(userId, false);
Loading