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

Commit 25adb935 authored by Willie Koomson's avatar Willie Koomson Committed by Android (Google) Code Review
Browse files

Merge changes I89b03bd9,Ide0a9fdd into main

* changes:
  Allow Play Store to pin widgets for other apps
  Convert AppWidgetServiceImplTest to JUnit4
parents 48ea9ec9 a33f399c
Loading
Loading
Loading
Loading
+11 −1
Original line number Diff line number Diff line
@@ -110,3 +110,13 @@ flag {
  description: "Remote document features in Q4 2025 release"
  bug: "412684851"
}

flag {
  name: "play_store_pin_widgets"
  namespace: "app_widgets"
  description: "Allow apps with INSTALL_PACKAGES to pin widgets for other packages"
  bug: "414841181"
  metadata {
    purpose: PURPOSE_BUGFIX
  }
}
+22 −8
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.server.appwidget;

import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL;
import static android.appwidget.flags.Flags.playStorePinWidgets;
import static android.appwidget.flags.Flags.remoteAdapterConversion;
import static android.appwidget.flags.Flags.remoteViewsProto;
import static android.appwidget.flags.Flags.removeAppWidgetServiceIoFromCriticalPath;
@@ -138,6 +139,7 @@ import android.widget.RemoteViews;

import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.SuspendedAppActivity;
import com.android.internal.app.UnlaunchableAppActivity;
import com.android.internal.appwidget.IAppWidgetHost;
@@ -349,6 +351,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
    // and package events, as well as various internal events within
    // AppWidgetService.
    private Handler mCallbackHandler;
    // ServiceThread on which the callback handler runs
    private ServiceThread mServiceThread;
    // Map of user id to the next app widget id (monotonically increasing integer)
    // that can be allocated for a new app widget.
    // See {@link AppWidgetHost#allocateAppWidgetId}.
@@ -392,10 +396,10 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
            mSaveStateHandler = BackgroundThread.getHandler();
        }
        mSavePreviewsHandler = new Handler(BackgroundThread.get().getLooper());
        final ServiceThread serviceThread = new ServiceThread(TAG,
        mServiceThread = new ServiceThread(TAG,
            android.os.Process.THREAD_PRIORITY_FOREGROUND, false /* allowIo */);
        serviceThread.start();
        mCallbackHandler = new CallbackHandler(serviceThread.getLooper());
        mServiceThread.start();
        mCallbackHandler = new CallbackHandler(mServiceThread.getLooper());
        mBackupRestoreController = new BackupRestoreController();
        mSecurityPolicy = new SecurityPolicy();
        mIsCombinedBroadcastEnabled = DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI,
@@ -443,6 +447,11 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
        return mMaxWidgetBitmapMemory;
    }

    @VisibleForTesting
    ServiceThread getServiceThread() {
        return mServiceThread;
    }

    /**
     * Signals that system services (esp. ActivityManagerService) are ready.
     */
