blob: 2831d37ed70b28b0a016aafb89c2ef8dbbd5a833 [file] [log] [blame]
Julia Reynoldse261db32019-10-21 11:37:35 -04001/*
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 */
16package com.android.server.notification;
17
18import android.app.NotificationHistory;
19import android.app.NotificationHistory.HistoricalNotification;
20import android.content.res.Resources;
21import android.graphics.drawable.Icon;
22import android.util.Slog;
23import android.util.proto.ProtoInputStream;
24import android.util.proto.ProtoOutputStream;
25
26import com.android.server.notification.NotificationHistoryProto.Notification;
27
28import java.io.IOException;
29import java.io.InputStream;
30import java.io.OutputStream;
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.List;
34
35/**
36 * Notification history reader/writer for Protocol Buffer format
37 */
38final 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}