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

Commit 16ac7c3b authored by Vadim Tryshev's avatar Vadim Tryshev Committed by Android (Google) Code Review
Browse files

Merge "Checking for unlauncheable apps."

parents 5afe8f20 1bec0c47
Loading
Loading
Loading
Loading
+84 −52
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ import android.animation.LayoutTransition;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.AppGlobals;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ClipDescription;
@@ -29,13 +28,10 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.ActivityInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.AttributeSet;
@@ -49,6 +45,7 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Toast;

import com.android.internal.content.PackageMonitor;
import com.android.systemui.R;

import java.util.List;
@@ -75,6 +72,8 @@ class NavigationBarApps extends LinearLayout {
    private final PackageManager mPackageManager;
    private final UserManager mUserManager;
    private final LayoutInflater mLayoutInflater;
    private final AppPackageMonitor mAppPackageMonitor;


    // This view has two roles:
    // 1) If the drag started outside the pinned apps list, it is a placeholder icon with a null
@@ -106,6 +105,7 @@ class NavigationBarApps extends LinearLayout {
        mPackageManager = context.getPackageManager();
        mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
        mLayoutInflater = LayoutInflater.from(context);
        mAppPackageMonitor = new AppPackageMonitor();

        // Dragging an icon removes and adds back the dragged icon. Use the layout transitions to
        // trigger animation. By default all transitions animate, so turn off the unneeded ones.
@@ -121,6 +121,76 @@ class NavigationBarApps extends LinearLayout {
        setLayoutTransition(transition);
    }

    // Monitor that catches events like "app uninstalled".
    private class AppPackageMonitor extends PackageMonitor {
        @Override
        public void onPackageRemoved(String packageName, int uid) {
            postRemoveIfUnlauncheable(packageName, new UserHandle(getChangingUserId()));
            super.onPackageRemoved(packageName, uid);
        }

        @Override
        public void onPackageModified(String packageName) {
            postRemoveIfUnlauncheable(packageName, new UserHandle(getChangingUserId()));
            super.onPackageModified(packageName);
        }

        @Override
        public void onPackagesAvailable(String[] packages) {
            if (isReplacing()) {
                UserHandle user = new UserHandle(getChangingUserId());

                for (String packageName : packages) {
                    postRemoveIfUnlauncheable(packageName, user);
                }
            }
            super.onPackagesAvailable(packages);
        }

        @Override
        public void onPackagesUnavailable(String[] packages) {
            if (!isReplacing()) {
                UserHandle user = new UserHandle(getChangingUserId());

                for (String packageName : packages) {
                    postRemoveIfUnlauncheable(packageName, user);
                }
            }
            super.onPackagesUnavailable(packages);
        }
    }

    private void postRemoveIfUnlauncheable(final String packageName, final UserHandle user) {
        // This method doesn't necessarily get called in the main thread. Redirect the call into
        // the main thread.
        post(new Runnable() {
            @Override
            public void run() {
                if (!isAttachedToWindow()) return;
                removeIfUnlauncheable(packageName, user);
            }
        });
    }

    private void removeIfUnlauncheable(String packageName, UserHandle user) {
        long appUserSerialNumber = mUserManager.getSerialNumberForUser(user);

        // Remove icons for all apps that match a package that perhaps became unlauncheable.
        for(int i = sAppsModel.getAppCount() - 1; i >= 0; --i) {
            AppInfo appInfo = sAppsModel.getApp(i);
            if (appInfo.getUserSerialNumber() != appUserSerialNumber) continue;

            ComponentName appComponentName = appInfo.getComponentName();
            if (!appComponentName.getPackageName().equals(packageName)) continue;

            if (sAppsModel.buildAppLaunchIntent(appComponentName, user) != null) continue;

            removeViewAt(i);
            sAppsModel.removeApp(i);
            sAppsModel.savePrefs();
        }
    }

    @Override
    protected void onAttachedToWindow() {
      super.onAttachedToWindow();
@@ -145,12 +215,15 @@ class NavigationBarApps extends LinearLayout {
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_USER_SWITCHED);
        mContext.registerReceiver(mBroadcastReceiver, filter);

        mAppPackageMonitor.register(mContext, null, UserHandle.ALL, true);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mContext.unregisterReceiver(mBroadcastReceiver);
        mAppPackageMonitor.unregister();
    }

    /**
@@ -470,7 +543,6 @@ class NavigationBarApps extends LinearLayout {
            ComponentName component = appInfo.getComponentName();

            long appUserSerialNumber = appInfo.getUserSerialNumber();

            UserHandle appUser = mUserManager.getUserForSerialNumber(appUserSerialNumber);
            if (appUser == null) {
                Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show();
@@ -478,7 +550,12 @@ class NavigationBarApps extends LinearLayout {
                        " because its user doesn't exist.");
                return;
            }
            int appUserId = appUser.getIdentifier();

            Intent launchIntent = sAppsModel.buildAppLaunchIntent(component, appUser);
            if (launchIntent == null) {
                Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show();
                return;
            }

            // Play a scale-up animation while launching the activity.
            // TODO: Consider playing a different animation, or no animation, if the activity is
@@ -489,54 +566,9 @@ class NavigationBarApps extends LinearLayout {
            ActivityOptions opts =
                    ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight());
            Bundle optsBundle = opts.toBundle();

            // Launch the activity. This code is based on LauncherAppsService.startActivityAsUser code.
            Intent launchIntent = new Intent(Intent.ACTION_MAIN);
            launchIntent.addCategory(Intent.CATEGORY_LAUNCHER);
            launchIntent.setSourceBounds(sourceBounds);
            launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            launchIntent.setPackage(component.getPackageName());

            IPackageManager pm = AppGlobals.getPackageManager();
            try {
                ActivityInfo info = pm.getActivityInfo(component, 0, appUserId);
                if (info == null) {
                    Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show();
                    Log.e(TAG, "Can't start activity " + component + " because it's not installed.");
                    return;
                }

                if (!info.exported) {
                    Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show();
                    Log.e(TAG, "Can't start activity " + component + " because it doesn't have 'exported' attribute.");
                    return;
                }
            } catch (RemoteException e) {
                Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show();
                Log.e(TAG, "Failed to get activity info for " + component, e);
                return;
            }

            // Check that the component actually has Intent.CATEGORY_LAUCNCHER
            // as calling startActivityAsUser ignores the category and just
            // resolves based on the component if present.
            List<ResolveInfo> apps = getContext().getPackageManager().queryIntentActivitiesAsUser(launchIntent,
                    0 /* flags */, appUserId);
            final int size = apps.size();
            for (int i = 0; i < size; ++i) {
                ActivityInfo activityInfo = apps.get(i).activityInfo;
                if (activityInfo.packageName.equals(component.getPackageName()) &&
                        activityInfo.name.equals(component.getClassName())) {
                    // Found an activity with category launcher that matches
                    // this component so ok to launch.
                    launchIntent.setComponent(component);
            mContext.startActivityAsUser(launchIntent, optsBundle, appUser);
                    return;
                }
            }

            Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show();
            Log.e(TAG, "Attempt to launch activity without category Intent.CATEGORY_LAUNCHER " + component);
        }
    }

