diff options
4 files changed, 287 insertions, 2 deletions
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index f19bfc669997..c94438e3cee8 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java @@ -536,6 +536,12 @@ public final class SystemUiDeviceConfigFlags { */ public static final String CLIPBOARD_OVERLAY_ENABLED = "clipboard_overlay_enabled"; + /** + * (boolean) Whether widget provider info would be saved to / loaded from system persistence + * layer as opposed to individual manifests in respective apps. + */ + public static final String PERSISTS_WIDGET_PROVIDER_INFO = "persists_widget_provider_info"; + private SystemUiDeviceConfigFlags() { } } diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index d7554cc42749..bc4b2a6f5247 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -20,9 +20,11 @@ import static android.content.Context.KEYGUARD_SERVICE; import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.res.Resources.ID_NULL; +import static android.provider.DeviceConfig.NAMESPACE_SYSTEMUI; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; +import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; @@ -83,6 +85,7 @@ import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; +import android.provider.DeviceConfig; import android.service.appwidget.AppWidgetServiceDumpProto; import android.service.appwidget.WidgetProto; import android.text.TextUtils; @@ -113,6 +116,7 @@ import com.android.internal.app.SuspendedAppActivity; import com.android.internal.app.UnlaunchableAppActivity; import com.android.internal.appwidget.IAppWidgetHost; import com.android.internal.appwidget.IAppWidgetService; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.os.BackgroundThread; import com.android.internal.os.SomeArgs; import com.android.internal.util.ArrayUtils; @@ -150,6 +154,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku private static final String TAG = "AppWidgetServiceImpl"; private static final boolean DEBUG = false; + private static final boolean DEBUG_PROVIDER_INFO_CACHE = true; private static final String OLD_KEYGUARD_HOST_PACKAGE = "android"; private static final String NEW_KEYGUARD_HOST_PACKAGE = "com.android.keyguard"; @@ -246,6 +251,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku private boolean mSafeMode; private int mMaxWidgetBitmapMemory; + private boolean mIsProviderInfoPersisted; AppWidgetServiceImpl(Context context) { mContext = context; @@ -263,6 +269,12 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku mCallbackHandler = new CallbackHandler(mContext.getMainLooper()); mBackupRestoreController = new BackupRestoreController(); mSecurityPolicy = new SecurityPolicy(); + mIsProviderInfoPersisted = !ActivityManager.isLowRamDeviceStatic() + && DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.PERSISTS_WIDGET_PROVIDER_INFO, true); + if (DEBUG_PROVIDER_INFO_CACHE && !mIsProviderInfoPersisted) { + Slog.d(TAG, "App widget provider info will not be persisted on this device"); + } computeMaximumWidgetBitmapMemory(); registerBroadcastReceiver(); @@ -607,10 +619,12 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + @GuardedBy("mLock") private void ensureGroupStateLoadedLocked(int userId) { ensureGroupStateLoadedLocked(userId, /* enforceUserUnlockingOrUnlocked */ true ); } + @GuardedBy("mLock") private void ensureGroupStateLoadedLocked(int userId, boolean enforceUserUnlockingOrUnlocked) { if (enforceUserUnlockingOrUnlocked && !isUserRunningAndUnlocked(userId)) { throw new IllegalStateException( @@ -2184,6 +2198,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + @GuardedBy("mLock") private void loadGroupWidgetProvidersLocked(int[] profileIds) { List<ResolveInfo> allReceivers = null; Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); @@ -2409,7 +2424,24 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } - private static void serializeProvider(TypedXmlSerializer out, Provider p) throws IOException { + private static void serializeProvider( + @NonNull final TypedXmlSerializer out, @NonNull final Provider p) throws IOException { + Objects.requireNonNull(out); + Objects.requireNonNull(p); + serializeProviderInner(out, p, false /* persistsProviderInfo */); + } + + private static void serializeProviderWithProviderInfo( + @NonNull final TypedXmlSerializer out, @NonNull final Provider p) throws IOException { + Objects.requireNonNull(out); + Objects.requireNonNull(p); + serializeProviderInner(out, p, true /* persistsProviderInfo */); + } + + private static void serializeProviderInner(@NonNull final TypedXmlSerializer out, + @NonNull final Provider p, final boolean persistsProviderInfo) throws IOException { + Objects.requireNonNull(out); + Objects.requireNonNull(p); out.startTag(null, "p"); out.attribute(null, "pkg", p.id.componentName.getPackageName()); out.attribute(null, "cl", p.id.componentName.getClassName()); @@ -2417,6 +2449,12 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku if (!TextUtils.isEmpty(p.infoTag)) { out.attribute(null, "info_tag", p.infoTag); } + if (DEBUG_PROVIDER_INFO_CACHE && persistsProviderInfo && !p.mInfoParsed) { + Slog.d(TAG, "Provider info from " + p.id.componentName + " won't be persisted."); + } + if (persistsProviderInfo && p.mInfoParsed) { + AppWidgetXmlUtil.writeAppWidgetProviderInfoLocked(out, p.info); + } out.endTag(null, "p"); } @@ -2768,6 +2806,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } // only call from initialization -- it assumes that the data structures are all empty + @GuardedBy("mLock") private void loadGroupStateLocked(int[] profileIds) { // We can bind the widgets to host and providers only after // reading the host and providers for all users since a widget @@ -2959,6 +2998,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku return false; } + @GuardedBy("mLock") private void saveStateLocked(int userId) { tagProvidersAndHosts(); @@ -3012,6 +3052,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + @GuardedBy("mLock") private boolean writeProfileStateToFileLocked(FileOutputStream stream, int userId) { int N; @@ -3028,7 +3069,9 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku if (provider.getUserId() != userId) { continue; } - if (provider.shouldBePersisted()) { + if (mIsProviderInfoPersisted) { + serializeProviderWithProviderInfo(out, provider); + } else if (provider.shouldBePersisted()) { serializeProvider(out, provider); } } @@ -3074,6 +3117,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + @GuardedBy("mLock") private int readProfileStateFromFileLocked(FileInputStream stream, int userId, List<LoadedWidgetState> outLoadedWidgets) { int version = -1; @@ -3127,6 +3171,18 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku provider.zombie = true; provider.id = providerId; mProviders.add(provider); + } else if (mIsProviderInfoPersisted) { + final AppWidgetProviderInfo info = + AppWidgetXmlUtil.readAppWidgetProviderInfoLocked(parser); + if (DEBUG_PROVIDER_INFO_CACHE && info == null) { + Slog.d(TAG, "Unable to load widget provider info from xml for " + + providerId.componentName); + } + if (info != null) { + info.provider = providerId.componentName; + info.providerInfo = providerInfo; + provider.setInfoLocked(info); + } } final int providerTag = parser.getAttributeIntHex(null, "tag", diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java new file mode 100644 index 000000000000..297575ca168f --- /dev/null +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2022 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.appwidget; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.os.Build; +import android.text.TextUtils; +import android.util.TypedXmlPullParser; +import android.util.TypedXmlSerializer; + +import java.io.IOException; +import java.util.Objects; + +/** + * @hide + */ +public class AppWidgetXmlUtil { + + private static final String ATTR_MIN_WIDTH = "min_width"; + private static final String ATTR_MIN_HEIGHT = "min_height"; + private static final String ATTR_MIN_RESIZE_WIDTH = "min_resize_width"; + private static final String ATTR_MIN_RESIZE_HEIGHT = "min_resize_height"; + private static final String ATTR_MAX_RESIZE_WIDTH = "max_resize_width"; + private static final String ATTR_MAX_RESIZE_HEIGHT = "max_resize_height"; + private static final String ATTR_TARGET_CELL_WIDTH = "target_cell_width"; + private static final String ATTR_TARGET_CELL_HEIGHT = "target_cell_height"; + private static final String ATTR_UPDATE_PERIOD_MILLIS = "update_period_millis"; + private static final String ATTR_INITIAL_LAYOUT = "initial_layout"; + private static final String ATTR_INITIAL_KEYGUARD_LAYOUT = "initial_keyguard_layout"; + private static final String ATTR_CONFIGURE = "configure"; + private static final String ATTR_LABEL = "label"; + private static final String ATTR_ICON = "icon"; + private static final String ATTR_PREVIEW_IMAGE = "preview_image"; + private static final String ATTR_PREVIEW_LAYOUT = "preview_layout"; + private static final String ATTR_AUTO_ADVANCED_VIEW_ID = "auto_advance_view_id"; + private static final String ATTR_RESIZE_MODE = "resize_mode"; + private static final String ATTR_WIDGET_CATEGORY = "widget_category"; + private static final String ATTR_WIDGET_FEATURES = "widget_features"; + private static final String ATTR_DESCRIPTION_RES = "description_res"; + private static final String ATTR_OS_FINGERPRINT = "os_fingerprint"; + + /** + * @hide + */ + public static void writeAppWidgetProviderInfoLocked(@NonNull final TypedXmlSerializer out, + @NonNull final AppWidgetProviderInfo info) throws IOException { + Objects.requireNonNull(out); + Objects.requireNonNull(info); + out.attributeInt(null, ATTR_MIN_WIDTH, info.minWidth); + out.attributeInt(null, ATTR_MIN_HEIGHT, info.minHeight); + out.attributeInt(null, ATTR_MIN_RESIZE_WIDTH, info.minResizeWidth); + out.attributeInt(null, ATTR_MIN_RESIZE_HEIGHT, info.minResizeHeight); + out.attributeInt(null, ATTR_MAX_RESIZE_WIDTH, info.maxResizeWidth); + out.attributeInt(null, ATTR_MAX_RESIZE_HEIGHT, info.maxResizeHeight); + out.attributeInt(null, ATTR_TARGET_CELL_WIDTH, info.targetCellWidth); + out.attributeInt(null, ATTR_TARGET_CELL_HEIGHT, info.targetCellHeight); + out.attributeInt(null, ATTR_UPDATE_PERIOD_MILLIS, info.updatePeriodMillis); + out.attributeInt(null, ATTR_INITIAL_LAYOUT, info.initialLayout); + out.attributeInt(null, ATTR_INITIAL_KEYGUARD_LAYOUT, info.initialKeyguardLayout); + if (info.configure != null) { + out.attribute(null, ATTR_CONFIGURE, info.configure.flattenToShortString()); + } + out.attribute(null, ATTR_LABEL, info.label); + out.attributeInt(null, ATTR_ICON, info.icon); + out.attributeInt(null, ATTR_PREVIEW_IMAGE, info.previewImage); + out.attributeInt(null, ATTR_PREVIEW_LAYOUT, info.previewLayout); + out.attributeInt(null, ATTR_AUTO_ADVANCED_VIEW_ID, info.autoAdvanceViewId); + out.attributeInt(null, ATTR_RESIZE_MODE, info.resizeMode); + out.attributeInt(null, ATTR_WIDGET_CATEGORY, info.widgetCategory); + out.attributeInt(null, ATTR_WIDGET_FEATURES, info.widgetFeatures); + out.attributeInt(null, ATTR_DESCRIPTION_RES, info.descriptionRes); + out.attribute(null, ATTR_OS_FINGERPRINT, Build.FINGERPRINT); + } + + /** + * @hide + */ + @Nullable + public static AppWidgetProviderInfo readAppWidgetProviderInfoLocked( + @NonNull final TypedXmlPullParser parser) { + Objects.requireNonNull(parser); + final String fingerprint = parser.getAttributeValue(null, ATTR_OS_FINGERPRINT); + if (!Build.FINGERPRINT.equals(fingerprint)) { + return null; + } + final AppWidgetProviderInfo info = new AppWidgetProviderInfo(); + info.minWidth = parser.getAttributeInt(null, ATTR_MIN_WIDTH, 0); + info.minHeight = parser.getAttributeInt(null, ATTR_MIN_HEIGHT, 0); + info.minResizeWidth = parser.getAttributeInt(null, ATTR_MIN_RESIZE_WIDTH, 0); + info.minResizeWidth = parser.getAttributeInt(null, ATTR_MIN_RESIZE_HEIGHT, 0); + info.maxResizeWidth = parser.getAttributeInt(null, ATTR_MAX_RESIZE_WIDTH, 0); + info.maxResizeHeight = parser.getAttributeInt(null, ATTR_MAX_RESIZE_HEIGHT, 0); + info.targetCellWidth = parser.getAttributeInt(null, ATTR_TARGET_CELL_WIDTH, 0); + info.targetCellHeight = parser.getAttributeInt(null, ATTR_TARGET_CELL_HEIGHT, 0); + info.updatePeriodMillis = parser.getAttributeInt(null, ATTR_UPDATE_PERIOD_MILLIS, 0); + info.initialLayout = parser.getAttributeInt(null, ATTR_INITIAL_LAYOUT, 0); + info.initialKeyguardLayout = parser.getAttributeInt( + null, ATTR_INITIAL_KEYGUARD_LAYOUT, 0); + final String configure = parser.getAttributeValue(null, ATTR_CONFIGURE); + if (!TextUtils.isEmpty(configure)) { + info.configure = ComponentName.unflattenFromString(configure); + } + info.label = parser.getAttributeValue(null, ATTR_LABEL); + info.icon = parser.getAttributeInt(null, ATTR_ICON, 0); + info.previewImage = parser.getAttributeInt(null, ATTR_PREVIEW_IMAGE, 0); + info.previewLayout = parser.getAttributeInt(null, ATTR_PREVIEW_LAYOUT, 0); + info.autoAdvanceViewId = parser.getAttributeInt(null, ATTR_AUTO_ADVANCED_VIEW_ID, 0); + info.resizeMode = parser.getAttributeInt(null, ATTR_RESIZE_MODE, 0); + info.widgetCategory = parser.getAttributeInt(null, ATTR_WIDGET_CATEGORY, 0); + info.widgetFeatures = parser.getAttributeInt(null, ATTR_WIDGET_FEATURES, 0); + info.descriptionRes = parser.getAttributeInt(null, ATTR_DESCRIPTION_RES, 0); + return info; + } +} diff --git a/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java index ff8fedce9368..7610b7ca5ec3 100644 --- a/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java @@ -28,6 +28,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AppOpsManagerInternal; import android.app.admin.DevicePolicyManagerInternal; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetManagerInternal; @@ -39,11 +42,16 @@ import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.LauncherApps; +import android.content.pm.PackageManagerInternal; import android.content.pm.ShortcutServiceInternal; import android.os.Handler; import android.os.UserHandle; import android.test.InstrumentationTestCase; import android.test.suitebuilder.annotation.SmallTest; +import android.util.AtomicFile; +import android.util.TypedXmlPullParser; +import android.util.TypedXmlSerializer; +import android.util.Xml; import android.widget.RemoteViews; import com.android.frameworks.servicestests.R; @@ -51,9 +59,16 @@ import com.android.internal.appwidget.IAppWidgetHost; import com.android.server.LocalServices; import org.mockito.ArgumentCaptor; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.Random; import java.util.concurrent.CountDownLatch; @@ -77,6 +92,8 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase { private AppWidgetManager mManager; private ShortcutServiceInternal mMockShortcutService; + private PackageManagerInternal mMockPackageManager; + private AppOpsManagerInternal mMockAppOpsManagerInternal; private IAppWidgetHost mMockHost; @Override @@ -85,6 +102,8 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase { LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class); LocalServices.removeServiceForTest(ShortcutServiceInternal.class); LocalServices.removeServiceForTest(AppWidgetManagerInternal.class); + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.removeServiceForTest(AppOpsManagerInternal.class); mTestContext = new TestContext(); mPkgName = mTestContext.getOpPackageName(); @@ -92,9 +111,16 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase { mManager = new AppWidgetManager(mTestContext, mService); mMockShortcutService = mock(ShortcutServiceInternal.class); + mMockPackageManager = mock(PackageManagerInternal.class); + mMockAppOpsManagerInternal = mock(AppOpsManagerInternal.class); mMockHost = mock(IAppWidgetHost.class); LocalServices.addService(ShortcutServiceInternal.class, mMockShortcutService); + LocalServices.addService(PackageManagerInternal.class, mMockPackageManager); + LocalServices.addService(AppOpsManagerInternal.class, mMockAppOpsManagerInternal); + when(mMockPackageManager.filterAppAccess(anyString(), anyInt(), anyInt())) + .thenReturn(false); mService.onStart(); + mService.systemServicesReady(); } public void testLoadDescription() { @@ -323,6 +349,34 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase { assertThat(info.previewLayout).isEqualTo(R.layout.widget_preview); } + public void testWidgetProviderInfoPersistence() throws IOException { + final AppWidgetProviderInfo original = new AppWidgetProviderInfo(); + original.minWidth = 40; + original.minHeight = 40; + original.maxResizeWidth = 250; + original.maxResizeHeight = 120; + original.targetCellWidth = 1; + original.targetCellHeight = 1; + original.updatePeriodMillis = 86400000; + original.previewLayout = R.layout.widget_preview; + original.label = "test"; + + final File file = new File(mTestContext.getDataDir(), "appwidget_provider_info.xml"); + saveWidgetProviderInfoLocked(file, original); + final AppWidgetProviderInfo target = loadAppWidgetProviderInfoLocked(file); + + assertThat(target.minWidth).isEqualTo(original.minWidth); + assertThat(target.minHeight).isEqualTo(original.minHeight); + assertThat(target.minResizeWidth).isEqualTo(original.minResizeWidth); + assertThat(target.minResizeHeight).isEqualTo(original.minResizeHeight); + assertThat(target.maxResizeWidth).isEqualTo(original.maxResizeWidth); + assertThat(target.maxResizeHeight).isEqualTo(original.maxResizeHeight); + assertThat(target.targetCellWidth).isEqualTo(original.targetCellWidth); + assertThat(target.targetCellHeight).isEqualTo(original.targetCellHeight); + assertThat(target.updatePeriodMillis).isEqualTo(original.updatePeriodMillis); + assertThat(target.previewLayout).isEqualTo(original.previewLayout); + } + private int setupHostAndWidget() { List<PendingHostUpdate> updates = mService.startListening( mMockHost, mPkgName, HOST_ID, new int[0]).getList(); @@ -353,6 +407,44 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase { return mTestContext.getResources().getInteger(resId); } + private static void saveWidgetProviderInfoLocked(@NonNull final File dst, + @Nullable final AppWidgetProviderInfo info) + throws IOException { + Objects.requireNonNull(dst); + if (info == null) { + return; + } + final AtomicFile file = new AtomicFile(dst); + final FileOutputStream stream = file.startWrite(); + final TypedXmlSerializer out = Xml.resolveSerializer(stream); + out.startDocument(null, true); + out.startTag(null, "p"); + AppWidgetXmlUtil.writeAppWidgetProviderInfoLocked(out, info); + out.endTag(null, "p"); + out.endDocument(); + file.finishWrite(stream); + } + + public static AppWidgetProviderInfo loadAppWidgetProviderInfoLocked(@NonNull final File dst) { + Objects.requireNonNull(dst); + final AtomicFile file = new AtomicFile(dst); + try (FileInputStream stream = file.openRead()) { + final TypedXmlPullParser parser = Xml.resolvePullParser(stream); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + // drain whitespace, comments, etc. + } + final String nodeName = parser.getName(); + if (!"p".equals(nodeName)) { + return null; + } + return AppWidgetXmlUtil.readAppWidgetProviderInfoLocked(parser); + } catch (IOException | XmlPullParserException e) { + return null; + } + } + private class TestContext extends ContextWrapper { public TestContext() { |