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

Commit e261db3f authored by Julia Reynolds's avatar Julia Reynolds
Browse files

Add ability to write and read Notification history.

Bug: 137396965
Test: atest
Change-Id: I6bfdd0f6906dcf9c58b592e03bef335c4920d94d
parent c11e23c0
Loading
Loading
Loading
Loading
+19 −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 android.app;

parcelable NotificationHistory;
 No newline at end of file
+506 −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 android.app;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.graphics.drawable.Icon;
import android.os.Parcel;
import android.os.Parcelable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * @hide
 */
public final class NotificationHistory implements Parcelable {

    /**
     * A historical notification. Any new fields added here should also be added to
     * {@link #readNotificationFromParcel} and
     * {@link #writeNotificationToParcel(HistoricalNotification, Parcel, int)}.
     */
    public static final class HistoricalNotification {
        private String mPackage;
        private String mChannelName;
        private String mChannelId;
        private int mUid;
        private @UserIdInt int mUserId;
        private long mPostedTimeMs;
        private String mTitle;
        private String mText;
        private Icon mIcon;

        private HistoricalNotification() {}

        public String getPackage() {
            return mPackage;
        }

        public String getChannelName() {
            return mChannelName;
        }

        public String getChannelId() {
            return mChannelId;
        }

        public int getUid() {
            return mUid;
        }

        public int getUserId() {
            return mUserId;
        }

        public long getPostedTimeMs() {
            return mPostedTimeMs;
        }

        public String getTitle() {
            return mTitle;
        }

        public String getText() {
            return mText;
        }

        public Icon getIcon() {
            return mIcon;
        }

        public String getKey() {
            return mPackage + "|" + mUid + "|" + mPostedTimeMs;
        }

        @Override
        public String toString() {
            return "HistoricalNotification{" +
                    "key='" + getKey() + '\'' +
                    ", mChannelName='" + mChannelName + '\'' +
                    ", mChannelId='" + mChannelId + '\'' +
                    ", mUserId=" + mUserId +
                    ", mTitle='" + mTitle + '\'' +
                    ", mText='" + mText + '\'' +
                    ", mIcon=" + mIcon +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            HistoricalNotification that = (HistoricalNotification) o;
            boolean iconsAreSame = getIcon() == null && that.getIcon() == null
                    || (getIcon() != null && that.getIcon() != null
                    && getIcon().sameAs(that.getIcon()));
            return getUid() == that.getUid() &&
                    getUserId() == that.getUserId() &&
                    getPostedTimeMs() == that.getPostedTimeMs() &&
                    Objects.equals(getPackage(), that.getPackage()) &&
                    Objects.equals(getChannelName(), that.getChannelName()) &&
                    Objects.equals(getChannelId(), that.getChannelId()) &&
                    Objects.equals(getTitle(), that.getTitle()) &&
                    Objects.equals(getText(), that.getText()) &&
                    iconsAreSame;
        }

        @Override
        public int hashCode() {
            return Objects.hash(getPackage(), getChannelName(), getChannelId(), getUid(),
                    getUserId(),
                    getPostedTimeMs(), getTitle(), getText(), getIcon());
        }

        public static final class Builder {
            private String mPackage;
            private String mChannelName;
            private String mChannelId;
            private int mUid;
            private @UserIdInt int mUserId;
            private long mPostedTimeMs;
            private String mTitle;
            private String mText;
            private Icon mIcon;

            public Builder() {}

            public Builder setPackage(String aPackage) {
                mPackage = aPackage;
                return this;
            }

            public Builder setChannelName(String channelName) {
                mChannelName = channelName;
                return this;
            }

            public Builder setChannelId(String channelId) {
                mChannelId = channelId;
                return this;
            }

            public Builder setUid(int uid) {
                mUid = uid;
                return this;
            }

            public Builder setUserId(int userId) {
                mUserId = userId;
                return this;
            }

            public Builder setPostedTimeMs(long postedTimeMs) {
                mPostedTimeMs = postedTimeMs;
                return this;
            }

