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

Commit 1dda953d authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add an app size collector."

parents 52d6b6a0 a59272eb
Loading
Loading
Loading
Loading
+160 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.storage;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageStatsObserver;
import android.content.pm.PackageManager;
import android.content.pm.PackageStats;
import android.content.pm.UserInfo;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserManager;
import android.os.storage.VolumeInfo;
import android.util.Log;
import com.android.internal.os.BackgroundThread;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * AppCollector asynchronously collects package sizes.
 */
public class AppCollector {
    private static String TAG = "AppCollector";

    private CompletableFuture<List<PackageStats>> mStats;
    private final BackgroundHandler mBackgroundHandler;

    /**
     * Constrcuts a new AppCollector which runs on the provided volume.
     * @param context Android context used to get
     * @param volume Volume to check for apps.
     */
    public AppCollector(Context context, VolumeInfo volume) {
        mBackgroundHandler = new BackgroundHandler(BackgroundThread.get().getLooper(),
                volume,
                context.getPackageManager(),
                (UserManager) context.getSystemService(Context.USER_SERVICE));
    }

    /**
     * Returns a list of package stats for the context and volume. Note that in a multi-user
     * environment, this may return stats for the same package multiple times. These "duplicate"
     * entries will have the package stats for the package for a given user, not the package in
     * aggregate.
     * @param timeoutMillis Milliseconds before timing out and returning early with null.
     */
    public List<PackageStats> getPackageStats(long timeoutMillis) {
        synchronized(this) {
            if (mStats == null) {
                mStats = new CompletableFuture<>();
                mBackgroundHandler.sendEmptyMessage(BackgroundHandler.MSG_START_LOADING_SIZES);
            }
        }

        List<PackageStats> value = null;
        try {
            value = mStats.get(timeoutMillis, TimeUnit.MILLISECONDS);
        } catch (InterruptedException | ExecutionException e) {
            Log.e(TAG, "An exception occurred while getting app storage", e);
        } catch (TimeoutException e) {
            Log.e(TAG, "AppCollector timed out");
        }
        return value;
    }

    private class StatsObserver extends IPackageStatsObserver.Stub {
        private AtomicInteger mCount;
        private final ArrayList<PackageStats> mPackageStats;

        public StatsObserver(int count) {
            mCount = new AtomicInteger(count);
            mPackageStats = new ArrayList<>(count);
        }

        @Override
        public void onGetStatsCompleted(PackageStats packageStats, boolean succeeded)
                throws RemoteException {
            if (succeeded) {
                mPackageStats.add(packageStats);
            }

            if (mCount.decrementAndGet() == 0) {
                mStats.complete(mPackageStats);
            }
        }
    }

    private class BackgroundHandler extends Handler {
        static final int MSG_START_LOADING_SIZES = 0;
        private final VolumeInfo mVolume;
        private final PackageManager mPm;
        private final UserManager mUm;

        BackgroundHandler(Looper looper, VolumeInfo volume, PackageManager pm, UserManager um) {
            super(looper);
            mVolume = volume;
            mPm = pm;
            mUm = um;
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_START_LOADING_SIZES: {
                    final List<ApplicationInfo> apps = mPm.getInstalledApplications(
                            PackageManager.GET_UNINSTALLED_PACKAGES
                                    | PackageManager.GET_DISABLED_COMPONENTS);

                    final List<ApplicationInfo> volumeApps = new ArrayList<>();
                    for (ApplicationInfo app : apps) {
                        if (Objects.equals(app.volumeUuid, mVolume.getFsUuid())) {
                            volumeApps.add(app);
                        }
                    }

                    List<UserInfo> users = mUm.getUsers();
                    final int count = users.size() * volumeApps.size();
                    if (count == 0) {
                        mStats.complete(new ArrayList<>());
                    }

                    // Kick off the async package size query for all apps.
                    final StatsObserver observer = new StatsObserver(count);
                    for (UserInfo user : users) {
                        for (ApplicationInfo app : volumeApps) {
                            mPm.getPackageSizeInfoAsUser(app.packageName, user.id,
                                    observer);
                        }
                    }
                }
            }
        }
    }
}
+2 −1
Original line number Diff line number Diff line
@@ -23,7 +23,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \
    android-support-test \
    mockito-target-minus-junit4 \
    platform-test-annotations \
    ShortcutManagerTestUtils
    ShortcutManagerTestUtils \
    truth-prebuilt

LOCAL_JAVA_LIBRARIES := android.test.runner

+201 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.storage;

import android.content.pm.UserInfo;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageStatsObserver;
import android.content.pm.PackageManager;
import android.content.pm.PackageStats;
import android.os.UserManager;
import android.os.storage.VolumeInfo;
import android.test.AndroidTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.when;

@RunWith(JUnit4.class)
public class AppCollectorTest extends AndroidTestCase {
    private static final long TIMEOUT = TimeUnit.MINUTES.toMillis(1);
    @Mock private Context mContext;
    @Mock private PackageManager mPm;
    @Mock private UserManager mUm;
    private List<ApplicationInfo> mApps;
    private List<UserInfo> mUsers;