+71 −1
Original line number Diff line number Diff line
@@ -16,16 +16,23 @@

package com.android.systemui.statusbar.phone;

import android.app.AppGlobals;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.IPackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -94,6 +101,58 @@ class NavigationBarAppsModel {
        }
    }

    @VisibleForTesting
    protected IPackageManager getPackageManager() {
        return AppGlobals.getPackageManager();
    }

    // Returns a launch intent for a given component, or null if the component is unlauncheable.
    public Intent buildAppLaunchIntent(ComponentName component, UserHandle appUser) {
        int appUserId = appUser.getIdentifier();

        // This code is based on LauncherAppsService.startActivityAsUser code.
        Intent launchIntent = new Intent(Intent.ACTION_MAIN);
        launchIntent.addCategory(Intent.CATEGORY_LAUNCHER);
        launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        launchIntent.setPackage(component.getPackageName());

        try {
            ActivityInfo info = getPackageManager().getActivityInfo(component, 0, appUserId);
            if (info == null) {
                Log.e(TAG, "Activity " + component + " is not installed.");
                return null;
            }

            if (!info.exported) {
                Log.e(TAG, "Activity " + component + " doesn't have 'exported' attribute.");
                return null;
            }
        } catch (RemoteException e) {
            Log.e(TAG, "Failed to get activity info for " + component, e);
            return null;
        }

        // Check that the component actually has Intent.CATEGORY_LAUNCHER
        // as calling startActivityAsUser ignores the category and just
        // resolves based on the component if present.
        List<ResolveInfo> apps = mContext.getPackageManager().queryIntentActivitiesAsUser(launchIntent,
                0 /* flags */, appUserId);
        final int size = apps.size();
        for (int i = 0; i < size; ++i) {
            ActivityInfo activityInfo = apps.get(i).activityInfo;
            if (activityInfo.packageName.equals(component.getPackageName()) &&
                    activityInfo.name.equals(component.getClassName())) {
                // Found an activity with category launcher that matches
                // this component so ok to launch.
                launchIntent.setComponent(component);
                return launchIntent;
            }
        }

        Log.e(TAG, "Activity doesn't have category Intent.CATEGORY_LAUNCHER " + component);
        return null;
    }

    /**
     * Reinitializes the model for a new user.
     */
