Julia Reynolds | e261db3 | 2019-10-21 11:37:35 -0400 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2019 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | package com.android.server.notification; |
| 17 | |
| 18 | import android.app.NotificationHistory; |
| 19 | import android.app.NotificationHistory.HistoricalNotification; |
| 20 | import android.content.res.Resources; |
| 21 | import android.graphics.drawable.Icon; |
| 22 | import android.util.Slog; |
| 23 | import android.util.proto.ProtoInputStream; |
| 24 | import android.util.proto.ProtoOutputStream; |
| 25 | |
| 26 | import com.android.server.notification.NotificationHistoryProto.Notification; |
| 27 | |
| 28 | import java.io.IOException; |
| 29 | import java.io.InputStream; |
| 30 | import java.io.OutputStream; |
| 31 | import java.util.ArrayList; |
| 32 | import java.util.Arrays; |
| 33 | import java.util.List; |
| 34 | |
| 35 | /** |
| 36 | * Notification history reader/writer for Protocol Buffer format |
| 37 | */ |
| 38 | final class NotificationHistoryProtoHelper { |
| 39 | private static final String TAG = "NotifHistoryProto"; |
| 40 | |
| 41 | // Static-only utility class. |
| 42 | private NotificationHistoryProtoHelper() {} |
| 43 | |
| 44 | private static List<String> readStringPool(ProtoInputStream proto) throws IOException { |
| 45 | final long token = proto.start(NotificationHistoryProto.STRING_POOL); |
| 46 | List<String> stringPool; |
| 47 | if (proto.nextField(NotificationHistoryProto.StringPool.SIZE)) { |
| 48 | stringPool = new ArrayList(proto.readInt(NotificationHistoryProto.StringPool.SIZE)); |
| 49 | } else { |
| 50 | stringPool = new ArrayList(); |
| 51 | } |
| 52 | while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) { |
| 53 | switch (proto.getFieldNumber()) { |
| 54 | case (int) NotificationHistoryProto.StringPool.STRINGS: |
| 55 | stringPool.add(proto.readString(NotificationHistoryProto.StringPool.STRINGS)); |
| 56 | break; |
| 57 | } |
| 58 | } |
| 59 | proto.end(token); |
| 60 | return stringPool; |
| 61 | } |
| 62 | |
| 63 | private static void writeStringPool(ProtoOutputStream proto, |
| 64 | final NotificationHistory notifications) { |
| 65 | final long token = proto.start(NotificationHistoryProto.STRING_POOL); |
| 66 | final String[] pooledStrings = notifications.getPooledStringsToWrite(); |
| 67 | proto.write(NotificationHistoryProto.StringPool.SIZE, pooledStrings.length); |
| 68 | for (int i = 0; i < pooledStrings.length; i++) { |
| 69 | proto.write(NotificationHistoryProto.StringPool.STRINGS, pooledStrings[i]); |
| 70 | } |
| 71 | proto.end(token); |
| 72 | } |
| 73 | |
| 74 | private static void readNotification(ProtoInputStream proto, List<String> stringPool, |
| 75 | NotificationHistory notifications, NotificationHistoryFilter filter) |
| 76 | throws IOException { |
| 77 | final long token = proto.start(NotificationHistoryProto.NOTIFICATION); |
| 78 | try { |
| 79 | HistoricalNotification notification = readNotification(proto, stringPool); |
| 80 | if (filter.matchesPackageAndChannelFilter(notification) |
| 81 | && filter.matchesCountFilter(notifications)) { |
| 82 | notifications.addNotificationToWrite(notification); |
| 83 | } |
| 84 | } catch (Exception e) { |
| 85 | Slog.e(TAG, "Error reading notification", e); |
| 86 | } finally { |
| 87 | proto.end(token); |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | private static HistoricalNotification readNotification(ProtoInputStream parser, |
| 92 | List<String> stringPool) throws IOException { |
| 93 | final HistoricalNotification.Builder notification = new HistoricalNotification.Builder(); |
| 94 | String pkg = null; |
| 95 | while (true) { |
| 96 | switch (parser.nextField()) { |
| 97 | case (int) NotificationHistoryProto.Notification.PACKAGE: |
| 98 | pkg = parser.readString(Notification.PACKAGE); |
| 99 | notification.setPackage(pkg); |
| 100 | stringPool.add(pkg); |
| 101 | break; |
| 102 | case (int) Notification.PACKAGE_INDEX: |
| 103 | pkg = stringPool.get(parser.readInt(Notification.PACKAGE_INDEX) - 1); |
| 104 | notification.setPackage(pkg); |
| 105 | break; |
| 106 | case (int) Notification.CHANNEL_NAME: |
| 107 | String channelName = parser.readString(Notification.CHANNEL_NAME); |
| 108 | notification.setChannelName(channelName); |
| 109 | stringPool.add(channelName); |
| 110 | break; |
| 111 | case (int) Notification.CHANNEL_NAME_INDEX: |
| 112 | notification.setChannelName(stringPool.get(parser.readInt( |
| 113 | Notification.CHANNEL_NAME_INDEX) - 1)); |
| 114 | break; |
| 115 | case (int) Notification.CHANNEL_ID: |
| 116 | String channelId = parser.readString(Notification.CHANNEL_ID); |
| 117 | notification.setChannelId(channelId); |
| 118 | stringPool.add(channelId); |
| 119 | break; |
| 120 | case (int) Notification.CHANNEL_ID_INDEX: |
| 121 | notification.setChannelId(stringPool.get(parser.readInt( |
| 122 | Notification.CHANNEL_ID_INDEX) - 1)); |
| 123 | break; |
| 124 | case (int) Notification.UID: |
| 125 | notification.setUid(parser.readInt(Notification.UID)); |
| 126 | break; |
| 127 | case (int) Notification.USER_ID: |
| 128 | notification.setUserId(parser.readInt(Notification.USER_ID)); |
| 129 | break; |
| 130 | case (int) Notification.POSTED_TIME_MS: |
| 131 | notification.setPostedTimeMs(parser.readLong(Notification.POSTED_TIME_MS)); |
| 132 | break; |
| 133 | case (int) Notification.TITLE: |
| 134 | notification.setTitle(parser.readString(Notification.TITLE)); |
| 135 | break; |
| 136 | case (int) Notification.TEXT: |
| 137 | notification.setText(parser.readString(Notification.TEXT)); |
| 138 | break; |
| 139 | case (int) Notification.ICON: |
| 140 | final long iconToken = parser.start(Notification.ICON); |
| 141 | loadIcon(parser, notification, pkg); |
| 142 | parser.end(iconToken); |
| 143 | break; |
| 144 | case ProtoInputStream.NO_MORE_FIELDS: |
| 145 | return notification.build(); |
| 146 | } |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | private static void loadIcon(ProtoInputStream parser, |
| 151 | HistoricalNotification.Builder notification, String pkg) throws IOException { |
| 152 | int iconType = Notification.TYPE_UNKNOWN; |
| 153 | String imageBitmapFileName = null; |
| 154 | int imageResourceId = Resources.ID_NULL; |
| 155 | String imageResourceIdPackage = null; |
| 156 | byte[] imageByteData = null; |
| 157 | int imageByteDataLength = 0; |
| 158 | int imageByteDataOffset = 0; |
| 159 | String imageUri = null; |
| 160 | |
| 161 | while (true) { |
| 162 | switch (parser.nextField()) { |
| 163 | case (int) Notification.Icon.IMAGE_TYPE: |
| 164 | iconType = parser.readInt(Notification.Icon.IMAGE_TYPE); |
| 165 | break; |
| 166 | case (int) Notification.Icon.IMAGE_DATA: |
| 167 | imageByteData = parser.readBytes(Notification.Icon.IMAGE_DATA); |
| 168 | break; |
| 169 | case (int) Notification.Icon.IMAGE_DATA_LENGTH: |
| 170 | imageByteDataLength = parser.readInt(Notification.Icon.IMAGE_DATA_LENGTH); |
| 171 | break; |
| 172 | case (int) Notification.Icon.IMAGE_DATA_OFFSET: |
| 173 | imageByteDataOffset = parser.readInt(Notification.Icon.IMAGE_DATA_OFFSET); |
| 174 | break; |
| 175 | case (int) Notification.Icon.IMAGE_BITMAP_FILENAME: |
| 176 | imageBitmapFileName = parser.readString( |
| 177 | Notification.Icon.IMAGE_BITMAP_FILENAME); |
| 178 | break; |
| 179 | case (int) Notification.Icon.IMAGE_RESOURCE_ID: |
| 180 | imageResourceId = parser.readInt(Notification.Icon.IMAGE_RESOURCE_ID); |
| 181 | break; |
| 182 | case (int) Notification.Icon.IMAGE_RESOURCE_ID_PACKAGE: |
| 183 | imageResourceIdPackage = parser.readString( |
| 184 | Notification.Icon.IMAGE_RESOURCE_ID_PACKAGE); |
| 185 | break; |
| 186 | case (int) Notification.Icon.IMAGE_URI: |
| 187 | imageUri = parser.readString(Notification.Icon.IMAGE_URI); |
| 188 | break; |
| 189 | case ProtoInputStream.NO_MORE_FIELDS: |
| 190 | if (iconType == Icon.TYPE_DATA) { |
| 191 | |
| 192 | if (imageByteData != null) { |
| 193 | notification.setIcon(Icon.createWithData( |
| 194 | imageByteData, imageByteDataOffset, imageByteDataLength)); |
| 195 | } |
| 196 | } else if (iconType == Icon.TYPE_RESOURCE) { |
| 197 | if (imageResourceId != Resources.ID_NULL) { |
| 198 | notification.setIcon(Icon.createWithResource( |
| 199 | imageResourceIdPackage != null |
| 200 | ? imageResourceIdPackage |
| 201 | : pkg, |
| 202 | imageResourceId)); |
| 203 | } |
| 204 | } else if (iconType == Icon.TYPE_URI) { |
| 205 | if (imageUri != null) { |
| 206 | notification.setIcon(Icon.createWithContentUri(imageUri)); |
| 207 | } |
| 208 | } else if (iconType == Icon.TYPE_BITMAP) { |
| 209 | // TODO: read file from disk |
| 210 | } |
| 211 | return; |
| 212 | } |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | private static void writeIcon(ProtoOutputStream proto, HistoricalNotification notification) { |
| 217 | final long token = proto.start(Notification.ICON); |
| 218 | |
| 219 | proto.write(Notification.Icon.IMAGE_TYPE, notification.getIcon().getType()); |
| 220 | switch (notification.getIcon().getType()) { |
| 221 | case Icon.TYPE_DATA: |
| 222 | proto.write(Notification.Icon.IMAGE_DATA, notification.getIcon().getDataBytes()); |
| 223 | proto.write(Notification.Icon.IMAGE_DATA_LENGTH, |
| 224 | notification.getIcon().getDataLength()); |
| 225 | proto.write(Notification.Icon.IMAGE_DATA_OFFSET, |
| 226 | notification.getIcon().getDataOffset()); |
| 227 | break; |
| 228 | case Icon.TYPE_RESOURCE: |
| 229 | proto.write(Notification.Icon.IMAGE_RESOURCE_ID, notification.getIcon().getResId()); |
| 230 | if (!notification.getPackage().equals(notification.getIcon().getResPackage())) { |
| 231 | proto.write(Notification.Icon.IMAGE_RESOURCE_ID_PACKAGE, |
| 232 | notification.getIcon().getResPackage()); |
| 233 | } |
| 234 | break; |
| 235 | case Icon.TYPE_URI: |
| 236 | proto.write(Notification.Icon.IMAGE_URI, notification.getIcon().getUriString()); |
| 237 | break; |
| 238 | case Icon.TYPE_BITMAP: |
| 239 | // TODO: write file to disk |
| 240 | break; |
| 241 | } |
| 242 | |
| 243 | proto.end(token); |
| 244 | } |
| 245 | |
| 246 | private static void writeNotification(ProtoOutputStream proto, |
| 247 | final String[] stringPool, final HistoricalNotification notification) { |
| 248 | final long token = proto.start(NotificationHistoryProto.NOTIFICATION); |
| 249 | final int packageIndex = Arrays.binarySearch(stringPool, notification.getPackage()); |
| 250 | if (packageIndex >= 0) { |
| 251 | proto.write(Notification.PACKAGE_INDEX, packageIndex + 1); |
| 252 | } else { |
| 253 | // Package not in Stringpool for some reason, write full string instead |
| 254 | Slog.w(TAG, "notification package name (" + notification.getPackage() |
| 255 | + ") not found in string cache"); |
| 256 | proto.write(Notification.PACKAGE, notification.getPackage()); |
| 257 | } |
| 258 | final int channelNameIndex = Arrays.binarySearch(stringPool, notification.getChannelName()); |
| 259 | if (channelNameIndex >= 0) { |
| 260 | proto.write(Notification.CHANNEL_NAME_INDEX, channelNameIndex + 1); |
| 261 | } else { |
| 262 | Slog.w(TAG, "notification channel name (" + notification.getChannelName() |
| 263 | + ") not found in string cache"); |
| 264 | proto.write(Notification.CHANNEL_NAME, notification.getChannelName()); |
| 265 | } |
| 266 | final int channelIdIndex = Arrays.binarySearch(stringPool, notification.getChannelId()); |
| 267 | if (channelIdIndex >= 0) { |
| 268 | proto.write(Notification.CHANNEL_ID_INDEX, channelIdIndex + 1); |
| 269 | } else { |
| 270 | Slog.w(TAG, "notification channel id (" + notification.getChannelId() |
| 271 | + ") not found in string cache"); |
| 272 | proto.write(Notification.CHANNEL_ID, notification.getChannelId()); |
| 273 | } |
| 274 | proto.write(Notification.UID, notification.getUid()); |
| 275 | proto.write(Notification.USER_ID, notification.getUserId()); |
| 276 | proto.write(Notification.POSTED_TIME_MS, notification.getPostedTimeMs()); |
| 277 | proto.write(Notification.TITLE, notification.getTitle()); |
| 278 | proto.write(Notification.TEXT, notification.getText()); |
| 279 | writeIcon(proto, notification); |
| 280 | proto.end(token); |
| 281 | } |
| 282 | |
| 283 | public static void read(InputStream in, NotificationHistory notifications, |
| 284 | NotificationHistoryFilter filter) throws IOException { |
| 285 | final ProtoInputStream proto = new ProtoInputStream(in); |
| 286 | List<String> stringPool = new ArrayList<>(); |
| 287 | while (true) { |
| 288 | switch (proto.nextField()) { |
| 289 | case (int) NotificationHistoryProto.STRING_POOL: |
| 290 | stringPool = readStringPool(proto); |
| 291 | break; |
| 292 | case (int) NotificationHistoryProto.NOTIFICATION: |
| 293 | readNotification(proto, stringPool, notifications, filter); |
| 294 | break; |
| 295 | case ProtoInputStream.NO_MORE_FIELDS: |
| 296 | if (filter.isFiltering()) { |
| 297 | notifications.poolStringsFromNotifications(); |
| 298 | } else { |
| 299 | notifications.addPooledStrings(stringPool); |
| 300 | } |
| 301 | return; |
| 302 | } |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | public static void write(OutputStream out, NotificationHistory notifications, int version) { |
| 307 | final ProtoOutputStream proto = new ProtoOutputStream(out); |
| 308 | proto.write(NotificationHistoryProto.MAJOR_VERSION, version); |
| 309 | // String pool should be written before the history itself |
| 310 | writeStringPool(proto, notifications); |
| 311 | |
| 312 | List<HistoricalNotification> notificationsToWrite = notifications.getNotificationsToWrite(); |
| 313 | final int count = notificationsToWrite.size(); |
| 314 | for (int i = 0; i < count; i++) { |
| 315 | writeNotification(proto, notifications.getPooledStringsToWrite(), |
| 316 | notificationsToWrite.get(i)); |
| 317 | } |
| 318 | |
| 319 | proto.flush(); |
| 320 | } |
| 321 | } |