@@ -2537,7 +2546,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
            if (!mPackageManagerInternal.isSameApp(pkg, callingUid, userId)) {
                // If the calling process is requesting to pin appwidgets from another process,
                // check if the calling process has the necessary permission.
                if (!injectHasAccessWidgetsPermission(Binder.getCallingPid(), callingUid)) {
                if (!injectHasPinWidgetsPermission(Binder.getCallingPid(), callingUid)) {
                    return false;
                }
                id = new ProviderId(mPackageManagerInternal.getPackageUid(
@@ -2563,9 +2572,14 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
    /**
     * Returns true if the caller has the proper permission to access app widgets.
     */
    private boolean injectHasAccessWidgetsPermission(int callingPid, int callingUid) {
        return mContext.checkPermission(Manifest.permission.CLEAR_APP_USER_DATA,
                callingPid, callingUid) == PackageManager.PERMISSION_GRANTED;
    private boolean injectHasPinWidgetsPermission(int callingPid, int callingUid) {
        boolean hasClearAppUserData = mContext.checkPermission(
                Manifest.permission.CLEAR_APP_USER_DATA, callingPid, callingUid)
                == PackageManager.PERMISSION_GRANTED;
        boolean hasInstallPackages = playStorePinWidgets() && mContext.checkPermission(
                Manifest.permission.INSTALL_PACKAGES, callingPid, callingUid)
                == PackageManager.PERMISSION_GRANTED;
        return hasClearAppUserData || hasInstallPackages;
    }

    /**
+74 −12
Original line number Diff line number Diff line
@@ -20,8 +20,14 @@ import static android.appwidget.flags.Flags.remoteAdapterConversion;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
@@ -32,9 +38,11 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManagerInternal;
import android.app.UiAutomation;
import android.app.admin.DevicePolicyManagerInternal;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetManagerInternal;
@@ -54,14 +62,15 @@ import android.os.Handler;
import android.os.UserHandle;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.test.InstrumentationTestCase;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.SparseArray;
import android.util.Xml;
import android.widget.RemoteViews;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.frameworks.servicestests.R;
import com.android.internal.appwidget.IAppWidgetHost;
@@ -69,7 +78,11 @@ import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.LocalServices;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -88,15 +101,11 @@ import java.util.concurrent.CountDownLatch;

/**
 * Tests for {@link AppWidgetManager} and {@link AppWidgetServiceImpl}.
 *
 m FrameworksServicesTests &&
 adb install \
 -r -g ${ANDROID_PRODUCT_OUT}/data/app/FrameworksServicesTests/FrameworksServicesTests.apk &&
 adb shell am instrument -e class com.android.server.appwidget.AppWidgetServiceImplTest \
 -w com.android.frameworks.servicestests/androidx.test.runner.AndroidJUnitRunner
 * To run: atest FrameworksServicesTests:AppWidgetServiceImplTest
 */
@SmallTest
public class AppWidgetServiceImplTest extends InstrumentationTestCase {
@RunWith(AndroidJUnit4.class)
public class AppWidgetServiceImplTest {

    @Rule
    public SetFlagsRule setFlagsRule = new SetFlagsRule();
@@ -112,10 +121,10 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
    private PackageManagerInternal mMockPackageManager;
    private AppOpsManagerInternal mMockAppOpsManagerInternal;
    private IAppWidgetHost mMockHost;
    private UiAutomation mUiAutomation;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
    @Before
    public void setUp() throws Exception {
        LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class);
        LocalServices.removeServiceForTest(ShortcutServiceInternal.class);
        LocalServices.removeServiceForTest(AppWidgetManagerInternal.class);
@@ -138,14 +147,23 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
                .thenReturn(false);
        mService.onStart();
        mService.systemServicesReady();

        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
    }

    @After
    public void tearDown() {
        mUiAutomation.dropShellPermissionIdentity();
    }

    @Test
    public void testLoadDescription() {
        AppWidgetProviderInfo info =
                mManager.getInstalledProvidersForPackage(mPkgName, null).get(0);
        assertEquals(info.loadDescription(mTestContext), "widget description string");
    }

    @Test
    public void testParseSizeConfiguration() {
        AppWidgetProviderInfo info =
                mManager.getInstalledProvidersForPackage(mPkgName, null).get(0);
@@ -166,6 +184,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
                .isEqualTo(getIntegerResource(R.integer.widget_target_cell_height));
    }

    @Test
    public void testRequestPinAppWidget_otherProvider() {
        ComponentName otherProvider = null;
        for (AppWidgetProviderInfo provider : mManager.getInstalledProviders()) {
@@ -180,6 +199,28 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        assertFalse(mManager.requestPinAppWidget(otherProvider, null, null));
    }

    @Test
    @EnableFlags(Flags.FLAG_PLAY_STORE_PIN_WIDGETS)
    public void testRequestPinAppWidget_otherProvider_installPackagesPermission() {
        mUiAutomation.adoptShellPermissionIdentity(Manifest.permission.INSTALL_PACKAGES);
        ComponentName otherProvider = null;
        int uid = 0;
        for (AppWidgetProviderInfo provider : mManager.getInstalledProviders()) {
            if (!provider.provider.getPackageName().equals(mTestContext.getPackageName())) {
                otherProvider = provider.provider;
                uid = provider.providerInfo.applicationInfo.uid;
                break;
            }
        }
        assumeNotNull(otherProvider);
        when(mMockShortcutService.requestPinAppWidget(anyString(), any(AppWidgetProviderInfo.class),
                eq(null), eq(null), anyInt())).thenReturn(true);
        when(mMockPackageManager.getPackageUid(eq(otherProvider.getPackageName()), anyLong(),
                anyInt())).thenReturn(uid);
        assertTrue(mManager.requestPinAppWidget(otherProvider, null, null));
    }

    @Test
    public void testRequestPinAppWidget() {
        ComponentName provider = new ComponentName(mTestContext, TestAppWidgetProvider.class);
        doReturn(true).when(mMockPackageManager).isSameApp(eq(mPkgName), anyInt(), anyInt());
@@ -196,6 +237,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        assertEquals(provider, providerCaptor.getValue().provider);
    }

    @Test
    public void testIsRequestPinAppWidgetSupported() {
        // Set up users.
        when(mMockShortcutService.isRequestPinItemSupported(anyInt(), anyInt()))
@@ -208,6 +250,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
    }

    @EnableFlags(Flags.FLAG_REMOTE_ADAPTER_CONVERSION)
    @Test
    public void testProviderUpdatesReceived() throws Exception {
        int widgetId = setupHostAndWidget();
        RemoteViews view = new RemoteViews(mPkgName, android.R.layout.simple_list_item_1);
@@ -225,6 +268,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        verify(mMockHost, never()).viewDataChanged(anyInt(), anyInt());
    }

    @Test
    public void testProviderUpdatesNotReceived() throws Exception {
        int widgetId = setupHostAndWidget();
        mService.stopListening(mPkgName, HOST_ID);
@@ -237,6 +281,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        verify(mMockHost, times(0)).viewDataChanged(anyInt(), eq(22));
    }

    @Test
    public void testNoUpdatesReceived_queueEmpty() {
        int widgetId = setupHostAndWidget();
        RemoteViews view = new RemoteViews(mPkgName, android.R.layout.simple_list_item_1);
@@ -270,6 +315,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        }
    }

    @Test
    public void testNoUpdatesReceived_queueNonEmpty_noWidgetId() {
        int widgetId = setupHostAndWidget();
        mService.stopListening(mPkgName, HOST_ID);
@@ -280,6 +326,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        assertTrue(updates.isEmpty());
    }

    @Test
    public void testUpdatesReceived_queueNotEmpty_widgetIdProvided() {
        int widgetId = setupHostAndWidget();
        int widgetId2 = bindNewWidget();
@@ -294,6 +341,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        assertEquals(remoteAdapterConversion() ? 1 : 3, updates.size());
    }

    @Test
    public void testUpdatesReceived_queueNotEmpty_widgetIdProvided2() {
        int widgetId = setupHostAndWidget();
        int widgetId2 = bindNewWidget();
@@ -308,6 +356,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        assertEquals(remoteAdapterConversion() ? 1 : 4, updates.size());
    }

    @Test
    public void testReceiveBroadcastBehavior_enableAndUpdate() {
        TestAppWidgetProvider testAppWidgetProvider = new TestAppWidgetProvider();
        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLE_AND_UPDATE)
@@ -318,6 +367,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        assertTrue(testAppWidgetProvider.isBehaviorSuccess());
    }

    @Test
    public void testUpdatesReceived_queueNotEmpty_multipleWidgetIdProvided() {
        int widgetId = setupHostAndWidget();
        int widgetId2 = bindNewWidget();
@@ -332,6 +382,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        assertEquals(remoteAdapterConversion() ? 2 : 7 , updates.size());
    }

    @Test
    public void testUpdatesReceived_queueEmptyAfterStartListening() {
        int widgetId = setupHostAndWidget();
        int widgetId2 = bindNewWidget();
@@ -352,11 +403,13 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        assertTrue(updates.isEmpty());
    }

    @Test
    public void testIsFirstConfigActivityPending_without_config() {
        int widgetId = setupHostAndWidget();
        assertFalse(mService.isFirstConfigActivityPending(mPkgName, widgetId));
    }

    @Test
    public void testIsFirstConfigActivityPending_with_config() {
        mManager.updateAppWidgetProviderInfo(
                new ComponentName(mTestContext, TestAppWidgetProvider.class), "info_with_config");
@@ -367,6 +420,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        assertFalse(mService.isFirstConfigActivityPending(mPkgName, widgetId));
    }

    @Test
    public void testGetInstalledProvidersForPackage() {
        List<AppWidgetProviderInfo> allProviders = mManager.getInstalledProviders();
        assertTrue(!allProviders.isEmpty());
@@ -386,6 +440,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        }
    }

    @Test
    public void testGetPreviewLayout() {
        AppWidgetProviderInfo info =
                mManager.getInstalledProvidersForPackage(mPkgName, null).get(0);
@@ -393,6 +448,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        assertThat(info.previewLayout).isEqualTo(R.layout.widget_preview);
    }

    @Test
    public void testWidgetProviderInfoPersistence() throws IOException {
        final AppWidgetProviderInfo original = new AppWidgetProviderInfo();
        original.minWidth = 40;
@@ -421,6 +477,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
        assertThat(target.previewLayout).isEqualTo(original.previewLayout);
    }

    @Test
    public void testBackupRestoreControllerStatePersistence() throws IOException {
        // Setup mock data
        final Set<String> mockPrunedApps = getMockPrunedApps();
@@ -526,8 +583,13 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {

    private void flushMainThread() throws Exception {
        CountDownLatch latch = new CountDownLatch(1);
        // Wait for main thread
        new Handler(mTestContext.getMainLooper()).post(latch::countDown);
        latch.await();
        // Wait for service thread
        latch = new CountDownLatch(1);
        mService.getServiceThread().getThreadHandler().post(latch::countDown);
        latch.await();
    }

    private int getDimensionResource(int resId) {
@@ -613,7 +675,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase {
    private class TestContext extends ContextWrapper {

        public TestContext() {
            super(getInstrumentation().getContext());
            super(InstrumentationRegistry.getInstrumentation().getTargetContext());
        }

        @Override