blob: 7ba993b6a16e495f7be1d823728a0ac31afa7bce [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;
Julia Reynolds30965c42020-02-14 10:21:28 -050022import android.text.TextUtils;
Julia Reynoldse261db32019-10-21 11:37:35 -040023import android.util.Slog;
24import android.util.proto.ProtoInputStream;
25import android.util.proto.ProtoOutputStream;
26
27import com.android.server.notification.NotificationHistoryProto.Notification;
28
29import java.io.IOException;
30import java.io.InputStream;
31import java.io.OutputStream;
32import java.util.ArrayList;
33import java.util.Arrays;
34import java.util.List;
35
36/**
37 * Notification history reader/writer for Protocol Buffer format
38 */
39final 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 Reynolds30965c42020-02-14 10:21:28 -0500145 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 Reynoldse261db32019-10-21 11:37:35 -0400155 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 Reynolds30965c42020-02-14 10:21:28 -0500285 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 Reynoldse261db32019-10-21 11:37:35 -0400296 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}