From 09ddda9fdac1e63f01a27854d46b4d006efc66b9 Mon Sep 17 00:00:00 2001 From: Pinyao Ting Date: Tue, 22 Mar 2022 23:29:21 +0000 Subject: Implement extra persistence layer for widget provider info Currently widget provider info are loaded from the manifests of respective packages, which is not performant since loading the resources from various apps could take a while. This CL does the following: 1. Instead of persisting only the providers that are bound to the workspace, we persists all providers. 2. In addition to providers, we also persists the provider info to avoid loading them from app's resource during a reboot. Bug: 202356231 Test: atest AppWidgetServiceImplTest Change-Id: I91fead0b61b0cb84d876dd05781a585805a01400 --- .../config/sysui/SystemUiDeviceConfigFlags.java | 6 + .../server/appwidget/AppWidgetServiceImpl.java | 60 +++++++++- .../android/server/appwidget/AppWidgetXmlUtil.java | 131 +++++++++++++++++++++ .../server/appwidget/AppWidgetServiceImplTest.java | 92 +++++++++++++++ 4 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java 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 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 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 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() { -- cgit v1.2.3-59-g8ed1b