            public Builder setTitle(String title) {
                mTitle = title;
                return this;
            }

            public Builder setText(String text) {
                mText = text;
                return this;
            }

            public Builder setIcon(Icon icon) {
                mIcon = icon;
                return this;
            }

            public HistoricalNotification build() {
                HistoricalNotification n = new HistoricalNotification();
                n.mPackage = mPackage;
                n.mChannelName = mChannelName;
                n.mChannelId = mChannelId;
                n.mUid = mUid;
                n.mUserId = mUserId;
                n.mPostedTimeMs = mPostedTimeMs;
                n.mTitle = mTitle;
                n.mText = mText;
                n.mIcon = mIcon;
                return n;
            }
        }
    }

    // Only used when creating the resulting history. Not used for reading/unparceling.
    private List<HistoricalNotification> mNotificationsToWrite = new ArrayList<>();
    // ditto
    private Set<String> mStringsToWrite = new HashSet<>();

    // Mostly used for reading/unparceling events.
    private Parcel mParcel = null;
    private int mHistoryCount;
    private int mIndex = 0;

    // Sorted array of commonly used strings to shrink the size of the parcel. populated from
    // mStringsToWrite on write and the parcel on read.
    private String[] mStringPool;

    /**
     * Construct the iterator from a parcel.
     */
    private NotificationHistory(Parcel in) {
        byte[] bytes = in.readBlob();
        Parcel data = Parcel.obtain();
        data.unmarshall(bytes, 0, bytes.length);
        data.setDataPosition(0);
        mHistoryCount = data.readInt();
        mIndex = data.readInt();
        if (mHistoryCount > 0) {
            mStringPool = data.createStringArray();

            final int listByteLength = data.readInt();
            final int positionInParcel = data.readInt();
            mParcel = Parcel.obtain();
            mParcel.setDataPosition(0);
            mParcel.appendFrom(data, data.dataPosition(), listByteLength);
            mParcel.setDataSize(mParcel.dataPosition());
            mParcel.setDataPosition(positionInParcel);
        }
    }

    /**
     * Create an empty iterator.
     */
    public NotificationHistory() {
        mHistoryCount = 0;
    }

    /**
     * Returns whether or not there are more events to read using {@link #getNextNotification()}.
     *
     * @return true if there are more events, false otherwise.
     */
    public boolean hasNextNotification() {
        return mIndex < mHistoryCount;
    }

    /**
     * Retrieve the next {@link HistoricalNotification} from the collection and put the
     * resulting data into {@code notificationOut}.
     *
     * @return The next {@link HistoricalNotification} or null if there are no more notifications.
     */
    public @Nullable HistoricalNotification getNextNotification() {
        if (!hasNextNotification()) {
            return null;
        }

        HistoricalNotification n = readNotificationFromParcel(mParcel);

        mIndex++;
        if (!hasNextNotification()) {
            mParcel.recycle();
            mParcel = null;
        }
        return n;
    }

    /**
     * Adds all of the pooled strings that have been read from disk
     */
    public void addPooledStrings(@NonNull List<String> strings) {
        mStringsToWrite.addAll(strings);
    }

    /**
     * Builds the pooled strings from pending notifications. Useful if the pooled strings on
     * disk contains strings that aren't relevant to the notifications in our collection.
     */
    public void poolStringsFromNotifications() {
        mStringsToWrite.clear();
        for (int i = 0; i < mNotificationsToWrite.size(); i++) {
            final HistoricalNotification notification = mNotificationsToWrite.get(i);
            mStringsToWrite.add(notification.getPackage());
            mStringsToWrite.add(notification.getChannelName());
            mStringsToWrite.add(notification.getChannelId());
        }
    }

    /**
     * Used when populating a history from disk; adds an historical notification.
     */
    public void addNotificationToWrite(@NonNull HistoricalNotification notification) {
        if (notification == null) {
            return;
        }
        mNotificationsToWrite.add(notification);
        mHistoryCount++;
    }

