Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 358d5006 authored by Varun Shah's avatar Varun Shah
Browse files

Catch exceptions in UsageStatsService on bad data.

Instead of throwing exceptions from UsageStatsService and potentially
crashing the system server, catch exception on reading/writing bad data
and log the exception.

Also ensure proper handling of deobfuscating bad data.

Bug: 140459061
Bug: 135484470
Test: atest IntervalStatsTests
Test: atest UsageStatsDatabaseTest
Change-Id: I17d6ad5e377a2636ff42c9b422fe6ddf1201fc08
parent e1ba9cde
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -78,6 +78,21 @@ public class EventList {
        mEvents.add(insertIndex, event);
    }

    /**
     * Removes the event at the given index.
     *
     * @param index the index of the event to remove
     * @return the event removed, or {@code null} if the index was out of bounds
     */
    public UsageEvents.Event remove(int index) {
        try {
            return mEvents.remove(index);
        } catch (IndexOutOfBoundsException e) {
            // catch and handle the exception here instead of throwing it to the client
            return null;
        }
    }

    /**
     * Finds the index of the first event whose timestamp is greater than or equal to the given
     * timestamp.
+191 −0
Original line number Diff line number Diff line
/*
 * 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.usage;

import static android.app.usage.UsageEvents.Event.MAX_EVENT_TYPE;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;

import android.app.usage.UsageEvents;
import android.content.res.Configuration;
import android.test.suitebuilder.annotation.SmallTest;

import androidx.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Locale;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class IntervalStatsTests {
    private static final int NUMBER_OF_PACKAGES = 7;
    private static final int NUMBER_OF_EVENTS_PER_PACKAGE = 200;
    private static final int NUMBER_OF_EVENTS = NUMBER_OF_PACKAGES * NUMBER_OF_EVENTS_PER_PACKAGE;

    private long mEndTime = 0;

    private void populateIntervalStats(IntervalStats intervalStats) {
        final int timeProgression = 23;
        long time = System.currentTimeMillis() - (NUMBER_OF_EVENTS * timeProgression);

        intervalStats.majorVersion = 7;
        intervalStats.minorVersion = 8;
        intervalStats.beginTime = time;
        intervalStats.interactiveTracker.count = 2;
        intervalStats.interactiveTracker.duration = 111111;
        intervalStats.nonInteractiveTracker.count = 3;
        intervalStats.nonInteractiveTracker.duration = 222222;
        intervalStats.keyguardShownTracker.count = 4;
        intervalStats.keyguardShownTracker.duration = 333333;
        intervalStats.keyguardHiddenTracker.count = 5;
        intervalStats.keyguardHiddenTracker.duration = 4444444;

        for (int i = 0; i < NUMBER_OF_EVENTS; i++) {
            UsageEvents.Event event = new UsageEvents.Event();
            final int packageInt = ((i / 3) % NUMBER_OF_PACKAGES); // clusters of 3 events
            event.mPackage = "fake.package.name" + packageInt;
            if (packageInt == 3) {
                // Third app is an instant app
                event.mFlags |= UsageEvents.Event.FLAG_IS_PACKAGE_INSTANT_APP;
            }

            final int instanceId = i % 11;
            event.mClass = ".fake.class.name" + instanceId;
            event.mTimeStamp = time;
            event.mEventType = i % (MAX_EVENT_TYPE + 1); //"random" event type
            event.mInstanceId = instanceId;


            final int rootPackageInt = (i % 5); // 5 "apps" start each task
            event.mTaskRootPackage = "fake.package.name" + rootPackageInt;

            final int rootClassInt = i % 6;
            event.mTaskRootClass = ".fake.class.name" + rootClassInt;

            switch (event.mEventType) {
                case UsageEvents.Event.CONFIGURATION_CHANGE:
                    event.mConfiguration = new Configuration(); //empty config
                    break;
                case UsageEvents.Event.SHORTCUT_INVOCATION:
                    event.mShortcutId = "shortcut" + (i % 8); //"random" shortcut
                    break;
                case UsageEvents.Event.STANDBY_BUCKET_CHANGED:
                    //"random" bucket and reason
                    event.mBucketAndReason = (((i % 5 + 1) * 10) << 16) & (i % 5 + 1) << 8;
                    break;
                case UsageEvents.Event.NOTIFICATION_INTERRUPTION:
                    event.mNotificationChannelId = "channel" + (i % 5); //"random" channel
                    break;
            }

            intervalStats.addEvent(event);
            intervalStats.update(event.mPackage, event.mClass, event.mTimeStamp, event.mEventType,
                    event.mInstanceId);

            time += timeProgression; // Arbitrary progression of time
        }
        mEndTime = time;

        final Configuration config1 = new Configuration();
        config1.fontScale = 3.3f;
        config1.mcc = 4;
        intervalStats.getOrCreateConfigurationStats(config1);

        final Configuration config2 = new Configuration();
        config2.mnc = 5;
        config2.setLocale(new Locale("en", "US"));
        intervalStats.getOrCreateConfigurationStats(config2);

        intervalStats.activeConfiguration = config2;
    }

    @Test
    public void testObfuscation() {
        final IntervalStats intervalStats = new IntervalStats();
        populateIntervalStats(intervalStats);

        final PackagesTokenData packagesTokenData = new PackagesTokenData();
        intervalStats.obfuscateData(packagesTokenData);

        // data is populated with 7 different "apps"
        assertEquals(packagesTokenData.tokensToPackagesMap.size(), NUMBER_OF_PACKAGES);
        assertEquals(packagesTokenData.packagesToTokensMap.size(), NUMBER_OF_PACKAGES);
        assertEquals(packagesTokenData.counter, NUMBER_OF_PACKAGES + 1);

        assertEquals(intervalStats.events.size(), NUMBER_OF_EVENTS);
        assertEquals(intervalStats.packageStats.size(), NUMBER_OF_PACKAGES);
    }

    @Test
    public void testDeobfuscation() {
        final IntervalStats intervalStats = new IntervalStats();
        populateIntervalStats(intervalStats);

        final PackagesTokenData packagesTokenData = new PackagesTokenData();
        intervalStats.obfuscateData(packagesTokenData);
        intervalStats.deobfuscateData(packagesTokenData);

        // ensure deobfuscation doesn't update any of the mappings data
        assertEquals(packagesTokenData.tokensToPackagesMap.size(), NUMBER_OF_PACKAGES);
        assertEquals(packagesTokenData.packagesToTokensMap.size(), NUMBER_OF_PACKAGES);
        assertEquals(packagesTokenData.counter, NUMBER_OF_PACKAGES + 1);

        // ensure deobfuscation didn't remove any events or usage stats
        assertEquals(intervalStats.events.size(), NUMBER_OF_EVENTS);
        assertEquals(intervalStats.packageStats.size(), NUMBER_OF_PACKAGES);
    }

    @Test
    public void testBadDataOnDeobfuscation() {
        final IntervalStats intervalStats = new IntervalStats();
        populateIntervalStats(intervalStats);

        final PackagesTokenData packagesTokenData = new PackagesTokenData();
        intervalStats.obfuscateData(packagesTokenData);
        intervalStats.packageStats.clear();

        // remove the mapping for token 2
        packagesTokenData.tokensToPackagesMap.remove(2);

        intervalStats.deobfuscateData(packagesTokenData);
        // deobfuscation should have removed all events mapped to package token 2
        assertEquals(intervalStats.events.size(),
                NUMBER_OF_EVENTS - NUMBER_OF_EVENTS_PER_PACKAGE - 1);
        assertEquals(intervalStats.packageStats.size(), NUMBER_OF_PACKAGES - 1);
    }

    @Test
    public void testBadPackageDataOnDeobfuscation() {
        final IntervalStats intervalStats = new IntervalStats();
        populateIntervalStats(intervalStats);

        final PackagesTokenData packagesTokenData = new PackagesTokenData();
        intervalStats.obfuscateData(packagesTokenData);
        intervalStats.packageStats.clear();

        // remove mapping number 2 within package 3 (random)
        packagesTokenData.tokensToPackagesMap.valueAt(3).remove(2);

        intervalStats.deobfuscateData(packagesTokenData);
        // deobfuscation should not have removed all events for a package - however, it's possible
        // that some events were removed because of how shortcut and notification events are handled
        assertTrue(intervalStats.events.size() > NUMBER_OF_EVENTS - NUMBER_OF_EVENTS_PER_PACKAGE);
        assertEquals(intervalStats.packageStats.size(), NUMBER_OF_PACKAGES);
    }
}
+49 −2
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ import android.content.res.Configuration;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.proto.ProtoInputStream;
@@ -55,6 +56,8 @@ import java.io.IOException;
import java.util.List;

public class IntervalStats {
    private static final String TAG = "IntervalStats";

    public static final int CURRENT_MAJOR_VERSION = 1;
    public static final int CURRENT_MINOR_VERSION = 1;
    public int majorVersion = CURRENT_MAJOR_VERSION;
@@ -453,6 +456,10 @@ public class IntervalStats {
            final UsageStats usageStats = packageStatsObfuscated.valueAt(statsIndex);
            usageStats.mPackageName = packagesTokenData.getString(packageToken,
                    PackagesTokenData.PACKAGE_NAME_INDEX);
            if (usageStats.mPackageName == null) {
                Slog.e(TAG, "Unable to parse usage stats package " + packageToken);
                continue;
            }

            // Update chooser counts
            final int chooserActionsSize = usageStats.mChooserCountsObfuscated.size();
@@ -460,6 +467,11 @@ public class IntervalStats {
                final ArrayMap<String, Integer> categoryCountsMap = new ArrayMap<>();
                final int actionToken = usageStats.mChooserCountsObfuscated.keyAt(actionIndex);
                final String action = packagesTokenData.getString(packageToken, actionToken);
                if (action == null) {
                    Slog.i(TAG, "Unable to parse chooser action " + actionToken
                            + " for package " + packageToken);
                    continue;
                }
                final SparseIntArray categoryCounts =
                        usageStats.mChooserCountsObfuscated.valueAt(actionIndex);
                final int categoriesSize = categoryCounts.size();
@@ -467,6 +479,11 @@ public class IntervalStats {
                    final int categoryToken = categoryCounts.keyAt(categoryIndex);
                    final String category = packagesTokenData.getString(packageToken,
                            categoryToken);
                    if (category == null) {
                        Slog.i(TAG, "Unable to parse chooser category " + categoryToken
                                + " for package " + packageToken);
                        continue;
                    }
                    categoryCountsMap.put(category, categoryCounts.valueAt(categoryIndex));
                }
                usageStats.mChooserCounts.put(action, categoryCountsMap);
@@ -481,22 +498,39 @@ public class IntervalStats {
     * shortcut or notification channel tokens.
     */
    private void deobfuscateEvents(PackagesTokenData packagesTokenData) {
        final int eventsSize = this.events.size();
        for (int i = 0; i < eventsSize; i++) {
        for (int i = this.events.size() - 1; i >= 0; i--) {
            final Event event = this.events.get(i);
            final int packageToken = event.mPackageToken;
            event.mPackage = packagesTokenData.getString(packageToken,
                    PackagesTokenData.PACKAGE_NAME_INDEX);
            if (event.mPackage == null) {
                Slog.e(TAG, "Unable to parse event package " + packageToken);
                this.events.remove(i);
                continue;
            }

            if (event.mClassToken != PackagesTokenData.UNASSIGNED_TOKEN) {
                event.mClass = packagesTokenData.getString(packageToken, event.mClassToken);
                if (event.mClass == null) {
                    Slog.i(TAG, "Unable to parse class " + event.mClassToken
                            + " for package " + packageToken);
                }
            }
            if (event.mTaskRootPackageToken != PackagesTokenData.UNASSIGNED_TOKEN) {
                event.mTaskRootPackage = packagesTokenData.getString(packageToken,
                        event.mTaskRootPackageToken);
                if (event.mTaskRootPackage == null) {
                    Slog.i(TAG, "Unable to parse task root package " + event.mTaskRootPackageToken
                            + " for package " + packageToken);
                }
            }
            if (event.mTaskRootClassToken != PackagesTokenData.UNASSIGNED_TOKEN) {
                event.mTaskRootClass = packagesTokenData.getString(packageToken,
                        event.mTaskRootClassToken);
                if (event.mTaskRootClass == null) {
                    Slog.i(TAG, "Unable to parse task root class " + event.mTaskRootClassToken
                            + " for package " + packageToken);
                }
            }
            switch (event.mEventType) {
                case CONFIGURATION_CHANGE:
@@ -507,10 +541,23 @@ public class IntervalStats {
                case SHORTCUT_INVOCATION:
                    event.mShortcutId = packagesTokenData.getString(packageToken,
                            event.mShortcutIdToken);
                    if (event.mShortcutId == null) {
                        Slog.e(TAG, "Unable to parse shortcut " + event.mShortcutIdToken
                                + " for package " + packageToken);
                        this.events.remove(i);
                        continue;
                    }
                    break;
                case NOTIFICATION_INTERRUPTION:
                    event.mNotificationChannelId = packagesTokenData.getString(packageToken,
                            event.mNotificationChannelIdToken);
                    if (event.mNotificationChannelId == null) {
                        Slog.e(TAG, "Unable to parse notification channel "
                                + event.mNotificationChannelIdToken + " for package "
                                + packageToken);
                        this.events.remove(i);
                        continue;
                    }
                    break;
            }
        }
+11 −2
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@
package com.android.server.usage;

import android.util.ArrayMap;
import android.util.Slog;
import android.util.SparseArray;

import java.util.ArrayList;
@@ -107,9 +108,17 @@ public final class PackagesTokenData {
     *
     * @param packageToken the package token for which this token belongs to
     * @param token the token whose string needs to be fetched
     * @return the string representing the given token
     * @return the string representing the given token or {@code null} if not found
     */
    public String getString(int packageToken, int token) {
        try {
            return tokensToPackagesMap.get(packageToken).get(token);
        } catch (NullPointerException npe) {
            Slog.e("PackagesTokenData",
                    "Unable to find tokenized strings for package " + packageToken, npe);
            return null;
        } catch (IndexOutOfBoundsException e) {
            return null;
        }
    }
}
+22 −4
Original line number Diff line number Diff line
@@ -869,11 +869,19 @@ public class UsageStatsDatabase {
                UsageStatsXml.write(out, stats);
                break;
            case 4:
                try {
                    UsageStatsProto.write(out, stats);
                } catch (IOException | IllegalArgumentException e) {
                    Slog.e(TAG, "Unable to write interval stats to proto.", e);
                }
                break;
            case 5:
                stats.obfuscateData(packagesTokenData);
                try {
                    UsageStatsProtoV2.write(out, stats);
                } catch (IOException | IllegalArgumentException e) {
                    Slog.e(TAG, "Unable to write interval stats to proto.", e);
                }
                break;
            default:
                throw new RuntimeException(
@@ -920,10 +928,18 @@ public class UsageStatsDatabase {
                UsageStatsXml.read(in, statsOut);
                break;
            case 4:
                try {
                    UsageStatsProto.read(in, statsOut);
                } catch (IOException e) {
                    Slog.e(TAG, "Unable to read interval stats from proto.", e);
                }
                break;
            case 5:
                try {
                    UsageStatsProtoV2.read(in, statsOut);
                } catch (IOException e) {
                    Slog.e(TAG, "Unable to read interval stats from proto.", e);
                }
                statsOut.deobfuscateData(packagesTokenData);
                break;
            default:
@@ -974,6 +990,8 @@ public class UsageStatsDatabase {
            UsageStatsProtoV2.writeObfuscatedData(fos, mPackagesTokenData);
            file.finishWrite(fos);
            fos = null;
        } catch (IOException | IllegalArgumentException e) {
            Slog.e(TAG, "Unable to write obfuscated data to proto.", e);
        } finally {
            file.failWrite(fos);
        }
Loading