diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto index d7aedabd5e14c..993db511d5da6 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -325,7 +325,7 @@ message Atom { } // Pulled events will start at field 10000. - // Next: 10062 + // Next: 10067 oneof pulled { WifiBytesTransfer wifi_bytes_transfer = 10000; WifiBytesTransferByFgBg wifi_bytes_transfer_by_fg_bg = 10001; @@ -390,6 +390,7 @@ message Atom { AppOps app_ops = 10060; ProcessSystemIonHeapSize process_system_ion_heap_size = 10061; VmsClientStats vms_client_stats = 10065; + NotificationRemoteViews notification_remote_views = 10066; } // DO NOT USE field numbers above 100,000 in AOSP. @@ -4751,6 +4752,24 @@ message ProcStatsPkgProc { optional ProcessStatsSectionProto proc_stats_section = 1; } +// Next Tag: 2 +message PackageRemoteViewInfoProto { + optional string package_name = 1; + // add per-package additional info here (like channels) +} + +// Next Tag: 2 +message NotificationRemoteViewsProto { + repeated PackageRemoteViewInfoProto package_remote_view_info = 1; +} + +/** + * Pulled from NotificationManagerService.java + */ +message NotificationRemoteViews { + optional NotificationRemoteViewsProto notification_remote_views = 1; +} + message PowerProfileProto { optional double cpu_suspend = 1; diff --git a/cmds/statsd/src/external/StatsPullerManager.cpp b/cmds/statsd/src/external/StatsPullerManager.cpp index f43025046be8b..893378cd96866 100644 --- a/cmds/statsd/src/external/StatsPullerManager.cpp +++ b/cmds/statsd/src/external/StatsPullerManager.cpp @@ -271,6 +271,9 @@ std::map StatsPullerManager::kAllPullAtomInfo = { {android::util::VMS_CLIENT_STATS, {.additiveFields = {5, 6, 7, 8, 9, 10}, .puller = new CarStatsPuller(android::util::VMS_CLIENT_STATS)}}, + // NotiifcationRemoteViews. + {android::util::NOTIFICATION_REMOTE_VIEWS, + {.puller = new StatsCompanionServicePuller(android::util::NOTIFICATION_REMOTE_VIEWS)}}, }; StatsPullerManager::StatsPullerManager() : mNextPullTimeNs(NO_ALARM_UPDATE) { diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 9f51db88e7dc6..32668980d1319 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -202,4 +202,6 @@ interface INotificationManager void setPrivateNotificationsAllowed(boolean allow); boolean getPrivateNotificationsAllowed(); + + long pullStats(long startNs, int report, boolean doAgg, out List stats); } diff --git a/core/proto/android/service/notification.proto b/core/proto/android/service/notification.proto index 1ec05fb5e9fc2..ecb4193a2c6c1 100644 --- a/core/proto/android/service/notification.proto +++ b/core/proto/android/service/notification.proto @@ -264,3 +264,14 @@ message ZenPolicyProto { optional Sender priority_calls = 16; optional Sender priority_messages = 17; } + +// Next Tag: 2 +message PackageRemoteViewInfoProto { + optional string package_name = 1; + // add per-package additional info here (like channels) +} + +// Next Tag: 2 +message NotificationRemoteViewsProto { + repeated PackageRemoteViewInfoProto package_remote_view_info = 1; +} \ No newline at end of file diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index f20957af69131..4a46910b63209 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -163,6 +163,7 @@ import android.os.IDeviceIdleController; import android.os.IInterface; import android.os.Looper; import android.os.Message; +import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; @@ -281,6 +282,9 @@ public class NotificationManagerService extends SystemService { public static final boolean ENABLE_CHILD_NOTIFICATIONS = SystemProperties.getBoolean("debug.child_notifs", true); + // pullStats report request: undecorated remote view stats + public static final int REPORT_REMOTE_VIEWS = 0x01; + static final boolean DEBUG_INTERRUPTIVENESS = SystemProperties.getBoolean( "debug.notification.interruptiveness", false); @@ -3734,6 +3738,8 @@ public class NotificationManagerService extends SystemService { try { if (filter.stats) { dumpJson(pw, filter); + } else if (filter.rvStats) { + dumpRemoteViewStats(pw, filter); } else if (filter.proto) { dumpProto(fd, filter); } else if (filter.criticalPriority) { @@ -4210,6 +4216,49 @@ public class NotificationManagerService extends SystemService { new NotificationShellCmd(NotificationManagerService.this) .exec(this, in, out, err, args, callback, resultReceiver); } + + /** + * Get stats committed after startNs + * + * @param startNs Report stats committed after this time in nanoseconds. + * @param report Indicatess which section to include in the stats. + * @param doAgg Whether to aggregate the stats or keep them separated. + * @param out List of protos of individual commits or one representing the + * aggregate. + * @return the report time in nanoseconds, or 0 on error. + */ + @Override + public long pullStats(long startNs, int report, boolean doAgg, + List out) { + checkCallerIsSystemOrShell(); + long startMs = TimeUnit.MILLISECONDS.convert(startNs, TimeUnit.NANOSECONDS); + + final long identity = Binder.clearCallingIdentity(); + try { + switch (report) { + case REPORT_REMOTE_VIEWS: + Slog.e(TAG, "pullStats REPORT_REMOTE_VIEWS from: " + + startMs + " wtih " + doAgg); + PulledStats stats = mUsageStats.remoteViewStats(startMs, doAgg); + if (stats != null) { + out.add(stats.toParcelFileDescriptor(report)); + Slog.e(TAG, "exiting pullStats with: " + out.size()); + long endNs = TimeUnit.NANOSECONDS + .convert(stats.endTimeMs(), TimeUnit.MILLISECONDS); + return endNs; + } + Slog.e(TAG, "null stats for: " + report); + } + } catch (IOException e) { + + Slog.e(TAG, "exiting pullStats: on error", e); + return 0; + } finally { + Binder.restoreCallingIdentity(identity); + } + Slog.e(TAG, "exiting pullStats: bad request"); + return 0; + } }; @VisibleForTesting @@ -4425,6 +4474,15 @@ public class NotificationManagerService extends SystemService { pw.println(dump); } + private void dumpRemoteViewStats(PrintWriter pw, @NonNull DumpFilter filter) { + PulledStats stats = mUsageStats.remoteViewStats(filter.since, true); + if (stats == null) { + pw.println("no remote view stats reported."); + return; + } + stats.dump(REPORT_REMOTE_VIEWS, pw, filter); + } + private void dumpProto(FileDescriptor fd, @NonNull DumpFilter filter) { final ProtoOutputStream proto = new ProtoOutputStream(fd); synchronized (mNotificationLock) { @@ -8559,6 +8617,7 @@ public class NotificationManagerService extends SystemService { public boolean zen; public long since; public boolean stats; + public boolean rvStats; public boolean redact = true; public boolean proto = false; public boolean criticalPriority = false; @@ -8594,6 +8653,14 @@ public class NotificationManagerService extends SystemService { } else { filter.since = 0; } + } else if ("--remote-view-stats".equals(a)) { + filter.rvStats = true; + if (ai < args.length-1) { + ai++; + filter.since = Long.parseLong(args[ai]); + } else { + filter.since = 0; + } } else if (PRIORITY_ARG.equals(a)) { // Bugreport will call the service twice with priority arguments, first to dump // critical sections and then non critical ones. Set approriate filters diff --git a/services/core/java/com/android/server/notification/NotificationUsageStats.java b/services/core/java/com/android/server/notification/NotificationUsageStats.java index d630b9ab54d6a..0c2b68381427a 100644 --- a/services/core/java/com/android/server/notification/NotificationUsageStats.java +++ b/services/core/java/com/android/server/notification/NotificationUsageStats.java @@ -149,6 +149,7 @@ public class NotificationUsageStats { stats.numPostedByApp++; stats.updateInterarrivalEstimate(now); stats.countApiUse(notification); + stats.numUndecoratedRemoteViews += (isUndecoratedRemoteView(notification) ? 1 : 0); } releaseAggregatedStatsLocked(aggregatedStatsArray); if (ENABLE_SQLITE_LOG) { @@ -156,6 +157,13 @@ public class NotificationUsageStats { } } + /** + * Does this notification use RemoveViews without a platform decoration? + */ + protected static boolean isUndecoratedRemoteView(NotificationRecord notification) { + return (notification.getNotification().getNotificationStyle() == null); + } + /** * Called when a notification has been updated. */ @@ -326,6 +334,15 @@ public class NotificationUsageStats { return dump; } + public PulledStats remoteViewStats(long startMs, boolean aggregate) { + if (ENABLE_SQLITE_LOG) { + if (aggregate) { + return mSQLiteLog.remoteViewAggStats(startMs); + } + } + return null; + } + public synchronized void dump(PrintWriter pw, String indent, DumpFilter filter) { if (ENABLE_AGGREGATED_IN_MEMORY_STATS) { for (AggregatedStats as : mStats.values()) { @@ -403,6 +420,7 @@ public class NotificationUsageStats { public int numRateViolations; public int numAlertViolations; public int numQuotaViolations; + public int numUndecoratedRemoteViews; public long mLastAccessTime; public AggregatedStats(Context context, String key) { @@ -669,6 +687,8 @@ public class NotificationUsageStats { output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n"); output.append(indentPlusTwo).append(quietImportance.toString()).append("\n"); output.append(indentPlusTwo).append(finalImportance.toString()).append("\n"); + output.append(indentPlusTwo); + output.append("numUndecorateRVs=").append(numUndecoratedRemoteViews).append("\n"); output.append(indent).append("}"); return output.toString(); } @@ -1027,7 +1047,7 @@ public class NotificationUsageStats { private static final int MSG_DISMISS = 4; private static final String DB_NAME = "notification_log.db"; - private static final int DB_VERSION = 5; + private static final int DB_VERSION = 7; /** Age in ms after which events are pruned from the DB. */ private static final long HORIZON_MS = 7 * 24 * 60 * 60 * 1000L; // 1 week @@ -1060,6 +1080,7 @@ public class NotificationUsageStats { private static final String COL_FIRST_EXPANSIONTIME_MS = "first_expansion_time_ms"; private static final String COL_AIRTIME_EXPANDED_MS = "expansion_airtime_ms"; private static final String COL_EXPAND_COUNT = "expansion_count"; + private static final String COL_UNDECORATED = "undecorated"; private static final int EVENT_TYPE_POST = 1; @@ -1085,12 +1106,20 @@ public class NotificationUsageStats { "COUNT(*) AS cnt, " + "SUM(" + COL_MUTED + ") as muted, " + "SUM(" + COL_NOISY + ") as noisy, " + - "SUM(" + COL_DEMOTED + ") as demoted " + + "SUM(" + COL_DEMOTED + ") as demoted, " + + "SUM(" + COL_UNDECORATED + ") as undecorated " + "FROM " + TAB_LOG + " " + "WHERE " + COL_EVENT_TYPE + "=" + EVENT_TYPE_POST + " AND " + COL_EVENT_TIME + " > %d " + " GROUP BY " + COL_EVENT_USER_ID + ", day, " + COL_PKG; + private static final String UNDECORATED_QUERY = "SELECT " + + COL_PKG + ", " + + "MAX(" + COL_EVENT_TIME + ") as max_time " + + "FROM " + TAB_LOG + " " + + "WHERE " + COL_UNDECORATED + "> 0 " + + " AND " + COL_EVENT_TIME + " > %d " + + "GROUP BY " + COL_PKG; public SQLiteLog(Context context) { HandlerThread backgroundThread = new HandlerThread("notification-sqlite-log", @@ -1146,7 +1175,8 @@ public class NotificationUsageStats { COL_AIRTIME_MS + " INT," + COL_FIRST_EXPANSIONTIME_MS + " INT," + COL_AIRTIME_EXPANDED_MS + " INT," + - COL_EXPAND_COUNT + " INT" + + COL_EXPAND_COUNT + " INT," + + COL_UNDECORATED + " INT" + ")"); } @@ -1256,6 +1286,7 @@ public class NotificationUsageStats { } else { putPosttimeVisibility(r, cv); } + cv.put(COL_UNDECORATED, (isUndecoratedRemoteView(r) ? 1 : 0)); SQLiteDatabase db = mHelper.getWritableDatabase(); if (db.insert(TAB_LOG, null, cv) < 0) { Log.wtf(TAG, "Error while trying to insert values: " + cv); @@ -1332,5 +1363,22 @@ public class NotificationUsageStats { } return dump; } + + public PulledStats remoteViewAggStats(long startMs) { + PulledStats stats = new PulledStats(startMs); + SQLiteDatabase db = mHelper.getReadableDatabase(); + String q = String.format(UNDECORATED_QUERY, startMs); + Cursor cursor = db.rawQuery(q, null); + try { + for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + String pkg = cursor.getString(0); + long maxTimeMs = cursor.getLong(1); + stats.addUndecoratedPackage(pkg, maxTimeMs); + } + } finally { + cursor.close(); + } + return stats; + } } } diff --git a/services/core/java/com/android/server/notification/PulledStats.java b/services/core/java/com/android/server/notification/PulledStats.java new file mode 100644 index 0000000000000..ada890a10361c --- /dev/null +++ b/services/core/java/com/android/server/notification/PulledStats.java @@ -0,0 +1,129 @@ +/* + * 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 static com.android.server.notification.NotificationManagerService.REPORT_REMOTE_VIEWS; + +import android.os.ParcelFileDescriptor; +import android.service.notification.NotificationRemoteViewsProto; +import android.service.notification.PackageRemoteViewInfoProto; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +public class PulledStats { + static final String TAG = "PulledStats"; + + private final long mTimePeriodStartMs; + private long mTimePeriodEndMs; + private List mUndecoratedPackageNames; + + public PulledStats(long startMs) { + mTimePeriodEndMs = mTimePeriodStartMs = startMs; + mUndecoratedPackageNames = new ArrayList<>(); + } + + ParcelFileDescriptor toParcelFileDescriptor(int report) + throws IOException { + final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe(); + switch(report) { + case REPORT_REMOTE_VIEWS: + Thread thr = new Thread("NotificationManager pulled metric output") { + public void run() { + try { + FileOutputStream fout = new ParcelFileDescriptor.AutoCloseOutputStream( + fds[1]); + final ProtoOutputStream proto = new ProtoOutputStream(fout); + writeToProto(report, proto); + proto.flush(); + fout.close(); + } catch (IOException e) { + Slog.w(TAG, "Failure writing pipe", e); + } + } + }; + thr.start(); + break; + + default: + Slog.w(TAG, "Unknown pulled stats request: " + report); + break; + } + return fds[0]; + } + + /* + * @return the most recent timestamp in the report, as nanoseconds. + */ + public long endTimeMs() { + return mTimePeriodEndMs; + } + + public void dump(int report, PrintWriter pw, NotificationManagerService.DumpFilter filter) { + switch(report) { + case REPORT_REMOTE_VIEWS: + pw.print(" Packages with undecordated notifications ("); + pw.print(mTimePeriodStartMs); + pw.print(" - "); + pw.print(mTimePeriodEndMs); + pw.println("):"); + if (mUndecoratedPackageNames.size() == 0) { + pw.println(" none"); + } else { + for (String pkg : mUndecoratedPackageNames) { + if (!filter.filtered || pkg.equals(filter.pkgFilter)) { + pw.println(" " + pkg); + } + } + } + break; + + default: + pw.println("Unknown pulled stats request: " + report); + break; + } + } + + @VisibleForTesting + void writeToProto(int report, ProtoOutputStream proto) { + switch(report) { + case REPORT_REMOTE_VIEWS: + for (String pkg: mUndecoratedPackageNames) { + long token = proto.start(NotificationRemoteViewsProto.PACKAGE_REMOTE_VIEW_INFO); + proto.write(PackageRemoteViewInfoProto.PACKAGE_NAME, pkg); + proto.end(token); + } + break; + + default: + Slog.w(TAG, "Unknown pulled stats request: " + report); + break; + } + } + + public void addUndecoratedPackage(String packageName, long timestampMs) { + mUndecoratedPackageNames.add(packageName); + mTimePeriodEndMs = Math.max(mTimePeriodEndMs, timestampMs); + } +} diff --git a/services/core/java/com/android/server/stats/StatsCompanionService.java b/services/core/java/com/android/server/stats/StatsCompanionService.java index c76bbb05a3595..d70b628c83ece 100644 --- a/services/core/java/com/android/server/stats/StatsCompanionService.java +++ b/services/core/java/com/android/server/stats/StatsCompanionService.java @@ -41,6 +41,7 @@ import android.app.AppOpsManager.HistoricalOps; import android.app.AppOpsManager.HistoricalOpsRequest; import android.app.AppOpsManager.HistoricalPackageOps; import android.app.AppOpsManager.HistoricalUidOps; +import android.app.INotificationManager; import android.app.ProcessMemoryState; import android.app.StatsManager; import android.bluetooth.BluetoothActivityEnergyInfo; @@ -140,6 +141,7 @@ import com.android.server.SystemService; import com.android.server.SystemServiceManager; import com.android.server.am.MemoryStatUtil.IonAllocations; import com.android.server.am.MemoryStatUtil.MemoryStat; +import com.android.server.notification.NotificationManagerService; import com.android.server.role.RoleManagerInternal; import com.android.server.storage.DiskStatsFileLogger; import com.android.server.storage.DiskStatsLoggingService; @@ -1625,14 +1627,7 @@ public class StatsCompanionService extends IStatsCompanionService.Stub { if (statsFiles.size() != 1) { return; } - InputStream stream = new ParcelFileDescriptor.AutoCloseInputStream( - statsFiles.get(0)); - int[] len = new int[1]; - byte[] stats = readFully(stream, len); - StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, - wallClockNanos); - e.writeStorage(Arrays.copyOf(stats, len[0])); - pulledData.add(e); + unpackStreamedData(tagId, elapsedNanos, wallClockNanos, pulledData, statsFiles); new File(mBaseDir.getAbsolutePath() + "/" + section + "_" + lastHighWaterMark).delete(); new File( @@ -1648,6 +1643,52 @@ public class StatsCompanionService extends IStatsCompanionService.Stub { } } + private INotificationManager mNotificationManager = + INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + + private void pullNotificationStats(int reportId, int tagId, long elapsedNanos, + long wallClockNanos, + List pulledData) { + final long callingToken = Binder.clearCallingIdentity(); + try { + // determine last pull tine. Copy file trick from pullProcessStats? + long lastNotificationStatsNs = wallClockNanos - + TimeUnit.NANOSECONDS.convert(1, TimeUnit.DAYS); + + List statsFiles = new ArrayList<>(); + long notificationStatsNs = mNotificationManager.pullStats( + lastNotificationStatsNs, reportId, true, statsFiles); + if (statsFiles.size() != 1) { + return; + } + unpackStreamedData(tagId, elapsedNanos, wallClockNanos, pulledData, statsFiles); + } catch (IOException e) { + Log.e(TAG, "Getting notistats failed: ", e); + + } catch (RemoteException e) { + Log.e(TAG, "Getting notistats failed: ", e); + } catch (SecurityException e) { + Log.e(TAG, "Getting notistats failed: ", e); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + + } + + static void unpackStreamedData(int tagId, long elapsedNanos, long wallClockNanos, + List pulledData, List statsFiles) + throws IOException { + InputStream stream = new ParcelFileDescriptor.AutoCloseInputStream( + statsFiles.get(0)); + int[] len = new int[1]; + byte[] stats = readFully(stream, len); + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, + wallClockNanos); + e.writeStorage(Arrays.copyOf(stats, len[0])); + pulledData.add(e); + } + static byte[] readFully(InputStream stream, int[] outLen) throws IOException { int pos = 0; final int initialAvail = stream.available(); @@ -2477,6 +2518,11 @@ public class StatsCompanionService extends IStatsCompanionService.Stub { pullAppOps(elapsedNanos, wallClockNanos, ret); break; } + case StatsLog.NOTIFICATION_REMOTE_VIEWS: { + pullNotificationStats(NotificationManagerService.REPORT_REMOTE_VIEWS, + tagId, elapsedNanos, wallClockNanos, ret); + break; + } default: Slog.w(TAG, "No such tagId data as " + tagId); return null; diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp index 4a9cef1f1cbd0..367962b318c4e 100644 --- a/services/tests/uiservicestests/Android.bp +++ b/services/tests/uiservicestests/Android.bp @@ -20,6 +20,7 @@ android_test { "androidx.test.rules", "hamcrest-library", "mockito-target-inline-minus-junit4", "platform-test-annotations", + "platformprotosnano", "hamcrest-library", "testables", "truth-prebuilt", diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PulledStatsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PulledStatsTest.java new file mode 100644 index 0000000000000..f685c68f4160e --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/PulledStatsTest.java @@ -0,0 +1,113 @@ +/* + * 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 static com.android.server.notification.NotificationManagerService.REPORT_REMOTE_VIEWS; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotSame; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.service.notification.nano.NotificationRemoteViewsProto; +import android.test.MoreAsserts; +import android.util.proto.ProtoOutputStream; + +import androidx.test.filters.SmallTest; + +import com.android.server.UiServiceTestCase; + +import com.google.protobuf.nano.InvalidProtocolBufferNanoException; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +@SmallTest +public class PulledStatsTest extends UiServiceTestCase { + + @Test + public void testPulledStats_Empty() { + PulledStats stats = new PulledStats(0L); + assertEquals(0L, stats.endTimeMs()); + } + + @Test + public void testPulledStats_UnknownReport() { + PulledStats stats = new PulledStats(0L); + stats.addUndecoratedPackage("foo", 456); + stats.addUndecoratedPackage("bar", 123); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + final ProtoOutputStream proto = new ProtoOutputStream(bytes); + stats.writeToProto(1023123, proto); // a very large number + proto.flush(); + + // expect empty output in response to an unrecognized request + assertEquals(0L, bytes.size()); + } + + @Test + public void testPulledStats_RemoteViewReportPackages() { + List expectedPkgs = new ArrayList<>(2); + expectedPkgs.add("foo"); + expectedPkgs.add("bar"); + + PulledStats stats = new PulledStats(0L); + for(String pkg: expectedPkgs) { + stats.addUndecoratedPackage(pkg, 111); + } + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + final ProtoOutputStream protoStream = new ProtoOutputStream(bytes); + stats.writeToProto(REPORT_REMOTE_VIEWS, protoStream); + protoStream.flush(); + + try { + NotificationRemoteViewsProto proto = + NotificationRemoteViewsProto.parseFrom(bytes.toByteArray()); + List actualPkgs = new ArrayList<>(2); + for(int i = 0 ; i < proto.packageRemoteViewInfo.length; i++) { + actualPkgs.add(proto.packageRemoteViewInfo[i].packageName); + } + assertEquals(2, actualPkgs.size()); + assertTrue("missing packages", actualPkgs.containsAll(expectedPkgs)); + assertTrue("unexpected packages", expectedPkgs.containsAll(actualPkgs)); + } catch (InvalidProtocolBufferNanoException e) { + e.printStackTrace(); + fail("writeToProto generated unparsable output"); + } + + } + @Test + public void testPulledStats_RemoteViewReportEndTime() { + List expectedPkgs = new ArrayList<>(2); + expectedPkgs.add("foo"); + expectedPkgs.add("bar"); + + PulledStats stats = new PulledStats(0L); + long t = 111; + for(String pkg: expectedPkgs) { + t += 1000; + stats.addUndecoratedPackage(pkg, t); + } + assertEquals(t, stats.endTimeMs()); + } + +}