    /**
     * Removes a package's historical notifications and regenerates the string pool
     */
    public void removeNotificationsFromWrite(String packageName) {
        for (int i = mNotificationsToWrite.size() - 1; i >= 0; i--) {
            if (packageName.equals(mNotificationsToWrite.get(i).getPackage())) {
                mNotificationsToWrite.remove(i);
            }
        }
        poolStringsFromNotifications();
    }

    /**
     * Gets pooled strings in order to write them to disk
     */
    public @NonNull String[] getPooledStringsToWrite() {
        String[] stringsToWrite = mStringsToWrite.toArray(new String[]{});
        Arrays.sort(stringsToWrite);
        return stringsToWrite;
    }

    /**
     * Gets the historical notifications in order to write them to disk
     */
    public @NonNull List<HistoricalNotification> getNotificationsToWrite() {
        return mNotificationsToWrite;
    }

    /**
     * Gets the number of notifications in the collection
     */
    public int getHistoryCount() {
        return mHistoryCount;
    }

    private int findStringIndex(String str) {
        final int index = Arrays.binarySearch(mStringPool, str);
        if (index < 0) {
            throw new IllegalStateException("String '" + str + "' is not in the string pool");
        }
        return index;
    }

    /**
     * Writes a single notification to the parcel. Modify this when updating member variables of
     * {@link HistoricalNotification}.
     */
    private void writeNotificationToParcel(HistoricalNotification notification, Parcel p,
            int flags) {
        final int packageIndex;
        if (notification.mPackage != null) {
            packageIndex = findStringIndex(notification.mPackage);
        } else {
            packageIndex = -1;
        }

        final int channelNameIndex;
        if (notification.getChannelName() != null) {
            channelNameIndex = findStringIndex(notification.getChannelName());
        } else {
            channelNameIndex = -1;
        }

        final int channelIdIndex;
        if (notification.getChannelId() != null) {
            channelIdIndex = findStringIndex(notification.getChannelId());
        } else {
            channelIdIndex = -1;
        }

        p.writeInt(packageIndex);
        p.writeInt(channelNameIndex);
        p.writeInt(channelIdIndex);
        p.writeInt(notification.getUid());
        p.writeInt(notification.getUserId());
        p.writeLong(notification.getPostedTimeMs());
        p.writeString(notification.getTitle());
        p.writeString(notification.getText());
        notification.getIcon().writeToParcel(p, flags);
    }

    /**
     * Reads a single notification from the parcel. Modify this when updating member variables of
     * {@link HistoricalNotification}.
     */
    private HistoricalNotification readNotificationFromParcel(Parcel p) {
        HistoricalNotification.Builder notificationOut = new HistoricalNotification.Builder();
        final int packageIndex = p.readInt();
        if (packageIndex >= 0) {
            notificationOut.mPackage = mStringPool[packageIndex];
        } else {
            notificationOut.mPackage = null;
        }

        final int channelNameIndex = p.readInt();
        if (channelNameIndex >= 0) {
            notificationOut.setChannelName(mStringPool[channelNameIndex]);
        } else {
            notificationOut.setChannelName(null);
        }

        final int channelIdIndex = p.readInt();
        if (channelIdIndex >= 0) {
            notificationOut.setChannelId(mStringPool[channelIdIndex]);
        } else {
            notificationOut.setChannelId(null);
        }

        notificationOut.setUid(p.readInt());
        notificationOut.setUserId(p.readInt());
        notificationOut.setPostedTimeMs(p.readLong());
        notificationOut.setTitle(p.readString());
        notificationOut.setText(p.readString());
        notificationOut.setIcon(Icon.CREATOR.createFromParcel(p));

        return notificationOut.build();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        Parcel data = Parcel.obtain();
        data.writeInt(mHistoryCount);
        data.writeInt(mIndex);
        if (mHistoryCount > 0) {
            mStringPool = getPooledStringsToWrite();
            data.writeStringArray(mStringPool);

            if (!mNotificationsToWrite.isEmpty()) {
                // typically system_server to a process

                // Write out the events
                Parcel p = Parcel.obtain();
                try {
                    p.setDataPosition(0);
                    for (int i = 0; i < mHistoryCount; i++) {
                        final HistoricalNotification notification = mNotificationsToWrite.get(i);
                        writeNotificationToParcel(notification, p, flags);
                    }

                    final int listByteLength = p.dataPosition();

                    // Write the total length of the data.
                    data.writeInt(listByteLength);

                    // Write our current position into the data.
                    data.writeInt(0);

                    // Write the data.
                    data.appendFrom(p, 0, listByteLength);
                } finally {
                    p.recycle();
                }

            } else if (mParcel != null) {
                // typically process to process as mNotificationsToWrite is not populated on
                // unparcel.

                // Write the total length of the data.
                data.writeInt(mParcel.dataSize());

                // Write out current position into the data.
                data.writeInt(mParcel.dataPosition());

                // Write the data.
                data.appendFrom(mParcel, 0, mParcel.dataSize());
            } else {
                throw new IllegalStateException(
                        "Either mParcel or mNotificationsToWrite must not be null");
            }
        }
        // Data can be too large for a transact. Write the data as a Blob, which will be written to
        // ashmem if too large.
        dest.writeBlob(data.marshall());
    }