@@ -199,6 +258,10 @@ class NavigationBarAppsModel {

    /** Loads the list of apps from SharedPreferences. */
    private void loadAppsFromPrefs() {
        UserManager mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);

        boolean hadUnlauncheableApps = false;

        int appCount = mPrefs.getInt(userPrefixed(APP_COUNT_PREF), -1);
        for (int i = 0; i < appCount; i++) {
            String prefValue = mPrefs.getString(prefNameForApp(i), null);
@@ -214,10 +277,17 @@ class NavigationBarAppsModel {
                // Couldn't find the saved state. Just skip this item.
                continue;
            }
            UserHandle appUser = mUserManager.getUserForSerialNumber(userSerialNumber);
            if (appUser != null && buildAppLaunchIntent(componentName, appUser) != null) {
                mApps.add(new AppInfo(componentName, userSerialNumber));
            } else {
                hadUnlauncheableApps = true;
            }
        }

        if (hadUnlauncheableApps) savePrefs();
    }

    /** Adds the first few apps from the owner profile. Used for demo purposes. */
    private void addDefaultApps() {
        // Get a list of all app activities.
+174 −1
Original line number Diff line number Diff line
@@ -16,21 +16,26 @@

package com.android.systemui.statusbar.phone;

import org.mockito.InOrder;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.test.AndroidTestCase;
@@ -45,6 +50,7 @@ import java.util.Map;
/** Tests for the data model for the navigation bar app icons. */
public class NavigationBarAppsModelTest extends AndroidTestCase {
    private PackageManager mMockPackageManager;
    private IPackageManager mMockIPackageManager;
    private SharedPreferences mMockPrefs;
    private SharedPreferences.Editor mMockEdit;
    private UserManager mMockUserManager;
@@ -61,6 +67,7 @@ public class NavigationBarAppsModelTest extends AndroidTestCase {

        final Context context = mock(Context.class);
        mMockPackageManager = mock(PackageManager.class);
        mMockIPackageManager = mock(IPackageManager.class);
        mMockPrefs = mock(SharedPreferences.class);
        mMockEdit = mock(SharedPreferences.Editor.class);
        mMockUserManager = mock(UserManager.class);
@@ -78,8 +85,71 @@ public class NavigationBarAppsModelTest extends AndroidTestCase {
        when(mMockPrefs.edit()).thenReturn(mMockEdit);

        when(mMockUserManager.getSerialNumberForUser(new UserHandle(2))).thenReturn(22L);
        when(mMockUserManager.getUserForSerialNumber(45L)).thenReturn(new UserHandle(4));
        when(mMockUserManager.getUserForSerialNumber(239L)).thenReturn(new UserHandle(5));

        mModel = new NavigationBarAppsModel(context);
        mModel = new NavigationBarAppsModel(context) {
            @Override
            protected IPackageManager getPackageManager() {
                return mMockIPackageManager;
            }
        };
    }

    /** Tests buildAppLaunchIntent(). */
    public void testBuildAppLaunchIntent() {
        ActivityInfo mockNonExportedActivityInfo = new ActivityInfo();
        mockNonExportedActivityInfo.exported = false;
        ActivityInfo mockExportedActivityInfo = new ActivityInfo();
        mockExportedActivityInfo.exported = true;
        try {
            when(mMockIPackageManager.getActivityInfo(
                    new ComponentName("package1", "class1"), 0, 4)).
                    thenReturn(mockNonExportedActivityInfo);
            when(mMockIPackageManager.getActivityInfo(
                    new ComponentName("package2", "class2"), 0, 5)).
                    thenThrow(new RemoteException());
            when(mMockIPackageManager.getActivityInfo(
                    new ComponentName("package3", "class3"), 0, 6)).
                    thenReturn(mockExportedActivityInfo);
            when(mMockIPackageManager.getActivityInfo(
                    new ComponentName("package4", "class4"), 0, 7)).
                    thenReturn(mockExportedActivityInfo);
        } catch (RemoteException e) {
            fail("RemoteException can't happen in the test, but it happened.");
        }

        // Assume some installed activities.
        ActivityInfo ai0 = new ActivityInfo();
        ai0.packageName = "package0";
        ai0.name = "class0";
        ActivityInfo ai1 = new ActivityInfo();
        ai1.packageName = "package4";
        ai1.name = "class4";
        ResolveInfo ri0 = new ResolveInfo();
        ri0.activityInfo = ai0;
        ResolveInfo ri1 = new ResolveInfo();
        ri1.activityInfo = ai1;
        when(mMockPackageManager
                .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class)))
                .thenReturn(Arrays.asList(ri0, ri1));

        // Unlauncheable (for various reasons) apps.
        assertEquals(null, mModel.buildAppLaunchIntent(
                new ComponentName("package0", "class0"), new UserHandle(3)));
        assertEquals(null, mModel.buildAppLaunchIntent(
                new ComponentName("package1", "class1"), new UserHandle(4)));
        assertEquals(null, mModel.buildAppLaunchIntent(
                new ComponentName("package2", "class2"), new UserHandle(5)));
        assertEquals(null, mModel.buildAppLaunchIntent(
                new ComponentName("package3", "class3"), new UserHandle(6)));

        // A launcheable app.
        Intent intent = mModel.buildAppLaunchIntent(
                new ComponentName("package4", "class4"), new UserHandle(7));
        assertNotNull(intent);
        assertEquals(new ComponentName("package4", "class4"), intent.getComponent());
        assertEquals("package4", intent.getPackage());
    }

    /** Initializes the model from SharedPreferences for a few app activites. */
@@ -93,6 +163,39 @@ public class NavigationBarAppsModelTest extends AndroidTestCase {
        when(mMockPrefs.getString("22|app_2", null)).thenReturn("package2/class2");
        when(mMockPrefs.getLong("22|app_user_2", -1)).thenReturn(239L);

        ActivityInfo mockActivityInfo = new ActivityInfo();
        mockActivityInfo.exported = true;
        try {
            when(mMockIPackageManager.getActivityInfo(
                    new ComponentName("package0", "class0"), 0, 5)).thenReturn(mockActivityInfo);
            when(mMockIPackageManager.getActivityInfo(
                    new ComponentName("package1", "class1"), 0, 4)).thenReturn(mockActivityInfo);
            when(mMockIPackageManager.getActivityInfo(
                    new ComponentName("package2", "class2"), 0, 5)).thenReturn(mockActivityInfo);
        } catch (RemoteException e) {
            fail("RemoteException can't happen in the test, but it happened.");
        }

        // Assume some installed activities.
        ActivityInfo ai0 = new ActivityInfo();
        ai0.packageName = "package0";
        ai0.name = "class0";
        ActivityInfo ai1 = new ActivityInfo();
        ai1.packageName = "package1";
        ai1.name = "class1";
        ActivityInfo ai2 = new ActivityInfo();
        ai2.packageName = "package2";
        ai2.name = "class2";
        ResolveInfo ri0 = new ResolveInfo();
        ri0.activityInfo = ai0;
        ResolveInfo ri1 = new ResolveInfo();
        ri1.activityInfo = ai1;
        ResolveInfo ri2 = new ResolveInfo();
        ri2.activityInfo = ai2;
        when(mMockPackageManager
                .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class)))
                .thenReturn(Arrays.asList(ri0, ri1, ri2));

        mModel.setCurrentUser(2);
    }

