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