    public static final @NonNull Creator<NotificationHistory> CREATOR
            = new Creator<NotificationHistory>() {
        @Override
        public NotificationHistory createFromParcel(Parcel source) {
            return new NotificationHistory(source);
        }

        @Override
        public NotificationHistory[] newArray(int size) {
            return new NotificationHistory[size];
        }
    };
}
+232 −0

File added.

Preview size limit exceeded, changes collapsed.

+300 −0

File added.

Preview size limit exceeded, changes collapsed.

+125 −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.app.NotificationHistory;
import android.app.NotificationHistory.HistoricalNotification;
import android.text.TextUtils;

import com.android.internal.util.Preconditions;

public final class NotificationHistoryFilter {
    private String mPackage;
    private String mChannel;
    private int mNotificationCount;

    private NotificationHistoryFilter() {}

    public String getPackage() {
        return mPackage;
    }

    public String getChannel() {
        return mChannel;
    }

    public int getMaxNotifications() {
        return mNotificationCount;
    }

    /**
     * Returns whether any of the filtering conditions are set
     */
    public boolean isFiltering() {
        return getPackage() != null || getChannel() != null
                || mNotificationCount < Integer.MAX_VALUE;
    }

    /**
     * Returns true if this notification passes the package and channel name filter, false
     * otherwise.
     */
    public boolean matchesPackageAndChannelFilter(HistoricalNotification notification) {
        if (!TextUtils.isEmpty(getPackage())) {
            if (!getPackage().equals(notification.getPackage())) {
                return false;
            } else {
                if (!TextUtils.isEmpty(getChannel())
                        && !getChannel().equals(notification.getChannelId())) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Returns true if the NotificationHistory can accept another notification.
     */
    public boolean matchesCountFilter(NotificationHistory notifications) {
        return notifications.getHistoryCount() < mNotificationCount;
    }

    public static final class Builder {
        private String mPackage = null;
        private String mChannel = null;
        private int mNotificationCount = Integer.MAX_VALUE;

        /**
         * Constructor
         */
        public Builder() {}

        /**
         * Sets a package name filter
         */
        public Builder setPackage(String aPackage) {
            mPackage = aPackage;
            return this;
        }

        /**
         * Sets a channel name filter. Only valid if there is also a package name filter
         */
        public Builder setChannel(String pkg, String channel) {
            setPackage(pkg);
            mChannel = channel;
            return this;
        }

        /**
         * Sets the max historical notifications
         */
        public Builder setMaxNotifications(int notificationCount) {
            mNotificationCount = notificationCount;
            return this;
        }

        /**
         * Makes a NotificationHistoryFilter
         */
        public NotificationHistoryFilter build() {
            NotificationHistoryFilter filter = new NotificationHistoryFilter();
            filter.mPackage = mPackage;
            filter.mChannel = mChannel;
            filter.mNotificationCount = mNotificationCount;
            return filter;
        }
    }
}
Loading