@@ -133,6 +236,15 @@ public class NavigationBarAppsModelTest extends AndroidTestCase {
        assertEquals(22L, mModel.getApp(0).getUserSerialNumber());
        assertEquals("package2/class2", mModel.getApp(1).getComponentName().flattenToString());
        assertEquals(22L, mModel.getApp(1).getUserSerialNumber());
        InOrder order = inOrder(mMockEdit);
        order.verify(mMockEdit).apply();
        order.verify(mMockEdit).putInt("22|app_count", 2);
        order.verify(mMockEdit).putString("22|app_0", "package1/class1");
        order.verify(mMockEdit).putLong("22|app_user_0", 22L);
        order.verify(mMockEdit).putString("22|app_1", "package2/class2");
        order.verify(mMockEdit).putLong("22|app_user_1", 22L);
        order.verify(mMockEdit).apply();
        verifyNoMoreInteractions(mMockEdit);
    }

    /** Tests initializing the model if one of the prefs is missing. */
@@ -145,11 +257,72 @@ public class NavigationBarAppsModelTest extends AndroidTestCase {
        // But assume one pref is missing.
        when(mMockPrefs.getString("22|app_1", null)).thenReturn(null);

        ActivityInfo mockActivityInfo = new ActivityInfo();
        mockActivityInfo.exported = true;
        try {
            when(mMockIPackageManager.getActivityInfo(
                    new ComponentName("package0", "class0"), 0, 5)).thenReturn(mockActivityInfo);
        } catch (RemoteException e) {
            fail("RemoteException can't happen in the test, but it happened.");
        }

        ActivityInfo ai0 = new ActivityInfo();
        ai0.packageName = "package0";
        ai0.name = "class0";
        ResolveInfo ri0 = new ResolveInfo();
        ri0.activityInfo = ai0;
        when(mMockPackageManager
                .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class)))
                .thenReturn(Arrays.asList(ri0));

        // Initializing the model should load from prefs and skip the missing one.
        mModel.setCurrentUser(2);
        assertEquals(1, mModel.getAppCount());
        assertEquals("package0/class0", mModel.getApp(0).getComponentName().flattenToString());
        assertEquals(239L, mModel.getApp(0).getUserSerialNumber());
        verifyNoMoreInteractions(mMockEdit);
    }

    /** Tests initializing the model if one of the apps is unlauncheable. */
    public void testInitializeWithUnlauncheableApp() {
        // Assume two apps are nominally stored.
        when(mMockPrefs.getInt("22|app_count", -1)).thenReturn(2);
        when(mMockPrefs.getString("22|app_0", null)).thenReturn("package0/class0");
        when(mMockPrefs.getLong("22|app_user_0", -1)).thenReturn(239L);
        when(mMockPrefs.getString("22|app_1", null)).thenReturn("package1/class1");
        when(mMockPrefs.getLong("22|app_user_1", -1)).thenReturn(45L);

        ActivityInfo mockActivityInfo = new ActivityInfo();
        mockActivityInfo.exported = true;
        try {
            when(mMockIPackageManager.getActivityInfo(
                    new ComponentName("package0", "class0"), 0, 5)).thenReturn(mockActivityInfo);
        } catch (RemoteException e) {
            fail("RemoteException can't happen in the test, but it happened.");
        }

        ActivityInfo ai0 = new ActivityInfo();
        ai0.packageName = "package0";
        ai0.name = "class0";
        ResolveInfo ri0 = new ResolveInfo();
        ri0.activityInfo = ai0;
        when(mMockPackageManager
                .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class)))
                .thenReturn(Arrays.asList(ri0));

        // Initializing the model should load from prefs and skip the unlauncheable one.
        mModel.setCurrentUser(2);
        assertEquals(1, mModel.getAppCount());
        assertEquals("package0/class0", mModel.getApp(0).getComponentName().flattenToString());
        assertEquals(239L, mModel.getApp(0).getUserSerialNumber());

        // Once an unlauncheable app is detected, the model should save all apps excluding the
        // unlauncheable one.
        verify(mMockEdit).putInt("22|app_count", 1);
        verify(mMockEdit).putString("22|app_0", "package0/class0");
        verify(mMockEdit).putLong("22|app_user_0", 239L);
        verify(mMockEdit).apply();
        verifyNoMoreInteractions(mMockEdit);
    }

    /** Tests saving the model to SharedPreferences. */