    @Before
    public void setUp() throws Exception {
        super.setUp();
        MockitoAnnotations.initMocks(this);
        mApps = new ArrayList<>();
        when(mContext.getPackageManager()).thenReturn(mPm);
        when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUm);

        // Set up the app list.
        when(mPm.getInstalledApplications(anyInt())).thenReturn(mApps);

        // Set up the user list with a single user (0).
        mUsers = new ArrayList<>();
        mUsers.add(new UserInfo(0, "", 0));
        when(mUm.getUsers()).thenReturn(mUsers);
    }

    @Test
    public void testNoApps() throws Exception {
        VolumeInfo volume = new VolumeInfo("testuuid", 0, null, null);
        volume.fsUuid = "testuuid";
        AppCollector collector = new AppCollector(mContext, volume);

        assertThat(collector.getPackageStats(TIMEOUT)).isEmpty();
    }

    @Test
    public void testAppOnExternalVolume() throws Exception {
        addApplication("com.test.app", "differentuuid");
        VolumeInfo volume = new VolumeInfo("testuuid", 0, null, null);
        volume.fsUuid = "testuuid";
        AppCollector collector = new AppCollector(mContext, volume);

        assertThat(collector.getPackageStats(TIMEOUT)).isEmpty();
    }

    @Test
    public void testOneValidApp() throws Exception {
        addApplication("com.test.app", "testuuid");
        VolumeInfo volume = new VolumeInfo("testuuid", 0, null, null);
        volume.fsUuid = "testuuid";
        AppCollector collector = new AppCollector(mContext, volume);
        PackageStats stats = new PackageStats("com.test.app");

        // Set up this to handle the asynchronous call to the PackageManager. This returns the
        // package info for the specified package.
        doAnswer(new Answer<Void>() {
             @Override
             public Void answer(InvocationOnMock invocation) {
                 try {
                     ((IPackageStatsObserver.Stub) invocation.getArguments()[2])
                             .onGetStatsCompleted(stats, true);
                 } catch (Exception e) {
                     // We fail instead of just letting the exception fly because throwing
                     // out of the callback like this on the background thread causes the test
                     // runner to crash, rather than reporting the failure.
                     fail();
                 }
                 return null;
             }
        }).when(mPm).getPackageSizeInfoAsUser(eq("com.test.app"), eq(0), any());


        // Because getPackageStats is a blocking call, we block execution of the test until the
        // call finishes. In order to finish the call, we need the above answer to execute.
        List<PackageStats> myStats = new ArrayList<>();
        CountDownLatch latch = new CountDownLatch(1);
        new Thread(new Runnable() {
            @Override
            public void run() {
                myStats.addAll(collector.getPackageStats(TIMEOUT));
                latch.countDown();
            }
        }).start();
        latch.await();

        assertThat(myStats).containsExactly(stats);
    }

    @Test
    public void testMultipleUsersOneApp() throws Exception {
        addApplication("com.test.app", "testuuid");
        ApplicationInfo otherUsersApp = new ApplicationInfo();
        otherUsersApp.packageName = "com.test.app";
        otherUsersApp.volumeUuid = "testuuid";
        otherUsersApp.uid = 1;
        mUsers.add(new UserInfo(1, "", 0));

        VolumeInfo volume = new VolumeInfo("testuuid", 0, null, null);
        volume.fsUuid = "testuuid";
        AppCollector collector = new AppCollector(mContext, volume);
        PackageStats stats = new PackageStats("com.test.app");
        PackageStats otherStats = new PackageStats("com.test.app");
        otherStats.userHandle = 1;

        // Set up this to handle the asynchronous call to the PackageManager. This returns the
        // package info for our packages.
        doAnswer(new Answer<Void>() {
             @Override
             public Void answer(InvocationOnMock invocation) {
                 try {
                     ((IPackageStatsObserver.Stub) invocation.getArguments()[2])
                             .onGetStatsCompleted(stats, true);

                     // Now callback for the other uid.
                     ((IPackageStatsObserver.Stub) invocation.getArguments()[2])
                             .onGetStatsCompleted(otherStats, true);
                 } catch (Exception e) {
                     // We fail instead of just letting the exception fly because throwing
                     // out of the callback like this on the background thread causes the test
                     // runner to crash, rather than reporting the failure.
                     fail();
                 }
                 return null;
             }
        }).when(mPm).getPackageSizeInfoAsUser(eq("com.test.app"), eq(0), any());


        // Because getPackageStats is a blocking call, we block execution of the test until the
        // call finishes. In order to finish the call, we need the above answer to execute.
        List<PackageStats> myStats = new ArrayList<>();
        CountDownLatch latch = new CountDownLatch(1);
        new Thread(new Runnable() {
            @Override
            public void run() {
                myStats.addAll(collector.getPackageStats(TIMEOUT));
                latch.countDown();
            }
        }).start();
        latch.await();

        // This should
        assertThat(myStats).containsAllOf(stats, otherStats);
    }

    private void addApplication(String packageName, String uuid) {
        ApplicationInfo info = new ApplicationInfo();
        info.packageName = packageName;
        info.volumeUuid = uuid;
        mApps.add(info);
    }

}